Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38ff3209ad | ||
|
|
2789102d27 | ||
|
|
c1f88eb0ad | ||
|
|
5cf621396d | ||
|
|
2bece7b4c5 | ||
|
|
7a96893093 | ||
|
|
180fd44a4d | ||
|
|
a2edd2d911 | ||
|
|
e6cf552aad | ||
|
|
be6cc1c53a | ||
|
|
f50ffc07d7 | ||
|
|
e700552f93 | ||
|
|
d680d52b85 | ||
|
|
bf35297e4a | ||
|
|
c91b9af0e0 | ||
|
|
82a61e72e1 | ||
|
|
52fb09ff95 | ||
|
|
572cd22b4d | ||
|
|
9ee39aa07f | ||
|
|
372284b419 | ||
|
|
714bdca3f3 | ||
|
|
5613af032d | ||
|
|
8fb5e22e43 | ||
|
|
b19e05669e | ||
|
|
35b5d5ba35 | ||
|
|
84cd1b8cb8 | ||
|
|
fffd283b0c | ||
|
|
6d378844ae | ||
|
|
68cfccedee | ||
|
|
a433315346 | ||
|
|
77cc3b5470 | ||
|
|
dc412d21f7 | ||
|
|
96d9d3447b | ||
|
|
aed078b580 | ||
|
|
33c337785c | ||
|
|
20a6dbbe65 | ||
|
|
bd3b5f1edb | ||
|
|
e5703a588f | ||
|
|
3431a93499 | ||
|
|
8745091a16 | ||
|
|
e6bcdba5ad | ||
|
|
96af34f240 | ||
|
|
b7fcf14f6e | ||
|
|
ebc6a269d3 | ||
|
|
0595c04909 | ||
|
|
e60c87d750 | ||
|
|
d5bcae6182 | ||
|
|
dbc3aa7234 | ||
|
|
e626fb2dbf | ||
|
|
34be6de4d8 | ||
|
|
20be93217b | ||
|
|
c580d29a5d | ||
|
|
f337ddb267 | ||
|
|
1e05caf809 | ||
|
|
44884a8886 | ||
|
|
a01fd15812 | ||
|
|
d513629984 | ||
|
|
f1acf68bd0 | ||
|
|
3c9fc7858b | ||
|
|
bf2ff47df0 | ||
|
|
6318282873 | ||
|
|
3043e43418 | ||
|
|
595ebd11ac | ||
|
|
1157b8e12d | ||
|
|
f653944849 | ||
|
|
01c077da3d | ||
|
|
27ff386115 | ||
|
|
35437fb3a3 | ||
|
|
18e5c2e4cb | ||
|
|
2674656b49 | ||
|
|
fe56c0a7ac | ||
|
|
ddac5284fe | ||
|
|
b2020383dd | ||
|
|
563377a58a | ||
|
|
eadb671414 | ||
|
|
b3ccabd2fe | ||
|
|
e8bf596959 | ||
|
|
abc9b6b3f3 | ||
|
|
79bbc2be23 | ||
|
|
b763535c92 | ||
|
|
de62153d49 | ||
|
|
80413a7628 | ||
|
|
3f0fae1d10 | ||
|
|
c0bb32d768 | ||
|
|
5e168e6fef | ||
|
|
4bf6e64c35 | ||
|
|
1afc49c1e4 | ||
|
|
eebabd84ef | ||
|
|
6b04380c85 | ||
|
|
f3199c6510 | ||
|
|
c523126bae | ||
|
|
45f34924b0 | ||
|
|
25af07a0c7 | ||
|
|
bac28222fb | ||
|
|
1c4e63f71e | ||
|
|
e8abaa8bea | ||
|
|
5b8fd14470 | ||
|
|
d4339fe769 | ||
|
|
877ed69004 | ||
|
|
e360d57203 | ||
|
|
0235b0f6f3 | ||
|
|
a128dfa06b | ||
|
|
ffc91ba6b6 | ||
|
|
24c000739c | ||
|
|
00367ef23e | ||
|
|
527a0b329b | ||
|
|
2e4bba2374 | ||
|
|
4cafea0b97 | ||
|
|
cfc9cc8a10 | ||
|
|
c7fa024b8f | ||
|
|
cc50746043 | ||
|
|
bf0ea4dc5a | ||
|
|
9ddc243c52 | ||
|
|
9832e6edba | ||
|
|
afc78b0277 | ||
|
|
f561e94244 | ||
|
|
eed6c7194b | ||
|
|
167b5712ea | ||
|
|
7bc17da534 | ||
|
|
24497592a8 | ||
|
|
ed9e99e946 | ||
|
|
d42517b7ed | ||
|
|
93d4f987e7 | ||
|
|
6a1d60b1b3 | ||
|
|
19843e39fd | ||
|
|
ec18489723 | ||
|
|
9130428a55 | ||
|
|
236ec28491 | ||
|
|
f070e8b7f9 | ||
|
|
3321a1b922 | ||
|
|
f935acc6bf | ||
|
|
6d90d130bd | ||
|
|
bfd33ec0df | ||
|
|
9af6c5300b | ||
|
|
5c887d0709 | ||
|
|
c9589e2118 | ||
|
|
75cbbbe52d | ||
|
|
0455b80604 | ||
|
|
71a75bccf5 | ||
|
|
97609970b2 | ||
|
|
48ed701960 | ||
|
|
d401d77cbd | ||
|
|
5e97c9927b | ||
|
|
76660f3e44 | ||
|
|
92c98eda2e | ||
|
|
85df019c2a | ||
|
|
2fceeb120b | ||
|
|
8aa47ea6bc | ||
|
|
704f7e369c | ||
|
|
e4dd613ed1 | ||
|
|
63494a3717 | ||
|
|
5898550101 | ||
|
|
f1d2340861 | ||
|
|
e71ef42155 | ||
|
|
7f3daa19cd | ||
|
|
ebeaa24e9d | ||
|
|
e2447dd0a7 | ||
|
|
2feba8af94 | ||
|
|
386fc040b1 | ||
|
|
9094536d37 | ||
|
|
02b8a4cf11 | ||
|
|
849a0b973d | ||
|
|
01c4647dfc | ||
|
|
07c110a1fc | ||
|
|
85e94cc259 | ||
|
|
f0da5b9ea4 | ||
|
|
9ef843d492 | ||
|
|
b0245a7eff | ||
|
|
975e078f4d | ||
|
|
979c4290f7 | ||
|
|
bcf78b4efa | ||
|
|
098ac18762 | ||
|
|
4ea2eb7939 | ||
|
|
10f27ebacf | ||
|
|
7b913ea032 | ||
|
|
d63e52600d | ||
|
|
4ca7c8d548 | ||
|
|
69d32a94ab | ||
|
|
1c14b54eb9 | ||
|
|
c1b97a723f | ||
|
|
e2534e3703 | ||
|
|
2f8164d75f | ||
|
|
8215a2023e | ||
|
|
c095a7c291 | ||
|
|
57d178456a | ||
|
|
c1d85f8a6f | ||
|
|
29a750c4ca | ||
|
|
db5c4be674 | ||
|
|
6fbddb0702 | ||
|
|
ce9a5d12e0 | ||
|
|
c19429848b | ||
|
|
8f1563a7a5 | ||
|
|
2325660520 | ||
|
|
9be56d68e0 | ||
|
|
99a43cc998 | ||
|
|
be038ab878 | ||
|
|
8df7db1e17 | ||
|
|
0c79ac0479 | ||
|
|
47a33acfeb | ||
|
|
e91a49c455 | ||
|
|
730befce45 | ||
|
|
f8f9f27ae8 | ||
|
|
790a305c55 | ||
|
|
f63866b72a | ||
|
|
b22415d456 | ||
|
|
dbf7b15539 | ||
|
|
c5eac5a1c7 | ||
|
|
e399b38654 | ||
|
|
b040a55591 | ||
|
|
2968312b1c | ||
|
|
8c97476140 | ||
|
|
543707eefd | ||
|
|
cebf733b9d | ||
|
|
81fb53e757 | ||
|
|
add5269b89 | ||
|
|
615e9e348f | ||
|
|
510afab37c | ||
|
|
e9ba8d74d2 | ||
|
|
ddf06329a5 | ||
|
|
cac03fbcb1 | ||
|
|
4ad7ef84a4 | ||
|
|
7449634290 | ||
|
|
69822e4c25 | ||
|
|
7bc6a87697 | ||
|
|
9755cd5baa | ||
|
|
984dbf13ab | ||
|
|
7d88689f51 | ||
|
|
dcb92f7f74 | ||
|
|
02a241f831 | ||
|
|
fafaf48cb5 | ||
|
|
6b4f344b96 | ||
|
|
5ee2f1729a | ||
|
|
15657627a6 | ||
|
|
1c1d2c254d | ||
|
|
72090fab88 | ||
|
|
e92680a566 | ||
|
|
66f6123d76 | ||
|
|
c9c9660459 | ||
|
|
f9adfc50e4 | ||
|
|
ad1b461b6c | ||
|
|
232de33a86 | ||
|
|
d5f60b119c | ||
|
|
1092ee9c1c | ||
|
|
7075837849 | ||
|
|
b508f184ee | ||
|
|
06a6136671 | ||
|
|
89e8813188 | ||
|
|
488f448f04 | ||
|
|
95295061d0 | ||
|
|
adb009f388 | ||
|
|
748cb7d446 | ||
|
|
348186c405 | ||
|
|
08f7c2fd63 | ||
|
|
ed326f02c0 | ||
|
|
1ec668f697 | ||
|
|
b591891464 | ||
|
|
c0259aad67 | ||
|
|
e318f92177 | ||
|
|
53b277e575 | ||
|
|
589de36af7 | ||
|
|
5245d037ac | ||
|
|
58bead0398 | ||
|
|
9e9306fd2b | ||
|
|
0ea9e19d79 | ||
|
|
86bc0c8c73 | ||
|
|
858f5ae1fe | ||
|
|
cc3312157b | ||
|
|
4809d363b3 | ||
|
|
b1d204fdd4 | ||
|
|
29c6b253ca | ||
|
|
4a536484a6 | ||
|
|
25d09363df | ||
|
|
aa489be53b | ||
|
|
e3d253b040 | ||
|
|
db801aee79 | ||
|
|
4e0b32b505 | ||
|
|
2d7d6cfffc | ||
|
|
ef5af1e273 | ||
|
|
0546ad58be | ||
|
|
f3e5700d49 | ||
|
|
ed13da8aba | ||
|
|
48e37973a3 | ||
|
|
e2a8ad5fca | ||
|
|
190b934ab5 | ||
|
|
1e76dbc9a0 | ||
|
|
b3da09f52c | ||
|
|
4ab5050379 | ||
|
|
a615308111 | ||
|
|
40c1b49e6d | ||
|
|
8e94618c51 | ||
|
|
dcb32f78fe | ||
|
|
83efebe06b | ||
|
|
e6fad5ccb0 | ||
|
|
424141d1da | ||
|
|
4651db8c09 | ||
|
|
5997774ab8 | ||
|
|
760c62739a | ||
|
|
e627b8bf21 | ||
|
|
31124d9deb | ||
|
|
56c93bc2ac | ||
|
|
f83eb7326f | ||
|
|
8422d3ea79 | ||
|
|
77426266d2 | ||
|
|
7d4f9134bc | ||
|
|
4d8ba5c7f0 | ||
|
|
4148d70ec0 | ||
|
|
6f6be2c03f | ||
|
|
bfdefbf6e7 | ||
|
|
063dabbf4a | ||
|
|
302c5074e9 | ||
|
|
f202a95661 | ||
|
|
e82d9c873b | ||
|
|
489c45ffdf | ||
|
|
81b7cdfed7 | ||
|
|
67df928c7a | ||
|
|
743bbae5d1 | ||
|
|
2717fe7c20 | ||
|
|
51191168bc | ||
|
|
a59fb6b9eb | ||
|
|
3c382d4c6c | ||
|
|
8651bec915 | ||
|
|
a41b195f46 | ||
|
|
5e458d490a | ||
|
|
948f2e913e | ||
|
|
aa4b2cc36f | ||
|
|
df7517f9c4 | ||
|
|
98369fba22 | ||
|
|
7a1f1d36a1 | ||
|
|
040ea70585 | ||
|
|
1cf4fa96c1 | ||
|
|
d19143dd2b | ||
|
|
fe3291acb5 | ||
|
|
9873cad3d6 | ||
|
|
1e919abda3 | ||
|
|
c533ed91f5 | ||
|
|
38321355d3 | ||
|
|
efa258c695 | ||
|
|
4570f6fb0e | ||
|
|
e1e66f708f |
No files matched your search
+2
-1
@@ -10,7 +10,8 @@ OPENAI_API_KEY=''
|
|||||||
# DO NOT TRACK
|
# DO NOT TRACK
|
||||||
SCARF_NO_ANALYTICS=true
|
SCARF_NO_ANALYTICS=true
|
||||||
DO_NOT_TRACK=true
|
DO_NOT_TRACK=true
|
||||||
|
ANONYMIZED_TELEMETRY=false
|
||||||
|
|
||||||
# Use locally bundled version of the LiteLLM cost map json
|
# Use locally bundled version of the LiteLLM cost map json
|
||||||
# to avoid repetitive startup connections
|
# to avoid repetitive startup connections
|
||||||
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||||
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:svelte/recommended',
|
'plugin:svelte/recommended',
|
||||||
|
'plugin:cypress/recommended',
|
||||||
'prettier'
|
'prettier'
|
||||||
],
|
],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/backend"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "13:00"
|
||||||
|
groups:
|
||||||
|
python-packages:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
- [ ] **Description:** Briefly describe the changes in this pull request.
|
- [ ] **Description:** Briefly describe the changes in this pull request.
|
||||||
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
||||||
- [ ] **Documentation:** Have you updated relevant documentation?
|
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
|
||||||
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
||||||
|
- [ ] **Testing:** Have you written and run sufficient tests for the changes?
|
||||||
|
- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
[Insert a brief description of the changes made in this pull request]
|
[Insert a brief description of the changes made in this pull request, including any relevant motivation and impact.]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,16 +19,32 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [List any new features or additions]
|
- [List any new features, functionalities, or additions]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- [List any fixes or corrections]
|
- [List any fixes, corrections, or bug fixes]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- [List any changes or updates]
|
- [List any changes, updates, refactorings, or optimizations]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- [List any removed features or files]
|
- [List any removed features, files, or deprecated functionalities]
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- [List any new or updated security-related changes, including vulnerability fixes]
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [List any breaking changes affecting compatibility or functionality]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Additional Information
|
||||||
|
|
||||||
|
- [Insert any additional context, notes, or explanations for the changes]
|
||||||
|
|
||||||
|
- [Reference any related issues, commits, or other relevant information]
|
||||||
@@ -29,6 +29,9 @@ jobs:
|
|||||||
- name: Format Frontend
|
- name: Format Frontend
|
||||||
run: npm run format
|
run: npm run format
|
||||||
|
|
||||||
|
- name: Run i18next
|
||||||
|
run: npm run i18n:parse
|
||||||
|
|
||||||
- name: Check for Changes After Format
|
- name: Check for Changes After Format
|
||||||
run: git diff --exit-code
|
run: git diff --exit-code
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
name: Integration Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cypress-run:
|
||||||
|
name: Run Cypress Integration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and run Compose Stack
|
||||||
|
run: |
|
||||||
|
docker compose up --detach --build
|
||||||
|
|
||||||
|
- name: Preload Ollama model
|
||||||
|
run: |
|
||||||
|
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
||||||
|
|
||||||
|
- name: Cypress run
|
||||||
|
uses: cypress-io/github-action@v6
|
||||||
|
with:
|
||||||
|
browser: chrome
|
||||||
|
wait-on: 'http://localhost:3000'
|
||||||
|
config: baseUrl=http://localhost:3000
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
name: Upload Cypress videos
|
||||||
|
with:
|
||||||
|
name: cypress-videos
|
||||||
|
path: cypress/videos
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
- name: Extract Compose logs
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
docker compose logs > compose-logs.txt
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
name: Upload Compose logs
|
||||||
|
with:
|
||||||
|
name: compose-logs
|
||||||
|
path: compose-logs.txt
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
migration_test:
|
||||||
|
name: Run Migration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
# mysql:
|
||||||
|
# image: mysql
|
||||||
|
# env:
|
||||||
|
# MYSQL_ROOT_PASSWORD: mysql
|
||||||
|
# MYSQL_DATABASE: mysql
|
||||||
|
# options: >-
|
||||||
|
# --health-cmd "mysqladmin ping -h localhost"
|
||||||
|
# --health-interval 10s
|
||||||
|
# --health-timeout 5s
|
||||||
|
# --health-retries 5
|
||||||
|
# ports:
|
||||||
|
# - 3306:3306
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Set up uv
|
||||||
|
uses: yezz123/setup-uv@v4
|
||||||
|
with:
|
||||||
|
uv-venv: venv
|
||||||
|
|
||||||
|
- name: Activate virtualenv
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
echo PATH=$PATH >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
uv pip install -r backend/requirements.txt
|
||||||
|
|
||||||
|
- name: Test backend with SQLite
|
||||||
|
id: sqlite
|
||||||
|
env:
|
||||||
|
WEBUI_SECRET_KEY: secret-key
|
||||||
|
GLOBAL_LOG_LEVEL: debug
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
||||||
|
UVICORN_PID=$!
|
||||||
|
# Wait up to 20 seconds for the server to start
|
||||||
|
for i in {1..20}; do
|
||||||
|
curl -s http://localhost:8080/api/config > /dev/null && break
|
||||||
|
sleep 1
|
||||||
|
if [ $i -eq 20 ]; then
|
||||||
|
echo "Server failed to start"
|
||||||
|
kill -9 $UVICORN_PID
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Check that the server is still running after 5 seconds
|
||||||
|
sleep 5
|
||||||
|
if ! kill -0 $UVICORN_PID; then
|
||||||
|
echo "Server has stopped"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
- name: Test backend with Postgres
|
||||||
|
if: success() || steps.sqlite.conclusion == 'failure'
|
||||||
|
env:
|
||||||
|
WEBUI_SECRET_KEY: secret-key
|
||||||
|
GLOBAL_LOG_LEVEL: debug
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
|
||||||
|
UVICORN_PID=$!
|
||||||
|
# Wait up to 20 seconds for the server to start
|
||||||
|
for i in {1..20}; do
|
||||||
|
curl -s http://localhost:8081/api/config > /dev/null && break
|
||||||
|
sleep 1
|
||||||
|
if [ $i -eq 20 ]; then
|
||||||
|
echo "Server failed to start"
|
||||||
|
kill -9 $UVICORN_PID
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Check that the server is still running after 5 seconds
|
||||||
|
sleep 5
|
||||||
|
if ! kill -0 $UVICORN_PID; then
|
||||||
|
echo "Server has stopped"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# - name: Test backend with MySQL
|
||||||
|
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
|
||||||
|
# env:
|
||||||
|
# WEBUI_SECRET_KEY: secret-key
|
||||||
|
# GLOBAL_LOG_LEVEL: debug
|
||||||
|
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
||||||
|
# run: |
|
||||||
|
# cd backend
|
||||||
|
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
|
||||||
|
# UVICORN_PID=$!
|
||||||
|
# # Wait up to 20 seconds for the server to start
|
||||||
|
# for i in {1..20}; do
|
||||||
|
# curl -s http://localhost:8083/api/config > /dev/null && break
|
||||||
|
# sleep 1
|
||||||
|
# if [ $i -eq 20 ]; then
|
||||||
|
# echo "Server failed to start"
|
||||||
|
# kill -9 $UVICORN_PID
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
# done
|
||||||
|
# # Check that the server is still running after 5 seconds
|
||||||
|
# sleep 5
|
||||||
|
# if ! kill -0 $UVICORN_PID; then
|
||||||
|
# echo "Server has stopped"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
+5
-1
@@ -297,4 +297,8 @@ dist
|
|||||||
.yarn/unplugged
|
.yarn/unplugged
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
# cypress artifacts
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
@@ -5,6 +5,69 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.123] - 2024-05-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
|
||||||
|
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
|
||||||
|
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
|
||||||
|
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
|
||||||
|
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
|
||||||
|
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
|
||||||
|
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
|
||||||
|
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
|
||||||
|
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
|
||||||
|
- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
|
||||||
|
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
|
||||||
|
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
|
||||||
|
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
|
||||||
|
|
||||||
|
## [0.1.122] - 2024-04-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
|
||||||
|
- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
|
||||||
|
- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
|
||||||
|
- **👨💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
|
||||||
|
- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
|
||||||
|
- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
|
||||||
|
- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
|
||||||
|
- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
|
||||||
|
- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
|
||||||
|
- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
|
||||||
|
- **🔤 Font Fallback**: Corrected font fallback issue.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
|
||||||
|
|
||||||
|
## [0.1.121] - 2024-04-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Translation Issues**: Addressed various translation discrepancies.
|
||||||
|
- **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
|
||||||
|
- **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
|
||||||
|
- **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
|
||||||
|
- **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
|
||||||
|
- **🏷️ Tag Display**: Corrected tag display inconsistencies.
|
||||||
|
- **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
|
||||||
|
- **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
|
||||||
|
|
||||||
## [0.1.120] - 2024-04-20
|
## [0.1.120] - 2024-04-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+21
-10
@@ -8,8 +8,9 @@ ARG USE_CUDA_VER=cu121
|
|||||||
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
||||||
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
||||||
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
||||||
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||||
ARG USE_EMBEDDING_MODEL=all-MiniLM-L6-v2
|
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||||
|
ARG USE_RERANKING_MODEL=""
|
||||||
|
|
||||||
######## WebUI frontend ########
|
######## WebUI frontend ########
|
||||||
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
||||||
@@ -30,6 +31,7 @@ ARG USE_CUDA
|
|||||||
ARG USE_OLLAMA
|
ARG USE_OLLAMA
|
||||||
ARG USE_CUDA_VER
|
ARG USE_CUDA_VER
|
||||||
ARG USE_EMBEDDING_MODEL
|
ARG USE_EMBEDDING_MODEL
|
||||||
|
ARG USE_RERANKING_MODEL
|
||||||
|
|
||||||
## Basis ##
|
## Basis ##
|
||||||
ENV ENV=prod \
|
ENV ENV=prod \
|
||||||
@@ -38,7 +40,8 @@ ENV ENV=prod \
|
|||||||
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
||||||
USE_CUDA_DOCKER=${USE_CUDA} \
|
USE_CUDA_DOCKER=${USE_CUDA} \
|
||||||
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
||||||
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
|
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
||||||
|
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
||||||
|
|
||||||
## Basis URL Config ##
|
## Basis URL Config ##
|
||||||
ENV OLLAMA_BASE_URL="/ollama" \
|
ENV OLLAMA_BASE_URL="/ollama" \
|
||||||
@@ -48,7 +51,8 @@ ENV OLLAMA_BASE_URL="/ollama" \
|
|||||||
ENV OPENAI_API_KEY="" \
|
ENV OPENAI_API_KEY="" \
|
||||||
WEBUI_SECRET_KEY="" \
|
WEBUI_SECRET_KEY="" \
|
||||||
SCARF_NO_ANALYTICS=true \
|
SCARF_NO_ANALYTICS=true \
|
||||||
DO_NOT_TRACK=true
|
DO_NOT_TRACK=true \
|
||||||
|
ANONYMIZED_TELEMETRY=false
|
||||||
|
|
||||||
# Use locally bundled version of the LiteLLM cost map json
|
# Use locally bundled version of the LiteLLM cost map json
|
||||||
# to avoid repetitive startup connections
|
# to avoid repetitive startup connections
|
||||||
@@ -62,12 +66,19 @@ ENV WHISPER_MODEL="base" \
|
|||||||
|
|
||||||
## RAG Embedding model settings ##
|
## RAG Embedding model settings ##
|
||||||
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||||
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
|
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
||||||
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
||||||
|
|
||||||
|
## Hugging Face download cache ##
|
||||||
|
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
||||||
#### Other models ##########################################################
|
#### Other models ##########################################################
|
||||||
|
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
|
|
||||||
|
ENV HOME /root
|
||||||
|
RUN mkdir -p $HOME/.cache/chroma
|
||||||
|
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
|
||||||
|
|
||||||
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
# Install pandoc and netcat
|
# Install pandoc and netcat
|
||||||
@@ -98,13 +109,13 @@ RUN pip3 install uv && \
|
|||||||
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
||||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
|
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||||
python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \
|
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||||
else \
|
else \
|
||||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
|
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||||
python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \
|
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -123,4 +134,4 @@ COPY ./backend .
|
|||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD [ "bash", "start.sh"]
|
CMD [ "bash", "start.sh"]
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
|
|
||||||
|
ifneq ($(shell which docker-compose 2>/dev/null),)
|
||||||
|
DOCKER_COMPOSE := docker-compose
|
||||||
|
else
|
||||||
|
DOCKER_COMPOSE := docker compose
|
||||||
|
endif
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@docker-compose up -d
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
remove:
|
remove:
|
||||||
@chmod +x confirm_remove.sh
|
@chmod +x confirm_remove.sh
|
||||||
@./confirm_remove.sh
|
@./confirm_remove.sh
|
||||||
|
|
||||||
|
|
||||||
start:
|
start:
|
||||||
@docker-compose start
|
$(DOCKER_COMPOSE) start
|
||||||
startAndBuild:
|
startAndBuild:
|
||||||
docker-compose up -d --build
|
$(DOCKER_COMPOSE) up -d --build
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@docker-compose stop
|
$(DOCKER_COMPOSE) stop
|
||||||
|
|
||||||
update:
|
update:
|
||||||
# Calls the LLM update script
|
# Calls the LLM update script
|
||||||
chmod +x update_ollama_models.sh
|
chmod +x update_ollama_models.sh
|
||||||
@./update_ollama_models.sh
|
@./update_ollama_models.sh
|
||||||
@git pull
|
@git pull
|
||||||
@docker-compose down
|
$(DOCKER_COMPOSE) down
|
||||||
# Make sure the ollama-webui container is stopped before rebuilding
|
# Make sure the ollama-webui container is stopped before rebuilding
|
||||||
@docker stop open-webui || true
|
@docker stop open-webui || true
|
||||||
@docker-compose up --build -d
|
$(DOCKER_COMPOSE) up --build -d
|
||||||
@docker-compose start
|
$(DOCKER_COMPOSE) start
|
||||||
|
|
||||||
@@ -25,22 +25,28 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
|||||||
|
|
||||||
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
|
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
|
||||||
|
|
||||||
|
- 🌈 **Theme Customization**: Choose from a variety of themes to personalize your Open WebUI experience.
|
||||||
|
|
||||||
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
|
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
|
||||||
|
|
||||||
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
||||||
|
|
||||||
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
|
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
|
||||||
|
|
||||||
|
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
|
||||||
|
|
||||||
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
||||||
|
|
||||||
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
|
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
|
||||||
|
|
||||||
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
|
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, followed by the option to provide textual feedback, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
|
||||||
|
|
||||||
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
|
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
|
||||||
|
|
||||||
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
|
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
|
||||||
|
|
||||||
|
- 🔄 **Update All Ollama Models**: Easily update locally installed models all at once with a convenient button, streamlining model management.
|
||||||
|
|
||||||
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
|
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
|
||||||
|
|
||||||
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
||||||
@@ -53,28 +59,42 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
|||||||
|
|
||||||
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
|
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
|
||||||
|
|
||||||
|
- 🗨️ **Local Chat Sharing**: Generate and share chat links seamlessly between users, enhancing collaboration and communication.
|
||||||
|
|
||||||
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
|
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
|
||||||
|
|
||||||
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
||||||
|
|
||||||
|
- 📬 **Archive Chats**: Effortlessly store away completed conversations with LLMs for future reference, maintaining a tidy and clutter-free chat interface while allowing for easy retrieval and reference.
|
||||||
|
|
||||||
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
||||||
|
|
||||||
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
|
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
|
||||||
|
|
||||||
|
- 🔊 **Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
||||||
|
|
||||||
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
||||||
|
|
||||||
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using AUTOMATIC1111 API (local) and DALL-E, enriching your chat experience with dynamic visual content.
|
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API (local), ComfyUI (local), and DALL-E, enriching your chat experience with dynamic visual content.
|
||||||
|
|
||||||
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
|
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
|
||||||
|
|
||||||
- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
|
- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
|
||||||
|
|
||||||
|
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries, simplifying integration and development.
|
||||||
|
|
||||||
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
||||||
|
|
||||||
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
|
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
|
||||||
|
|
||||||
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
||||||
|
|
||||||
|
- 🔗 **Webhook Integration**: Subscribe to new user sign-up events via webhook (compatible with Google Chat and Microsoft Teams), providing real-time notifications and automation capabilities.
|
||||||
|
|
||||||
|
- 🛡️ **Model Whitelisting**: Admins can whitelist models for users with the 'user' role, enhancing security and access control.
|
||||||
|
|
||||||
|
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header, adding an additional layer of security and authentication.
|
||||||
|
|
||||||
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
||||||
|
|
||||||
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
||||||
|
|||||||
+63
-38
@@ -24,6 +24,7 @@ from utils.misc import calculate_sha256
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import mimetypes
|
||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
@@ -32,11 +33,15 @@ import logging
|
|||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
|
IMAGE_GENERATION_ENGINE,
|
||||||
ENABLE_IMAGE_GENERATION,
|
ENABLE_IMAGE_GENERATION,
|
||||||
AUTOMATIC1111_BASE_URL,
|
AUTOMATIC1111_BASE_URL,
|
||||||
COMFYUI_BASE_URL,
|
COMFYUI_BASE_URL,
|
||||||
OPENAI_API_BASE_URL,
|
IMAGES_OPENAI_API_BASE_URL,
|
||||||
OPENAI_API_KEY,
|
IMAGES_OPENAI_API_KEY,
|
||||||
|
IMAGE_GENERATION_MODEL,
|
||||||
|
IMAGE_SIZE,
|
||||||
|
IMAGE_STEPS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -55,21 +60,21 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.state.ENGINE = ""
|
app.state.ENGINE = IMAGE_GENERATION_ENGINE
|
||||||
app.state.ENABLED = ENABLE_IMAGE_GENERATION
|
app.state.ENABLED = ENABLE_IMAGE_GENERATION
|
||||||
|
|
||||||
app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL
|
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||||
app.state.OPENAI_API_KEY = OPENAI_API_KEY
|
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||||
|
|
||||||
app.state.MODEL = ""
|
app.state.MODEL = IMAGE_GENERATION_MODEL
|
||||||
|
|
||||||
|
|
||||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
app.state.IMAGE_SIZE = "512x512"
|
app.state.IMAGE_SIZE = IMAGE_SIZE
|
||||||
app.state.IMAGE_STEPS = 50
|
app.state.IMAGE_STEPS = IMAGE_STEPS
|
||||||
|
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
@@ -135,27 +140,33 @@ async def update_engine_url(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OpenAIKeyUpdateForm(BaseModel):
|
class OpenAIConfigUpdateForm(BaseModel):
|
||||||
|
url: str
|
||||||
key: str
|
key: str
|
||||||
|
|
||||||
|
|
||||||
@app.get("/key")
|
@app.get("/openai/config")
|
||||||
async def get_openai_key(user=Depends(get_admin_user)):
|
async def get_openai_config(user=Depends(get_admin_user)):
|
||||||
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
return {
|
||||||
|
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/key/update")
|
@app.post("/openai/config/update")
|
||||||
async def update_openai_key(
|
async def update_openai_config(
|
||||||
form_data: OpenAIKeyUpdateForm, user=Depends(get_admin_user)
|
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
if form_data.key == "":
|
if form_data.key == "":
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||||
|
|
||||||
|
app.state.OPENAI_API_BASE_URL = form_data.url
|
||||||
app.state.OPENAI_API_KEY = form_data.key
|
app.state.OPENAI_API_KEY = form_data.key
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
|
||||||
"status": True,
|
"status": True,
|
||||||
|
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -305,38 +316,50 @@ class GenerateImageForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def save_b64_image(b64_str):
|
def save_b64_image(b64_str):
|
||||||
image_id = str(uuid.uuid4())
|
|
||||||
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Split the base64 string to get the actual image data
|
header, encoded = b64_str.split(",", 1)
|
||||||
img_data = base64.b64decode(b64_str)
|
mime_type = header.split(";")[0]
|
||||||
|
|
||||||
# Write the image data to a file
|
img_data = base64.b64decode(encoded)
|
||||||
|
|
||||||
|
image_id = str(uuid.uuid4())
|
||||||
|
image_format = mimetypes.guess_extension(mime_type)
|
||||||
|
|
||||||
|
image_filename = f"{image_id}{image_format}"
|
||||||
|
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
f.write(img_data)
|
f.write(img_data)
|
||||||
|
return image_filename
|
||||||
return image_id
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error saving image: {e}")
|
log.exception(f"Error saving image: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_url_image(url):
|
def save_url_image(url):
|
||||||
image_id = str(uuid.uuid4())
|
image_id = str(uuid.uuid4())
|
||||||
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url)
|
r = requests.get(url)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
if r.headers["content-type"].split("/")[0] == "image":
|
||||||
|
|
||||||
with open(file_path, "wb") as image_file:
|
mime_type = r.headers["content-type"]
|
||||||
image_file.write(r.content)
|
image_format = mimetypes.guess_extension(mime_type)
|
||||||
|
|
||||||
|
if not image_format:
|
||||||
|
raise ValueError("Could not determine image type from MIME type")
|
||||||
|
|
||||||
|
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}{image_format}")
|
||||||
|
with open(file_path, "wb") as image_file:
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
image_file.write(chunk)
|
||||||
|
return image_id, image_format
|
||||||
|
else:
|
||||||
|
log.error(f"Url does not point to an image.")
|
||||||
|
return None, None
|
||||||
|
|
||||||
return image_id
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error saving image: {e}")
|
log.exception(f"Error saving image: {e}")
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/generations")
|
@app.post("/generations")
|
||||||
@@ -375,8 +398,8 @@ def generate_image(
|
|||||||
images = []
|
images = []
|
||||||
|
|
||||||
for image in res["data"]:
|
for image in res["data"]:
|
||||||
image_id = save_b64_image(image["b64_json"])
|
image_filename = save_b64_image(image["b64_json"])
|
||||||
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
||||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
with open(file_body_path, "w") as f:
|
with open(file_body_path, "w") as f:
|
||||||
@@ -412,8 +435,10 @@ def generate_image(
|
|||||||
images = []
|
images = []
|
||||||
|
|
||||||
for image in res["data"]:
|
for image in res["data"]:
|
||||||
image_id = save_url_image(image["url"])
|
image_id, image_format = save_url_image(image["url"])
|
||||||
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
images.append(
|
||||||
|
{"url": f"/cache/image/generations/{image_id}{image_format}"}
|
||||||
|
)
|
||||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
with open(file_body_path, "w") as f:
|
with open(file_body_path, "w") as f:
|
||||||
@@ -450,8 +475,8 @@ def generate_image(
|
|||||||
images = []
|
images = []
|
||||||
|
|
||||||
for image in res["images"]:
|
for image in res["images"]:
|
||||||
image_id = save_b64_image(image)
|
image_filename = save_b64_image(image)
|
||||||
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
||||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
with open(file_body_path, "w") as f:
|
with open(file_body_path, "w") as f:
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class ImageGenerationPayload(BaseModel):
|
|||||||
def comfyui_generate_image(
|
def comfyui_generate_image(
|
||||||
model: str, payload: ImageGenerationPayload, client_id, base_url
|
model: str, payload: ImageGenerationPayload, client_id, base_url
|
||||||
):
|
):
|
||||||
host = base_url.replace("http://", "").replace("https://", "")
|
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
|
|
||||||
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ def comfyui_generate_image(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ws = websocket.WebSocket()
|
ws = websocket.WebSocket()
|
||||||
ws.connect(f"ws://{host}/ws?clientId={client_id}")
|
ws.connect(f"{ws_url}/ws?clientId={client_id}")
|
||||||
log.info("WebSocket connection established.")
|
log.info("WebSocket connection established.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Failed to connect to WebSocket server: {e}")
|
log.exception(f"Failed to connect to WebSocket server: {e}")
|
||||||
|
|||||||
+336
-64
@@ -1,100 +1,372 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from litellm.proxy.proxy_server import ProxyConfig, initialize
|
|
||||||
from litellm.proxy.proxy_server import app
|
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status, Response
|
from fastapi import FastAPI, Request, Depends, status, Response
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
from starlette.responses import StreamingResponse
|
from starlette.responses import StreamingResponse
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
from utils.utils import get_http_authorization_cred, get_current_user
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from utils.utils import get_verified_user, get_current_user, get_admin_user
|
||||||
from config import SRC_LOG_LEVELS, ENV
|
from config import SRC_LOG_LEVELS, ENV
|
||||||
|
from constants import MESSAGES
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
||||||
|
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
MODEL_FILTER_ENABLED,
|
ENABLE_LITELLM,
|
||||||
|
ENABLE_MODEL_FILTER,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
|
DATA_DIR,
|
||||||
|
LITELLM_PROXY_PORT,
|
||||||
|
LITELLM_PROXY_HOST,
|
||||||
|
)
|
||||||
|
|
||||||
|
from litellm.utils import get_llm_provider
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
origins = ["*"]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "r") as file:
|
||||||
|
litellm_config = yaml.safe_load(file)
|
||||||
|
|
||||||
|
|
||||||
async def config():
|
app.state.ENABLE = ENABLE_LITELLM
|
||||||
router, model_list, general_settings = await proxy_config.load_config(
|
app.state.CONFIG = litellm_config
|
||||||
router=None, config_file_path="./data/litellm/config.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
await initialize(config="./data/litellm/config.yaml", telemetry=False)
|
# Global variable to store the subprocess reference
|
||||||
|
background_process = None
|
||||||
|
|
||||||
|
CONFLICT_ENV_VARS = [
|
||||||
|
# Uvicorn uses PORT, so LiteLLM might use it as well
|
||||||
|
"PORT",
|
||||||
|
# LiteLLM uses DATABASE_URL for Prisma connections
|
||||||
|
"DATABASE_URL",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def startup():
|
async def run_background_process(command):
|
||||||
await config()
|
global background_process
|
||||||
|
log.info("run_background_process")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Log the command to be executed
|
||||||
|
log.info(f"Executing command: {command}")
|
||||||
|
# Filter environment variables known to conflict with litellm
|
||||||
|
env = {k: v for k, v in os.environ.items() if k not in CONFLICT_ENV_VARS}
|
||||||
|
# Execute the command and create a subprocess
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
|
||||||
|
)
|
||||||
|
background_process = process
|
||||||
|
log.info("Subprocess started successfully.")
|
||||||
|
|
||||||
|
# Capture STDERR for debugging purposes
|
||||||
|
stderr_output = await process.stderr.read()
|
||||||
|
stderr_text = stderr_output.decode().strip()
|
||||||
|
if stderr_text:
|
||||||
|
log.info(f"Subprocess STDERR: {stderr_text}")
|
||||||
|
|
||||||
|
# log.info output line by line
|
||||||
|
async for line in process.stdout:
|
||||||
|
log.info(line.decode().strip())
|
||||||
|
|
||||||
|
# Wait for the process to finish
|
||||||
|
returncode = await process.wait()
|
||||||
|
log.info(f"Subprocess exited with return code {returncode}")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to start subprocess: {e}")
|
||||||
|
raise # Optionally re-raise the exception if you want it to propagate
|
||||||
|
|
||||||
|
|
||||||
|
async def start_litellm_background():
|
||||||
|
log.info("start_litellm_background")
|
||||||
|
# Command to run in the background
|
||||||
|
command = [
|
||||||
|
"litellm",
|
||||||
|
"--port",
|
||||||
|
str(LITELLM_PROXY_PORT),
|
||||||
|
"--host",
|
||||||
|
LITELLM_PROXY_HOST,
|
||||||
|
"--telemetry",
|
||||||
|
"False",
|
||||||
|
"--config",
|
||||||
|
LITELLM_CONFIG_DIR,
|
||||||
|
]
|
||||||
|
|
||||||
|
await run_background_process(command)
|
||||||
|
|
||||||
|
|
||||||
|
async def shutdown_litellm_background():
|
||||||
|
log.info("shutdown_litellm_background")
|
||||||
|
global background_process
|
||||||
|
if background_process:
|
||||||
|
background_process.terminate()
|
||||||
|
await background_process.wait() # Ensure the process has terminated
|
||||||
|
log.info("Subprocess terminated")
|
||||||
|
background_process = None
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def on_startup():
|
async def startup_event():
|
||||||
await startup()
|
log.info("startup_event")
|
||||||
|
# TODO: Check config.yaml file and create one
|
||||||
|
asyncio.create_task(start_litellm_background())
|
||||||
|
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.get("/")
|
||||||
async def auth_middleware(request: Request, call_next):
|
async def get_status():
|
||||||
auth_header = request.headers.get("Authorization", "")
|
return {"status": True}
|
||||||
request.state.user = None
|
|
||||||
|
|
||||||
|
async def restart_litellm():
|
||||||
|
"""
|
||||||
|
Endpoint to restart the litellm background service.
|
||||||
|
"""
|
||||||
|
log.info("Requested restart of litellm service.")
|
||||||
|
try:
|
||||||
|
# Shut down the existing process if it is running
|
||||||
|
await shutdown_litellm_background()
|
||||||
|
log.info("litellm service shutdown complete.")
|
||||||
|
|
||||||
|
# Restart the background service
|
||||||
|
|
||||||
|
asyncio.create_task(start_litellm_background())
|
||||||
|
log.info("litellm service restart complete.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "litellm service restarted successfully.",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.info(f"Error restarting litellm service: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/restart")
|
||||||
|
async def restart_litellm_handler(user=Depends(get_admin_user)):
|
||||||
|
return await restart_litellm()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/config")
|
||||||
|
async def get_config(user=Depends(get_admin_user)):
|
||||||
|
return app.state.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class LiteLLMConfigForm(BaseModel):
|
||||||
|
general_settings: Optional[dict] = None
|
||||||
|
litellm_settings: Optional[dict] = None
|
||||||
|
model_list: Optional[List[dict]] = None
|
||||||
|
router_settings: Optional[dict] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/config/update")
|
||||||
|
async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.CONFIG = form_data.model_dump(exclude_none=True)
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||||
|
yaml.dump(app.state.CONFIG, file)
|
||||||
|
|
||||||
|
await restart_litellm()
|
||||||
|
return app.state.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/models")
|
||||||
|
@app.get("/v1/models")
|
||||||
|
async def get_models(user=Depends(get_current_user)):
|
||||||
|
|
||||||
|
if app.state.ENABLE:
|
||||||
|
while not background_process:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
r = requests.request(method="GET", url=f"{url}/models")
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if app.state.ENABLE_MODEL_FILTER:
|
||||||
|
if user and user.role == "user":
|
||||||
|
data["data"] = list(
|
||||||
|
filter(
|
||||||
|
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
|
||||||
|
data["data"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": model["model_name"],
|
||||||
|
"object": "model",
|
||||||
|
"created": int(time.time()),
|
||||||
|
"owned_by": "openai",
|
||||||
|
}
|
||||||
|
for model in app.state.CONFIG["model_list"]
|
||||||
|
],
|
||||||
|
"object": "list",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"data": [],
|
||||||
|
"object": "list",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/model/info")
|
||||||
|
async def get_model_list(user=Depends(get_admin_user)):
|
||||||
|
return {"data": app.state.CONFIG["model_list"]}
|
||||||
|
|
||||||
|
|
||||||
|
class AddLiteLLMModelForm(BaseModel):
|
||||||
|
model_name: str
|
||||||
|
litellm_params: dict
|
||||||
|
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/model/new")
|
||||||
|
async def add_model_to_config(
|
||||||
|
form_data: AddLiteLLMModelForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
get_llm_provider(model=form_data.model_name)
|
||||||
|
app.state.CONFIG["model_list"].append(form_data.model_dump())
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||||
|
yaml.dump(app.state.CONFIG, file)
|
||||||
|
|
||||||
|
await restart_litellm()
|
||||||
|
|
||||||
|
return {"message": MESSAGES.MODEL_ADDED(form_data.model_name)}
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteLiteLLMModelForm(BaseModel):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/model/delete")
|
||||||
|
async def delete_model_from_config(
|
||||||
|
form_data: DeleteLiteLLMModelForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
app.state.CONFIG["model_list"] = [
|
||||||
|
model
|
||||||
|
for model in app.state.CONFIG["model_list"]
|
||||||
|
if model["model_name"] != form_data.id
|
||||||
|
]
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||||
|
yaml.dump(app.state.CONFIG, file)
|
||||||
|
|
||||||
|
await restart_litellm()
|
||||||
|
|
||||||
|
return {"message": MESSAGES.MODEL_DELETED(form_data.id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||||
|
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
|
body = await request.body()
|
||||||
|
|
||||||
|
url = f"http://localhost:{LITELLM_PROXY_PORT}"
|
||||||
|
|
||||||
|
target_url = f"{url}/{path}"
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
# headers["Authorization"] = f"Bearer {key}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
r = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = get_current_user(get_http_authorization_cred(auth_header))
|
r = requests.request(
|
||||||
log.debug(f"user: {user}")
|
method=request.method,
|
||||||
request.state.user = user
|
url=target_url,
|
||||||
|
data=body,
|
||||||
|
headers=headers,
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# Check if response is SSE
|
||||||
|
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||||
|
return StreamingResponse(
|
||||||
|
r.iter_content(chunk_size=8192),
|
||||||
|
status_code=r.status_code,
|
||||||
|
headers=dict(r.headers),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response_data = r.json()
|
||||||
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(status_code=400, content={"detail": str(e)})
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
response = await call_next(request)
|
raise HTTPException(
|
||||||
return response
|
status_code=r.status_code if r else 500, detail=error_detail
|
||||||
|
)
|
||||||
|
|
||||||
class ModifyModelsResponseMiddleware(BaseHTTPMiddleware):
|
|
||||||
async def dispatch(
|
|
||||||
self, request: Request, call_next: RequestResponseEndpoint
|
|
||||||
) -> Response:
|
|
||||||
|
|
||||||
response = await call_next(request)
|
|
||||||
user = request.state.user
|
|
||||||
|
|
||||||
if "/models" in request.url.path:
|
|
||||||
if isinstance(response, StreamingResponse):
|
|
||||||
# Read the content of the streaming response
|
|
||||||
body = b""
|
|
||||||
async for chunk in response.body_iterator:
|
|
||||||
body += chunk
|
|
||||||
|
|
||||||
data = json.loads(body.decode("utf-8"))
|
|
||||||
|
|
||||||
if app.state.MODEL_FILTER_ENABLED:
|
|
||||||
if user and user.role == "user":
|
|
||||||
data["data"] = list(
|
|
||||||
filter(
|
|
||||||
lambda model: model["id"]
|
|
||||||
in app.state.MODEL_FILTER_LIST,
|
|
||||||
data["data"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Modified Flag
|
|
||||||
data["modified"] = True
|
|
||||||
return JSONResponse(content=data)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(ModifyModelsResponseMiddleware)
|
|
||||||
@@ -16,6 +16,7 @@ from fastapi.concurrency import run_in_threadpool
|
|||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import copy
|
import copy
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
@@ -36,7 +37,7 @@ from utils.utils import decode_token, get_current_user, get_admin_user
|
|||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
OLLAMA_BASE_URLS,
|
OLLAMA_BASE_URLS,
|
||||||
MODEL_FILTER_ENABLED,
|
ENABLE_MODEL_FILTER,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
)
|
)
|
||||||
@@ -55,7 +56,7 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||||
@@ -168,7 +169,7 @@ async def get_ollama_tags(
|
|||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
|
|
||||||
if app.state.MODEL_FILTER_ENABLED:
|
if app.state.ENABLE_MODEL_FILTER:
|
||||||
if user.role == "user":
|
if user.role == "user":
|
||||||
models["models"] = list(
|
models["models"] = list(
|
||||||
filter(
|
filter(
|
||||||
@@ -216,7 +217,9 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
|||||||
if len(responses) > 0:
|
if len(responses) > 0:
|
||||||
lowest_version = min(
|
lowest_version = min(
|
||||||
responses,
|
responses,
|
||||||
key=lambda x: tuple(map(int, x["version"].split("-")[0].split("."))),
|
key=lambda x: tuple(
|
||||||
|
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"version": lowest_version["version"]}
|
return {"version": lowest_version["version"]}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from config import (
|
|||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
MODEL_FILTER_ENABLED,
|
ENABLE_MODEL_FILTER,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
)
|
)
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
@@ -45,7 +45,7 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||||
@@ -80,6 +80,7 @@ async def get_openai_urls(user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
@app.post("/urls/update")
|
@app.post("/urls/update")
|
||||||
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
||||||
|
await get_all_models()
|
||||||
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
||||||
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||||
|
|
||||||
@@ -170,6 +171,7 @@ async def fetch_url(url, key):
|
|||||||
|
|
||||||
|
|
||||||
def merge_models_lists(model_lists):
|
def merge_models_lists(model_lists):
|
||||||
|
log.info(f"merge_models_lists {model_lists}")
|
||||||
merged_list = []
|
merged_list = []
|
||||||
|
|
||||||
for idx, models in enumerate(model_lists):
|
for idx, models in enumerate(model_lists):
|
||||||
@@ -198,14 +200,16 @@ async def get_all_models():
|
|||||||
]
|
]
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
|
log.info(f"get_all_models:responses() {responses}")
|
||||||
|
|
||||||
models = {
|
models = {
|
||||||
"data": merge_models_lists(
|
"data": merge_models_lists(
|
||||||
list(
|
list(
|
||||||
map(
|
map(
|
||||||
lambda response: (
|
lambda response: (
|
||||||
response["data"]
|
response["data"]
|
||||||
if response and "data" in response
|
if (response and "data" in response)
|
||||||
else None
|
else (response if isinstance(response, list) else None)
|
||||||
),
|
),
|
||||||
responses,
|
responses,
|
||||||
)
|
)
|
||||||
@@ -224,7 +228,7 @@ async def get_all_models():
|
|||||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
|
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
if app.state.MODEL_FILTER_ENABLED:
|
if app.state.ENABLE_MODEL_FILTER:
|
||||||
if user.role == "user":
|
if user.role == "user":
|
||||||
models["data"] = list(
|
models["data"] = list(
|
||||||
filter(
|
filter(
|
||||||
|
|||||||
+234
-134
@@ -13,7 +13,6 @@ import os, shutil, logging, re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from chromadb.utils import embedding_functions
|
|
||||||
from chromadb.utils.batch_utils import create_batches
|
from chromadb.utils.batch_utils import create_batches
|
||||||
|
|
||||||
from langchain_community.document_loaders import (
|
from langchain_community.document_loaders import (
|
||||||
@@ -29,17 +28,22 @@ from langchain_community.document_loaders import (
|
|||||||
UnstructuredXMLLoader,
|
UnstructuredXMLLoader,
|
||||||
UnstructuredRSTLoader,
|
UnstructuredRSTLoader,
|
||||||
UnstructuredExcelLoader,
|
UnstructuredExcelLoader,
|
||||||
|
YoutubeLoader,
|
||||||
)
|
)
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
|
|
||||||
|
import validators
|
||||||
|
import urllib.parse
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import sentence_transformers
|
||||||
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
|
|
||||||
|
|
||||||
from apps.web.models.documents import (
|
from apps.web.models.documents import (
|
||||||
Documents,
|
Documents,
|
||||||
@@ -48,12 +52,12 @@ from apps.web.models.documents import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from apps.rag.utils import (
|
from apps.rag.utils import (
|
||||||
|
get_model_path,
|
||||||
|
get_embedding_function,
|
||||||
query_doc,
|
query_doc,
|
||||||
query_embeddings_doc,
|
query_doc_with_hybrid_search,
|
||||||
query_collection,
|
query_collection,
|
||||||
query_embeddings_collection,
|
query_collection_with_hybrid_search,
|
||||||
get_embedding_model_path,
|
|
||||||
generate_openai_embeddings,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from utils.misc import (
|
from utils.misc import (
|
||||||
@@ -63,13 +67,22 @@ from utils.misc import (
|
|||||||
extract_folders_after_data_docs,
|
extract_folders_after_data_docs,
|
||||||
)
|
)
|
||||||
from utils.utils import get_current_user, get_admin_user
|
from utils.utils import get_current_user, get_admin_user
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
|
RAG_TOP_K,
|
||||||
|
RAG_RELEVANCE_THRESHOLD,
|
||||||
RAG_EMBEDDING_ENGINE,
|
RAG_EMBEDDING_ENGINE,
|
||||||
RAG_EMBEDDING_MODEL,
|
RAG_EMBEDDING_MODEL,
|
||||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
||||||
|
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||||
|
ENABLE_RAG_HYBRID_SEARCH,
|
||||||
|
RAG_RERANKING_MODEL,
|
||||||
|
PDF_EXTRACT_IMAGES,
|
||||||
|
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
||||||
|
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||||
RAG_OPENAI_API_BASE_URL,
|
RAG_OPENAI_API_BASE_URL,
|
||||||
RAG_OPENAI_API_KEY,
|
RAG_OPENAI_API_KEY,
|
||||||
DEVICE_TYPE,
|
DEVICE_TYPE,
|
||||||
@@ -77,6 +90,7 @@ from config import (
|
|||||||
CHUNK_SIZE,
|
CHUNK_SIZE,
|
||||||
CHUNK_OVERLAP,
|
CHUNK_OVERLAP,
|
||||||
RAG_TEMPLATE,
|
RAG_TEMPLATE,
|
||||||
|
ENABLE_LOCAL_WEB_FETCH,
|
||||||
)
|
)
|
||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
@@ -86,34 +100,75 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.state.TOP_K = RAG_TOP_K
|
||||||
|
app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
|
||||||
|
|
||||||
|
app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
||||||
|
|
||||||
app.state.TOP_K = 4
|
|
||||||
app.state.CHUNK_SIZE = CHUNK_SIZE
|
app.state.CHUNK_SIZE = CHUNK_SIZE
|
||||||
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||||
|
|
||||||
|
|
||||||
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||||
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||||
|
app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||||
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
||||||
|
|
||||||
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
||||||
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
||||||
|
|
||||||
app.state.PDF_EXTRACT_IMAGES = False
|
app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
||||||
|
|
||||||
|
|
||||||
app.state.sentence_transformer_ef = (
|
def update_embedding_model(
|
||||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
embedding_model: str,
|
||||||
model_name=get_embedding_model_path(
|
update_model: bool = False,
|
||||||
app.state.RAG_EMBEDDING_MODEL, RAG_EMBEDDING_MODEL_AUTO_UPDATE
|
):
|
||||||
),
|
if embedding_model and app.state.RAG_EMBEDDING_ENGINE == "":
|
||||||
device=DEVICE_TYPE,
|
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
||||||
)
|
get_model_path(embedding_model, update_model),
|
||||||
|
device=DEVICE_TYPE,
|
||||||
|
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app.state.sentence_transformer_ef = None
|
||||||
|
|
||||||
|
|
||||||
|
def update_reranking_model(
|
||||||
|
reranking_model: str,
|
||||||
|
update_model: bool = False,
|
||||||
|
):
|
||||||
|
if reranking_model:
|
||||||
|
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
||||||
|
get_model_path(reranking_model, update_model),
|
||||||
|
device=DEVICE_TYPE,
|
||||||
|
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app.state.sentence_transformer_rf = None
|
||||||
|
|
||||||
|
|
||||||
|
update_embedding_model(
|
||||||
|
app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_reranking_model(
|
||||||
|
app.state.RAG_RERANKING_MODEL,
|
||||||
|
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||||
|
app.state.RAG_EMBEDDING_ENGINE,
|
||||||
|
app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
app.state.sentence_transformer_ef,
|
||||||
|
app.state.OPENAI_API_KEY,
|
||||||
|
app.state.OPENAI_API_BASE_URL,
|
||||||
|
)
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=origins,
|
||||||
@@ -127,7 +182,7 @@ class CollectionNameForm(BaseModel):
|
|||||||
collection_name: Optional[str] = "test"
|
collection_name: Optional[str] = "test"
|
||||||
|
|
||||||
|
|
||||||
class StoreWebForm(CollectionNameForm):
|
class UrlForm(CollectionNameForm):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
@@ -140,6 +195,7 @@ async def get_status():
|
|||||||
"template": app.state.RAG_TEMPLATE,
|
"template": app.state.RAG_TEMPLATE,
|
||||||
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
"reranking_model": app.state.RAG_RERANKING_MODEL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +212,11 @@ async def get_embedding_config(user=Depends(get_admin_user)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/reranking")
|
||||||
|
async def get_reraanking_config(user=Depends(get_admin_user)):
|
||||||
|
return {"status": True, "reranking_model": app.state.RAG_RERANKING_MODEL}
|
||||||
|
|
||||||
|
|
||||||
class OpenAIConfigForm(BaseModel):
|
class OpenAIConfigForm(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
key: str
|
key: str
|
||||||
@@ -176,25 +237,22 @@ async def update_embedding_config(
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
||||||
|
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||||
|
|
||||||
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||||
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
|
||||||
app.state.sentence_transformer_ef = None
|
|
||||||
|
|
||||||
if form_data.openai_config != None:
|
if form_data.openai_config != None:
|
||||||
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
|
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||||
app.state.OPENAI_API_KEY = form_data.openai_config.key
|
app.state.OPENAI_API_KEY = form_data.openai_config.key
|
||||||
else:
|
|
||||||
sentence_transformer_ef = (
|
update_embedding_model(app.state.RAG_EMBEDDING_MODEL, True)
|
||||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
|
||||||
model_name=get_embedding_model_path(
|
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||||
form_data.embedding_model, True
|
app.state.RAG_EMBEDDING_ENGINE,
|
||||||
),
|
app.state.RAG_EMBEDDING_MODEL,
|
||||||
device=DEVICE_TYPE,
|
app.state.sentence_transformer_ef,
|
||||||
)
|
app.state.OPENAI_API_KEY,
|
||||||
)
|
app.state.OPENAI_API_BASE_URL,
|
||||||
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
)
|
||||||
app.state.sentence_transformer_ef = sentence_transformer_ef
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
@@ -205,7 +263,6 @@ async def update_embedding_config(
|
|||||||
"key": app.state.OPENAI_API_KEY,
|
"key": app.state.OPENAI_API_KEY,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Problem updating embedding model: {e}")
|
log.exception(f"Problem updating embedding model: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -214,6 +271,34 @@ async def update_embedding_config(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RerankingModelUpdateForm(BaseModel):
|
||||||
|
reranking_model: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/reranking/update")
|
||||||
|
async def update_reranking_config(
|
||||||
|
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
log.info(
|
||||||
|
f"Updating reranking model: {app.state.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
app.state.RAG_RERANKING_MODEL = form_data.reranking_model
|
||||||
|
|
||||||
|
update_reranking_model(app.state.RAG_RERANKING_MODEL, True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"reranking_model": app.state.RAG_RERANKING_MODEL,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Problem updating reranking model: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
async def get_rag_config(user=Depends(get_admin_user)):
|
async def get_rag_config(user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
@@ -266,12 +351,16 @@ async def get_query_settings(user=Depends(get_admin_user)):
|
|||||||
"status": True,
|
"status": True,
|
||||||
"template": app.state.RAG_TEMPLATE,
|
"template": app.state.RAG_TEMPLATE,
|
||||||
"k": app.state.TOP_K,
|
"k": app.state.TOP_K,
|
||||||
|
"r": app.state.RELEVANCE_THRESHOLD,
|
||||||
|
"hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class QuerySettingsForm(BaseModel):
|
class QuerySettingsForm(BaseModel):
|
||||||
k: Optional[int] = None
|
k: Optional[int] = None
|
||||||
|
r: Optional[float] = None
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
|
hybrid: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/query/settings/update")
|
@app.post("/query/settings/update")
|
||||||
@@ -280,13 +369,23 @@ async def update_query_settings(
|
|||||||
):
|
):
|
||||||
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
|
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
|
||||||
app.state.TOP_K = form_data.k if form_data.k else 4
|
app.state.TOP_K = form_data.k if form_data.k else 4
|
||||||
return {"status": True, "template": app.state.RAG_TEMPLATE}
|
app.state.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
|
||||||
|
app.state.ENABLE_RAG_HYBRID_SEARCH = form_data.hybrid if form_data.hybrid else False
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"template": app.state.RAG_TEMPLATE,
|
||||||
|
"k": app.state.TOP_K,
|
||||||
|
"r": app.state.RELEVANCE_THRESHOLD,
|
||||||
|
"hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class QueryDocForm(BaseModel):
|
class QueryDocForm(BaseModel):
|
||||||
collection_name: str
|
collection_name: str
|
||||||
query: str
|
query: str
|
||||||
k: Optional[int] = None
|
k: Optional[int] = None
|
||||||
|
r: Optional[float] = None
|
||||||
|
hybrid: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/query/doc")
|
@app.post("/query/doc")
|
||||||
@@ -294,39 +393,23 @@ def query_doc_handler(
|
|||||||
form_data: QueryDocForm,
|
form_data: QueryDocForm,
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if app.state.RAG_EMBEDDING_ENGINE == "":
|
if app.state.ENABLE_RAG_HYBRID_SEARCH:
|
||||||
|
return query_doc_with_hybrid_search(
|
||||||
|
collection_name=form_data.collection_name,
|
||||||
|
query=form_data.query,
|
||||||
|
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||||
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
|
reranking_function=app.state.sentence_transformer_rf,
|
||||||
|
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
|
||||||
|
)
|
||||||
|
else:
|
||||||
return query_doc(
|
return query_doc(
|
||||||
collection_name=form_data.collection_name,
|
collection_name=form_data.collection_name,
|
||||||
query=form_data.query,
|
query=form_data.query,
|
||||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if app.state.RAG_EMBEDDING_ENGINE == "ollama":
|
|
||||||
query_embeddings = generate_ollama_embeddings(
|
|
||||||
GenerateEmbeddingsForm(
|
|
||||||
**{
|
|
||||||
"model": app.state.RAG_EMBEDDING_MODEL,
|
|
||||||
"prompt": form_data.query,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
|
|
||||||
query_embeddings = generate_openai_embeddings(
|
|
||||||
model=app.state.RAG_EMBEDDING_MODEL,
|
|
||||||
text=form_data.query,
|
|
||||||
key=app.state.OPENAI_API_KEY,
|
|
||||||
url=app.state.OPENAI_API_BASE_URL,
|
|
||||||
)
|
|
||||||
|
|
||||||
return query_embeddings_doc(
|
|
||||||
collection_name=form_data.collection_name,
|
|
||||||
query_embeddings=query_embeddings,
|
|
||||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -339,6 +422,8 @@ class QueryCollectionsForm(BaseModel):
|
|||||||
collection_names: List[str]
|
collection_names: List[str]
|
||||||
query: str
|
query: str
|
||||||
k: Optional[int] = None
|
k: Optional[int] = None
|
||||||
|
r: Optional[float] = None
|
||||||
|
hybrid: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/query/collection")
|
@app.post("/query/collection")
|
||||||
@@ -347,35 +432,20 @@ def query_collection_handler(
|
|||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if app.state.RAG_EMBEDDING_ENGINE == "":
|
if app.state.ENABLE_RAG_HYBRID_SEARCH:
|
||||||
|
return query_collection_with_hybrid_search(
|
||||||
|
collection_names=form_data.collection_names,
|
||||||
|
query=form_data.query,
|
||||||
|
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||||
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
|
reranking_function=app.state.sentence_transformer_rf,
|
||||||
|
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
|
||||||
|
)
|
||||||
|
else:
|
||||||
return query_collection(
|
return query_collection(
|
||||||
collection_names=form_data.collection_names,
|
collection_names=form_data.collection_names,
|
||||||
query=form_data.query,
|
query=form_data.query,
|
||||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
|
|
||||||
if app.state.RAG_EMBEDDING_ENGINE == "ollama":
|
|
||||||
query_embeddings = generate_ollama_embeddings(
|
|
||||||
GenerateEmbeddingsForm(
|
|
||||||
**{
|
|
||||||
"model": app.state.RAG_EMBEDDING_MODEL,
|
|
||||||
"prompt": form_data.query,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
|
|
||||||
query_embeddings = generate_openai_embeddings(
|
|
||||||
model=app.state.RAG_EMBEDDING_MODEL,
|
|
||||||
text=form_data.query,
|
|
||||||
key=app.state.OPENAI_API_KEY,
|
|
||||||
url=app.state.OPENAI_API_BASE_URL,
|
|
||||||
)
|
|
||||||
|
|
||||||
return query_embeddings_collection(
|
|
||||||
collection_names=form_data.collection_names,
|
|
||||||
query_embeddings=query_embeddings,
|
|
||||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -387,11 +457,10 @@ def query_collection_handler(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/web")
|
@app.post("/youtube")
|
||||||
def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
|
||||||
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
|
||||||
try:
|
try:
|
||||||
loader = WebBaseLoader(form_data.url)
|
loader = YoutubeLoader.from_youtube_url(form_data.url, add_video_info=False)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
|
|
||||||
collection_name = form_data.collection_name
|
collection_name = form_data.collection_name
|
||||||
@@ -412,6 +481,62 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/web")
|
||||||
|
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
|
||||||
|
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
||||||
|
try:
|
||||||
|
loader = get_web_loader(form_data.url)
|
||||||
|
data = loader.load()
|
||||||
|
|
||||||
|
collection_name = form_data.collection_name
|
||||||
|
if collection_name == "":
|
||||||
|
collection_name = calculate_sha256_string(form_data.url)[:63]
|
||||||
|
|
||||||
|
store_data_in_vector_db(data, collection_name, overwrite=True)
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"collection_name": collection_name,
|
||||||
|
"filename": form_data.url,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_web_loader(url: str):
|
||||||
|
# Check if the URL is valid
|
||||||
|
if isinstance(validators.url(url), validators.ValidationError):
|
||||||
|
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||||
|
if not ENABLE_LOCAL_WEB_FETCH:
|
||||||
|
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
|
||||||
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
|
# Get IPv4 and IPv6 addresses
|
||||||
|
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
|
||||||
|
# Check if any of the resolved addresses are private
|
||||||
|
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
|
||||||
|
for ip in ipv4_addresses:
|
||||||
|
if validators.ipv4(ip, private=True):
|
||||||
|
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||||
|
for ip in ipv6_addresses:
|
||||||
|
if validators.ipv6(ip, private=True):
|
||||||
|
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||||
|
return WebBaseLoader(url)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_hostname(hostname):
|
||||||
|
# Get address information
|
||||||
|
addr_info = socket.getaddrinfo(hostname, None)
|
||||||
|
|
||||||
|
# Extract IP addresses from address information
|
||||||
|
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
|
||||||
|
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
|
||||||
|
|
||||||
|
return ipv4_addresses, ipv6_addresses
|
||||||
|
|
||||||
|
|
||||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter(
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
@@ -454,52 +579,27 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
|||||||
log.info(f"deleting existing collection {collection_name}")
|
log.info(f"deleting existing collection {collection_name}")
|
||||||
CHROMA_CLIENT.delete_collection(name=collection_name)
|
CHROMA_CLIENT.delete_collection(name=collection_name)
|
||||||
|
|
||||||
if app.state.RAG_EMBEDDING_ENGINE == "":
|
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
||||||
|
|
||||||
collection = CHROMA_CLIENT.create_collection(
|
embedding_func = get_embedding_function(
|
||||||
name=collection_name,
|
app.state.RAG_EMBEDDING_ENGINE,
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
app.state.RAG_EMBEDDING_MODEL,
|
||||||
)
|
app.state.sentence_transformer_ef,
|
||||||
|
app.state.OPENAI_API_KEY,
|
||||||
|
app.state.OPENAI_API_BASE_URL,
|
||||||
|
)
|
||||||
|
|
||||||
for batch in create_batches(
|
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
||||||
api=CHROMA_CLIENT,
|
embeddings = embedding_func(embedding_texts)
|
||||||
ids=[str(uuid.uuid1()) for _ in texts],
|
|
||||||
metadatas=metadatas,
|
|
||||||
documents=texts,
|
|
||||||
):
|
|
||||||
collection.add(*batch)
|
|
||||||
|
|
||||||
else:
|
for batch in create_batches(
|
||||||
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
api=CHROMA_CLIENT,
|
||||||
|
ids=[str(uuid.uuid1()) for _ in texts],
|
||||||
if app.state.RAG_EMBEDDING_ENGINE == "ollama":
|
metadatas=metadatas,
|
||||||
embeddings = [
|
embeddings=embeddings,
|
||||||
generate_ollama_embeddings(
|
documents=texts,
|
||||||
GenerateEmbeddingsForm(
|
):
|
||||||
**{"model": app.state.RAG_EMBEDDING_MODEL, "prompt": text}
|
collection.add(*batch)
|
||||||
)
|
|
||||||
)
|
|
||||||
for text in texts
|
|
||||||
]
|
|
||||||
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
|
|
||||||
embeddings = [
|
|
||||||
generate_openai_embeddings(
|
|
||||||
model=app.state.RAG_EMBEDDING_MODEL,
|
|
||||||
text=text,
|
|
||||||
key=app.state.OPENAI_API_KEY,
|
|
||||||
url=app.state.OPENAI_API_BASE_URL,
|
|
||||||
)
|
|
||||||
for text in texts
|
|
||||||
]
|
|
||||||
|
|
||||||
for batch in create_batches(
|
|
||||||
api=CHROMA_CLIENT,
|
|
||||||
ids=[str(uuid.uuid1()) for _ in texts],
|
|
||||||
metadatas=metadatas,
|
|
||||||
embeddings=embeddings,
|
|
||||||
documents=texts,
|
|
||||||
):
|
|
||||||
collection.add(*batch)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+320
-149
@@ -1,14 +1,24 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from apps.ollama.main import (
|
||||||
|
generate_ollama_embeddings,
|
||||||
|
GenerateEmbeddingsForm,
|
||||||
|
)
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download
|
||||||
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
|
|
||||||
|
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
from langchain_community.retrievers import BM25Retriever
|
||||||
|
from langchain.retrievers import (
|
||||||
|
ContextualCompressionRetriever,
|
||||||
|
EnsembleRetriever,
|
||||||
|
)
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||||
|
|
||||||
|
|
||||||
@@ -16,128 +26,164 @@ log = logging.getLogger(__name__)
|
|||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
def query_doc(collection_name: str, query: str, k: int, embedding_function):
|
def query_doc(
|
||||||
|
collection_name: str,
|
||||||
|
query: str,
|
||||||
|
embedding_function,
|
||||||
|
k: int,
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
# if you use docker use the model from the environment variable
|
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
query_embeddings = embedding_function(query)
|
||||||
name=collection_name,
|
|
||||||
embedding_function=embedding_function,
|
|
||||||
)
|
|
||||||
result = collection.query(
|
|
||||||
query_texts=[query],
|
|
||||||
n_results=k,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def query_embeddings_doc(collection_name: str, query_embeddings, k: int):
|
|
||||||
try:
|
|
||||||
# if you use docker use the model from the environment variable
|
|
||||||
log.info(f"query_embeddings_doc {query_embeddings}")
|
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
|
||||||
name=collection_name,
|
|
||||||
)
|
|
||||||
result = collection.query(
|
result = collection.query(
|
||||||
query_embeddings=[query_embeddings],
|
query_embeddings=[query_embeddings],
|
||||||
n_results=k,
|
n_results=k,
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(f"query_embeddings_doc:result {result}")
|
log.info(f"query_doc:result {result}")
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def merge_and_sort_query_results(query_results, k):
|
def query_doc_with_hybrid_search(
|
||||||
|
collection_name: str,
|
||||||
|
query: str,
|
||||||
|
embedding_function,
|
||||||
|
k: int,
|
||||||
|
reranking_function,
|
||||||
|
r: float,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||||
|
documents = collection.get() # get all documents
|
||||||
|
|
||||||
|
bm25_retriever = BM25Retriever.from_texts(
|
||||||
|
texts=documents.get("documents"),
|
||||||
|
metadatas=documents.get("metadatas"),
|
||||||
|
)
|
||||||
|
bm25_retriever.k = k
|
||||||
|
|
||||||
|
chroma_retriever = ChromaRetriever(
|
||||||
|
collection=collection,
|
||||||
|
embedding_function=embedding_function,
|
||||||
|
top_n=k,
|
||||||
|
)
|
||||||
|
|
||||||
|
ensemble_retriever = EnsembleRetriever(
|
||||||
|
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
|
||||||
|
)
|
||||||
|
|
||||||
|
compressor = RerankCompressor(
|
||||||
|
embedding_function=embedding_function,
|
||||||
|
top_n=k,
|
||||||
|
reranking_function=reranking_function,
|
||||||
|
r_score=r,
|
||||||
|
)
|
||||||
|
|
||||||
|
compression_retriever = ContextualCompressionRetriever(
|
||||||
|
base_compressor=compressor, base_retriever=ensemble_retriever
|
||||||
|
)
|
||||||
|
|
||||||
|
result = compression_retriever.invoke(query)
|
||||||
|
result = {
|
||||||
|
"distances": [[d.metadata.get("score") for d in result]],
|
||||||
|
"documents": [[d.page_content for d in result]],
|
||||||
|
"metadatas": [[d.metadata for d in result]],
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(f"query_doc_with_hybrid_search:result {result}")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def merge_and_sort_query_results(query_results, k, reverse=False):
|
||||||
# Initialize lists to store combined data
|
# Initialize lists to store combined data
|
||||||
combined_ids = []
|
|
||||||
combined_distances = []
|
combined_distances = []
|
||||||
combined_metadatas = []
|
|
||||||
combined_documents = []
|
combined_documents = []
|
||||||
|
combined_metadatas = []
|
||||||
|
|
||||||
# Combine data from each dictionary
|
|
||||||
for data in query_results:
|
for data in query_results:
|
||||||
combined_ids.extend(data["ids"][0])
|
|
||||||
combined_distances.extend(data["distances"][0])
|
combined_distances.extend(data["distances"][0])
|
||||||
combined_metadatas.extend(data["metadatas"][0])
|
|
||||||
combined_documents.extend(data["documents"][0])
|
combined_documents.extend(data["documents"][0])
|
||||||
|
combined_metadatas.extend(data["metadatas"][0])
|
||||||
|
|
||||||
# Create a list of tuples (distance, id, metadata, document)
|
# Create a list of tuples (distance, document, metadata)
|
||||||
combined = list(
|
combined = list(zip(combined_distances, combined_documents, combined_metadatas))
|
||||||
zip(combined_distances, combined_ids, combined_metadatas, combined_documents)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort the list based on distances
|
# Sort the list based on distances
|
||||||
combined.sort(key=lambda x: x[0])
|
combined.sort(key=lambda x: x[0], reverse=reverse)
|
||||||
|
|
||||||
# Unzip the sorted list
|
# We don't have anything :-(
|
||||||
sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = zip(*combined)
|
if not combined:
|
||||||
|
sorted_distances = []
|
||||||
|
sorted_documents = []
|
||||||
|
sorted_metadatas = []
|
||||||
|
else:
|
||||||
|
# Unzip the sorted list
|
||||||
|
sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
|
||||||
|
|
||||||
# Slicing the lists to include only k elements
|
# Slicing the lists to include only k elements
|
||||||
sorted_distances = list(sorted_distances)[:k]
|
sorted_distances = list(sorted_distances)[:k]
|
||||||
sorted_ids = list(sorted_ids)[:k]
|
sorted_documents = list(sorted_documents)[:k]
|
||||||
sorted_metadatas = list(sorted_metadatas)[:k]
|
sorted_metadatas = list(sorted_metadatas)[:k]
|
||||||
sorted_documents = list(sorted_documents)[:k]
|
|
||||||
|
|
||||||
# Create the output dictionary
|
# Create the output dictionary
|
||||||
merged_query_results = {
|
result = {
|
||||||
"ids": [sorted_ids],
|
|
||||||
"distances": [sorted_distances],
|
"distances": [sorted_distances],
|
||||||
"metadatas": [sorted_metadatas],
|
|
||||||
"documents": [sorted_documents],
|
"documents": [sorted_documents],
|
||||||
"embeddings": None,
|
"metadatas": [sorted_metadatas],
|
||||||
"uris": None,
|
|
||||||
"data": None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged_query_results
|
return result
|
||||||
|
|
||||||
|
|
||||||
def query_collection(
|
def query_collection(
|
||||||
collection_names: List[str], query: str, k: int, embedding_function
|
collection_names: List[str],
|
||||||
|
query: str,
|
||||||
|
embedding_function,
|
||||||
|
k: int,
|
||||||
):
|
):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for collection_name in collection_names:
|
for collection_name in collection_names:
|
||||||
try:
|
try:
|
||||||
# if you use docker use the model from the environment variable
|
result = query_doc(
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
collection_name=collection_name,
|
||||||
name=collection_name,
|
query=query,
|
||||||
|
k=k,
|
||||||
embedding_function=embedding_function,
|
embedding_function=embedding_function,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = collection.query(
|
|
||||||
query_texts=[query],
|
|
||||||
n_results=k,
|
|
||||||
)
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
return merge_and_sort_query_results(results, k=k)
|
||||||
return merge_and_sort_query_results(results, k)
|
|
||||||
|
|
||||||
|
|
||||||
def query_embeddings_collection(collection_names: List[str], query_embeddings, k: int):
|
def query_collection_with_hybrid_search(
|
||||||
|
collection_names: List[str],
|
||||||
|
query: str,
|
||||||
|
embedding_function,
|
||||||
|
k: int,
|
||||||
|
reranking_function,
|
||||||
|
r: float,
|
||||||
|
):
|
||||||
results = []
|
results = []
|
||||||
log.info(f"query_embeddings_collection {query_embeddings}")
|
|
||||||
|
|
||||||
for collection_name in collection_names:
|
for collection_name in collection_names:
|
||||||
try:
|
try:
|
||||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
result = query_doc_with_hybrid_search(
|
||||||
|
collection_name=collection_name,
|
||||||
result = collection.query(
|
query=query,
|
||||||
query_embeddings=[query_embeddings],
|
embedding_function=embedding_function,
|
||||||
n_results=k,
|
k=k,
|
||||||
|
reranking_function=reranking_function,
|
||||||
|
r=r,
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
return merge_and_sort_query_results(results, k=k, reverse=True)
|
||||||
return merge_and_sort_query_results(results, k)
|
|
||||||
|
|
||||||
|
|
||||||
def rag_template(template: str, context: str, query: str):
|
def rag_template(template: str, context: str, query: str):
|
||||||
@@ -146,20 +192,53 @@ def rag_template(template: str, context: str, query: str):
|
|||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def rag_messages(
|
def get_embedding_function(
|
||||||
docs,
|
|
||||||
messages,
|
|
||||||
template,
|
|
||||||
k,
|
|
||||||
embedding_engine,
|
embedding_engine,
|
||||||
embedding_model,
|
embedding_model,
|
||||||
embedding_function,
|
embedding_function,
|
||||||
openai_key,
|
openai_key,
|
||||||
openai_url,
|
openai_url,
|
||||||
):
|
):
|
||||||
log.debug(
|
if embedding_engine == "":
|
||||||
f"docs: {docs} {messages} {embedding_engine} {embedding_model} {embedding_function} {openai_key} {openai_url}"
|
return lambda query: embedding_function.encode(query).tolist()
|
||||||
)
|
elif embedding_engine in ["ollama", "openai"]:
|
||||||
|
if embedding_engine == "ollama":
|
||||||
|
func = lambda query: generate_ollama_embeddings(
|
||||||
|
GenerateEmbeddingsForm(
|
||||||
|
**{
|
||||||
|
"model": embedding_model,
|
||||||
|
"prompt": query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif embedding_engine == "openai":
|
||||||
|
func = lambda query: generate_openai_embeddings(
|
||||||
|
model=embedding_model,
|
||||||
|
text=query,
|
||||||
|
key=openai_key,
|
||||||
|
url=openai_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_multiple(query, f):
|
||||||
|
if isinstance(query, list):
|
||||||
|
return [f(q) for q in query]
|
||||||
|
else:
|
||||||
|
return f(query)
|
||||||
|
|
||||||
|
return lambda query: generate_multiple(query, func)
|
||||||
|
|
||||||
|
|
||||||
|
def rag_messages(
|
||||||
|
docs,
|
||||||
|
messages,
|
||||||
|
template,
|
||||||
|
embedding_function,
|
||||||
|
k,
|
||||||
|
reranking_function,
|
||||||
|
r,
|
||||||
|
hybrid_search,
|
||||||
|
):
|
||||||
|
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
|
||||||
|
|
||||||
last_user_message_idx = None
|
last_user_message_idx = None
|
||||||
for i in range(len(messages) - 1, -1, -1):
|
for i in range(len(messages) - 1, -1, -1):
|
||||||
@@ -186,75 +265,69 @@ def rag_messages(
|
|||||||
content_type = None
|
content_type = None
|
||||||
query = ""
|
query = ""
|
||||||
|
|
||||||
|
extracted_collections = []
|
||||||
relevant_contexts = []
|
relevant_contexts = []
|
||||||
|
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
try:
|
collection = doc.get("collection_name")
|
||||||
|
if collection:
|
||||||
|
collection = [collection]
|
||||||
|
else:
|
||||||
|
collection = doc.get("collection_names", [])
|
||||||
|
|
||||||
|
collection = set(collection).difference(extracted_collections)
|
||||||
|
if not collection:
|
||||||
|
log.debug(f"skipping {doc} as it has already been extracted")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
if doc["type"] == "text":
|
if doc["type"] == "text":
|
||||||
context = doc["content"]
|
context = doc["content"]
|
||||||
else:
|
else:
|
||||||
if embedding_engine == "":
|
if hybrid_search:
|
||||||
if doc["type"] == "collection":
|
context = query_collection_with_hybrid_search(
|
||||||
context = query_collection(
|
collection_names=(
|
||||||
collection_names=doc["collection_names"],
|
doc["collection_names"]
|
||||||
query=query,
|
if doc["type"] == "collection"
|
||||||
k=k,
|
else [doc["collection_name"]]
|
||||||
embedding_function=embedding_function,
|
),
|
||||||
)
|
query=query,
|
||||||
else:
|
embedding_function=embedding_function,
|
||||||
context = query_doc(
|
k=k,
|
||||||
collection_name=doc["collection_name"],
|
reranking_function=reranking_function,
|
||||||
query=query,
|
r=r,
|
||||||
k=k,
|
)
|
||||||
embedding_function=embedding_function,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if embedding_engine == "ollama":
|
context = query_collection(
|
||||||
query_embeddings = generate_ollama_embeddings(
|
collection_names=(
|
||||||
GenerateEmbeddingsForm(
|
doc["collection_names"]
|
||||||
**{
|
if doc["type"] == "collection"
|
||||||
"model": embedding_model,
|
else [doc["collection_name"]]
|
||||||
"prompt": query,
|
),
|
||||||
}
|
query=query,
|
||||||
)
|
embedding_function=embedding_function,
|
||||||
)
|
k=k,
|
||||||
elif embedding_engine == "openai":
|
)
|
||||||
query_embeddings = generate_openai_embeddings(
|
|
||||||
model=embedding_model,
|
|
||||||
text=query,
|
|
||||||
key=openai_key,
|
|
||||||
url=openai_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
if doc["type"] == "collection":
|
|
||||||
context = query_embeddings_collection(
|
|
||||||
collection_names=doc["collection_names"],
|
|
||||||
query_embeddings=query_embeddings,
|
|
||||||
k=k,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
context = query_embeddings_doc(
|
|
||||||
collection_name=doc["collection_name"],
|
|
||||||
query_embeddings=query_embeddings,
|
|
||||||
k=k,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
relevant_contexts.append(context)
|
if context:
|
||||||
|
relevant_contexts.append(context)
|
||||||
|
|
||||||
log.debug(f"relevant_contexts: {relevant_contexts}")
|
extracted_collections.extend(collection)
|
||||||
|
|
||||||
context_string = ""
|
context_string = ""
|
||||||
for context in relevant_contexts:
|
for context in relevant_contexts:
|
||||||
if context:
|
try:
|
||||||
context_string += " ".join(context["documents"][0]) + "\n"
|
if "documents" in context:
|
||||||
|
items = [item for item in context["documents"][0] if item is not None]
|
||||||
|
context_string += "\n\n".join(items)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
context_string = context_string.strip()
|
||||||
|
|
||||||
ra_content = rag_template(
|
ra_content = rag_template(
|
||||||
template=template,
|
template=template,
|
||||||
@@ -262,6 +335,8 @@ def rag_messages(
|
|||||||
query=query,
|
query=query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.debug(f"ra_content: {ra_content}")
|
||||||
|
|
||||||
if content_type == "list":
|
if content_type == "list":
|
||||||
new_content = []
|
new_content = []
|
||||||
for content_item in user_message["content"]:
|
for content_item in user_message["content"]:
|
||||||
@@ -283,44 +358,42 @@ def rag_messages(
|
|||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
def get_embedding_model_path(
|
def get_model_path(model: str, update_model: bool = False):
|
||||||
embedding_model: str, update_embedding_model: bool = False
|
|
||||||
):
|
|
||||||
# Construct huggingface_hub kwargs with local_files_only to return the snapshot path
|
# Construct huggingface_hub kwargs with local_files_only to return the snapshot path
|
||||||
cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME")
|
cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME")
|
||||||
|
|
||||||
local_files_only = not update_embedding_model
|
local_files_only = not update_model
|
||||||
|
|
||||||
snapshot_kwargs = {
|
snapshot_kwargs = {
|
||||||
"cache_dir": cache_dir,
|
"cache_dir": cache_dir,
|
||||||
"local_files_only": local_files_only,
|
"local_files_only": local_files_only,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(f"embedding_model: {embedding_model}")
|
log.debug(f"model: {model}")
|
||||||
log.debug(f"snapshot_kwargs: {snapshot_kwargs}")
|
log.debug(f"snapshot_kwargs: {snapshot_kwargs}")
|
||||||
|
|
||||||
# Inspiration from upstream sentence_transformers
|
# Inspiration from upstream sentence_transformers
|
||||||
if (
|
if (
|
||||||
os.path.exists(embedding_model)
|
os.path.exists(model)
|
||||||
or ("\\" in embedding_model or embedding_model.count("/") > 1)
|
or ("\\" in model or model.count("/") > 1)
|
||||||
and local_files_only
|
and local_files_only
|
||||||
):
|
):
|
||||||
# If fully qualified path exists, return input, else set repo_id
|
# If fully qualified path exists, return input, else set repo_id
|
||||||
return embedding_model
|
return model
|
||||||
elif "/" not in embedding_model:
|
elif "/" not in model:
|
||||||
# Set valid repo_id for model short-name
|
# Set valid repo_id for model short-name
|
||||||
embedding_model = "sentence-transformers" + "/" + embedding_model
|
model = "sentence-transformers" + "/" + model
|
||||||
|
|
||||||
snapshot_kwargs["repo_id"] = embedding_model
|
snapshot_kwargs["repo_id"] = model
|
||||||
|
|
||||||
# Attempt to query the huggingface_hub library to determine the local path and/or to update
|
# Attempt to query the huggingface_hub library to determine the local path and/or to update
|
||||||
try:
|
try:
|
||||||
embedding_model_repo_path = snapshot_download(**snapshot_kwargs)
|
model_repo_path = snapshot_download(**snapshot_kwargs)
|
||||||
log.debug(f"embedding_model_repo_path: {embedding_model_repo_path}")
|
log.debug(f"model_repo_path: {model_repo_path}")
|
||||||
return embedding_model_repo_path
|
return model_repo_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Cannot determine embedding model snapshot path: {e}")
|
log.exception(f"Cannot determine model snapshot path: {e}")
|
||||||
return embedding_model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def generate_openai_embeddings(
|
def generate_openai_embeddings(
|
||||||
@@ -344,3 +417,101 @@ def generate_openai_embeddings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from langchain_core.retrievers import BaseRetriever
|
||||||
|
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
||||||
|
|
||||||
|
|
||||||
|
class ChromaRetriever(BaseRetriever):
|
||||||
|
collection: Any
|
||||||
|
embedding_function: Any
|
||||||
|
top_n: int
|
||||||
|
|
||||||
|
def _get_relevant_documents(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
run_manager: CallbackManagerForRetrieverRun,
|
||||||
|
) -> List[Document]:
|
||||||
|
query_embeddings = self.embedding_function(query)
|
||||||
|
|
||||||
|
results = self.collection.query(
|
||||||
|
query_embeddings=[query_embeddings],
|
||||||
|
n_results=self.top_n,
|
||||||
|
)
|
||||||
|
|
||||||
|
ids = results["ids"][0]
|
||||||
|
metadatas = results["metadatas"][0]
|
||||||
|
documents = results["documents"][0]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for idx in range(len(ids)):
|
||||||
|
results.append(
|
||||||
|
Document(
|
||||||
|
metadata=metadatas[idx],
|
||||||
|
page_content=documents[idx],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
|
from langchain_core.documents import BaseDocumentCompressor, Document
|
||||||
|
from langchain_core.callbacks import Callbacks
|
||||||
|
from langchain_core.pydantic_v1 import Extra
|
||||||
|
|
||||||
|
from sentence_transformers import util
|
||||||
|
|
||||||
|
|
||||||
|
class RerankCompressor(BaseDocumentCompressor):
|
||||||
|
embedding_function: Any
|
||||||
|
top_n: int
|
||||||
|
reranking_function: Any
|
||||||
|
r_score: float
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = Extra.forbid
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
def compress_documents(
|
||||||
|
self,
|
||||||
|
documents: Sequence[Document],
|
||||||
|
query: str,
|
||||||
|
callbacks: Optional[Callbacks] = None,
|
||||||
|
) -> Sequence[Document]:
|
||||||
|
reranking = self.reranking_function is not None
|
||||||
|
|
||||||
|
if reranking:
|
||||||
|
scores = self.reranking_function.predict(
|
||||||
|
[(query, doc.page_content) for doc in documents]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query_embedding = self.embedding_function(query)
|
||||||
|
document_embedding = self.embedding_function(
|
||||||
|
[doc.page_content for doc in documents]
|
||||||
|
)
|
||||||
|
scores = util.cos_sim(query_embedding, document_embedding)[0]
|
||||||
|
|
||||||
|
docs_with_scores = list(zip(documents, scores.tolist()))
|
||||||
|
if self.r_score:
|
||||||
|
docs_with_scores = [
|
||||||
|
(d, s) for d, s in docs_with_scores if s >= self.r_score
|
||||||
|
]
|
||||||
|
|
||||||
|
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True)
|
||||||
|
final_results = []
|
||||||
|
for doc, doc_score in result[: self.top_n]:
|
||||||
|
metadata = doc.metadata
|
||||||
|
metadata["score"] = doc_score
|
||||||
|
doc = Document(
|
||||||
|
page_content=doc.page_content,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
final_results.append(doc)
|
||||||
|
return final_results
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from peewee import *
|
from peewee import *
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
from config import SRC_LOG_LEVELS, DATA_DIR
|
from playhouse.db_url import connect
|
||||||
|
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ log.setLevel(SRC_LOG_LEVELS["DB"])
|
|||||||
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
||||||
# Rename the file
|
# Rename the file
|
||||||
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
||||||
log.info("File renamed successfully.")
|
log.info("Database migrated from Ollama-WebUI successfully.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
DB = connect(DATABASE_URL)
|
||||||
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
log.info(f"Connected to a {DB.__class__.__name__} database.")
|
||||||
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
|
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
|
||||||
router.run()
|
router.run()
|
||||||
DB.connect(reuse_if_open=True)
|
DB.connect(reuse_if_open=True)
|
||||||
@@ -37,6 +37,18 @@ with suppress(ImportError):
|
|||||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
"""Write your migrations here."""
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
# We perform different migrations for SQLite and other databases
|
||||||
|
# This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
|
||||||
|
# will require per-database SQL queries.
|
||||||
|
# Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
|
||||||
|
# schema instead of trying to migrate from an older schema.
|
||||||
|
if isinstance(database, pw.SqliteDatabase):
|
||||||
|
migrate_sqlite(migrator, database, fake=fake)
|
||||||
|
else:
|
||||||
|
migrate_external(migrator, database, fake=fake)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
@migrator.create_model
|
@migrator.create_model
|
||||||
class Auth(pw.Model):
|
class Auth(pw.Model):
|
||||||
id = pw.CharField(max_length=255, unique=True)
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
@@ -53,7 +65,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
user_id = pw.CharField(max_length=255)
|
user_id = pw.CharField(max_length=255)
|
||||||
title = pw.CharField()
|
title = pw.CharField()
|
||||||
chat = pw.TextField()
|
chat = pw.TextField()
|
||||||
timestamp = pw.DateField()
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "chat"
|
table_name = "chat"
|
||||||
@@ -64,7 +76,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
tag_name = pw.CharField(max_length=255)
|
tag_name = pw.CharField(max_length=255)
|
||||||
chat_id = pw.CharField(max_length=255)
|
chat_id = pw.CharField(max_length=255)
|
||||||
user_id = pw.CharField(max_length=255)
|
user_id = pw.CharField(max_length=255)
|
||||||
timestamp = pw.DateField()
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "chatidtag"
|
table_name = "chatidtag"
|
||||||
@@ -78,7 +90,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
filename = pw.CharField()
|
filename = pw.CharField()
|
||||||
content = pw.TextField(null=True)
|
content = pw.TextField(null=True)
|
||||||
user_id = pw.CharField(max_length=255)
|
user_id = pw.CharField(max_length=255)
|
||||||
timestamp = pw.DateField()
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "document"
|
table_name = "document"
|
||||||
@@ -89,7 +101,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
tag_name = pw.CharField(max_length=255, unique=True)
|
tag_name = pw.CharField(max_length=255, unique=True)
|
||||||
user_id = pw.CharField(max_length=255)
|
user_id = pw.CharField(max_length=255)
|
||||||
modelfile = pw.TextField()
|
modelfile = pw.TextField()
|
||||||
timestamp = pw.DateField()
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "modelfile"
|
table_name = "modelfile"
|
||||||
@@ -101,7 +113,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
user_id = pw.CharField(max_length=255)
|
user_id = pw.CharField(max_length=255)
|
||||||
title = pw.CharField()
|
title = pw.CharField()
|
||||||
content = pw.TextField()
|
content = pw.TextField()
|
||||||
timestamp = pw.DateField()
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "prompt"
|
table_name = "prompt"
|
||||||
@@ -123,7 +135,100 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
email = pw.CharField(max_length=255)
|
email = pw.CharField(max_length=255)
|
||||||
role = pw.CharField(max_length=255)
|
role = pw.CharField(max_length=255)
|
||||||
profile_image_url = pw.CharField(max_length=255)
|
profile_image_url = pw.CharField(max_length=255)
|
||||||
timestamp = pw.DateField()
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "user"
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
@migrator.create_model
|
||||||
|
class Auth(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
email = pw.CharField(max_length=255)
|
||||||
|
password = pw.TextField()
|
||||||
|
active = pw.BooleanField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "auth"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Chat(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
title = pw.TextField()
|
||||||
|
chat = pw.TextField()
|
||||||
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "chat"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class ChatIdTag(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
tag_name = pw.CharField(max_length=255)
|
||||||
|
chat_id = pw.CharField(max_length=255)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "chatidtag"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Document(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
collection_name = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255, unique=True)
|
||||||
|
title = pw.TextField()
|
||||||
|
filename = pw.TextField()
|
||||||
|
content = pw.TextField(null=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "document"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Modelfile(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
tag_name = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
modelfile = pw.TextField()
|
||||||
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "modelfile"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Prompt(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
command = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
title = pw.TextField()
|
||||||
|
content = pw.TextField()
|
||||||
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "prompt"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Tag(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
data = pw.TextField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "tag"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class User(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255)
|
||||||
|
email = pw.CharField(max_length=255)
|
||||||
|
role = pw.CharField(max_length=255)
|
||||||
|
profile_image_url = pw.TextField()
|
||||||
|
timestamp = pw.BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "user"
|
table_name = "user"
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ with suppress(ImportError):
|
|||||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
"""Write your migrations here."""
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
if isinstance(database, pw.SqliteDatabase):
|
||||||
|
migrate_sqlite(migrator, database, fake=fake)
|
||||||
|
else:
|
||||||
|
migrate_external(migrator, database, fake=fake)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
# Adding fields created_at and updated_at to the 'chat' table
|
# Adding fields created_at and updated_at to the 'chat' table
|
||||||
migrator.add_fields(
|
migrator.add_fields(
|
||||||
"chat",
|
"chat",
|
||||||
@@ -60,9 +67,40 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
# Adding fields created_at and updated_at to the 'chat' table
|
||||||
|
migrator.add_fields(
|
||||||
|
"chat",
|
||||||
|
created_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||||
|
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate the new fields from an existing 'timestamp' field
|
||||||
|
migrator.sql(
|
||||||
|
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now that the data has been copied, remove the original 'timestamp' field
|
||||||
|
migrator.remove_fields("chat", "timestamp")
|
||||||
|
|
||||||
|
# Update the fields to be not null now that they are populated
|
||||||
|
migrator.change_fields(
|
||||||
|
"chat",
|
||||||
|
created_at=pw.BigIntegerField(null=False),
|
||||||
|
updated_at=pw.BigIntegerField(null=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
"""Write your rollback migrations here."""
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
if isinstance(database, pw.SqliteDatabase):
|
||||||
|
rollback_sqlite(migrator, database, fake=fake)
|
||||||
|
else:
|
||||||
|
rollback_external(migrator, database, fake=fake)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
# Recreate the timestamp field initially allowing null values for safe transition
|
# Recreate the timestamp field initially allowing null values for safe transition
|
||||||
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
|
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
|
||||||
|
|
||||||
@@ -75,3 +113,18 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
|||||||
|
|
||||||
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||||
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
|
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
|
||||||
|
|
||||||
|
|
||||||
|
def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
# Recreate the timestamp field initially allowing null values for safe transition
|
||||||
|
migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
|
||||||
|
|
||||||
|
# Copy the earliest created_at date back into the new timestamp field
|
||||||
|
# This assumes created_at was originally a copy of timestamp
|
||||||
|
migrator.sql("UPDATE chat SET timestamp = created_at")
|
||||||
|
|
||||||
|
# Remove the created_at and updated_at fields
|
||||||
|
migrator.remove_fields("chat", "created_at", "updated_at")
|
||||||
|
|
||||||
|
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||||
|
migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
# Alter the tables with timestamps
|
||||||
|
migrator.change_fields(
|
||||||
|
"chatidtag",
|
||||||
|
timestamp=pw.BigIntegerField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"document",
|
||||||
|
timestamp=pw.BigIntegerField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"modelfile",
|
||||||
|
timestamp=pw.BigIntegerField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"prompt",
|
||||||
|
timestamp=pw.BigIntegerField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"user",
|
||||||
|
timestamp=pw.BigIntegerField(),
|
||||||
|
)
|
||||||
|
# Alter the tables with varchar to text where necessary
|
||||||
|
migrator.change_fields(
|
||||||
|
"auth",
|
||||||
|
password=pw.TextField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"chat",
|
||||||
|
title=pw.TextField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"document",
|
||||||
|
title=pw.TextField(),
|
||||||
|
filename=pw.TextField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"prompt",
|
||||||
|
title=pw.TextField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"user",
|
||||||
|
profile_image_url=pw.TextField(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
if isinstance(database, pw.SqliteDatabase):
|
||||||
|
# Alter the tables with timestamps
|
||||||
|
migrator.change_fields(
|
||||||
|
"chatidtag",
|
||||||
|
timestamp=pw.DateField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"document",
|
||||||
|
timestamp=pw.DateField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"modelfile",
|
||||||
|
timestamp=pw.DateField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"prompt",
|
||||||
|
timestamp=pw.DateField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"user",
|
||||||
|
timestamp=pw.DateField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"auth",
|
||||||
|
password=pw.CharField(max_length=255),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"chat",
|
||||||
|
title=pw.CharField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"document",
|
||||||
|
title=pw.CharField(),
|
||||||
|
filename=pw.CharField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"prompt",
|
||||||
|
title=pw.CharField(),
|
||||||
|
)
|
||||||
|
migrator.change_fields(
|
||||||
|
"user",
|
||||||
|
profile_image_url=pw.CharField(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
# Adding fields created_at and updated_at to the 'user' table
|
||||||
|
migrator.add_fields(
|
||||||
|
"user",
|
||||||
|
created_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||||
|
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||||
|
last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate the new fields from an existing 'timestamp' field
|
||||||
|
migrator.sql(
|
||||||
|
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now that the data has been copied, remove the original 'timestamp' field
|
||||||
|
migrator.remove_fields("user", "timestamp")
|
||||||
|
|
||||||
|
# Update the fields to be not null now that they are populated
|
||||||
|
migrator.change_fields(
|
||||||
|
"user",
|
||||||
|
created_at=pw.BigIntegerField(null=False),
|
||||||
|
updated_at=pw.BigIntegerField(null=False),
|
||||||
|
last_active_at=pw.BigIntegerField(null=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
# Recreate the timestamp field initially allowing null values for safe transition
|
||||||
|
migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
|
||||||
|
|
||||||
|
# Copy the earliest created_at date back into the new timestamp field
|
||||||
|
# This assumes created_at was originally a copy of timestamp
|
||||||
|
migrator.sql('UPDATE "user" SET timestamp = created_at')
|
||||||
|
|
||||||
|
# Remove the created_at and updated_at fields
|
||||||
|
migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
|
||||||
|
|
||||||
|
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||||
|
migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))
|
||||||
@@ -23,7 +23,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
class Auth(Model):
|
class Auth(Model):
|
||||||
id = CharField(unique=True)
|
id = CharField(unique=True)
|
||||||
email = CharField()
|
email = CharField()
|
||||||
password = CharField()
|
password = TextField()
|
||||||
active = BooleanField()
|
active = BooleanField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -89,6 +89,10 @@ class SignupForm(BaseModel):
|
|||||||
profile_image_url: Optional[str] = "/user.png"
|
profile_image_url: Optional[str] = "/user.png"
|
||||||
|
|
||||||
|
|
||||||
|
class AddUserForm(SignupForm):
|
||||||
|
role: Optional[str] = "pending"
|
||||||
|
|
||||||
|
|
||||||
class AuthsTable:
|
class AuthsTable:
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ from apps.web.internal.db import DB
|
|||||||
class Chat(Model):
|
class Chat(Model):
|
||||||
id = CharField(unique=True)
|
id = CharField(unique=True)
|
||||||
user_id = CharField()
|
user_id = CharField()
|
||||||
title = CharField()
|
title = TextField()
|
||||||
chat = TextField() # Save Chat JSON as Text
|
chat = TextField() # Save Chat JSON as Text
|
||||||
|
|
||||||
created_at = DateTimeField()
|
created_at = BigIntegerField()
|
||||||
updated_at = DateTimeField()
|
updated_at = BigIntegerField()
|
||||||
|
|
||||||
share_id = CharField(null=True, unique=True)
|
share_id = CharField(null=True, unique=True)
|
||||||
archived = BooleanField(default=False)
|
archived = BooleanField(default=False)
|
||||||
@@ -191,7 +191,7 @@ class ChatTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_archived_chat_lists_by_user_id(
|
def get_archived_chat_list_by_user_id(
|
||||||
self, user_id: str, skip: int = 0, limit: int = 50
|
self, user_id: str, skip: int = 0, limit: int = 50
|
||||||
) -> List[ChatModel]:
|
) -> List[ChatModel]:
|
||||||
return [
|
return [
|
||||||
@@ -204,7 +204,7 @@ class ChatTable:
|
|||||||
# .offset(skip)
|
# .offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_chat_lists_by_user_id(
|
def get_chat_list_by_user_id(
|
||||||
self, user_id: str, skip: int = 0, limit: int = 50
|
self, user_id: str, skip: int = 0, limit: int = 50
|
||||||
) -> List[ChatModel]:
|
) -> List[ChatModel]:
|
||||||
return [
|
return [
|
||||||
@@ -217,7 +217,7 @@ class ChatTable:
|
|||||||
# .offset(skip)
|
# .offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_chat_lists_by_chat_ids(
|
def get_chat_list_by_chat_ids(
|
||||||
self, chat_ids: List[str], skip: int = 0, limit: int = 50
|
self, chat_ids: List[str], skip: int = 0, limit: int = 50
|
||||||
) -> List[ChatModel]:
|
) -> List[ChatModel]:
|
||||||
return [
|
return [
|
||||||
@@ -228,20 +228,6 @@ class ChatTable:
|
|||||||
.order_by(Chat.updated_at.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_all_chats(self) -> List[ChatModel]:
|
|
||||||
return [
|
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select().order_by(Chat.updated_at.desc())
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
|
||||||
return [
|
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select()
|
|
||||||
.where(Chat.user_id == user_id)
|
|
||||||
.order_by(Chat.updated_at.desc())
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = Chat.get(Chat.id == id)
|
chat = Chat.get(Chat.id == id)
|
||||||
@@ -271,9 +257,28 @@ class ChatTable:
|
|||||||
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
|
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
|
||||||
return [
|
return [
|
||||||
ChatModel(**model_to_dict(chat))
|
ChatModel(**model_to_dict(chat))
|
||||||
for chat in Chat.select().limit(limit).offset(skip)
|
for chat in Chat.select().order_by(Chat.updated_at.desc())
|
||||||
|
# .limit(limit).offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||||
|
return [
|
||||||
|
ChatModel(**model_to_dict(chat))
|
||||||
|
for chat in Chat.select()
|
||||||
|
.where(Chat.user_id == user_id)
|
||||||
|
.order_by(Chat.updated_at.desc())
|
||||||
|
# .limit(limit).offset(skip)
|
||||||
|
]
|
||||||
|
|
||||||
|
def delete_chat_by_id(self, id: str) -> bool:
|
||||||
|
try:
|
||||||
|
query = Chat.delete().where((Chat.id == id))
|
||||||
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
class Document(Model):
|
class Document(Model):
|
||||||
collection_name = CharField(unique=True)
|
collection_name = CharField(unique=True)
|
||||||
name = CharField(unique=True)
|
name = CharField(unique=True)
|
||||||
title = CharField()
|
title = TextField()
|
||||||
filename = CharField()
|
filename = TextField()
|
||||||
content = TextField(null=True)
|
content = TextField(null=True)
|
||||||
user_id = CharField()
|
user_id = CharField()
|
||||||
timestamp = DateField()
|
timestamp = BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Modelfile(Model):
|
|||||||
tag_name = CharField(unique=True)
|
tag_name = CharField(unique=True)
|
||||||
user_id = CharField()
|
user_id = CharField()
|
||||||
modelfile = TextField()
|
modelfile = TextField()
|
||||||
timestamp = DateField()
|
timestamp = BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import json
|
|||||||
class Prompt(Model):
|
class Prompt(Model):
|
||||||
command = CharField(unique=True)
|
command = CharField(unique=True)
|
||||||
user_id = CharField()
|
user_id = CharField()
|
||||||
title = CharField()
|
title = TextField()
|
||||||
content = TextField()
|
content = TextField()
|
||||||
timestamp = DateField()
|
timestamp = BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ChatIdTag(Model):
|
|||||||
tag_name = CharField()
|
tag_name = CharField()
|
||||||
chat_id = CharField()
|
chat_id = CharField()
|
||||||
user_id = CharField()
|
user_id = CharField()
|
||||||
timestamp = DateField()
|
timestamp = BigIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
@@ -136,7 +136,9 @@ class TagTable:
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
TagModel(**model_to_dict(tag))
|
TagModel(**model_to_dict(tag))
|
||||||
for tag in Tag.select().where(Tag.name.in_(tag_names))
|
for tag in Tag.select()
|
||||||
|
.where(Tag.user_id == user_id)
|
||||||
|
.where(Tag.name.in_(tag_names))
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_tags_by_chat_id_and_user_id(
|
def get_tags_by_chat_id_and_user_id(
|
||||||
@@ -151,7 +153,9 @@ class TagTable:
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
TagModel(**model_to_dict(tag))
|
TagModel(**model_to_dict(tag))
|
||||||
for tag in Tag.select().where(Tag.name.in_(tag_names))
|
for tag in Tag.select()
|
||||||
|
.where(Tag.user_id == user_id)
|
||||||
|
.where(Tag.name.in_(tag_names))
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_chat_ids_by_tag_name_and_user_id(
|
def get_chat_ids_by_tag_name_and_user_id(
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ class User(Model):
|
|||||||
name = CharField()
|
name = CharField()
|
||||||
email = CharField()
|
email = CharField()
|
||||||
role = CharField()
|
role = CharField()
|
||||||
profile_image_url = CharField()
|
profile_image_url = TextField()
|
||||||
timestamp = DateField()
|
|
||||||
|
last_active_at = BigIntegerField()
|
||||||
|
updated_at = BigIntegerField()
|
||||||
|
created_at = BigIntegerField()
|
||||||
|
|
||||||
api_key = CharField(null=True, unique=True)
|
api_key = CharField(null=True, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -32,7 +36,11 @@ class UserModel(BaseModel):
|
|||||||
email: str
|
email: str
|
||||||
role: str = "pending"
|
role: str = "pending"
|
||||||
profile_image_url: str
|
profile_image_url: str
|
||||||
timestamp: int # timestamp in epoch
|
|
||||||
|
last_active_at: int # timestamp in epoch
|
||||||
|
updated_at: int # timestamp in epoch
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
api_key: Optional[str] = None
|
api_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +81,9 @@ class UsersTable:
|
|||||||
"email": email,
|
"email": email,
|
||||||
"role": role,
|
"role": role,
|
||||||
"profile_image_url": profile_image_url,
|
"profile_image_url": profile_image_url,
|
||||||
"timestamp": int(time.time()),
|
"last_active_at": int(time.time()),
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"updated_at": int(time.time()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result = User.create(**user.model_dump())
|
result = User.create(**user.model_dump())
|
||||||
@@ -113,6 +123,13 @@ class UsersTable:
|
|||||||
def get_num_users(self) -> Optional[int]:
|
def get_num_users(self) -> Optional[int]:
|
||||||
return User.select().count()
|
return User.select().count()
|
||||||
|
|
||||||
|
def get_first_user(self) -> UserModel:
|
||||||
|
try:
|
||||||
|
user = User.select().order_by(User.created_at).first()
|
||||||
|
return UserModel(**model_to_dict(user))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
|
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
query = User.update(role=role).where(User.id == id)
|
query = User.update(role=role).where(User.id == id)
|
||||||
@@ -137,6 +154,16 @@ class UsersTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
|
||||||
|
try:
|
||||||
|
query = User.update(last_active_at=int(time.time())).where(User.id == id)
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
user = User.get(User.id == id)
|
||||||
|
return UserModel(**model_to_dict(user))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
|
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
query = User.update(**updated).where(User.id == id)
|
query = User.update(**updated).where(User.id == id)
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
from fastapi import Request
|
import logging
|
||||||
|
|
||||||
|
from fastapi import Request, UploadFile, File
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
from apps.web.models.auths import (
|
from apps.web.models.auths import (
|
||||||
SigninForm,
|
SigninForm,
|
||||||
SignupForm,
|
SignupForm,
|
||||||
|
AddUserForm,
|
||||||
UpdateProfileForm,
|
UpdateProfileForm,
|
||||||
UpdatePasswordForm,
|
UpdatePasswordForm,
|
||||||
UserResponse,
|
UserResponse,
|
||||||
@@ -203,6 +208,51 @@ async def signup(request: Request, form_data: SignupForm):
|
|||||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# AddUser
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add", response_model=SigninResponse)
|
||||||
|
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
if not validate_email_format(form_data.email.lower()):
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||||
|
)
|
||||||
|
|
||||||
|
if Users.get_user_by_email(form_data.email.lower()):
|
||||||
|
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
print(form_data)
|
||||||
|
hashed = get_password_hash(form_data.password)
|
||||||
|
user = Auths.insert_new_auth(
|
||||||
|
form_data.email.lower(),
|
||||||
|
hashed,
|
||||||
|
form_data.name,
|
||||||
|
form_data.profile_image_url,
|
||||||
|
form_data.role,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
token = create_token(data={"id": user.id})
|
||||||
|
return {
|
||||||
|
"token": token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"id": user.id,
|
||||||
|
"email": user.email,
|
||||||
|
"name": user.name,
|
||||||
|
"role": user.role,
|
||||||
|
"profile_image_url": user.profile_image_url,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||||
|
except Exception as err:
|
||||||
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# ToggleSignUp
|
# ToggleSignUp
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from apps.web.models.tags import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
@@ -36,15 +36,49 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetChats
|
# GetChatList
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[ChatTitleIdResponse])
|
@router.get("/", response_model=List[ChatTitleIdResponse])
|
||||||
async def get_user_chats(
|
@router.get("/list", response_model=List[ChatTitleIdResponse])
|
||||||
|
async def get_session_user_chat_list(
|
||||||
user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
||||||
):
|
):
|
||||||
return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
|
return Chats.get_chat_list_by_user_id(user.id, skip, limit)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# DeleteAllChats
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/", response_model=bool)
|
||||||
|
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.role == "user"
|
||||||
|
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = Chats.delete_chats_by_user_id(user.id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetUserChatList
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
):
|
||||||
|
return Chats.get_chat_list_by_user_id(user_id, skip, limit)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -53,22 +87,22 @@ async def get_user_chats(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/archived", response_model=List[ChatTitleIdResponse])
|
@router.get("/archived", response_model=List[ChatTitleIdResponse])
|
||||||
async def get_archived_user_chats(
|
async def get_archived_session_user_chat_list(
|
||||||
user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
||||||
):
|
):
|
||||||
return Chats.get_archived_chat_lists_by_user_id(user.id, skip, limit)
|
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetAllChats
|
# GetChats
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all", response_model=List[ChatResponse])
|
@router.get("/all", response_model=List[ChatResponse])
|
||||||
async def get_all_user_chats(user=Depends(get_current_user)):
|
async def get_user_chats(user=Depends(get_current_user)):
|
||||||
return [
|
return [
|
||||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
for chat in Chats.get_all_chats_by_user_id(user.id)
|
for chat in Chats.get_chats_by_user_id(user.id)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -79,9 +113,14 @@ async def get_all_user_chats(user=Depends(get_current_user)):
|
|||||||
|
|
||||||
@router.get("/all/db", response_model=List[ChatResponse])
|
@router.get("/all/db", response_model=List[ChatResponse])
|
||||||
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||||
|
if not ENABLE_ADMIN_EXPORT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
for chat in Chats.get_all_chats()
|
for chat in Chats.get_chats()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -102,45 +141,6 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
|
||||||
# GetAllTags
|
|
||||||
############################
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tags/all", response_model=List[TagModel])
|
|
||||||
async def get_all_tags(user=Depends(get_current_user)):
|
|
||||||
try:
|
|
||||||
tags = Tags.get_tags_by_user_id(user.id)
|
|
||||||
return tags
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
############################
|
|
||||||
# GetChatsByTags
|
|
||||||
############################
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
|
|
||||||
async def get_user_chats_by_tag_name(
|
|
||||||
tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
|
||||||
):
|
|
||||||
chat_ids = [
|
|
||||||
chat_id_tag.chat_id
|
|
||||||
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
|
|
||||||
]
|
|
||||||
|
|
||||||
chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
|
|
||||||
|
|
||||||
if len(chats) == 0:
|
|
||||||
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
|
|
||||||
|
|
||||||
return chats
|
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetChatById
|
# GetChatById
|
||||||
############################
|
############################
|
||||||
@@ -188,17 +188,18 @@ async def update_chat_by_id(
|
|||||||
@router.delete("/{id}", response_model=bool)
|
@router.delete("/{id}", response_model=bool)
|
||||||
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
|
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
|
||||||
|
|
||||||
if (
|
if user.role == "admin":
|
||||||
user.role == "user"
|
result = Chats.delete_chat_by_id(id)
|
||||||
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
|
return result
|
||||||
):
|
else:
|
||||||
raise HTTPException(
|
if not request.app.state.USER_PERMISSIONS["chat"]["deletion"]:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
raise HTTPException(
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
)
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
|
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -298,6 +299,45 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetAllTags
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tags/all", response_model=List[TagModel])
|
||||||
|
async def get_all_tags(user=Depends(get_current_user)):
|
||||||
|
try:
|
||||||
|
tags = Tags.get_tags_by_user_id(user.id)
|
||||||
|
return tags
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetChatsByTags
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
|
||||||
|
async def get_user_chat_list_by_tag_name(
|
||||||
|
tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
||||||
|
):
|
||||||
|
chat_ids = [
|
||||||
|
chat_id_tag.chat_id
|
||||||
|
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
chats = Chats.get_chat_list_by_chat_ids(chat_ids, skip, limit)
|
||||||
|
|
||||||
|
if len(chats) == 0:
|
||||||
|
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
|
||||||
|
|
||||||
|
return chats
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetChatTagsById
|
# GetChatTagsById
|
||||||
############################
|
############################
|
||||||
@@ -378,24 +418,3 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
|
||||||
# DeleteAllChats
|
|
||||||
############################
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/", response_model=bool)
|
|
||||||
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
|
|
||||||
|
|
||||||
if (
|
|
||||||
user.role == "user"
|
|
||||||
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
|
|
||||||
):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = Chats.delete_chats_by_user_id(user.id)
|
|
||||||
return result
|
|
||||||
@@ -58,7 +58,7 @@ async def update_user_permissions(
|
|||||||
@router.post("/update/role", response_model=Optional[UserModel])
|
@router.post("/update/role", response_model=Optional[UserModel])
|
||||||
async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)):
|
async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
if user.id != form_data.id:
|
if user.id != form_data.id and form_data.id != Users.get_first_user().id:
|
||||||
return Users.update_user_role_by_id(form_data.id, form_data.role)
|
return Users.update_user_role_by_id(form_data.id, form_data.role)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from fastapi import APIRouter, UploadFile, File, Response
|
from fastapi import APIRouter, UploadFile, File, Response
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from peewee import SqliteDatabase
|
||||||
from starlette.responses import StreamingResponse, FileResponse
|
from starlette.responses import StreamingResponse, FileResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -7,11 +8,11 @@ from pydantic import BaseModel
|
|||||||
from fpdf import FPDF
|
from fpdf import FPDF
|
||||||
import markdown
|
import markdown
|
||||||
|
|
||||||
|
from apps.web.internal.db import DB
|
||||||
from utils.utils import get_admin_user
|
from utils.utils import get_admin_user
|
||||||
from utils.misc import calculate_sha256, get_gravatar_url
|
from utils.misc import calculate_sha256, get_gravatar_url
|
||||||
|
|
||||||
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
|
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -91,9 +92,18 @@ async def download_chat_as_pdf(
|
|||||||
|
|
||||||
@router.get("/db/download")
|
@router.get("/db/download")
|
||||||
async def download_db(user=Depends(get_admin_user)):
|
async def download_db(user=Depends(get_admin_user)):
|
||||||
|
if not ENABLE_ADMIN_EXPORT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
if not isinstance(DB, SqliteDatabase):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
|
||||||
|
)
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
f"{DATA_DIR}/webui.db",
|
DB.database,
|
||||||
media_type="application/octet-stream",
|
media_type="application/octet-stream",
|
||||||
filename="webui.db",
|
filename="webui.db",
|
||||||
)
|
)
|
||||||
+129
-21
@@ -71,6 +71,9 @@ except ImportError:
|
|||||||
log.warning("dotenv not installed, skipping...")
|
log.warning("dotenv not installed, skipping...")
|
||||||
|
|
||||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||||
|
if WEBUI_NAME != "Open WebUI":
|
||||||
|
WEBUI_NAME += " (Open WebUI)"
|
||||||
|
|
||||||
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -165,7 +168,11 @@ except:
|
|||||||
|
|
||||||
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
|
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
|
||||||
|
|
||||||
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
|
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
|
||||||
|
if os.path.exists(frontend_favicon):
|
||||||
|
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
|
||||||
|
else:
|
||||||
|
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# CUSTOM_NAME
|
# CUSTOM_NAME
|
||||||
@@ -195,9 +202,6 @@ if CUSTOM_NAME:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
if WEBUI_NAME != "Open WebUI":
|
|
||||||
WEBUI_NAME += " (Open WebUI)"
|
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -220,7 +224,7 @@ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
|||||||
# Docs DIR
|
# Docs DIR
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
DOCS_DIR = f"{DATA_DIR}/docs"
|
DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
|
||||||
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -322,9 +326,14 @@ OPENAI_API_BASE_URLS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
OPENAI_API_KEY = ""
|
OPENAI_API_KEY = ""
|
||||||
OPENAI_API_KEY = OPENAI_API_KEYS[
|
|
||||||
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
try:
|
||||||
]
|
OPENAI_API_KEY = OPENAI_API_KEYS[
|
||||||
|
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||||
|
]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
|
||||||
@@ -358,6 +367,17 @@ DEFAULT_PROMPT_SUGGESTIONS = (
|
|||||||
"title": ["Show me a code snippet", "of a website's sticky header"],
|
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": [
|
||||||
|
"Explain options trading",
|
||||||
|
"if I'm familiar with buying and selling stocks",
|
||||||
|
],
|
||||||
|
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": ["Overcome procrastination", "give me tips"],
|
||||||
|
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -370,13 +390,14 @@ USER_PERMISSIONS_CHAT_DELETION = (
|
|||||||
|
|
||||||
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
|
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
|
||||||
|
|
||||||
|
ENABLE_MODEL_FILTER = os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true"
|
||||||
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true"
|
|
||||||
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
||||||
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
||||||
|
|
||||||
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
||||||
|
|
||||||
|
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_VERSION
|
# WEBUI_VERSION
|
||||||
####################################
|
####################################
|
||||||
@@ -411,17 +432,56 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
||||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2)
|
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
|
||||||
|
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
|
||||||
|
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
|
||||||
|
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
|
||||||
|
# Comma-separated list of header=value pairs
|
||||||
|
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
|
||||||
|
if CHROMA_HTTP_HEADERS:
|
||||||
|
CHROMA_HTTP_HEADERS = dict(
|
||||||
|
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
CHROMA_HTTP_HEADERS = None
|
||||||
|
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
||||||
|
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||||
|
|
||||||
|
RAG_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
|
||||||
|
RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
|
||||||
|
|
||||||
|
ENABLE_RAG_HYBRID_SEARCH = (
|
||||||
|
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
|
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
|
||||||
|
|
||||||
RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
|
||||||
|
|
||||||
|
RAG_EMBEDDING_MODEL = os.environ.get(
|
||||||
|
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
|
||||||
|
)
|
||||||
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
|
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
|
||||||
|
|
||||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
||||||
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||||
|
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
RAG_RERANKING_MODEL = os.environ.get("RAG_RERANKING_MODEL", "")
|
||||||
|
if not RAG_RERANKING_MODEL == "":
|
||||||
|
log.info(f"Reranking model set: {RAG_RERANKING_MODEL}"),
|
||||||
|
|
||||||
|
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
||||||
|
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
|
||||||
|
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||||
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
||||||
@@ -431,16 +491,28 @@ if USE_CUDA.lower() == "true":
|
|||||||
else:
|
else:
|
||||||
DEVICE_TYPE = "cpu"
|
DEVICE_TYPE = "cpu"
|
||||||
|
|
||||||
|
if CHROMA_HTTP_HOST != "":
|
||||||
|
CHROMA_CLIENT = chromadb.HttpClient(
|
||||||
|
host=CHROMA_HTTP_HOST,
|
||||||
|
port=CHROMA_HTTP_PORT,
|
||||||
|
headers=CHROMA_HTTP_HEADERS,
|
||||||
|
ssl=CHROMA_HTTP_SSL,
|
||||||
|
tenant=CHROMA_TENANT,
|
||||||
|
database=CHROMA_DATABASE,
|
||||||
|
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
CHROMA_CLIENT = chromadb.PersistentClient(
|
||||||
|
path=CHROMA_DATA_PATH,
|
||||||
|
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||||
|
tenant=CHROMA_TENANT,
|
||||||
|
database=CHROMA_DATABASE,
|
||||||
|
)
|
||||||
|
|
||||||
CHROMA_CLIENT = chromadb.PersistentClient(
|
CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
|
||||||
path=CHROMA_DATA_PATH,
|
CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
|
||||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
|
||||||
)
|
|
||||||
CHUNK_SIZE = 1500
|
|
||||||
CHUNK_OVERLAP = 100
|
|
||||||
|
|
||||||
|
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
|
||||||
RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
|
|
||||||
<context>
|
<context>
|
||||||
[context]
|
[context]
|
||||||
</context>
|
</context>
|
||||||
@@ -450,13 +522,17 @@ When answer to user:
|
|||||||
- If you don't know when you are not sure, ask for clarification.
|
- If you don't know when you are not sure, ask for clarification.
|
||||||
Avoid mentioning that you obtained the information from the context.
|
Avoid mentioning that you obtained the information from the context.
|
||||||
And answer according to the language of the user's question.
|
And answer according to the language of the user's question.
|
||||||
|
|
||||||
Given the context information, answer the query.
|
Given the context information, answer the query.
|
||||||
Query: [query]"""
|
Query: [query]"""
|
||||||
|
|
||||||
|
RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
|
||||||
|
|
||||||
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
||||||
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
|
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||||
|
|
||||||
|
ENABLE_LOCAL_WEB_FETCH = os.getenv("ENABLE_LOCAL_WEB_FETCH", "False").lower() == "true"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Transcribe
|
# Transcribe
|
||||||
####################################
|
####################################
|
||||||
@@ -472,12 +548,25 @@ WHISPER_MODEL_AUTO_UPDATE = (
|
|||||||
# Images
|
# Images
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
|
IMAGE_GENERATION_ENGINE = os.getenv("IMAGE_GENERATION_ENGINE", "")
|
||||||
|
|
||||||
ENABLE_IMAGE_GENERATION = (
|
ENABLE_IMAGE_GENERATION = (
|
||||||
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
|
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
|
||||||
)
|
)
|
||||||
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
||||||
|
|
||||||
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
||||||
|
|
||||||
|
IMAGES_OPENAI_API_BASE_URL = os.getenv(
|
||||||
|
"IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
|
||||||
|
)
|
||||||
|
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||||
|
|
||||||
|
IMAGE_SIZE = os.getenv("IMAGE_SIZE", "512x512")
|
||||||
|
|
||||||
|
IMAGE_STEPS = int(os.getenv("IMAGE_STEPS", 50))
|
||||||
|
|
||||||
|
IMAGE_GENERATION_MODEL = os.getenv("IMAGE_GENERATION_MODEL", "")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Audio
|
# Audio
|
||||||
@@ -485,3 +574,22 @@ COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
|||||||
|
|
||||||
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
||||||
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
|
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# LiteLLM
|
||||||
|
####################################
|
||||||
|
|
||||||
|
|
||||||
|
ENABLE_LITELLM = os.environ.get("ENABLE_LITELLM", "True").lower() == "true"
|
||||||
|
|
||||||
|
LITELLM_PROXY_PORT = int(os.getenv("LITELLM_PROXY_PORT", "14365"))
|
||||||
|
if LITELLM_PROXY_PORT < 0 or LITELLM_PROXY_PORT > 65535:
|
||||||
|
raise ValueError("Invalid port number for LITELLM_PROXY_PORT")
|
||||||
|
LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# Database
|
||||||
|
####################################
|
||||||
|
|
||||||
|
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
|
||||||
@@ -3,6 +3,10 @@ from enum import Enum
|
|||||||
|
|
||||||
class MESSAGES(str, Enum):
|
class MESSAGES(str, Enum):
|
||||||
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
||||||
|
MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully."
|
||||||
|
MODEL_DELETED = (
|
||||||
|
lambda model="": f"The model '{model}' has been deleted successfully."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WEBHOOK_MESSAGES(str, Enum):
|
class WEBHOOK_MESSAGES(str, Enum):
|
||||||
@@ -65,3 +69,9 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
|
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
|
||||||
|
|
||||||
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
||||||
|
|
||||||
|
DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
|
||||||
|
|
||||||
|
INVALID_URL = (
|
||||||
|
"Oops! The URL you provided is invalid. Please double-check and try again."
|
||||||
|
)
|
||||||
@@ -18,6 +18,18 @@
|
|||||||
{
|
{
|
||||||
"title": ["Show me a code snippet", "of a website's sticky header"],
|
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": ["Explain options trading", "if I'm familiar with buying and selling stocks"],
|
||||||
|
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": ["Overcome procrastination", "give me tips"],
|
||||||
|
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": ["Grammar check", "rewrite it for better readability "],
|
||||||
|
"content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Regular → Executable
File mode changed.
+44
-25
@@ -20,12 +20,17 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
from apps.ollama.main import app as ollama_app
|
from apps.ollama.main import app as ollama_app
|
||||||
from apps.openai.main import app as openai_app
|
from apps.openai.main import app as openai_app
|
||||||
|
|
||||||
from apps.litellm.main import app as litellm_app, startup as litellm_app_startup
|
from apps.litellm.main import (
|
||||||
|
app as litellm_app,
|
||||||
|
start_litellm_background,
|
||||||
|
shutdown_litellm_background,
|
||||||
|
)
|
||||||
from apps.audio.main import app as audio_app
|
from apps.audio.main import app as audio_app
|
||||||
from apps.images.main import app as images_app
|
from apps.images.main import app as images_app
|
||||||
from apps.rag.main import app as rag_app
|
from apps.rag.main import app as rag_app
|
||||||
from apps.web.main import app as webui_app
|
from apps.web.main import app as webui_app
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -42,11 +47,13 @@ from config import (
|
|||||||
FRONTEND_BUILD_DIR,
|
FRONTEND_BUILD_DIR,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
STATIC_DIR,
|
STATIC_DIR,
|
||||||
MODEL_FILTER_ENABLED,
|
ENABLE_LITELLM,
|
||||||
|
ENABLE_MODEL_FILTER,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
GLOBAL_LOG_LEVEL,
|
GLOBAL_LOG_LEVEL,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
WEBHOOK_URL,
|
WEBHOOK_URL,
|
||||||
|
ENABLE_ADMIN_EXPORT,
|
||||||
)
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
@@ -83,7 +90,7 @@ https://github.com/open-webui/open-webui
|
|||||||
|
|
||||||
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||||
@@ -110,15 +117,14 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
|||||||
if "docs" in data:
|
if "docs" in data:
|
||||||
data = {**data}
|
data = {**data}
|
||||||
data["messages"] = rag_messages(
|
data["messages"] = rag_messages(
|
||||||
data["docs"],
|
docs=data["docs"],
|
||||||
data["messages"],
|
messages=data["messages"],
|
||||||
rag_app.state.RAG_TEMPLATE,
|
template=rag_app.state.RAG_TEMPLATE,
|
||||||
rag_app.state.TOP_K,
|
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
|
||||||
rag_app.state.RAG_EMBEDDING_ENGINE,
|
k=rag_app.state.TOP_K,
|
||||||
rag_app.state.RAG_EMBEDDING_MODEL,
|
reranking_function=rag_app.state.sentence_transformer_rf,
|
||||||
rag_app.state.sentence_transformer_ef,
|
r=rag_app.state.RELEVANCE_THRESHOLD,
|
||||||
rag_app.state.RAG_OPENAI_API_KEY,
|
hybrid_search=rag_app.state.ENABLE_RAG_HYBRID_SEARCH,
|
||||||
rag_app.state.RAG_OPENAI_API_BASE_URL,
|
|
||||||
)
|
)
|
||||||
del data["docs"]
|
del data["docs"]
|
||||||
|
|
||||||
@@ -170,7 +176,8 @@ async def check_url(request: Request, call_next):
|
|||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def on_startup():
|
async def on_startup():
|
||||||
await litellm_app_startup()
|
if ENABLE_LITELLM:
|
||||||
|
asyncio.create_task(start_litellm_background())
|
||||||
|
|
||||||
|
|
||||||
app.mount("/api/v1", webui_app)
|
app.mount("/api/v1", webui_app)
|
||||||
@@ -202,13 +209,14 @@ async def get_app_config():
|
|||||||
"default_models": webui_app.state.DEFAULT_MODELS,
|
"default_models": webui_app.state.DEFAULT_MODELS,
|
||||||
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||||
|
"admin_export_enabled": ENABLE_ADMIN_EXPORT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/config/model/filter")
|
@app.get("/api/config/model/filter")
|
||||||
async def get_model_filter_config(user=Depends(get_admin_user)):
|
async def get_model_filter_config(user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
"enabled": app.state.MODEL_FILTER_ENABLED,
|
"enabled": app.state.ENABLE_MODEL_FILTER,
|
||||||
"models": app.state.MODEL_FILTER_LIST,
|
"models": app.state.MODEL_FILTER_LIST,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,20 +230,20 @@ class ModelFilterConfigForm(BaseModel):
|
|||||||
async def update_model_filter_config(
|
async def update_model_filter_config(
|
||||||
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
app.state.MODEL_FILTER_ENABLED = form_data.enabled
|
app.state.ENABLE_MODEL_FILTER = form_data.enabled
|
||||||
app.state.MODEL_FILTER_LIST = form_data.models
|
app.state.MODEL_FILTER_LIST = form_data.models
|
||||||
|
|
||||||
ollama_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
ollama_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
|
||||||
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
openai_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
|
||||||
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
litellm_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
|
||||||
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"enabled": app.state.MODEL_FILTER_ENABLED,
|
"enabled": app.state.ENABLE_MODEL_FILTER,
|
||||||
"models": app.state.MODEL_FILTER_LIST,
|
"models": app.state.MODEL_FILTER_LIST,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,15 +311,26 @@ async def get_manifest_json():
|
|||||||
"background_color": "#343541",
|
"background_color": "#343541",
|
||||||
"theme_color": "#343541",
|
"theme_color": "#343541",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
|
"icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
|
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
|
||||||
|
|
||||||
app.mount(
|
if os.path.exists(FRONTEND_BUILD_DIR):
|
||||||
"/",
|
app.mount(
|
||||||
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
"/",
|
||||||
name="spa-static-files",
|
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
||||||
)
|
name="spa-static-files",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
if ENABLE_LITELLM:
|
||||||
|
await shutdown_litellm_background()
|
||||||
+51
-42
@@ -1,53 +1,62 @@
|
|||||||
fastapi
|
fastapi==0.109.2
|
||||||
uvicorn[standard]
|
uvicorn[standard]==0.22.0
|
||||||
pydantic
|
pydantic==2.7.1
|
||||||
python-multipart
|
python-multipart==0.0.9
|
||||||
|
|
||||||
flask
|
Flask==3.0.3
|
||||||
flask_cors
|
Flask-Cors==4.0.0
|
||||||
|
|
||||||
python-socketio
|
python-socketio==5.11.2
|
||||||
python-jose
|
python-jose==3.3.0
|
||||||
passlib[bcrypt]
|
passlib[bcrypt]==1.7.4
|
||||||
uuid
|
uuid==1.30
|
||||||
|
|
||||||
requests
|
requests==2.31.0
|
||||||
aiohttp
|
aiohttp==3.9.5
|
||||||
peewee
|
peewee==3.17.3
|
||||||
peewee-migrate
|
peewee-migrate==1.12.2
|
||||||
bcrypt
|
psycopg2-binary==2.9.9
|
||||||
|
PyMySQL==1.1.0
|
||||||
|
bcrypt==4.1.2
|
||||||
|
|
||||||
litellm==1.30.7
|
litellm==1.35.28
|
||||||
boto3
|
litellm[proxy]==1.35.28
|
||||||
|
|
||||||
argon2-cffi
|
boto3==1.34.95
|
||||||
apscheduler
|
|
||||||
google-generativeai
|
|
||||||
|
|
||||||
langchain
|
argon2-cffi==23.1.0
|
||||||
langchain-community
|
APScheduler==3.10.4
|
||||||
fake_useragent
|
google-generativeai==0.5.2
|
||||||
chromadb
|
|
||||||
sentence_transformers
|
|
||||||
pypdf
|
|
||||||
docx2txt
|
|
||||||
unstructured
|
|
||||||
markdown
|
|
||||||
pypandoc
|
|
||||||
pandas
|
|
||||||
openpyxl
|
|
||||||
pyxlsb
|
|
||||||
xlrd
|
|
||||||
|
|
||||||
opencv-python-headless
|
langchain==0.1.16
|
||||||
rapidocr-onnxruntime
|
langchain-community==0.0.34
|
||||||
|
langchain-chroma==0.1.0
|
||||||
|
|
||||||
fpdf2
|
fake-useragent==1.5.1
|
||||||
|
chromadb==0.4.24
|
||||||
|
sentence-transformers==2.7.0
|
||||||
|
pypdf==4.2.0
|
||||||
|
docx2txt==0.8
|
||||||
|
unstructured==0.11.8
|
||||||
|
Markdown==3.6
|
||||||
|
pypandoc==1.13
|
||||||
|
pandas==2.2.2
|
||||||
|
openpyxl==3.1.2
|
||||||
|
pyxlsb==1.0.10
|
||||||
|
xlrd==2.0.1
|
||||||
|
validators==0.28.1
|
||||||
|
|
||||||
faster-whisper
|
opencv-python-headless==4.9.0.80
|
||||||
|
rapidocr-onnxruntime==1.2.3
|
||||||
|
|
||||||
PyJWT
|
fpdf2==2.7.8
|
||||||
pyjwt[crypto]
|
rank-bm25==0.2.2
|
||||||
|
|
||||||
black
|
faster-whisper==1.0.1
|
||||||
langfuse
|
|
||||||
|
PyJWT==2.8.0
|
||||||
|
PyJWT[crypto]==2.8.0
|
||||||
|
|
||||||
|
black==24.4.2
|
||||||
|
langfuse==2.27.3
|
||||||
|
youtube-transcript-api
|
||||||
+2
-1
@@ -6,6 +6,7 @@ cd "$SCRIPT_DIR" || exit
|
|||||||
KEY_FILE=.webui_secret_key
|
KEY_FILE=.webui_secret_key
|
||||||
|
|
||||||
PORT="${PORT:-8080}"
|
PORT="${PORT:-8080}"
|
||||||
|
HOST="${HOST:-0.0.0.0}"
|
||||||
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
||||||
echo "No WEBUI_SECRET_KEY provided"
|
echo "No WEBUI_SECRET_KEY provided"
|
||||||
|
|
||||||
@@ -29,4 +30,4 @@ 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"
|
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
|
fi
|
||||||
|
|
||||||
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
|
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*'
|
||||||
@@ -7,7 +7,7 @@ SET "SCRIPT_DIR=%~dp0"
|
|||||||
cd /d "%SCRIPT_DIR%" || exit /b
|
cd /d "%SCRIPT_DIR%" || exit /b
|
||||||
|
|
||||||
SET "KEY_FILE=.webui_secret_key"
|
SET "KEY_FILE=.webui_secret_key"
|
||||||
SET "PORT=%PORT:8080%"
|
IF "%PORT%"=="" SET PORT=8080
|
||||||
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||||
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
Name,Email,Password,Role
|
||||||
|
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -89,6 +89,8 @@ def get_current_user(
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
Users.update_user_last_active_by_id(user.id)
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -99,11 +101,15 @@ def get_current_user(
|
|||||||
|
|
||||||
def get_current_user_by_api_key(api_key: str):
|
def get_current_user_by_api_key(api_key: str):
|
||||||
user = Users.get_user_by_api_key(api_key)
|
user = Users.get_user_by_api_key(api_key)
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
Users.update_user_last_active_by_id(user.id)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -2,7 +2,12 @@
|
|||||||
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
|
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
|
||||||
read ans
|
read ans
|
||||||
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
|
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
|
||||||
docker-compose down -v
|
command docker-compose 2>/dev/null
|
||||||
|
if [ "$?" == "0" ]; then
|
||||||
|
docker-compose down -v
|
||||||
|
else
|
||||||
|
docker compose down -v
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "Operation cancelled."
|
echo "Operation cancelled."
|
||||||
fi
|
fi
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:8080'
|
||||||
|
},
|
||||||
|
video: true
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../support/index.d.ts" />
|
||||||
|
|
||||||
|
// These tests run through the chat flow.
|
||||||
|
describe('Settings', () => {
|
||||||
|
// Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
|
||||||
|
after(() => {
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Login as the admin user
|
||||||
|
cy.loginAdmin();
|
||||||
|
// Visit the home page
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Ollama', () => {
|
||||||
|
it('user can select a model', () => {
|
||||||
|
// Click on the model selector
|
||||||
|
cy.get('button[aria-label="Select a model"]').click();
|
||||||
|
// Select the first model
|
||||||
|
cy.get('div[role="menuitem"]').first().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user can perform text chat', () => {
|
||||||
|
// Click on the model selector
|
||||||
|
cy.get('button[aria-label="Select a model"]').click();
|
||||||
|
// Select the first model
|
||||||
|
cy.get('div[role="menuitem"]').first().click();
|
||||||
|
// Type a message
|
||||||
|
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
// Send the message
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
// 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../support/index.d.ts" />
|
||||||
|
import { adminUser } from '../support/e2e';
|
||||||
|
|
||||||
|
// These tests assume the following defaults:
|
||||||
|
// 1. No users exist in the database or that the test admin user is an admin
|
||||||
|
// 2. Language is set to English
|
||||||
|
// 3. The default role for new users is 'pending'
|
||||||
|
describe('Registration and Login', () => {
|
||||||
|
// Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
|
||||||
|
after(() => {
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a new user as pending', () => {
|
||||||
|
const userName = `Test User - ${Date.now()}`;
|
||||||
|
const userEmail = `cypress-${Date.now()}@example.com`;
|
||||||
|
// Toggle from sign in to sign up
|
||||||
|
cy.contains('Sign up').click();
|
||||||
|
// Fill out the form
|
||||||
|
cy.get('input[autocomplete="name"]').type(userName);
|
||||||
|
cy.get('input[autocomplete="email"]').type(userEmail);
|
||||||
|
cy.get('input[type="password"]').type('password');
|
||||||
|
// Submit the form
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
// Wait until the user is redirected to the home page
|
||||||
|
cy.contains(userName);
|
||||||
|
// Expect the user to be pending
|
||||||
|
cy.contains('Check Again');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can login with the admin user', () => {
|
||||||
|
// Fill out the form
|
||||||
|
cy.get('input[autocomplete="email"]').type(adminUser.email);
|
||||||
|
cy.get('input[type="password"]').type(adminUser.password);
|
||||||
|
// Submit the form
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
// Wait until the user is redirected to the home page
|
||||||
|
cy.contains(adminUser.name);
|
||||||
|
// Dismiss the changelog dialog if it is visible
|
||||||
|
cy.getAllLocalStorage().then((ls) => {
|
||||||
|
if (!ls['version']) {
|
||||||
|
cy.get('button').contains("Okay, Let's Go!").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../support/index.d.ts" />
|
||||||
|
import { adminUser } from '../support/e2e';
|
||||||
|
|
||||||
|
// These tests run through the various settings pages, ensuring that the user can interact with them as expected
|
||||||
|
describe('Settings', () => {
|
||||||
|
// Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
|
||||||
|
after(() => {
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Login as the admin user
|
||||||
|
cy.loginAdmin();
|
||||||
|
// Visit the home page
|
||||||
|
cy.visit('/');
|
||||||
|
// Open the sidebar if it is not already open
|
||||||
|
cy.get('[aria-label="Open sidebar"]').then(() => {
|
||||||
|
cy.get('button[id="sidebar-toggle-button"]').click();
|
||||||
|
});
|
||||||
|
// Click on the profile link
|
||||||
|
cy.get('button').contains(adminUser.name).click();
|
||||||
|
// Click on the settings link
|
||||||
|
cy.get('button').contains('Settings').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('General', () => {
|
||||||
|
it('user can open the General modal and hit save', () => {
|
||||||
|
cy.get('button').contains('General').click();
|
||||||
|
cy.get('button').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Connections', () => {
|
||||||
|
it('user can open the Connections modal and hit save', () => {
|
||||||
|
cy.get('button').contains('Connections').click();
|
||||||
|
cy.get('button').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Models', () => {
|
||||||
|
it('user can open the Models modal', () => {
|
||||||
|
cy.get('button').contains('Models').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Interface', () => {
|
||||||
|
it('user can open the Interface modal and hit save', () => {
|
||||||
|
cy.get('button').contains('Interface').click();
|
||||||
|
cy.get('button').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Audio', () => {
|
||||||
|
it('user can open the Audio modal and hit save', () => {
|
||||||
|
cy.get('button').contains('Audio').click();
|
||||||
|
cy.get('button').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Images', () => {
|
||||||
|
it('user can open the Images modal and hit save', () => {
|
||||||
|
cy.get('button').contains('Images').click();
|
||||||
|
// Currently fails because the backend requires a valid URL
|
||||||
|
// cy.get('button').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Chats', () => {
|
||||||
|
it('user can open the Chats modal', () => {
|
||||||
|
cy.get('button').contains('Chats').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Account', () => {
|
||||||
|
it('user can open the Account modal and hit save', () => {
|
||||||
|
cy.get('button').contains('Account').click();
|
||||||
|
cy.get('button').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('About', () => {
|
||||||
|
it('user can open the About modal', () => {
|
||||||
|
cy.get('button').contains('About').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export const adminUser = {
|
||||||
|
name: 'Admin User',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
password: 'password'
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = (email: string, password: string) => {
|
||||||
|
return cy.session(
|
||||||
|
email,
|
||||||
|
() => {
|
||||||
|
// Visit auth page
|
||||||
|
cy.visit('/auth');
|
||||||
|
// Fill out the form
|
||||||
|
cy.get('input[autocomplete="email"]').type(email);
|
||||||
|
cy.get('input[type="password"]').type(password);
|
||||||
|
// Submit the form
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
// Wait until the user is redirected to the home page
|
||||||
|
cy.get('#chat-search').should('exist');
|
||||||
|
// Get the current version to skip the changelog dialog
|
||||||
|
if (localStorage.getItem('version') === null) {
|
||||||
|
cy.get('button').contains("Okay, Let's Go!").click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validate: () => {
|
||||||
|
cy.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/auths/',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = (name: string, email: string, password: string) => {
|
||||||
|
return cy
|
||||||
|
.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/auths/signup',
|
||||||
|
body: {
|
||||||
|
name: name,
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.status).to.be.oneOf([200, 400]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerAdmin = () => {
|
||||||
|
return register(adminUser.name, adminUser.email, adminUser.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginAdmin = () => {
|
||||||
|
return login(adminUser.email, adminUser.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (email, password) => login(email, password));
|
||||||
|
Cypress.Commands.add('register', (name, email, password) => register(name, email, password));
|
||||||
|
Cypress.Commands.add('registerAdmin', () => registerAdmin());
|
||||||
|
Cypress.Commands.add('loginAdmin', () => loginAdmin());
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.registerAdmin();
|
||||||
|
});
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
// load the global Cypress types
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
login(email: string, password: string): Chainable<Element>;
|
||||||
|
register(name: string, email: string, password: string): Chainable<Element>;
|
||||||
|
registerAdmin(): Chainable<Element>;
|
||||||
|
loginAdmin(): Chainable<Element>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"sourceMap": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1609
-169
File diff suppressed because it is too large.
Load diff
+6
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.120",
|
"version": "0.1.123",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
"lint:backend": "pylint backend/",
|
"lint:backend": "pylint backend/",
|
||||||
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
||||||
"format:backend": "black . --exclude \"/venv/\"",
|
"format:backend": "black . --exclude \"/venv/\"",
|
||||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'",
|
||||||
|
"cy:open": "cypress open"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
@@ -25,8 +26,10 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/parser": "^6.17.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
"cypress": "^13.8.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-cypress": "^3.0.2",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
"i18next-parser": "^8.13.0",
|
"i18next-parser": "^8.13.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.19.7",
|
"bits-ui": "^0.19.7",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"eventsource-parser": "^1.1.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
@@ -53,7 +57,6 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
"jspdf": "^2.5.1",
|
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
|
|||||||
+2
-1
@@ -82,6 +82,7 @@ usage() {
|
|||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 --drop"
|
echo " $0 --drop"
|
||||||
echo " $0 --enable-gpu[count=1]"
|
echo " $0 --enable-gpu[count=1]"
|
||||||
|
echo " $0 --enable-gpu[count=all]"
|
||||||
echo " $0 --enable-api[port=11435]"
|
echo " $0 --enable-api[port=11435]"
|
||||||
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000]"
|
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000]"
|
||||||
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data]"
|
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data]"
|
||||||
@@ -160,7 +161,7 @@ else
|
|||||||
if [[ $enable_gpu == true ]]; then
|
if [[ $enable_gpu == true ]]; then
|
||||||
# Validate and process command-line arguments
|
# Validate and process command-line arguments
|
||||||
if [[ -n $gpu_count ]]; then
|
if [[ -n $gpu_count ]]; then
|
||||||
if ! [[ $gpu_count =~ ^[0-9]+$ ]]; then
|
if ! [[ $gpu_count =~ ^([0-9]+|all)$ ]]; then
|
||||||
echo "Invalid GPU count: $gpu_count"
|
echo "Invalid GPU count: $gpu_count"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -43,9 +43,46 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<title>Open WebUI</title>
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="splash-screen"
|
||||||
|
style="
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
background: #fff;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<style type="text/css" nonce="">
|
||||||
|
html {
|
||||||
|
overflow-y: scroll !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<img
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
top: 46%;
|
||||||
|
left: 50%;
|
||||||
|
margin: -40px 0 0 -40px;
|
||||||
|
"
|
||||||
|
src="/logo.svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- <span style="position: absolute; bottom: 32px; left: 50%; margin: -36px 0 0 -36px">
|
||||||
|
Footer content
|
||||||
|
</span> -->
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -95,6 +95,45 @@ export const userSignUp = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addUser = async (
|
||||||
|
token: string,
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
role: string = 'pending'
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
role: role
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateUserProfile = async (token: string, name: string, profileImageUrl: string) => {
|
export const updateUserProfile = async (token: string, name: string, profileImageUrl: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,37 @@ export const getChatList = async (token: string = '') => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getChatListByUserId = async (token: string = '', userId: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/list/user/${userId}`, {
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getArchivedChatList = async (token: string = '') => {
|
export const getArchivedChatList = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ export const updateImageGenerationConfig = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOpenAIKey = async (token: string = '') => {
|
export const getOpenAIConfig = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/key`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -101,13 +101,13 @@ export const getOpenAIKey = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateOpenAIKey = async (token: string = '', key: string) => {
|
export const updateOpenAIConfig = async (token: string = '', url: string, key: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/key/update`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -115,6 +115,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
url: url,
|
||||||
key: key
|
key: key
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -136,7 +137,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
||||||
|
|||||||
@@ -211,10 +211,12 @@ export const generateOpenAIChatCompletion = async (
|
|||||||
token: string = '',
|
token: string = '',
|
||||||
body: object,
|
body: object,
|
||||||
url: string = OPENAI_API_BASE_URL
|
url: string = OPENAI_API_BASE_URL
|
||||||
) => {
|
): Promise<[Response | null, AbortController]> => {
|
||||||
|
const controller = new AbortController();
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${url}/chat/completions`, {
|
const res = await fetch(`${url}/chat/completions`, {
|
||||||
|
signal: controller.signal,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -231,7 +233,7 @@ export const generateOpenAIChatCompletion = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return [res, controller];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const synthesizeOpenAISpeech = async (
|
export const synthesizeOpenAISpeech = async (
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export const getQuerySettings = async (token: string) => {
|
|||||||
|
|
||||||
type QuerySettings = {
|
type QuerySettings = {
|
||||||
k: number | null;
|
k: number | null;
|
||||||
|
r: number | null;
|
||||||
template: string | null;
|
template: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,6 +221,37 @@ export const uploadWebToVectorDB = async (token: string, collection_name: string
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadYoutubeTranscriptionToVectorDB = async (token: string, url: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/youtube`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const queryDoc = async (
|
export const queryDoc = async (
|
||||||
token: string,
|
token: string,
|
||||||
collection_name: string,
|
collection_name: string,
|
||||||
@@ -413,3 +445,64 @@ export const updateEmbeddingConfig = async (token: string, payload: EmbeddingMod
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRerankingConfig = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/reranking`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RerankingModelUpdateForm = {
|
||||||
|
reranking_model: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateRerankingConfig = async (token: string, payload: RerankingModelUpdateForm) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/reranking/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||||
|
import type { ParsedEvent } from 'eventsource-parser';
|
||||||
|
|
||||||
|
type TextStreamUpdate = {
|
||||||
|
done: boolean;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// createOpenAITextStream takes a responseBody with a SSE response,
|
||||||
|
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
|
||||||
|
export async function createOpenAITextStream(
|
||||||
|
responseBody: ReadableStream<Uint8Array>,
|
||||||
|
splitLargeDeltas: boolean
|
||||||
|
): Promise<AsyncGenerator<TextStreamUpdate>> {
|
||||||
|
const eventStream = responseBody
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(new EventSourceParserStream())
|
||||||
|
.getReader();
|
||||||
|
let iterator = openAIStreamToIterator(eventStream);
|
||||||
|
if (splitLargeDeltas) {
|
||||||
|
iterator = streamLargeDeltasAsRandomChunks(iterator);
|
||||||
|
}
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* openAIStreamToIterator(
|
||||||
|
reader: ReadableStreamDefaultReader<ParsedEvent>
|
||||||
|
): AsyncGenerator<TextStreamUpdate> {
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
yield { done: true, value: '' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const data = value.data;
|
||||||
|
if (data.startsWith('[DONE]')) {
|
||||||
|
yield { done: true, value: '' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
console.log(parsedData);
|
||||||
|
|
||||||
|
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error extracting delta from SSE event:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters
|
||||||
|
// This is to simulate a more fluid streaming, even though some providers may send large chunks of text at once
|
||||||
|
async function* streamLargeDeltasAsRandomChunks(
|
||||||
|
iterator: AsyncGenerator<TextStreamUpdate>
|
||||||
|
): AsyncGenerator<TextStreamUpdate> {
|
||||||
|
for await (const textStreamUpdate of iterator) {
|
||||||
|
if (textStreamUpdate.done) {
|
||||||
|
yield textStreamUpdate;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let content = textStreamUpdate.value;
|
||||||
|
if (content.length < 5) {
|
||||||
|
yield { done: false, value: content };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (content != '') {
|
||||||
|
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
|
||||||
|
const chunk = content.slice(0, chunkSize);
|
||||||
|
yield { done: false, value: chunk };
|
||||||
|
// Do not sleep if the tab is hidden
|
||||||
|
// Timers are throttled to 1s in hidden tabs
|
||||||
|
if (document?.visibilityState !== 'hidden') {
|
||||||
|
await sleep(5);
|
||||||
|
}
|
||||||
|
content = content.slice(chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -83,9 +83,9 @@ export const downloadDatabase = async (token: string) => {
|
|||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Network response was not ok');
|
throw await response.json();
|
||||||
}
|
}
|
||||||
return response.blob();
|
return response.blob();
|
||||||
})
|
})
|
||||||
@@ -100,7 +100,11 @@ export const downloadDatabase = async (token: string) => {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
error = err;
|
error = err.detail;
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div class="px-5 py-4 dark:text-gray-300">
|
<div class="px-5 py-4 dark:text-gray-300 text-gray-700">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="text-xl font-bold">
|
<div class="text-xl font-bold">
|
||||||
{$i18n.t('What’s New in')}
|
{$i18n.t('What’s New in')}
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
<hr class=" dark:border-gray-800" />
|
<hr class=" dark:border-gray-800" />
|
||||||
|
|
||||||
<div class=" w-full p-4 px-5">
|
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
|
||||||
<div class=" overflow-y-scroll max-h-80">
|
<div class=" overflow-y-scroll max-h-80">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{#if changelog}
|
{#if changelog}
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
localStorage.version = $config.version;
|
localStorage.version = $config.version;
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
>
|
>
|
||||||
<span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
|
<span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { addUser } from '$lib/apis/auths';
|
||||||
|
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
let tab = '';
|
||||||
|
let inputFiles;
|
||||||
|
|
||||||
|
let _user = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
role: 'user'
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
_user = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
role: 'user'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
const stopLoading = () => {
|
||||||
|
dispatch('save');
|
||||||
|
loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tab === '') {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
const res = await addUser(
|
||||||
|
localStorage.token,
|
||||||
|
_user.name,
|
||||||
|
_user.email,
|
||||||
|
_user.password,
|
||||||
|
_user.role
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
stopLoading();
|
||||||
|
show = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (inputFiles) {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
const file = inputFiles[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const csv = e.target.result;
|
||||||
|
const rows = csv.split('\n');
|
||||||
|
|
||||||
|
let userCount = 0;
|
||||||
|
|
||||||
|
for (const [idx, row] of rows.entries()) {
|
||||||
|
const columns = row.split(',').map((col) => col.trim());
|
||||||
|
console.log(idx, columns);
|
||||||
|
|
||||||
|
if (idx > 0) {
|
||||||
|
if (columns.length === 4 && ['admin', 'user', 'pending'].includes(columns[3])) {
|
||||||
|
const res = await addUser(
|
||||||
|
localStorage.token,
|
||||||
|
columns[0],
|
||||||
|
columns[1],
|
||||||
|
columns[2],
|
||||||
|
columns[3]
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(`Row ${idx + 1}: ${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
userCount = userCount + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(`Row ${idx + 1}: invalid format.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`Successfully imported ${userCount} users.`);
|
||||||
|
inputFiles = null;
|
||||||
|
const uploadInputElement = document.getElementById('upload-user-csv-input');
|
||||||
|
|
||||||
|
if (uploadInputElement) {
|
||||||
|
uploadInputElement.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading();
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
} else {
|
||||||
|
toast.error(`File not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||||
|
<div class=" text-lg font-medium self-center">{$i18n.t('Add User')}</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<form
|
||||||
|
class="flex flex-col w-full"
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
|
||||||
|
<button
|
||||||
|
class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
tab = '';
|
||||||
|
}}>Form</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
tab = 'import';
|
||||||
|
}}>CSV Import</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="px-1">
|
||||||
|
{#if tab === ''}
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<select
|
||||||
|
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
|
bind:value={_user.role}
|
||||||
|
placeholder={$i18n.t('Enter Your Role')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="pending"> pending </option>
|
||||||
|
<option value="user"> user </option>
|
||||||
|
<option value="admin"> admin </option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full mt-2">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
|
type="text"
|
||||||
|
bind:value={_user.name}
|
||||||
|
placeholder={$i18n.t('Enter Your Full Name')}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-800 my-3 w-full" />
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
|
type="email"
|
||||||
|
bind:value={_user.email}
|
||||||
|
placeholder={$i18n.t('Enter Your Email')}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full mt-2">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
|
type="password"
|
||||||
|
bind:value={_user.password}
|
||||||
|
placeholder={$i18n.t('Enter Your Password')}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if tab === 'import'}
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 w-full">
|
||||||
|
<input
|
||||||
|
id="upload-user-csv-input"
|
||||||
|
hidden
|
||||||
|
bind:files={inputFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full text-sm font-medium py-3 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-user-csv-input')?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if inputFiles}
|
||||||
|
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Click here to select a csv file.')}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-xs text-gray-500">
|
||||||
|
ⓘ {$i18n.t(
|
||||||
|
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
class="underline dark:text-gray-200"
|
||||||
|
href="{WEBUI_BASE_URL}/static/user-import.csv"
|
||||||
|
>
|
||||||
|
Click here to download user import template file.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
|
||||||
|
? ' cursor-not-allowed'
|
||||||
|
: ''}"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t('Submit')}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="ml-2 self-center">
|
||||||
|
<svg
|
||||||
|
class=" w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><style>
|
||||||
|
.spinner_ajPY {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner_AtaB 0.75s infinite linear;
|
||||||
|
}
|
||||||
|
@keyframes spinner_AtaB {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style><path
|
||||||
|
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||||
|
opacity=".25"
|
||||||
|
/><path
|
||||||
|
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||||
|
class="spinner_ajPY"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
/* display: none; <- Crashes Chrome on hover */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar {
|
||||||
|
display: none; /* for Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield; /* Firefox */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
{$i18n.t('Created at')}
|
{$i18n.t('Created at')}
|
||||||
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { downloadDatabase } from '$lib/apis/utils';
|
import { downloadDatabase } from '$lib/apis/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { config } from '$lib/stores';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -24,42 +26,47 @@
|
|||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
||||||
|
|
||||||
<button
|
{#if $config?.admin_export_enabled ?? true}
|
||||||
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
<button
|
||||||
type="button"
|
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
type="button"
|
||||||
// exportAllUserChats();
|
on:click={() => {
|
||||||
|
// exportAllUserChats();
|
||||||
|
|
||||||
downloadDatabase(localStorage.token);
|
downloadDatabase(localStorage.token).catch((error) => {
|
||||||
}}
|
toast.error(error);
|
||||||
>
|
});
|
||||||
<div class=" self-center mr-3">
|
}}
|
||||||
<svg
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div class=" self-center mr-3">
|
||||||
viewBox="0 0 16 16"
|
<svg
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-4 h-4"
|
viewBox="0 0 16 16"
|
||||||
>
|
fill="currentColor"
|
||||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
class="w-4 h-4"
|
||||||
<path
|
>
|
||||||
fill-rule="evenodd"
|
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
<path
|
||||||
clip-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
/>
|
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||||
</svg>
|
clip-rule="evenodd"
|
||||||
</div>
|
/>
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
</svg>
|
||||||
</button>
|
</div>
|
||||||
|
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
|
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div> -->
|
</div> -->
|
||||||
</form>
|
</form>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { getContext, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let user;
|
||||||
|
|
||||||
|
let chats = [];
|
||||||
|
|
||||||
|
const deleteChatHandler = async (chatId) => {
|
||||||
|
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
chats = await getChatListByUserId(localStorage.token, user.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
(async () => {
|
||||||
|
if (user.id) {
|
||||||
|
chats = await getChatListByUserId(localStorage.token, user.id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="lg" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
|
<div class=" text-lg font-medium self-center capitalize">
|
||||||
|
{$i18n.t("{{user}}'s Chats", { user: user.name })}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
{#if chats.length > 0}
|
||||||
|
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
|
||||||
|
<div class="relative overflow-x-auto">
|
||||||
|
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
||||||
|
<thead
|
||||||
|
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
||||||
|
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
|
||||||
|
<th scope="col" class="px-3 py-2 text-right" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each chats as chat, idx}
|
||||||
|
<tr
|
||||||
|
class="bg-transparent {idx !== chats.length - 1 &&
|
||||||
|
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
|
||||||
|
>
|
||||||
|
<td class="px-3 py-1 w-2/3">
|
||||||
|
<a href="/s/{chat.id}" target="_blank">
|
||||||
|
<div class=" underline line-clamp-1">
|
||||||
|
{chat.title}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
|
||||||
|
<div class="my-auto">
|
||||||
|
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-3 py-1 text-right">
|
||||||
|
<div class="flex justify-end w-full">
|
||||||
|
<Tooltip content={$i18n.t('Delete Chat')}>
|
||||||
|
<button
|
||||||
|
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
on:click={async () => {
|
||||||
|
deleteChatHandler(chat.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- {#each chats as chat}
|
||||||
|
<div>
|
||||||
|
{JSON.stringify(chat)}
|
||||||
|
</div>
|
||||||
|
{/each} -->
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-left text-sm w-full mb-8">
|
||||||
|
{user.name}
|
||||||
|
{$i18n.t('has no conversations.')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -87,6 +87,17 @@
|
|||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
await tick();
|
await tick();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmSelectYoutube = async (url) => {
|
||||||
|
dispatch('youtube', url);
|
||||||
|
|
||||||
|
prompt = removeFirstHashWord(prompt);
|
||||||
|
const chatInputElement = document.getElementById('chat-textarea');
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
chatInputElement?.focus();
|
||||||
|
await tick();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||||
@@ -132,7 +143,30 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('https://www.youtube.com')}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||||
|
if (isValidHttpUrl(url)) {
|
||||||
|
confirmSelectYoutube(url);
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(
|
||||||
|
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" font-medium text-black line-clamp-1">
|
||||||
|
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
|
||||||
|
</button>
|
||||||
|
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import { generatePrompt } from '$lib/apis/ollama';
|
import { generatePrompt } from '$lib/apis/ollama';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
@@ -7,6 +9,8 @@
|
|||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let user = null;
|
export let user = null;
|
||||||
|
|
||||||
@@ -17,12 +21,7 @@
|
|||||||
let filteredModels = [];
|
let filteredModels = [];
|
||||||
|
|
||||||
$: filteredModels = $models
|
$: filteredModels = $models
|
||||||
.filter(
|
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
|
||||||
(p) =>
|
|
||||||
p.name !== 'hr' &&
|
|
||||||
!p.external &&
|
|
||||||
p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
$: if (prompt) {
|
$: if (prompt) {
|
||||||
@@ -38,6 +37,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmSelect = async (model) => {
|
const confirmSelect = async (model) => {
|
||||||
|
prompt = '';
|
||||||
|
dispatch('select', model);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmSelectCollaborativeChat = async (model) => {
|
||||||
// dispatch('select', model);
|
// dispatch('select', model);
|
||||||
prompt = '';
|
prompt = '';
|
||||||
user = JSON.parse(JSON.stringify(model.name));
|
user = JSON.parse(JSON.stringify(model.name));
|
||||||
@@ -127,40 +131,42 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredModels.length > 0}
|
{#if prompt.charAt(0) === '@'}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
{#if filteredModels.length > 0}
|
||||||
<div class="flex w-full px-2">
|
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
<div class="flex w-full px-2">
|
||||||
<div class=" text-lg font-semibold mt-2">@</div>
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
</div>
|
<div class=" text-lg font-semibold mt-2">@</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||||
{#each filteredModels as model, modelIdx}
|
{#each filteredModels as model, modelIdx}
|
||||||
<button
|
<button
|
||||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||||
? ' bg-gray-100 selected-command-option-button'
|
? ' bg-gray-100 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
confirmSelect(model);
|
confirmSelect(model);
|
||||||
}}
|
}}
|
||||||
on:mousemove={() => {
|
on:mousemove={() => {
|
||||||
selectedIdx = modelIdx;
|
selectedIdx = modelIdx;
|
||||||
}}
|
}}
|
||||||
on:focus={() => {}}
|
on:focus={() => {}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black line-clamp-1">
|
<div class=" font-medium text-black line-clamp-1">
|
||||||
{model.name}
|
{model.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</div> -->
|
</div> -->
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1,56 +1,116 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Bolt from '$lib/components/icons/Bolt.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let submitPrompt: Function;
|
export let submitPrompt: Function;
|
||||||
export let suggestionPrompts = [];
|
export let suggestionPrompts = [];
|
||||||
|
|
||||||
let prompts = [];
|
let prompts = [];
|
||||||
|
|
||||||
$: prompts =
|
$: prompts = suggestionPrompts
|
||||||
suggestionPrompts.length <= 4
|
.reduce((acc, current) => [...acc, ...[current]], [])
|
||||||
? suggestionPrompts
|
.sort(() => Math.random() - 0.5);
|
||||||
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
|
// suggestionPrompts.length <= 4
|
||||||
|
// ? suggestionPrompts
|
||||||
|
// : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const containerElement = document.getElementById('suggestions-container');
|
||||||
|
|
||||||
|
if (containerElement) {
|
||||||
|
containerElement.addEventListener('wheel', function (event) {
|
||||||
|
if (event.deltaY !== 0) {
|
||||||
|
// If scrolling vertically, prevent default behavior
|
||||||
|
event.preventDefault();
|
||||||
|
// Adjust horizontal scroll position based on vertical scroll
|
||||||
|
containerElement.scrollLeft += event.deltaY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" mb-3 md:p-1 text-left w-full">
|
{#if prompts.length > 0}
|
||||||
<div class=" flex flex-wrap-reverse px-2 text-left">
|
<div class="mb-2 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
|
||||||
|
<Bolt />
|
||||||
|
Suggested
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div
|
||||||
|
class="relative w-full flex gap-2 snap-x snap-mandatory md:snap-none overflow-x-auto tabs"
|
||||||
|
id="suggestions-container"
|
||||||
|
>
|
||||||
{#each prompts as prompt, promptIdx}
|
{#each prompts as prompt, promptIdx}
|
||||||
<div
|
<div class="snap-center shrink-0">
|
||||||
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
|
class="flex flex-col flex-1 shrink-0 w-64 justify-between h-36 p-5 px-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-3xl transition group"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
submitPrompt(prompt.content);
|
submitPrompt(prompt.content);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col text-left self-center">
|
<div class="flex flex-col text-left">
|
||||||
{#if prompt.title && prompt.title[0] !== ''}
|
{#if prompt.title && prompt.title[0] !== ''}
|
||||||
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
|
<div
|
||||||
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
|
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition"
|
||||||
|
>
|
||||||
|
{prompt.title[0]}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
|
<div
|
||||||
|
class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
|
||||||
|
>
|
||||||
{prompt.content}
|
{prompt.content}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="w-full flex justify-between">
|
||||||
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
|
<div
|
||||||
>
|
class="text-xs text-gray-400 group-hover:text-gray-500 dark:text-gray-600 dark:group-hover:text-gray-500 transition self-center"
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
Prompt
|
||||||
fill-rule="evenodd"
|
</div>
|
||||||
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
|
||||||
clip-rule="evenodd"
|
<div
|
||||||
/>
|
class="self-end p-1 rounded-lg text-gray-300 group-hover:text-gray-800 dark:text-gray-700 dark:group-hover:text-gray-100 transition"
|
||||||
</svg>
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<!-- <div class="snap-center shrink-0">
|
||||||
|
<img
|
||||||
|
class="shrink-0 w-80 h-40 rounded-lg shadow-xl bg-white"
|
||||||
|
src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80"
|
||||||
|
/>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs::-webkit-scrollbar {
|
||||||
|
display: none; /* for Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
import Placeholder from './Messages/Placeholder.svelte';
|
import Placeholder from './Messages/Placeholder.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
export let regenerateResponse: Function;
|
export let regenerateResponse: Function;
|
||||||
|
|
||||||
|
export let prompt;
|
||||||
|
export let suggestionPrompts;
|
||||||
export let processing = '';
|
export let processing = '';
|
||||||
export let bottomPadding = false;
|
export let bottomPadding = false;
|
||||||
export let autoScroll;
|
export let autoScroll;
|
||||||
@@ -236,108 +238,111 @@
|
|||||||
history: history
|
history: history
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const messageDeleteHandler = async (messageId) => {
|
|
||||||
// const message = history.messages[messageId];
|
|
||||||
// const parentId = message.parentId;
|
|
||||||
// const childrenIds = message.childrenIds ?? [];
|
|
||||||
// const grandchildrenIds = [];
|
|
||||||
|
|
||||||
// // Iterate through childrenIds to find grandchildrenIds
|
|
||||||
// for (const childId of childrenIds) {
|
|
||||||
// const childMessage = history.messages[childId];
|
|
||||||
// const grandChildrenIds = childMessage.childrenIds ?? [];
|
|
||||||
|
|
||||||
// for (const grandchildId of grandchildrenIds) {
|
|
||||||
// const childMessage = history.messages[grandchildId];
|
|
||||||
// childMessage.parentId = parentId;
|
|
||||||
// }
|
|
||||||
// grandchildrenIds.push(...grandChildrenIds);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
|
|
||||||
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
|
|
||||||
// (id) => id !== messageId
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Select latest message
|
|
||||||
// let currentMessageId = grandchildrenIds.at(-1);
|
|
||||||
// if (currentMessageId) {
|
|
||||||
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
|
||||||
// while (messageChildrenIds.length !== 0) {
|
|
||||||
// currentMessageId = messageChildrenIds.at(-1);
|
|
||||||
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
|
||||||
// }
|
|
||||||
// history.currentId = currentMessageId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await updateChatById(localStorage.token, chatId, { messages, history });
|
|
||||||
// };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if messages.length == 0}
|
<div class="h-full flex mb-16">
|
||||||
<Placeholder models={selectedModels} modelfiles={selectedModelfiles} />
|
{#if messages.length == 0}
|
||||||
{:else}
|
<Placeholder
|
||||||
<div class=" pb-10">
|
models={selectedModels}
|
||||||
{#key chatId}
|
modelfiles={selectedModelfiles}
|
||||||
{#each messages as message, messageIdx}
|
{suggestionPrompts}
|
||||||
<div class=" w-full">
|
submitPrompt={async (p) => {
|
||||||
<div
|
let text = p;
|
||||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
|
||||||
? 'max-w-full'
|
|
||||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
|
||||||
>
|
|
||||||
{#if message.role === 'user'}
|
|
||||||
<UserMessage
|
|
||||||
on:delete={() => messageDeleteHandler(message.id)}
|
|
||||||
user={$user}
|
|
||||||
{readOnly}
|
|
||||||
{message}
|
|
||||||
isFirstMessage={messageIdx === 0}
|
|
||||||
siblings={message.parentId !== null
|
|
||||||
? history.messages[message.parentId]?.childrenIds ?? []
|
|
||||||
: Object.values(history.messages)
|
|
||||||
.filter((message) => message.parentId === null)
|
|
||||||
.map((message) => message.id) ?? []}
|
|
||||||
{confirmEditMessage}
|
|
||||||
{showPreviousMessage}
|
|
||||||
{showNextMessage}
|
|
||||||
copyToClipboard={copyToClipboardWithToast}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<ResponseMessage
|
|
||||||
{message}
|
|
||||||
modelfiles={selectedModelfiles}
|
|
||||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
|
||||||
isLastMessage={messageIdx + 1 === messages.length}
|
|
||||||
{readOnly}
|
|
||||||
{updateChatMessages}
|
|
||||||
{confirmEditResponseMessage}
|
|
||||||
{showPreviousMessage}
|
|
||||||
{showNextMessage}
|
|
||||||
{rateMessage}
|
|
||||||
copyToClipboard={copyToClipboardWithToast}
|
|
||||||
{continueGeneration}
|
|
||||||
{regenerateResponse}
|
|
||||||
on:save={async (e) => {
|
|
||||||
console.log('save', e);
|
|
||||||
|
|
||||||
const message = e.detail;
|
if (p.includes('{{CLIPBOARD}}')) {
|
||||||
history.messages[message.id] = message;
|
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||||
await updateChatById(localStorage.token, chatId, {
|
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||||
messages: messages,
|
return '{{CLIPBOARD}}';
|
||||||
history: history
|
});
|
||||||
});
|
|
||||||
}}
|
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||||
/>
|
}
|
||||||
{/if}
|
|
||||||
|
prompt = text;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
const chatInputElement = document.getElementById('chat-textarea');
|
||||||
|
if (chatInputElement) {
|
||||||
|
prompt = p;
|
||||||
|
|
||||||
|
chatInputElement.style.height = '';
|
||||||
|
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||||
|
chatInputElement.focus();
|
||||||
|
|
||||||
|
const words = findWordIndices(prompt);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
const word = words.at(0);
|
||||||
|
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="w-full pt-2">
|
||||||
|
{#key chatId}
|
||||||
|
{#each messages as message, messageIdx}
|
||||||
|
<div class=" w-full {messageIdx === messages.length - 1 ? 'pb-28' : ''}">
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||||
|
? 'max-w-full'
|
||||||
|
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||||
|
>
|
||||||
|
{#if message.role === 'user'}
|
||||||
|
<UserMessage
|
||||||
|
on:delete={() => messageDeleteHandler(message.id)}
|
||||||
|
user={$user}
|
||||||
|
{readOnly}
|
||||||
|
{message}
|
||||||
|
isFirstMessage={messageIdx === 0}
|
||||||
|
siblings={message.parentId !== null
|
||||||
|
? history.messages[message.parentId]?.childrenIds ?? []
|
||||||
|
: Object.values(history.messages)
|
||||||
|
.filter((message) => message.parentId === null)
|
||||||
|
.map((message) => message.id) ?? []}
|
||||||
|
{confirmEditMessage}
|
||||||
|
{showPreviousMessage}
|
||||||
|
{showNextMessage}
|
||||||
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<ResponseMessage
|
||||||
|
{message}
|
||||||
|
modelfiles={selectedModelfiles}
|
||||||
|
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||||
|
isLastMessage={messageIdx + 1 === messages.length}
|
||||||
|
{readOnly}
|
||||||
|
{updateChatMessages}
|
||||||
|
{confirmEditResponseMessage}
|
||||||
|
{showPreviousMessage}
|
||||||
|
{showNextMessage}
|
||||||
|
{rateMessage}
|
||||||
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
|
{continueGeneration}
|
||||||
|
{regenerateResponse}
|
||||||
|
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}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if bottomPadding}
|
{#if bottomPadding}
|
||||||
<div class=" mb-10" />
|
<div class=" pb-20" />
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -31,7 +31,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre class=" rounded-b-lg hljs p-4 px-5 overflow-x-auto rounded-t-none"><code
|
<pre
|
||||||
|
class=" hljs p-4 px-5 overflow-x-auto"
|
||||||
|
style="border-top-left-radius: 0px; border-top-right-radius: 0px;"><code
|
||||||
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
|
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
|
||||||
></pre>
|
></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,11 +3,19 @@
|
|||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { blur, fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
import Suggestions from '../MessageInput/Suggestions.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let models = [];
|
export let models = [];
|
||||||
export let modelfiles = [];
|
export let modelfiles = [];
|
||||||
|
|
||||||
|
export let submitPrompt;
|
||||||
|
export let suggestionPrompts;
|
||||||
|
|
||||||
|
let mounted = false;
|
||||||
let modelfile = null;
|
let modelfile = null;
|
||||||
let selectedModelIdx = 0;
|
let selectedModelIdx = 0;
|
||||||
|
|
||||||
@@ -17,12 +25,16 @@
|
|||||||
$: if (models.length > 0) {
|
$: if (models.length > 0) {
|
||||||
selectedModelIdx = models.length - 1;
|
selectedModelIdx = models.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if models.length > 0}
|
{#key mounted}
|
||||||
<div class="m-auto text-center max-w-md px-2">
|
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-16">
|
||||||
<div class="flex justify-center mt-8">
|
<div class="flex justify-start">
|
||||||
<div class="flex -space-x-4 mb-1">
|
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
|
||||||
{#each models as model, modelIdx}
|
{#each models as model, modelIdx}
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -33,15 +45,15 @@
|
|||||||
<img
|
<img
|
||||||
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
alt="modelfile"
|
alt="modelfile"
|
||||||
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<img
|
<img
|
||||||
src={models.length === 1
|
src={$i18n.language === 'dg-DG'
|
||||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
? `/doge.png`
|
||||||
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
@@ -50,26 +62,42 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
|
|
||||||
{#if modelfile}
|
|
||||||
<span class=" capitalize">
|
|
||||||
{modelfile.title}
|
|
||||||
</span>
|
|
||||||
<div class="mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400">
|
|
||||||
{modelfile.desc}
|
|
||||||
</div>
|
|
||||||
{#if modelfile.user}
|
|
||||||
<div class="mt-0.5 text-sm font-normal text-gray-500 dark:text-gray-500">
|
|
||||||
By <a href="https://openwebui.com/m/{modelfile.user.username}"
|
|
||||||
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
|
|
||||||
|
|
||||||
<div>{$i18n.t('How can I help you today?')}</div>
|
<div
|
||||||
{/if}
|
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
||||||
|
{#if modelfile}
|
||||||
|
{modelfile.title}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Hello, {{name}}', { name: $user.name })}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div in:fade={{ duration: 200, delay: 200 }}>
|
||||||
|
{#if modelfile}
|
||||||
|
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
{modelfile.desc}
|
||||||
|
</div>
|
||||||
|
{#if modelfile.user}
|
||||||
|
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
|
||||||
|
By <a href="https://openwebui.com/m/{modelfile.user.username}"
|
||||||
|
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class=" font-medium text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t('How can I help you today?')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
|
||||||
|
<Suggestions {suggestionPrompts} {submitPrompt} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/key}
|
||||||
@@ -133,23 +133,26 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderLatex = () => {
|
const renderLatex = () => {
|
||||||
let chatMessageElements = document.getElementsByClassName('chat-assistant');
|
let chatMessageElements = document
|
||||||
// let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
|
.getElementById(`message-${message.id}`)
|
||||||
|
?.getElementsByClassName('chat-assistant');
|
||||||
|
|
||||||
for (const element of chatMessageElements) {
|
if (chatMessageElements) {
|
||||||
auto_render(element, {
|
for (const element of chatMessageElements) {
|
||||||
// customised options
|
auto_render(element, {
|
||||||
// • auto-render specific keys, e.g.:
|
// customised options
|
||||||
delimiters: [
|
// • auto-render specific keys, e.g.:
|
||||||
{ left: '$$', right: '$$', display: false },
|
delimiters: [
|
||||||
{ left: '$ ', right: ' $', display: false },
|
{ left: '$$', right: '$$', display: false },
|
||||||
{ left: '\\(', right: '\\)', display: false },
|
{ left: '$ ', right: ' $', display: false },
|
||||||
{ left: '\\[', right: '\\]', display: false },
|
{ left: '\\(', right: '\\)', display: false },
|
||||||
{ left: '[ ', right: ' ]', display: false }
|
{ left: '\\[', right: '\\]', display: false },
|
||||||
],
|
{ left: '[ ', right: ' ]', display: false }
|
||||||
// • rendering keys, e.g.:
|
],
|
||||||
throwOnError: false
|
// • rendering keys, e.g.:
|
||||||
});
|
throwOnError: false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -322,9 +325,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key message.id}
|
{#key message.id}
|
||||||
<div class=" flex w-full message-{message.id}">
|
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
src={modelfiles[message.model]?.imageUrl ??
|
||||||
|
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="w-full overflow-hidden">
|
<div class="w-full overflow-hidden">
|
||||||
@@ -376,7 +380,7 @@
|
|||||||
|
|
||||||
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
|
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
}}
|
}}
|
||||||
@@ -491,7 +495,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<Tooltip content="Edit" placement="bottom">
|
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
@@ -518,7 +522,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Copy" placement="bottom">
|
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
@@ -545,7 +549,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<Tooltip content="Good Response" placement="bottom">
|
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
@@ -580,7 +584,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="Bad Response" placement="bottom">
|
<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
@@ -615,7 +619,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Read Aloud" placement="bottom">
|
<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
id="speak-button-{message.id}"
|
id="speak-button-{message.id}"
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
@@ -764,7 +768,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.info}
|
{#if message.info}
|
||||||
<Tooltip content="Generation Info" placement="bottom">
|
<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class=" {isLastMessage
|
class=" {isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
@@ -793,7 +797,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isLastMessage && !readOnly}
|
{#if isLastMessage && !readOnly}
|
||||||
<Tooltip content="Continue Response" placement="bottom">
|
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
@@ -825,7 +829,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="Regenerate" placement="bottom">
|
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
id="save-edit-message-button"
|
id="save-edit-message-button"
|
||||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
|
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
}}
|
}}
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<Tooltip content="Edit" placement="bottom">
|
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Copy" placement="bottom">
|
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if !isFirstMessage && !readOnly}
|
{#if !isFirstMessage && !readOnly}
|
||||||
<Tooltip content="Delete" placement="bottom">
|
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
export let selectedModels = [''];
|
export let selectedModels = [''];
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
|
export let showSetDefault = true;
|
||||||
|
|
||||||
const saveDefaultModel = async () => {
|
const saveDefaultModel = async () => {
|
||||||
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
||||||
if (hasEmptyModel.length) {
|
if (hasEmptyModel.length) {
|
||||||
@@ -38,9 +40,9 @@
|
|||||||
|
|
||||||
<div class="flex flex-col mt-0.5 w-full">
|
<div class="flex flex-col mt-0.5 w-full">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full max-w-fit">
|
||||||
<div class="overflow-hidden w-full">
|
<div class="overflow-hidden w-full">
|
||||||
<div class="mr-0.5 max-w-full">
|
<div class="mr-1 max-w-full">
|
||||||
<Selector
|
<Selector
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
items={$models
|
items={$models
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
|
|
||||||
{#if selectedModelIdx === 0}
|
{#if selectedModelIdx === 0}
|
||||||
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
|
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
|
||||||
<Tooltip content="Add Model">
|
<Tooltip content={$i18n.t('Add Model')}>
|
||||||
<button
|
<button
|
||||||
class=" "
|
class=" "
|
||||||
{disabled}
|
{disabled}
|
||||||
@@ -69,9 +71,9 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="size-3.5"
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -92,9 +94,9 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="size-3.5"
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -106,6 +108,8 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
{#if showSetDefault}
|
||||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
||||||
</div>
|
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select } from 'bits-ui';
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||||
@@ -21,10 +21,15 @@
|
|||||||
export let value = '';
|
export let value = '';
|
||||||
export let placeholder = 'Select a model';
|
export let placeholder = 'Select a model';
|
||||||
export let searchEnabled = true;
|
export let searchEnabled = true;
|
||||||
export let searchPlaceholder = 'Search a model';
|
export let searchPlaceholder = $i18n.t('Search a model');
|
||||||
|
|
||||||
export let items = [{ value: 'mango', label: 'Mango' }];
|
export let items = [{ value: 'mango', label: 'Mango' }];
|
||||||
|
|
||||||
|
export let className = 'max-w-lg';
|
||||||
|
|
||||||
|
let selectedModel = '';
|
||||||
|
$: selectedModel = items.find((item) => item.value === value) ?? '';
|
||||||
|
|
||||||
let searchValue = '';
|
let searchValue = '';
|
||||||
let ollamaVersion = null;
|
let ollamaVersion = null;
|
||||||
|
|
||||||
@@ -175,27 +180,28 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select.Root
|
<DropdownMenu.Root
|
||||||
{items}
|
|
||||||
onOpenChange={async () => {
|
onOpenChange={async () => {
|
||||||
searchValue = '';
|
searchValue = '';
|
||||||
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||||
}}
|
}}
|
||||||
selected={items.find((item) => item.value === value) ?? ''}
|
|
||||||
onSelectedChange={(selectedItem) => {
|
|
||||||
value = selectedItem.value;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Select.Trigger class="relative w-full" aria-label={placeholder}>
|
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
|
||||||
<Select.Value
|
<div
|
||||||
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
||||||
{placeholder}
|
>
|
||||||
/>
|
{#if selectedModel}
|
||||||
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
{selectedModel.label}
|
||||||
</Select.Trigger>
|
{:else}
|
||||||
<Select.Content
|
{placeholder}
|
||||||
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
{/if}
|
||||||
|
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class=" z-40 w-full {className} justify-start rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none "
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
|
side={'bottom-start'}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
@@ -214,12 +220,13 @@
|
|||||||
<hr class="border-gray-100 dark:border-gray-800" />
|
<hr class="border-gray-100 dark:border-gray-800" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="px-3 my-2 max-h-72 overflow-y-auto">
|
<div class="px-3 my-2 max-h-72 overflow-y-auto scrollbar-none">
|
||||||
{#each filteredItems as item}
|
{#each filteredItems as item}
|
||||||
<Select.Item
|
<DropdownMenu.Item
|
||||||
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
value={item.value}
|
on:click={() => {
|
||||||
label={item.label}
|
value = item.value;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
@@ -287,7 +294,7 @@
|
|||||||
<Check />
|
<Check />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Select.Item>
|
</DropdownMenu.Item>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||||
@@ -385,5 +392,16 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</Select.Content>
|
</DropdownMenu.Content>
|
||||||
</Select.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.scrollbar-none:active::-webkit-scrollbar-thumb,
|
||||||
|
.scrollbar-none:focus::-webkit-scrollbar-thumb,
|
||||||
|
.scrollbar-none:hover::-webkit-scrollbar-thumb {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.scrollbar-none::-webkit-scrollbar-thumb {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
saveSettings({
|
saveSettings({
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -492,8 +492,8 @@
|
|||||||
<input
|
<input
|
||||||
id="steps-range"
|
id="steps-range"
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="-1"
|
||||||
max="16000"
|
max="10240000"
|
||||||
step="1"
|
step="1"
|
||||||
bind:value={options.num_ctx}
|
bind:value={options.num_ctx}
|
||||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||||
@@ -504,9 +504,8 @@
|
|||||||
bind:value={options.num_ctx}
|
bind:value={options.num_ctx}
|
||||||
type="number"
|
type="number"
|
||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="1"
|
min="-1"
|
||||||
max="16000"
|
step="10"
|
||||||
step="1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,14 +75,16 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateConfigHandler = async () => {
|
const updateConfigHandler = async () => {
|
||||||
const res = await updateAudioConfig(localStorage.token, {
|
if (TTSEngine === 'openai') {
|
||||||
url: OpenAIUrl,
|
const res = await updateAudioConfig(localStorage.token, {
|
||||||
key: OpenAIKey
|
url: OpenAIUrl,
|
||||||
});
|
key: OpenAIKey
|
||||||
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||||
OpenAIKey = res.OPENAI_API_KEY;
|
OpenAIKey = res.OPENAI_API_KEY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { chats, user } from '$lib/stores';
|
import { chats, user, config } from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createNewChat,
|
createNewChat,
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin' && ($config?.admin_export_enabled ?? true)}
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -147,8 +147,8 @@
|
|||||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||||
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
|
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
|
||||||
<option value="light">☀️ {$i18n.t('Light')}</option>
|
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||||
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
<!-- <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
||||||
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
updateImageSize,
|
updateImageSize,
|
||||||
getImageSteps,
|
getImageSteps,
|
||||||
updateImageSteps,
|
updateImageSteps,
|
||||||
getOpenAIKey,
|
getOpenAIConfig,
|
||||||
updateOpenAIKey
|
updateOpenAIConfig
|
||||||
} from '$lib/apis/images';
|
} from '$lib/apis/images';
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
let AUTOMATIC1111_BASE_URL = '';
|
let AUTOMATIC1111_BASE_URL = '';
|
||||||
let COMFYUI_BASE_URL = '';
|
let COMFYUI_BASE_URL = '';
|
||||||
|
|
||||||
|
let OPENAI_API_BASE_URL = '';
|
||||||
let OPENAI_API_KEY = '';
|
let OPENAI_API_KEY = '';
|
||||||
|
|
||||||
let selectedModel = '';
|
let selectedModel = '';
|
||||||
@@ -131,7 +132,10 @@
|
|||||||
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
||||||
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
||||||
|
|
||||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
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);
|
imageSize = await getImageSize(localStorage.token);
|
||||||
steps = await getImageSteps(localStorage.token);
|
steps = await getImageSteps(localStorage.token);
|
||||||
@@ -149,7 +153,7 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
if (imageGenerationEngine === 'openai') {
|
if (imageGenerationEngine === 'openai') {
|
||||||
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
await updateOpenAIConfig(localStorage.token, OPENAI_API_BASE_URL, OPENAI_API_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||||
@@ -300,13 +304,22 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if imageGenerationEngine === 'openai'}
|
{:else if imageGenerationEngine === 'openai'}
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
|
<div>
|
||||||
<div class="flex w-full">
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
|
||||||
<div class="flex-1 mr-2">
|
|
||||||
|
<div class="flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter API Key')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={OPENAI_API_BASE_URL}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
bind:value={OPENAI_API_KEY}
|
bind:value={OPENAI_API_KEY}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,19 +332,39 @@
|
|||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<select
|
{#if imageGenerationEngine === 'openai' && !OPENAI_API_BASE_URL.includes('https://api.openai.com')}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
<div class="flex w-full">
|
||||||
bind:value={selectedModel}
|
<div class="flex-1">
|
||||||
placeholder={$i18n.t('Select a model')}
|
<input
|
||||||
required
|
list="model-list"
|
||||||
>
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
{#if !selectedModel}
|
bind:value={selectedModel}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
placeholder="Select a model"
|
||||||
{/if}
|
/>
|
||||||
{#each models ?? [] as model}
|
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
<datalist id="model-list">
|
||||||
{/each}
|
{#each models ?? [] as model}
|
||||||
</select>
|
<option value={model.id}>{model.name}</option>
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm 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>
|
||||||
|
|||||||
@@ -17,11 +17,17 @@
|
|||||||
let titleAutoGenerateModelExternal = '';
|
let titleAutoGenerateModelExternal = '';
|
||||||
let fullScreenMode = false;
|
let fullScreenMode = false;
|
||||||
let titleGenerationPrompt = '';
|
let titleGenerationPrompt = '';
|
||||||
|
let splitLargeChunks = false;
|
||||||
|
|
||||||
// Interface
|
// Interface
|
||||||
let promptSuggestions = [];
|
let promptSuggestions = [];
|
||||||
let showUsername = false;
|
let showUsername = false;
|
||||||
|
|
||||||
|
const toggleSplitLargeChunks = async () => {
|
||||||
|
splitLargeChunks = !splitLargeChunks;
|
||||||
|
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||||
|
};
|
||||||
|
|
||||||
const toggleFullScreenMode = async () => {
|
const toggleFullScreenMode = async () => {
|
||||||
fullScreenMode = !fullScreenMode;
|
fullScreenMode = !fullScreenMode;
|
||||||
saveSettings({ fullScreenMode: fullScreenMode });
|
saveSettings({ fullScreenMode: fullScreenMode });
|
||||||
@@ -100,6 +106,7 @@
|
|||||||
responseAutoCopy = settings.responseAutoCopy ?? false;
|
responseAutoCopy = settings.responseAutoCopy ?? false;
|
||||||
showUsername = settings.showUsername ?? false;
|
showUsername = settings.showUsername ?? false;
|
||||||
fullScreenMode = settings.fullScreenMode ?? false;
|
fullScreenMode = settings.fullScreenMode ?? false;
|
||||||
|
splitLargeChunks = settings.splitLargeChunks ?? false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -197,6 +204,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Fluidly stream large external response chunks')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
on:click={() => {
|
||||||
|
toggleSplitLargeChunks();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if splitLargeChunks === true}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
uploadModel
|
uploadModel
|
||||||
} from '$lib/apis/ollama';
|
} from '$lib/apis/ollama';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { WEBUI_NAME, models, user } from '$lib/stores';
|
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user } from '$lib/stores';
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
let liteLLMRPM = '';
|
let liteLLMRPM = '';
|
||||||
let liteLLMMaxTokens = '';
|
let liteLLMMaxTokens = '';
|
||||||
|
|
||||||
let deleteLiteLLMModelId = '';
|
let deleteLiteLLMModelName = '';
|
||||||
|
|
||||||
$: liteLLMModelName = liteLLMModel;
|
$: liteLLMModelName = liteLLMModel;
|
||||||
|
|
||||||
@@ -50,12 +50,6 @@
|
|||||||
let showExperimentalOllama = false;
|
let showExperimentalOllama = false;
|
||||||
let ollamaVersion = '';
|
let ollamaVersion = '';
|
||||||
const MAX_PARALLEL_DOWNLOADS = 3;
|
const MAX_PARALLEL_DOWNLOADS = 3;
|
||||||
const modelDownloadQueue = queue(
|
|
||||||
(task: { modelName: string }, cb) =>
|
|
||||||
pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }),
|
|
||||||
MAX_PARALLEL_DOWNLOADS
|
|
||||||
);
|
|
||||||
let modelDownloadStatus: Record<string, any> = {};
|
|
||||||
|
|
||||||
let modelTransferring = false;
|
let modelTransferring = false;
|
||||||
let modelTag = '';
|
let modelTag = '';
|
||||||
@@ -140,7 +134,8 @@
|
|||||||
|
|
||||||
const pullModelHandler = async () => {
|
const pullModelHandler = async () => {
|
||||||
const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||||
if (modelDownloadStatus[sanitizedModelTag]) {
|
console.log($MODEL_DOWNLOAD_POOL);
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
||||||
modelTag: sanitizedModelTag
|
modelTag: sanitizedModelTag
|
||||||
@@ -148,40 +143,117 @@
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Object.keys(modelDownloadStatus).length === 3) {
|
if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modelTransferring = true;
|
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
modelDownloadQueue.push(
|
if (res) {
|
||||||
{ modelName: sanitizedModelTag },
|
const reader = res.body
|
||||||
async (data: { modelName: string; success: boolean; error?: Error }) => {
|
.pipeThrough(new TextDecoderStream())
|
||||||
const { modelName } = data;
|
.pipeThrough(splitStream('\n'))
|
||||||
// Remove the downloaded model
|
.getReader();
|
||||||
delete modelDownloadStatus[modelName];
|
|
||||||
|
|
||||||
modelDownloadStatus = { ...modelDownloadStatus };
|
while (true) {
|
||||||
|
try {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
if (!data.success) {
|
let lines = value.split('\n');
|
||||||
toast.error(data.error);
|
|
||||||
} else {
|
|
||||||
toast.success(
|
|
||||||
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName })
|
|
||||||
);
|
|
||||||
|
|
||||||
const notification = new Notification($WEBUI_NAME, {
|
for (const line of lines) {
|
||||||
body: $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }),
|
if (line !== '') {
|
||||||
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
let data = JSON.parse(line);
|
||||||
});
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
throw data.error;
|
||||||
|
}
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
|
|
||||||
models.set(await getModels());
|
if (data.id) {
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
requestId: data.id,
|
||||||
|
reader,
|
||||||
|
done: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status) {
|
||||||
|
if (data.digest) {
|
||||||
|
let downloadProgress = 0;
|
||||||
|
if (data.completed) {
|
||||||
|
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
|
||||||
|
} else {
|
||||||
|
downloadProgress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
pullProgress: downloadProgress,
|
||||||
|
digest: data.digest
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success(data.status);
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
done: data.status === 'success'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
if (typeof error !== 'string') {
|
||||||
|
error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error);
|
||||||
|
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
|
||||||
|
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
|
||||||
|
toast.success(
|
||||||
|
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
|
||||||
|
modelName: sanitizedModelTag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
} else {
|
||||||
|
toast.error('Download canceled');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
modelTag = '';
|
modelTag = '';
|
||||||
modelTransferring = false;
|
modelTransferring = false;
|
||||||
@@ -352,88 +424,18 @@
|
|||||||
models.set(await getModels());
|
models.set(await getModels());
|
||||||
};
|
};
|
||||||
|
|
||||||
const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
|
const cancelModelPullHandler = async (model: string) => {
|
||||||
const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
|
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model];
|
||||||
(error) => {
|
if (reader) {
|
||||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
await reader.cancel();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res) {
|
await cancelOllamaRequest(localStorage.token, requestId);
|
||||||
const reader = res.body
|
delete $MODEL_DOWNLOAD_POOL[model];
|
||||||
.pipeThrough(new TextDecoderStream())
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
.pipeThrough(splitStream('\n'))
|
...$MODEL_DOWNLOAD_POOL
|
||||||
.getReader();
|
});
|
||||||
|
await deleteModel(localStorage.token, model);
|
||||||
while (true) {
|
toast.success(`${model} download has been canceled`);
|
||||||
try {
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
let lines = value.split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line !== '') {
|
|
||||||
let data = JSON.parse(line);
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
throw data.error;
|
|
||||||
}
|
|
||||||
if (data.detail) {
|
|
||||||
throw data.detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.id) {
|
|
||||||
modelDownloadStatus[opts.modelName] = {
|
|
||||||
...modelDownloadStatus[opts.modelName],
|
|
||||||
requestId: data.id,
|
|
||||||
reader,
|
|
||||||
done: false
|
|
||||||
};
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.status) {
|
|
||||||
if (data.digest) {
|
|
||||||
let downloadProgress = 0;
|
|
||||||
if (data.completed) {
|
|
||||||
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
|
|
||||||
} else {
|
|
||||||
downloadProgress = 100;
|
|
||||||
}
|
|
||||||
modelDownloadStatus[opts.modelName] = {
|
|
||||||
...modelDownloadStatus[opts.modelName],
|
|
||||||
pullProgress: downloadProgress,
|
|
||||||
digest: data.digest
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
toast.success(data.status);
|
|
||||||
|
|
||||||
modelDownloadStatus[opts.modelName] = {
|
|
||||||
...modelDownloadStatus[opts.modelName],
|
|
||||||
done: data.status === 'success'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
if (typeof error !== 'string') {
|
|
||||||
error = error.message;
|
|
||||||
}
|
|
||||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(modelDownloadStatus[opts.modelName]);
|
|
||||||
|
|
||||||
if (modelDownloadStatus[opts.modelName].done) {
|
|
||||||
opts.callback({ success: true, modelName: opts.modelName });
|
|
||||||
} else {
|
|
||||||
opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -472,7 +474,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteLiteLLMModelHandler = async () => {
|
const deleteLiteLLMModelHandler = async () => {
|
||||||
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelId).catch(
|
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelName).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
@@ -485,7 +487,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLiteLLMModelId = '';
|
deleteLiteLLMModelName = '';
|
||||||
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
||||||
models.set(await getModels());
|
models.set(await getModels());
|
||||||
};
|
};
|
||||||
@@ -503,18 +505,6 @@
|
|||||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||||
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelModelPullHandler = async (model: string) => {
|
|
||||||
const { reader, requestId } = modelDownloadStatus[model];
|
|
||||||
if (reader) {
|
|
||||||
await reader.cancel();
|
|
||||||
|
|
||||||
await cancelOllamaRequest(localStorage.token, requestId);
|
|
||||||
delete modelDownloadStatus[model];
|
|
||||||
await deleteModel(localStorage.token, model);
|
|
||||||
toast.success(`${model} download has been canceled`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
@@ -643,9 +633,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if Object.keys(modelDownloadStatus).length > 0}
|
{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
|
||||||
{#each Object.keys(modelDownloadStatus) as model}
|
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||||
{#if 'pullProgress' in modelDownloadStatus[model]}
|
{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="font-medium mb-1">{model}</div>
|
<div class="font-medium mb-1">{model}</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
@@ -655,10 +645,10 @@
|
|||||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||||
style="width: {Math.max(
|
style="width: {Math.max(
|
||||||
15,
|
15,
|
||||||
modelDownloadStatus[model].pullProgress ?? 0
|
$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
|
||||||
)}%"
|
)}%"
|
||||||
>
|
>
|
||||||
{modelDownloadStatus[model].pullProgress ?? 0}%
|
{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -689,9 +679,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{#if 'digest' in modelDownloadStatus[model]}
|
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
|
||||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||||
{modelDownloadStatus[model].digest}
|
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1099,14 +1089,14 @@
|
|||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={deleteLiteLLMModelId}
|
bind:value={deleteLiteLLMModelName}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
{#if !deleteLiteLLMModelId}
|
{#if !deleteLiteLLMModelName}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each liteLLMModelInfo as model}
|
{#each liteLLMModelInfo as model}
|
||||||
<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.model_name} class="bg-gray-100 dark:bg-gray-700"
|
||||||
>{model.model_name}</option
|
>{model.model_name}</option
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { models, settings, user } from '$lib/stores';
|
import { models, settings, user } from '$lib/stores';
|
||||||
|
|
||||||
import { getModels } from '$lib/utils';
|
import { getModels as _getModels } from '$lib/utils';
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Account from './Settings/Account.svelte';
|
import Account from './Settings/Account.svelte';
|
||||||
@@ -23,16 +23,20 @@
|
|||||||
const saveSettings = async (updated) => {
|
const saveSettings = async (updated) => {
|
||||||
console.log(updated);
|
console.log(updated);
|
||||||
await settings.set({ ...$settings, ...updated });
|
await settings.set({ ...$settings, ...updated });
|
||||||
await models.set(await getModels(localStorage.token));
|
await models.set(await getModels());
|
||||||
localStorage.setItem('settings', JSON.stringify($settings));
|
localStorage.setItem('settings', JSON.stringify($settings));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getModels = async () => {
|
||||||
|
return await _getModels(localStorage.token);
|
||||||
|
};
|
||||||
|
|
||||||
let selectedTab = 'general';
|
let selectedTab = 'general';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div class="text-gray-700 dark:text-gray-100">
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@@ -52,7 +56,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
<Modal bind:show size="sm">
|
<Modal bind:show size="sm">
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-0.5">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@@ -91,10 +91,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
|
||||||
|
|
||||||
{#if chat}
|
{#if chat}
|
||||||
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
<div class="px-5 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||||
<div class=" text-sm dark:text-gray-300 mb-1">
|
<div class=" text-sm dark:text-gray-300 mb-1">
|
||||||
{#if chat.share_id}
|
{#if chat.share_id}
|
||||||
<a href="/s/{chat.share_id}" target="_blank"
|
<a href="/s/{chat.share_id}" target="_blank"
|
||||||
@@ -134,11 +133,36 @@
|
|||||||
<button
|
<button
|
||||||
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
|
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||||
type="button"
|
type="button"
|
||||||
on:pointerdown={() => {
|
|
||||||
shareLocalChat();
|
|
||||||
}}
|
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
copyToClipboard(shareUrl);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
if (isSafari) {
|
||||||
|
// Oh, Safari, you're so special, let's give you some extra love and attention
|
||||||
|
console.log('isSafari');
|
||||||
|
|
||||||
|
const getUrlPromise = async () => {
|
||||||
|
const url = await shareLocalChat();
|
||||||
|
return new Blob([url], { type: 'text/plain' });
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'text/plain': getUrlPromise()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
console.log('Async: Copying to clipboard was successful!');
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Async: Could not copy text: ', error);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
copyToClipboard(await shareLocalChat());
|
||||||
|
}
|
||||||
|
|
||||||
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
|
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div class="text-gray-700 dark:text-gray-100">
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
bind:this={modalElement}
|
bind:this={modalElement}
|
||||||
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
|
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
|
||||||
in:fade={{ duration: 10 }}
|
in:fade={{ duration: 10 }}
|
||||||
on:click={() => {
|
on:mousedown={() => {
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
size
|
size
|
||||||
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
||||||
in:flyAndScale
|
in:flyAndScale
|
||||||
on:click={(e) => {
|
on:mousedown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TagInput from './Tags/TagInput.svelte';
|
import TagInput from './Tags/TagInput.svelte';
|
||||||
import TagList from './Tags/TagList.svelte';
|
import TagList from './Tags/TagList.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let tags = [];
|
export let tags = [];
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TagInput
|
<TagInput
|
||||||
label={tags.length == 0 ? 'Add Tags' : ''}
|
label={tags.length == 0 ? $i18n.t('Add Tags') : ''}
|
||||||
on:add={(e) => {
|
on:add={(e) => {
|
||||||
addTag(e.detail);
|
addTag(e.detail);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
|
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@@ -116,8 +116,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
<form
|
<form
|
||||||
@@ -161,7 +159,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-5 text-sm font-medium">
|
<div class="flex justify-end pt-5 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
@@ -95,8 +95,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
<form
|
<form
|
||||||
@@ -111,28 +109,18 @@
|
|||||||
|
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div
|
<div
|
||||||
class="bg-gray-200 dark:bg-gray-600 font-bold px-3 py-1 border border-r-0 dark:border-gray-600 rounded-l-lg flex items-center"
|
class="bg-gray-200 dark:bg-gray-800 font-bold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center"
|
||||||
>
|
>
|
||||||
#
|
#
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-r-lg py-2.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full rounded-r-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={doc.name}
|
bind:value={doc.name}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="flex-1">
|
|
||||||
<input
|
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
|
||||||
type="text"
|
|
||||||
bind:value={doc.name}
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
@@ -140,7 +128,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={doc.title}
|
bind:value={doc.title}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -150,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
<div class=" mb-2 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
||||||
|
|
||||||
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
||||||
</div>
|
</div>
|
||||||
@@ -158,7 +146,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-5 text-sm font-medium">
|
<div class="flex justify-end pt-5 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
updateQuerySettings,
|
updateQuerySettings,
|
||||||
resetVectorDB,
|
resetVectorDB,
|
||||||
getEmbeddingConfig,
|
getEmbeddingConfig,
|
||||||
updateEmbeddingConfig
|
updateEmbeddingConfig,
|
||||||
|
getRerankingConfig,
|
||||||
|
updateRerankingConfig
|
||||||
} from '$lib/apis/rag';
|
} from '$lib/apis/rag';
|
||||||
|
|
||||||
import { documents, models } from '$lib/stores';
|
import { documents, models } from '$lib/stores';
|
||||||
@@ -23,11 +25,13 @@
|
|||||||
|
|
||||||
let scanDirLoading = false;
|
let scanDirLoading = false;
|
||||||
let updateEmbeddingModelLoading = false;
|
let updateEmbeddingModelLoading = false;
|
||||||
|
let updateRerankingModelLoading = false;
|
||||||
|
|
||||||
let showResetConfirm = false;
|
let showResetConfirm = false;
|
||||||
|
|
||||||
let embeddingEngine = '';
|
let embeddingEngine = '';
|
||||||
let embeddingModel = '';
|
let embeddingModel = '';
|
||||||
|
let rerankingModel = '';
|
||||||
|
|
||||||
let OpenAIKey = '';
|
let OpenAIKey = '';
|
||||||
let OpenAIUrl = '';
|
let OpenAIUrl = '';
|
||||||
@@ -38,7 +42,9 @@
|
|||||||
|
|
||||||
let querySettings = {
|
let querySettings = {
|
||||||
template: '',
|
template: '',
|
||||||
k: 4
|
r: 0.0,
|
||||||
|
k: 4,
|
||||||
|
hybrid: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const scanHandler = async () => {
|
const scanHandler = async () => {
|
||||||
@@ -115,6 +121,35 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rerankingModelUpdateHandler = async () => {
|
||||||
|
console.log('Update reranking model attempt:', rerankingModel);
|
||||||
|
|
||||||
|
updateRerankingModelLoading = true;
|
||||||
|
const res = await updateRerankingConfig(localStorage.token, {
|
||||||
|
reranking_model: rerankingModel
|
||||||
|
}).catch(async (error) => {
|
||||||
|
toast.error(error);
|
||||||
|
await setRerankingConfig();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
updateRerankingModelLoading = false;
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
console.log('rerankingModelUpdateHandler:', res);
|
||||||
|
if (res.status === true) {
|
||||||
|
if (rerankingModel === '') {
|
||||||
|
toast.success($i18n.t('Reranking model disabled', res), {
|
||||||
|
duration: 1000 * 10
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success($i18n.t('Reranking model set to "{{reranking_model}}"', res), {
|
||||||
|
duration: 1000 * 10
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
const res = await updateRAGConfig(localStorage.token, {
|
const res = await updateRAGConfig(localStorage.token, {
|
||||||
pdf_extract_images: pdfExtractImages,
|
pdf_extract_images: pdfExtractImages,
|
||||||
@@ -138,6 +173,20 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setRerankingConfig = async () => {
|
||||||
|
const rerankingConfig = await getRerankingConfig(localStorage.token);
|
||||||
|
|
||||||
|
if (rerankingConfig) {
|
||||||
|
rerankingModel = rerankingConfig.reranking_model;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleHybridSearch = async () => {
|
||||||
|
querySettings.hybrid = !querySettings.hybrid;
|
||||||
|
|
||||||
|
querySettings = await updateQuerySettings(localStorage.token, querySettings);
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const res = await getRAGConfig(localStorage.token);
|
const res = await getRAGConfig(localStorage.token);
|
||||||
|
|
||||||
@@ -149,6 +198,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await setEmbeddingConfig();
|
await setEmbeddingConfig();
|
||||||
|
await setRerankingConfig();
|
||||||
|
|
||||||
querySettings = await getQuerySettings(localStorage.token);
|
querySettings = await getQuerySettings(localStorage.token);
|
||||||
});
|
});
|
||||||
@@ -165,6 +215,24 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
on:click={() => {
|
||||||
|
toggleHybridSearch();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if querySettings.hybrid === true}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
@@ -180,7 +248,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default (SentenceTransformer)')}</option>
|
<option value="">{$i18n.t('Default (SentenceTransformers)')}</option>
|
||||||
<option value="ollama">{$i18n.t('Ollama')}</option>
|
<option value="ollama">{$i18n.t('Ollama')}</option>
|
||||||
<option value="openai">{$i18n.t('OpenAI')}</option>
|
<option value="openai">{$i18n.t('OpenAI')}</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -349,6 +417,75 @@
|
|||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
|
{#if querySettings.hybrid === true}
|
||||||
|
<div class=" ">
|
||||||
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Reranking Model')}</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Update reranking model (e.g. {{model}})', {
|
||||||
|
model: rerankingModel.slice(-40)
|
||||||
|
})}
|
||||||
|
bind:value={rerankingModel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
on:click={() => {
|
||||||
|
rerankingModelUpdateHandler();
|
||||||
|
}}
|
||||||
|
disabled={updateRerankingModelLoading}
|
||||||
|
>
|
||||||
|
{#if updateRerankingModelLoading}
|
||||||
|
<div class="self-center">
|
||||||
|
<svg
|
||||||
|
class=" w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><style>
|
||||||
|
.spinner_ajPY {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner_AtaB 0.75s infinite linear;
|
||||||
|
}
|
||||||
|
@keyframes spinner_AtaB {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style><path
|
||||||
|
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||||
|
opacity=".25"
|
||||||
|
/><path
|
||||||
|
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||||
|
class="spinner_ajPY"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
|
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
|
||||||
@@ -453,12 +590,12 @@
|
|||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
<div>
|
<div class=" ">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
||||||
|
|
||||||
<div class=" flex">
|
<div class=" flex">
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class="self-center text-xs font-medium flex-1">{$i18n.t('Top K')}</div>
|
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
|
||||||
|
|
||||||
<div class="self-center p-3">
|
<div class="self-center p-3">
|
||||||
<input
|
<input
|
||||||
@@ -471,8 +608,39 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if querySettings.hybrid === true}
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit">
|
||||||
|
{$i18n.t('Minimum Score')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="self-center p-3">
|
||||||
|
<input
|
||||||
|
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder={$i18n.t('Enter Score')}
|
||||||
|
bind:value={querySettings.r}
|
||||||
|
autocomplete="off"
|
||||||
|
min="0.0"
|
||||||
|
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if querySettings.hybrid === true}
|
||||||
|
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -483,8 +651,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
|
||||||
|
|
||||||
{#if showResetConfirm}
|
{#if showResetConfirm}
|
||||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
@@ -581,7 +747,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
{$i18n.t('Save')}
|
||||||
|
|||||||
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user