Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e843ab563 | ||
|
|
b3a0d47a67 | ||
|
|
d492d29153 | ||
|
|
ed21f9f906 | ||
|
|
bdadfc958c | ||
|
|
3bbd238d7d | ||
|
|
4eecdbadd3 | ||
|
|
a33b0abbe0 | ||
|
|
3d0f457306 | ||
|
|
ca5f1c1efb | ||
|
|
e72592eac3 | ||
|
|
72e9cecb17 | ||
|
|
27e7494853 | ||
|
|
f0a8aca0e3 | ||
|
|
4514595811 | ||
|
|
e0bcbf0b79 | ||
|
|
285725eaab | ||
|
|
9a4770eb17 | ||
|
|
8a11985fce | ||
|
|
731b3654d8 | ||
|
|
da4e976d5c | ||
|
|
b74739f91b | ||
|
|
6b5c452a3c | ||
|
|
40075f69f9 | ||
|
|
7702803e5c | ||
|
|
3731f1950a | ||
|
|
9ab47cf628 | ||
|
|
676688aabf | ||
|
|
5958d8775c | ||
|
|
dfd5de9381 | ||
|
|
ac0164c4cc | ||
|
|
9e258ea466 | ||
|
|
0aacb95700 | ||
|
|
3668179dfe | ||
|
|
fa26b8d7cf | ||
|
|
07283ca0cf | ||
|
|
917dde7edf | ||
|
|
8102347296 | ||
|
|
736d8586f1 | ||
|
|
c97d4bbb50 | ||
|
|
553dcd208c | ||
|
|
e19c327279 | ||
|
|
6d0e4852cc | ||
|
|
d258523a3c | ||
|
|
dbc352f01b | ||
|
|
5e8a74ef74 | ||
|
|
3cc3671e74 | ||
|
|
d4d7c3d8b6 | ||
|
|
69083b6485 | ||
|
|
feba1f2e3c | ||
|
|
bb20251b43 | ||
|
|
1ec2e74b07 | ||
|
|
db6f6e62b9 | ||
|
|
a99743c1c8 | ||
|
|
04dd0f2b72 | ||
|
|
b4eb630fc2 | ||
|
|
8494b97e05 | ||
|
|
a98ff3c151 | ||
|
|
b16967a29f | ||
|
|
73afa73286 | ||
|
|
63ab526bfb | ||
|
|
cf92909d9a | ||
|
|
ca6a71749c | ||
|
|
79778fa221 | ||
|
|
6806b01327 | ||
|
|
6f06577f18 | ||
|
|
bc4bb0b4e5 | ||
|
|
f40aee9d2e | ||
|
|
26bf1ef621 | ||
|
|
03618fd2e0 | ||
|
|
fa3721bcde | ||
|
|
a049b38ab5 | ||
|
|
540a669dae | ||
|
|
e266504463 | ||
|
|
f07172bfe6 | ||
|
|
856845e5f2 | ||
|
|
14c0efe300 | ||
|
|
eb10001eb7 | ||
|
|
90c3d68f00 | ||
|
|
97098edfeb | ||
|
|
9f9122b6d7 | ||
|
|
8dcb3d78dc | ||
|
|
4dd77b785a | ||
|
|
7d7a29cfb9 | ||
|
|
f462744fc8 | ||
|
|
9ab97b834a | ||
|
|
7ba7b959a8 | ||
|
|
b094153af2 | ||
|
|
0ef27bfc5e | ||
|
|
a62b0924df | ||
|
|
eff736acd2 | ||
|
|
38cc0da990 | ||
|
|
37ff651723 | ||
|
|
fae7db89e6 | ||
|
|
ef21397f0d | ||
|
|
2011cbd210 | ||
|
|
ebcc5be1bf | ||
|
|
3661dd16b8 | ||
|
|
2aa5d26735 | ||
|
|
d50639971a | ||
|
|
fede1e9e3b | ||
|
|
f7777f489e | ||
|
|
93ae6084d6 | ||
|
|
3b39334bae | ||
|
|
da8ee43481 | ||
|
|
a843e81aaf | ||
|
|
9bcd4ce5c0 | ||
|
|
b38abf23bb | ||
|
|
7f2f45e176 | ||
|
|
0444497eea | ||
|
|
1d20c27553 | ||
|
|
8f23df5749 | ||
|
|
144581a7df | ||
|
|
7ffd75b991 | ||
|
|
ff474936f8 | ||
|
|
d7dd901f01 | ||
|
|
e3e02e04e8 | ||
|
|
f9e3c47d4a | ||
|
|
24ef5af2a1 | ||
|
|
f8e2b31756 | ||
|
|
a11887cee1 | ||
|
|
78a5748727 | ||
|
|
666a8ea5c0 | ||
|
|
0bc46b19b3 | ||
|
|
3f8af02aaa | ||
|
|
c9f5029a9d | ||
|
|
aa896ba702 | ||
|
|
c2f4eab8ed | ||
|
|
47c76ab5fe | ||
|
|
2396010d6c | ||
|
|
37285b8749 | ||
|
|
0a08a4d2fb | ||
|
|
1b7ff1c5df | ||
|
|
1d979d9b75 | ||
|
|
d34e9ef935 | ||
|
|
d752002836 | ||
|
|
7df35d978f | ||
|
|
18ea8df685 | ||
|
|
a23146ebd1 | ||
|
|
446d85474e | ||
|
|
8942d0d0e2 | ||
|
|
eb1e176f14 | ||
|
|
30e2ec7544 | ||
|
|
9cea5f75bb | ||
|
|
b8d153ebb2 | ||
|
|
781ad70598 | ||
|
|
3598aee71e | ||
|
|
e8fbb8f181 | ||
|
|
088e7b02a9 | ||
|
|
4b6ee584c2 | ||
|
|
d3ef3a7494 | ||
|
|
68d775e1ab | ||
|
|
40abddff9a | ||
|
|
68d6e738ac | ||
|
|
3ddd88dad7 | ||
|
|
42742d03d7 | ||
|
|
87f656b029 | ||
|
|
489ef9b731 | ||
|
|
51ae387d59 | ||
|
|
642c5e035d | ||
|
|
18962104a2 | ||
|
|
9eb0f89db8 | ||
|
|
1b660453f0 | ||
|
|
3b27acc77e | ||
|
|
95426fc6c9 | ||
|
|
39d3dcd032 | ||
|
|
36c0f7f273 | ||
|
|
3b112375ee | ||
|
|
47ec2425ee | ||
|
|
d157af2fc7 | ||
|
|
c1d706dc5a | ||
|
|
a7fb560e84 | ||
|
|
44bbb40551 | ||
|
|
d9a393eae1 | ||
|
|
842a6a6ee0 | ||
|
|
404b47274e | ||
|
|
8eb03c02da | ||
|
|
1bb2724282 | ||
|
|
f971ba0c0b | ||
|
|
d51e866505 | ||
|
|
068dff6aff | ||
|
|
7e6c5193d6 | ||
|
|
a6dd62082f | ||
|
|
4ae0a1f3f8 | ||
|
|
21b8072f35 | ||
|
|
a73a9c7310 | ||
|
|
08f0a19812 | ||
|
|
c9b85bd4a2 | ||
|
|
0d70d7c9ac | ||
|
|
e8ed48bd7a | ||
|
|
d5716ae751 | ||
|
|
8f6f76682b | ||
|
|
4e75150174 | ||
|
|
1436bb7c61 | ||
|
|
d60f06608e | ||
|
|
97a8491866 | ||
|
|
73899e1c0d | ||
|
|
70efbef0c7 | ||
|
|
c3c15cbb7e | ||
|
|
89e6044bdb | ||
|
|
3928ac1905 | ||
|
|
983fe4b2bc | ||
|
|
8fd128ef1c | ||
|
|
45fae65ec1 | ||
|
|
8381346378 | ||
|
|
67c2ab006d | ||
|
|
55b7c30028 | ||
|
|
ca3f8e6cb5 | ||
|
|
f611533764 | ||
|
|
78ba18a680 | ||
|
|
db58bb5f0f | ||
|
|
d5c0876a0b | ||
|
|
2389c36a70 | ||
|
|
d20601dc47 | ||
|
|
0527755600 | ||
|
|
740b6f5c17 | ||
|
|
9a6cbafdef | ||
|
|
8b13755d56 | ||
|
|
8fe2a7bb75 | ||
|
|
37a5d2c06b | ||
|
|
864646094e | ||
|
|
1b65df3acc | ||
|
|
bfc53b49fd | ||
|
|
f6dcffab13 | ||
|
|
15f6f7bd15 | ||
|
|
0d78b63805 | ||
|
|
3efb8ab6ba | ||
|
|
f5a33ed3f9 | ||
|
|
08c024d752 | ||
|
|
c83704d6ca | ||
|
|
f548762892 | ||
|
|
d0e0aba593 | ||
|
|
4e433d9015 | ||
|
|
2fedd91ed9 | ||
|
|
3cd0b1077d | ||
|
|
1f026a1811 | ||
|
|
49b4211c06 | ||
|
|
24c7990fd4 | ||
|
|
4d23957035 | ||
|
|
aa88022624 | ||
|
|
44a9b86eec | ||
|
|
647aa1966f | ||
|
|
d0e89a0318 | ||
|
|
2c061777ca | ||
|
|
f57a435576 | ||
|
|
cf317f8fdf | ||
|
|
655238dcd7 | ||
|
|
16fa454558 | ||
|
|
02f242e9e8 | ||
|
|
09514751b5 | ||
|
|
afd74213cc | ||
|
|
e4c85921b2 | ||
|
|
7725a877d6 | ||
|
|
fefa8a81d0 | ||
|
|
05ec71beb9 | ||
|
|
439ab7a335 | ||
|
|
044a0fbbbe | ||
|
|
d97a4d687e | ||
|
|
52cae406b4 | ||
|
|
d6dbd73ec9 | ||
|
|
a07051f51b | ||
|
|
e5895af7a0 | ||
|
|
c7a9b5ccfa | ||
|
|
b62d2a9b28 | ||
|
|
a392865615 | ||
|
|
3c1ea24374 | ||
|
|
62ba6a2413 | ||
|
|
b875efab1f | ||
|
|
bd45b7a04b | ||
|
|
9c01297191 | ||
|
|
6d350fb8bc | ||
|
|
7212b7c8aa | ||
|
|
9cc46629c2 | ||
|
|
a03629178e | ||
|
|
46a084d457 | ||
|
|
7aa35a3757 | ||
|
|
7f937af029 | ||
|
|
4041484a19 | ||
|
|
8a38b0a286 | ||
|
|
19d4c7f6cd | ||
|
|
4ead3dabd8 | ||
|
|
77f5d90be3 | ||
|
|
db495a1df0 | ||
|
|
8634140306 | ||
|
|
586de1f5ba | ||
|
|
2aecd7d0b9 | ||
|
|
a94c7e5c09 | ||
|
|
e475f025b7 | ||
|
|
6e934c2d17 | ||
|
|
f89fa061e8 | ||
|
|
f3c1ff9efc | ||
|
|
4547afe0b9 | ||
|
|
7955c9ba3b | ||
|
|
c52e16a844 | ||
|
|
c676a78cd7 | ||
|
|
61b2bcae47 | ||
|
|
ee59cbc634 | ||
|
|
0c552b4730 | ||
|
|
de3d49000e | ||
|
|
8c7fb0312c | ||
|
|
0ee984b444 | ||
|
|
2fadc0c68f | ||
|
|
3f31bb0975 | ||
|
|
5d1573fd1b | ||
|
|
5c6e30cb5d | ||
|
|
a55d6e6077 | ||
|
|
a48ac6a209 | ||
|
|
17c684369e | ||
|
|
11210fb6e4 | ||
|
|
824966ad4a | ||
|
|
f77073410e | ||
|
|
9cf622d981 | ||
|
|
d9a229b1ec | ||
|
|
7bc88eb00d | ||
|
|
f6efda9e2f | ||
|
|
bb53282cb0 | ||
|
|
c933865172 | ||
|
|
ce990d3859 | ||
|
|
315c8b2af3 | ||
|
|
66182bac75 | ||
|
|
5ee7da54a1 | ||
|
|
4b0c422ec5 | ||
|
|
5dacd41278 | ||
|
|
052c582930 | ||
|
|
3c0ce5d1b0 | ||
|
|
9d1aa4aebe | ||
|
|
e540f8a4e4 | ||
|
|
9c2313a1bd | ||
|
|
6ffa189814 | ||
|
|
24f84a6069 | ||
|
|
c138445944 | ||
|
|
9f32e9ef60 | ||
|
|
0c3f9a16e3 | ||
|
|
5391f4c1f7 | ||
|
|
df47c496d3 | ||
|
|
827b1e58e9 | ||
|
|
23e4d9daff | ||
|
|
d4b6b7c4e8 | ||
|
|
642c352c69 | ||
|
|
d88bd51e3c | ||
|
|
2fb27adbf6 | ||
|
|
8f939cf55b | ||
|
|
a9b148791d | ||
|
|
da403f3e3c | ||
|
|
eb01e8d275 | ||
|
|
c134eab27a | ||
|
|
320e658595 | ||
|
|
070d9083d5 | ||
|
|
bee835cb65 | ||
|
|
df09d0830a |
+2
-1
@@ -10,7 +10,8 @@ node_modules
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
__pycache__
|
__pycache__
|
||||||
.env
|
.idea
|
||||||
|
venv
|
||||||
_old
|
_old
|
||||||
uploads
|
uploads
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ jobs:
|
|||||||
done
|
done
|
||||||
echo "Service is up!"
|
echo "Service is up!"
|
||||||
|
|
||||||
|
- name: Delete Docker build cache
|
||||||
|
run: |
|
||||||
|
docker builder prune --all --force
|
||||||
|
|
||||||
- name: Preload Ollama model
|
- name: Preload Ollama model
|
||||||
run: |
|
run: |
|
||||||
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
||||||
@@ -43,7 +47,7 @@ jobs:
|
|||||||
uses: cypress-io/github-action@v6
|
uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
browser: chrome
|
browser: chrome
|
||||||
wait-on: "http://localhost:3000"
|
wait-on: 'http://localhost:3000'
|
||||||
config: baseUrl=http://localhost:3000
|
config: baseUrl=http://localhost:3000
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@@ -67,6 +71,28 @@ jobs:
|
|||||||
path: compose-logs.txt
|
path: compose-logs.txt
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
# pytest:
|
||||||
|
# name: Run Backend Tests
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# - name: Set up Python
|
||||||
|
# uses: actions/setup-python@v4
|
||||||
|
# with:
|
||||||
|
# python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
# - name: Install dependencies
|
||||||
|
# run: |
|
||||||
|
# python -m pip install --upgrade pip
|
||||||
|
# pip install -r backend/requirements.txt
|
||||||
|
|
||||||
|
# - name: pytest run
|
||||||
|
# run: |
|
||||||
|
# ls -al
|
||||||
|
# cd backend
|
||||||
|
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
|
||||||
|
|
||||||
migration_test:
|
migration_test:
|
||||||
name: Run Migration Tests
|
name: Run Migration Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -126,11 +152,11 @@ jobs:
|
|||||||
cd backend
|
cd backend
|
||||||
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
||||||
UVICORN_PID=$!
|
UVICORN_PID=$!
|
||||||
# Wait up to 20 seconds for the server to start
|
# Wait up to 40 seconds for the server to start
|
||||||
for i in {1..20}; do
|
for i in {1..40}; do
|
||||||
curl -s http://localhost:8080/api/config > /dev/null && break
|
curl -s http://localhost:8080/api/config > /dev/null && break
|
||||||
sleep 1
|
sleep 1
|
||||||
if [ $i -eq 20 ]; then
|
if [ $i -eq 40 ]; then
|
||||||
echo "Server failed to start"
|
echo "Server failed to start"
|
||||||
kill -9 $UVICORN_PID
|
kill -9 $UVICORN_PID
|
||||||
exit 1
|
exit 1
|
||||||
@@ -171,7 +197,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check that service will reconnect to postgres when connection will be closed
|
# Check that service will reconnect to postgres when connection will be closed
|
||||||
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
|
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
||||||
if [[ "$status_code" -ne 200 ]] ; then
|
if [[ "$status_code" -ne 200 ]] ; then
|
||||||
echo "Server has failed before postgres reconnect check"
|
echo "Server has failed before postgres reconnect check"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -183,7 +209,7 @@ jobs:
|
|||||||
cur = conn.cursor(); \
|
cur = conn.cursor(); \
|
||||||
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
|
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
|
||||||
|
|
||||||
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
|
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
||||||
if [[ "$status_code" -ne 200 ]] ; then
|
if [[ "$status_code" -ne 200 ]] ; then
|
||||||
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
|
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -306,3 +306,4 @@ dist
|
|||||||
# cypress artifacts
|
# cypress artifacts
|
||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
.vscode/settings.json
|
||||||
|
|||||||
@@ -5,6 +5,61 @@ 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.3.9] - 2024-07-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed.
|
||||||
|
- **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon.
|
||||||
|
- **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access.
|
||||||
|
- **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly.
|
||||||
|
- **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface.
|
||||||
|
- **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings.
|
||||||
|
- **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly.
|
||||||
|
- **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required.
|
||||||
|
- **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat.
|
||||||
|
- **✅ Boolean Values in Params**: Added support for boolean values in parameters.
|
||||||
|
- **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **⬆️ Dependency Updates**
|
||||||
|
- Upgraded 'pydantic' from version 2.7.1 to 2.8.2.
|
||||||
|
- Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31.
|
||||||
|
- Upgraded 'unstructured' from version 0.14.9 to 0.14.10.
|
||||||
|
- Upgraded 'chromadb' from version 0.5.3 to 0.5.4.
|
||||||
|
|
||||||
|
## [0.3.8] - 2024-07-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions.
|
||||||
|
- **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible.
|
||||||
|
- **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities.
|
||||||
|
- **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication.
|
||||||
|
- **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website.
|
||||||
|
- **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation.
|
||||||
|
- **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal.
|
||||||
|
- **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance.
|
||||||
|
- **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality.
|
||||||
|
- **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
|
||||||
|
- **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version.
|
||||||
|
- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
|
||||||
|
- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
|
||||||
|
- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
|
||||||
|
- **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title".
|
||||||
|
|
||||||
## [0.3.7] - 2024-06-29
|
## [0.3.7] - 2024-06-29
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -97,6 +97,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
|||||||
apt-get update && \
|
apt-get update && \
|
||||||
# Install pandoc and netcat
|
# Install pandoc and netcat
|
||||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
||||||
|
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||||
# for RAG OCR
|
# for RAG OCR
|
||||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||||
# install helper tools
|
# install helper tools
|
||||||
@@ -107,8 +108,9 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
|||||||
rm -rf /var/lib/apt/lists/*; \
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
else \
|
else \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
# Install pandoc and netcat
|
# Install pandoc, netcat and gcc
|
||||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
|
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
|
||||||
|
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||||
# for RAG OCR
|
# for RAG OCR
|
||||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||||
# cleanup
|
# cleanup
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = migrations
|
||||||
|
|
||||||
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
||||||
|
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||||
|
# string value is passed to ZoneInfo()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; This defaults
|
||||||
|
# to migrations/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path.
|
||||||
|
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||||
|
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||||
|
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||||
|
|
||||||
|
# set to 'true' to search source files recursively
|
||||||
|
# in each "version_locations" directory
|
||||||
|
# new in Alembic version 1.10
|
||||||
|
# recursive_version_locations = false
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
# sqlalchemy.url = REPLACE_WITH_DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||||
|
# hooks = ruff
|
||||||
|
# ruff.type = exec
|
||||||
|
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||||
|
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -14,7 +14,6 @@ from fastapi import (
|
|||||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from faster_whisper import WhisperModel
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
@@ -277,6 +276,8 @@ def transcribe(
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
if app.state.config.STT_ENGINE == "":
|
if app.state.config.STT_ENGINE == "":
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
whisper_kwargs = {
|
whisper_kwargs = {
|
||||||
"model_size_or_path": WHISPER_MODEL,
|
"model_size_or_path": WHISPER_MODEL,
|
||||||
"device": whisper_device_type,
|
"device": whisper_device_type,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from fastapi import (
|
|||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from faster_whisper import WhisperModel
|
|
||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
@@ -422,7 +421,7 @@ def save_url_image(url):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/generations")
|
@app.post("/generations")
|
||||||
def generate_image(
|
async def image_generations(
|
||||||
form_data: GenerateImageForm,
|
form_data: GenerateImageForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
|
|||||||
+100
-34
@@ -153,7 +153,7 @@ async def cleanup_response(
|
|||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
async def post_streaming_url(url: str, payload: str):
|
async def post_streaming_url(url: str, payload: str, stream: bool = True):
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
session = aiohttp.ClientSession(
|
session = aiohttp.ClientSession(
|
||||||
@@ -162,12 +162,20 @@ async def post_streaming_url(url: str, payload: str):
|
|||||||
r = await session.post(url, data=payload)
|
r = await session.post(url, data=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
return StreamingResponse(
|
if stream:
|
||||||
r.content,
|
return StreamingResponse(
|
||||||
status_code=r.status,
|
r.content,
|
||||||
headers=dict(r.headers),
|
status_code=r.status,
|
||||||
background=BackgroundTask(cleanup_response, response=r, session=session),
|
headers=dict(r.headers),
|
||||||
)
|
background=BackgroundTask(
|
||||||
|
cleanup_response, response=r, session=session
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
res = await r.json()
|
||||||
|
await cleanup_response(r, session)
|
||||||
|
return res
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -720,8 +728,10 @@ async def generate_chat_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
**form_data.model_dump(exclude_none=True),
|
**form_data.model_dump(exclude_none=True, exclude=["metadata"]),
|
||||||
}
|
}
|
||||||
|
if "metadata" in payload:
|
||||||
|
del payload["metadata"]
|
||||||
|
|
||||||
model_id = form_data.model
|
model_id = form_data.model
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
@@ -733,52 +743,85 @@ async def generate_chat_completion(
|
|||||||
model_info.params = model_info.params.model_dump()
|
model_info.params = model_info.params.model_dump()
|
||||||
|
|
||||||
if model_info.params:
|
if model_info.params:
|
||||||
payload["options"] = {}
|
if payload.get("options") is None:
|
||||||
|
payload["options"] = {}
|
||||||
|
|
||||||
if model_info.params.get("mirostat", None):
|
if (
|
||||||
|
model_info.params.get("mirostat", None)
|
||||||
|
and payload["options"].get("mirostat") is None
|
||||||
|
):
|
||||||
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
||||||
|
|
||||||
if model_info.params.get("mirostat_eta", None):
|
if (
|
||||||
|
model_info.params.get("mirostat_eta", None)
|
||||||
|
and payload["options"].get("mirostat_eta") is None
|
||||||
|
):
|
||||||
payload["options"]["mirostat_eta"] = model_info.params.get(
|
payload["options"]["mirostat_eta"] = model_info.params.get(
|
||||||
"mirostat_eta", None
|
"mirostat_eta", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("mirostat_tau", None):
|
if (
|
||||||
|
model_info.params.get("mirostat_tau", None)
|
||||||
|
and payload["options"].get("mirostat_tau") is None
|
||||||
|
):
|
||||||
payload["options"]["mirostat_tau"] = model_info.params.get(
|
payload["options"]["mirostat_tau"] = model_info.params.get(
|
||||||
"mirostat_tau", None
|
"mirostat_tau", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("num_ctx", None):
|
if (
|
||||||
|
model_info.params.get("num_ctx", None)
|
||||||
|
and payload["options"].get("num_ctx") is None
|
||||||
|
):
|
||||||
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
||||||
|
|
||||||
if model_info.params.get("num_batch", None):
|
if (
|
||||||
|
model_info.params.get("num_batch", None)
|
||||||
|
and payload["options"].get("num_batch") is None
|
||||||
|
):
|
||||||
payload["options"]["num_batch"] = model_info.params.get(
|
payload["options"]["num_batch"] = model_info.params.get(
|
||||||
"num_batch", None
|
"num_batch", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("num_keep", None):
|
if (
|
||||||
|
model_info.params.get("num_keep", None)
|
||||||
|
and payload["options"].get("num_keep") is None
|
||||||
|
):
|
||||||
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
|
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
|
||||||
|
|
||||||
if model_info.params.get("repeat_last_n", None):
|
if (
|
||||||
|
model_info.params.get("repeat_last_n", None)
|
||||||
|
and payload["options"].get("repeat_last_n") is None
|
||||||
|
):
|
||||||
payload["options"]["repeat_last_n"] = model_info.params.get(
|
payload["options"]["repeat_last_n"] = model_info.params.get(
|
||||||
"repeat_last_n", None
|
"repeat_last_n", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("frequency_penalty", None):
|
if (
|
||||||
|
model_info.params.get("frequency_penalty", None)
|
||||||
|
and payload["options"].get("frequency_penalty") is None
|
||||||
|
):
|
||||||
payload["options"]["repeat_penalty"] = model_info.params.get(
|
payload["options"]["repeat_penalty"] = model_info.params.get(
|
||||||
"frequency_penalty", None
|
"frequency_penalty", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("temperature", None) is not None:
|
if (
|
||||||
|
model_info.params.get("temperature", None)
|
||||||
|
and payload["options"].get("temperature") is None
|
||||||
|
):
|
||||||
payload["options"]["temperature"] = model_info.params.get(
|
payload["options"]["temperature"] = model_info.params.get(
|
||||||
"temperature", None
|
"temperature", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("seed", None):
|
if (
|
||||||
|
model_info.params.get("seed", None)
|
||||||
|
and payload["options"].get("seed") is None
|
||||||
|
):
|
||||||
payload["options"]["seed"] = model_info.params.get("seed", None)
|
payload["options"]["seed"] = model_info.params.get("seed", None)
|
||||||
|
|
||||||
if model_info.params.get("stop", None):
|
if (
|
||||||
|
model_info.params.get("stop", None)
|
||||||
|
and payload["options"].get("stop") is None
|
||||||
|
):
|
||||||
payload["options"]["stop"] = (
|
payload["options"]["stop"] = (
|
||||||
[
|
[
|
||||||
bytes(stop, "utf-8").decode("unicode_escape")
|
bytes(stop, "utf-8").decode("unicode_escape")
|
||||||
@@ -788,37 +831,56 @@ async def generate_chat_completion(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("tfs_z", None):
|
if (
|
||||||
|
model_info.params.get("tfs_z", None)
|
||||||
|
and payload["options"].get("tfs_z") is None
|
||||||
|
):
|
||||||
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
||||||
|
|
||||||
if model_info.params.get("max_tokens", None):
|
if (
|
||||||
|
model_info.params.get("max_tokens", None)
|
||||||
|
and payload["options"].get("max_tokens") is None
|
||||||
|
):
|
||||||
payload["options"]["num_predict"] = model_info.params.get(
|
payload["options"]["num_predict"] = model_info.params.get(
|
||||||
"max_tokens", None
|
"max_tokens", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("top_k", None):
|
if (
|
||||||
|
model_info.params.get("top_k", None)
|
||||||
|
and payload["options"].get("top_k") is None
|
||||||
|
):
|
||||||
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
||||||
|
|
||||||
if model_info.params.get("top_p", None):
|
if (
|
||||||
|
model_info.params.get("top_p", None)
|
||||||
|
and payload["options"].get("top_p") is None
|
||||||
|
):
|
||||||
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
||||||
|
|
||||||
if model_info.params.get("use_mmap", None):
|
if (
|
||||||
|
model_info.params.get("use_mmap", None)
|
||||||
|
and payload["options"].get("use_mmap") is None
|
||||||
|
):
|
||||||
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
||||||
|
|
||||||
if model_info.params.get("use_mlock", None):
|
if (
|
||||||
|
model_info.params.get("use_mlock", None)
|
||||||
|
and payload["options"].get("use_mlock") is None
|
||||||
|
):
|
||||||
payload["options"]["use_mlock"] = model_info.params.get(
|
payload["options"]["use_mlock"] = model_info.params.get(
|
||||||
"use_mlock", None
|
"use_mlock", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("num_thread", None):
|
if (
|
||||||
|
model_info.params.get("num_thread", None)
|
||||||
|
and payload["options"].get("num_thread") is None
|
||||||
|
):
|
||||||
payload["options"]["num_thread"] = model_info.params.get(
|
payload["options"]["num_thread"] = model_info.params.get(
|
||||||
"num_thread", None
|
"num_thread", None
|
||||||
)
|
)
|
||||||
|
|
||||||
system = model_info.params.get("system", None)
|
system = model_info.params.get("system", None)
|
||||||
if system:
|
if system:
|
||||||
# Check if the payload already has a system message
|
|
||||||
# If not, add a system message to the payload
|
|
||||||
system = prompt_template(
|
system = prompt_template(
|
||||||
system,
|
system,
|
||||||
**(
|
**(
|
||||||
@@ -885,10 +947,10 @@ async def generate_openai_chat_completion(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
form_data = OpenAIChatCompletionForm(**form_data)
|
form_data = OpenAIChatCompletionForm(**form_data)
|
||||||
|
payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])}
|
||||||
|
|
||||||
payload = {
|
if "metadata" in payload:
|
||||||
**form_data.model_dump(exclude_none=True),
|
del payload["metadata"]
|
||||||
}
|
|
||||||
|
|
||||||
model_id = form_data.model
|
model_id = form_data.model
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
@@ -963,7 +1025,11 @@ async def generate_openai_chat_completion(
|
|||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
return await post_streaming_url(f"{url}/v1/chat/completions", json.dumps(payload))
|
return await post_streaming_url(
|
||||||
|
f"{url}/v1/chat/completions",
|
||||||
|
json.dumps(payload),
|
||||||
|
stream=payload.get("stream", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v1/models")
|
@app.get("/v1/models")
|
||||||
|
|||||||
+25
-21
@@ -21,10 +21,12 @@ from utils.utils import (
|
|||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
from utils.task import prompt_template
|
from utils.task import prompt_template
|
||||||
|
from utils.misc import add_or_update_system_message
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
ENABLE_OPENAI_API,
|
ENABLE_OPENAI_API,
|
||||||
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
@@ -356,6 +358,8 @@ async def generate_chat_completion(
|
|||||||
):
|
):
|
||||||
idx = 0
|
idx = 0
|
||||||
payload = {**form_data}
|
payload = {**form_data}
|
||||||
|
if "metadata" in payload:
|
||||||
|
del payload["metadata"]
|
||||||
|
|
||||||
model_id = form_data.get("model")
|
model_id = form_data.get("model")
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
@@ -367,24 +371,33 @@ async def generate_chat_completion(
|
|||||||
model_info.params = model_info.params.model_dump()
|
model_info.params = model_info.params.model_dump()
|
||||||
|
|
||||||
if model_info.params:
|
if model_info.params:
|
||||||
if model_info.params.get("temperature", None) is not None:
|
if (
|
||||||
|
model_info.params.get("temperature", None)
|
||||||
|
and payload.get("temperature") is None
|
||||||
|
):
|
||||||
payload["temperature"] = float(model_info.params.get("temperature"))
|
payload["temperature"] = float(model_info.params.get("temperature"))
|
||||||
|
|
||||||
if model_info.params.get("top_p", None):
|
if model_info.params.get("top_p", None) and payload.get("top_p") is None:
|
||||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
payload["top_p"] = int(model_info.params.get("top_p", None))
|
||||||
|
|
||||||
if model_info.params.get("max_tokens", None):
|
if (
|
||||||
|
model_info.params.get("max_tokens", None)
|
||||||
|
and payload.get("max_tokens") is None
|
||||||
|
):
|
||||||
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||||
|
|
||||||
if model_info.params.get("frequency_penalty", None):
|
if (
|
||||||
|
model_info.params.get("frequency_penalty", None)
|
||||||
|
and payload.get("frequency_penalty") is None
|
||||||
|
):
|
||||||
payload["frequency_penalty"] = int(
|
payload["frequency_penalty"] = int(
|
||||||
model_info.params.get("frequency_penalty", None)
|
model_info.params.get("frequency_penalty", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("seed", None):
|
if model_info.params.get("seed", None) and payload.get("seed") is None:
|
||||||
payload["seed"] = model_info.params.get("seed", None)
|
payload["seed"] = model_info.params.get("seed", None)
|
||||||
|
|
||||||
if model_info.params.get("stop", None):
|
if model_info.params.get("stop", None) and payload.get("stop") is None:
|
||||||
payload["stop"] = (
|
payload["stop"] = (
|
||||||
[
|
[
|
||||||
bytes(stop, "utf-8").decode("unicode_escape")
|
bytes(stop, "utf-8").decode("unicode_escape")
|
||||||
@@ -409,21 +422,10 @@ async def generate_chat_completion(
|
|||||||
else {}
|
else {}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# Check if the payload already has a system message
|
|
||||||
# If not, add a system message to the payload
|
|
||||||
if payload.get("messages"):
|
if payload.get("messages"):
|
||||||
for message in payload["messages"]:
|
payload["messages"] = add_or_update_system_message(
|
||||||
if message.get("role") == "system":
|
system, payload["messages"]
|
||||||
message["content"] = system + message["content"]
|
)
|
||||||
break
|
|
||||||
else:
|
|
||||||
payload["messages"].insert(
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": system,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
@@ -463,7 +465,9 @@ async def generate_chat_completion(
|
|||||||
streaming = False
|
streaming = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = aiohttp.ClientSession(trust_env=True)
|
session = aiohttp.ClientSession(
|
||||||
|
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||||
|
)
|
||||||
r = await session.request(
|
r = await session.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/chat/completions",
|
url=f"{url}/chat/completions",
|
||||||
|
|||||||
+133
-48
@@ -48,8 +48,6 @@ import mimetypes
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import sentence_transformers
|
|
||||||
|
|
||||||
from apps.webui.models.documents import (
|
from apps.webui.models.documents import (
|
||||||
Documents,
|
Documents,
|
||||||
DocumentForm,
|
DocumentForm,
|
||||||
@@ -93,6 +91,8 @@ from config import (
|
|||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
|
CONTENT_EXTRACTION_ENGINE,
|
||||||
|
TIKA_SERVER_URL,
|
||||||
RAG_TOP_K,
|
RAG_TOP_K,
|
||||||
RAG_RELEVANCE_THRESHOLD,
|
RAG_RELEVANCE_THRESHOLD,
|
||||||
RAG_EMBEDDING_ENGINE,
|
RAG_EMBEDDING_ENGINE,
|
||||||
@@ -148,6 +148,9 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
|||||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
|
||||||
|
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
||||||
|
|
||||||
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
||||||
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||||
|
|
||||||
@@ -190,6 +193,8 @@ def update_embedding_model(
|
|||||||
update_model: bool = False,
|
update_model: bool = False,
|
||||||
):
|
):
|
||||||
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
|
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
|
||||||
|
import sentence_transformers
|
||||||
|
|
||||||
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
||||||
get_model_path(embedding_model, update_model),
|
get_model_path(embedding_model, update_model),
|
||||||
device=DEVICE_TYPE,
|
device=DEVICE_TYPE,
|
||||||
@@ -204,6 +209,8 @@ def update_reranking_model(
|
|||||||
update_model: bool = False,
|
update_model: bool = False,
|
||||||
):
|
):
|
||||||
if reranking_model:
|
if reranking_model:
|
||||||
|
import sentence_transformers
|
||||||
|
|
||||||
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
||||||
get_model_path(reranking_model, update_model),
|
get_model_path(reranking_model, update_model),
|
||||||
device=DEVICE_TYPE,
|
device=DEVICE_TYPE,
|
||||||
@@ -388,6 +395,10 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
|||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
|
"content_extraction": {
|
||||||
|
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
|
"tika_server_url": app.state.config.TIKA_SERVER_URL,
|
||||||
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||||
@@ -417,6 +428,11 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ContentExtractionConfig(BaseModel):
|
||||||
|
engine: str = ""
|
||||||
|
tika_server_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ChunkParamUpdateForm(BaseModel):
|
class ChunkParamUpdateForm(BaseModel):
|
||||||
chunk_size: int
|
chunk_size: int
|
||||||
chunk_overlap: int
|
chunk_overlap: int
|
||||||
@@ -450,6 +466,7 @@ class WebConfig(BaseModel):
|
|||||||
|
|
||||||
class ConfigUpdateForm(BaseModel):
|
class ConfigUpdateForm(BaseModel):
|
||||||
pdf_extract_images: Optional[bool] = None
|
pdf_extract_images: Optional[bool] = None
|
||||||
|
content_extraction: Optional[ContentExtractionConfig] = None
|
||||||
chunk: Optional[ChunkParamUpdateForm] = None
|
chunk: Optional[ChunkParamUpdateForm] = None
|
||||||
youtube: Optional[YoutubeLoaderConfig] = None
|
youtube: Optional[YoutubeLoaderConfig] = None
|
||||||
web: Optional[WebConfig] = None
|
web: Optional[WebConfig] = None
|
||||||
@@ -463,6 +480,11 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
|||||||
else app.state.config.PDF_EXTRACT_IMAGES
|
else app.state.config.PDF_EXTRACT_IMAGES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if form_data.content_extraction is not None:
|
||||||
|
log.info(f"Updating text settings: {form_data.content_extraction}")
|
||||||
|
app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
|
||||||
|
app.state.config.TIKA_SERVER_URL = form_data.content_extraction.tika_server_url
|
||||||
|
|
||||||
if form_data.chunk is not None:
|
if form_data.chunk is not None:
|
||||||
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
|
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
|
||||||
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
|
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
|
||||||
@@ -499,6 +521,10 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
|||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
|
"content_extraction": {
|
||||||
|
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
|
"tika_server_url": app.state.config.TIKA_SERVER_URL,
|
||||||
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||||
@@ -904,7 +930,9 @@ def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
def store_data_in_vector_db(
|
||||||
|
data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter(
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
chunk_size=app.state.config.CHUNK_SIZE,
|
chunk_size=app.state.config.CHUNK_SIZE,
|
||||||
@@ -916,7 +944,7 @@ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> b
|
|||||||
|
|
||||||
if len(docs) > 0:
|
if len(docs) > 0:
|
||||||
log.info(f"store_data_in_vector_db {docs}")
|
log.info(f"store_data_in_vector_db {docs}")
|
||||||
return store_docs_in_vector_db(docs, collection_name, overwrite), None
|
return store_docs_in_vector_db(docs, collection_name, metadata, overwrite), None
|
||||||
else:
|
else:
|
||||||
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
||||||
|
|
||||||
@@ -930,14 +958,16 @@ def store_text_in_vector_db(
|
|||||||
add_start_index=True,
|
add_start_index=True,
|
||||||
)
|
)
|
||||||
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
||||||
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
return store_docs_in_vector_db(docs, collection_name, overwrite=overwrite)
|
||||||
|
|
||||||
|
|
||||||
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
def store_docs_in_vector_db(
|
||||||
|
docs, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
|
||||||
|
) -> bool:
|
||||||
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
|
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
|
||||||
|
|
||||||
texts = [doc.page_content for doc in docs]
|
texts = [doc.page_content for doc in docs]
|
||||||
metadatas = [doc.metadata for doc in docs]
|
metadatas = [{**doc.metadata, **(metadata if metadata else {})} for doc in docs]
|
||||||
|
|
||||||
# ChromaDB does not like datetime formats
|
# ChromaDB does not like datetime formats
|
||||||
# for meta-data so convert them to string.
|
# for meta-data so convert them to string.
|
||||||
@@ -978,13 +1008,49 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
|
||||||
if e.__class__.__name__ == "UniqueConstraintError":
|
if e.__class__.__name__ == "UniqueConstraintError":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
log.exception(e)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TikaLoader:
|
||||||
|
def __init__(self, file_path, mime_type=None):
|
||||||
|
self.file_path = file_path
|
||||||
|
self.mime_type = mime_type
|
||||||
|
|
||||||
|
def load(self) -> List[Document]:
|
||||||
|
with open(self.file_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
if self.mime_type is not None:
|
||||||
|
headers = {"Content-Type": self.mime_type}
|
||||||
|
else:
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
endpoint = app.state.config.TIKA_SERVER_URL
|
||||||
|
if not endpoint.endswith("/"):
|
||||||
|
endpoint += "/"
|
||||||
|
endpoint += "tika/text"
|
||||||
|
|
||||||
|
r = requests.put(endpoint, data=data, headers=headers)
|
||||||
|
|
||||||
|
if r.ok:
|
||||||
|
raw_metadata = r.json()
|
||||||
|
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
|
||||||
|
|
||||||
|
if "Content-Type" in raw_metadata:
|
||||||
|
headers["Content-Type"] = raw_metadata["Content-Type"]
|
||||||
|
|
||||||
|
log.info("Tika extracted text: %s", text)
|
||||||
|
|
||||||
|
return [Document(page_content=text, metadata=headers)]
|
||||||
|
else:
|
||||||
|
raise Exception(f"Error calling Tika: {r.reason}")
|
||||||
|
|
||||||
|
|
||||||
def get_loader(filename: str, file_content_type: str, file_path: str):
|
def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||||
file_ext = filename.split(".")[-1].lower()
|
file_ext = filename.split(".")[-1].lower()
|
||||||
known_type = True
|
known_type = True
|
||||||
@@ -1035,47 +1101,58 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
"msg",
|
"msg",
|
||||||
]
|
]
|
||||||
|
|
||||||
if file_ext == "pdf":
|
if (
|
||||||
loader = PyPDFLoader(
|
app.state.config.CONTENT_EXTRACTION_ENGINE == "tika"
|
||||||
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
|
and app.state.config.TIKA_SERVER_URL
|
||||||
)
|
|
||||||
elif file_ext == "csv":
|
|
||||||
loader = CSVLoader(file_path)
|
|
||||||
elif file_ext == "rst":
|
|
||||||
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
|
||||||
elif file_ext == "xml":
|
|
||||||
loader = UnstructuredXMLLoader(file_path)
|
|
||||||
elif file_ext in ["htm", "html"]:
|
|
||||||
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
|
||||||
elif file_ext == "md":
|
|
||||||
loader = UnstructuredMarkdownLoader(file_path)
|
|
||||||
elif file_content_type == "application/epub+zip":
|
|
||||||
loader = UnstructuredEPubLoader(file_path)
|
|
||||||
elif (
|
|
||||||
file_content_type
|
|
||||||
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
||||||
or file_ext in ["doc", "docx"]
|
|
||||||
):
|
):
|
||||||
loader = Docx2txtLoader(file_path)
|
if file_ext in known_source_ext or (
|
||||||
elif file_content_type in [
|
file_content_type and file_content_type.find("text/") >= 0
|
||||||
"application/vnd.ms-excel",
|
):
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||||
] or file_ext in ["xls", "xlsx"]:
|
else:
|
||||||
loader = UnstructuredExcelLoader(file_path)
|
loader = TikaLoader(file_path, file_content_type)
|
||||||
elif file_content_type in [
|
|
||||||
"application/vnd.ms-powerpoint",
|
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
||||||
] or file_ext in ["ppt", "pptx"]:
|
|
||||||
loader = UnstructuredPowerPointLoader(file_path)
|
|
||||||
elif file_ext == "msg":
|
|
||||||
loader = OutlookMessageLoader(file_path)
|
|
||||||
elif file_ext in known_source_ext or (
|
|
||||||
file_content_type and file_content_type.find("text/") >= 0
|
|
||||||
):
|
|
||||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
|
||||||
else:
|
else:
|
||||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
if file_ext == "pdf":
|
||||||
known_type = False
|
loader = PyPDFLoader(
|
||||||
|
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
|
||||||
|
)
|
||||||
|
elif file_ext == "csv":
|
||||||
|
loader = CSVLoader(file_path)
|
||||||
|
elif file_ext == "rst":
|
||||||
|
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
||||||
|
elif file_ext == "xml":
|
||||||
|
loader = UnstructuredXMLLoader(file_path)
|
||||||
|
elif file_ext in ["htm", "html"]:
|
||||||
|
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
||||||
|
elif file_ext == "md":
|
||||||
|
loader = UnstructuredMarkdownLoader(file_path)
|
||||||
|
elif file_content_type == "application/epub+zip":
|
||||||
|
loader = UnstructuredEPubLoader(file_path)
|
||||||
|
elif (
|
||||||
|
file_content_type
|
||||||
|
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
or file_ext in ["doc", "docx"]
|
||||||
|
):
|
||||||
|
loader = Docx2txtLoader(file_path)
|
||||||
|
elif file_content_type in [
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
] or file_ext in ["xls", "xlsx"]:
|
||||||
|
loader = UnstructuredExcelLoader(file_path)
|
||||||
|
elif file_content_type in [
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
] or file_ext in ["ppt", "pptx"]:
|
||||||
|
loader = UnstructuredPowerPointLoader(file_path)
|
||||||
|
elif file_ext == "msg":
|
||||||
|
loader = OutlookMessageLoader(file_path)
|
||||||
|
elif file_ext in known_source_ext or (
|
||||||
|
file_content_type and file_content_type.find("text/") >= 0
|
||||||
|
):
|
||||||
|
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||||
|
else:
|
||||||
|
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||||
|
known_type = False
|
||||||
|
|
||||||
return loader, known_type
|
return loader, known_type
|
||||||
|
|
||||||
@@ -1164,13 +1241,21 @@ def process_doc(
|
|||||||
data = loader.load()
|
data = loader.load()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
result = store_data_in_vector_db(
|
||||||
|
data,
|
||||||
|
collection_name,
|
||||||
|
{
|
||||||
|
"file_id": form_data.file_id,
|
||||||
|
"name": file.meta.get("name", file.filename),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"collection_name": collection_name,
|
"collection_name": collection_name,
|
||||||
"known_type": known_type,
|
"known_type": known_type,
|
||||||
|
"filename": file.meta.get("name", file.filename),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -294,14 +294,16 @@ def get_rag_context(
|
|||||||
|
|
||||||
extracted_collections.extend(collection_names)
|
extracted_collections.extend(collection_names)
|
||||||
|
|
||||||
context_string = ""
|
contexts = []
|
||||||
|
|
||||||
citations = []
|
citations = []
|
||||||
|
|
||||||
for context in relevant_contexts:
|
for context in relevant_contexts:
|
||||||
try:
|
try:
|
||||||
if "documents" in context:
|
if "documents" in context:
|
||||||
context_string += "\n\n".join(
|
contexts.append(
|
||||||
[text for text in context["documents"][0] if text is not None]
|
"\n\n".join(
|
||||||
|
[text for text in context["documents"][0] if text is not None]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if "metadatas" in context:
|
if "metadatas" in context:
|
||||||
@@ -315,9 +317,7 @@ def get_rag_context(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
context_string = context_string.strip()
|
return contexts, citations
|
||||||
|
|
||||||
return context_string, citations
|
|
||||||
|
|
||||||
|
|
||||||
def get_model_path(model: str, update_model: bool = False):
|
def get_model_path(model: str, update_model: bool = False):
|
||||||
@@ -442,8 +442,6 @@ from langchain_core.documents import BaseDocumentCompressor, Document
|
|||||||
from langchain_core.callbacks import Callbacks
|
from langchain_core.callbacks import Callbacks
|
||||||
from langchain_core.pydantic_v1 import Extra
|
from langchain_core.pydantic_v1 import Extra
|
||||||
|
|
||||||
from sentence_transformers import util
|
|
||||||
|
|
||||||
|
|
||||||
class RerankCompressor(BaseDocumentCompressor):
|
class RerankCompressor(BaseDocumentCompressor):
|
||||||
embedding_function: Any
|
embedding_function: Any
|
||||||
@@ -468,6 +466,8 @@ class RerankCompressor(BaseDocumentCompressor):
|
|||||||
[(query, doc.page_content) for doc in documents]
|
[(query, doc.page_content) for doc in documents]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
from sentence_transformers import util
|
||||||
|
|
||||||
query_embedding = self.embedding_function(query)
|
query_embedding = self.embedding_function(query)
|
||||||
document_embedding = self.embedding_function(
|
document_embedding = self.embedding_function(
|
||||||
[doc.page_content for doc in documents]
|
[doc.page_content for doc in documents]
|
||||||
|
|||||||
@@ -137,3 +137,34 @@ async def disconnect(sid):
|
|||||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||||
else:
|
else:
|
||||||
print(f"Unknown session ID {sid} disconnected")
|
print(f"Unknown session ID {sid} disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_event_emitter(request_info):
|
||||||
|
async def __event_emitter__(event_data):
|
||||||
|
await sio.emit(
|
||||||
|
"chat-events",
|
||||||
|
{
|
||||||
|
"chat_id": request_info["chat_id"],
|
||||||
|
"message_id": request_info["message_id"],
|
||||||
|
"data": event_data,
|
||||||
|
},
|
||||||
|
to=request_info["session_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return __event_emitter__
|
||||||
|
|
||||||
|
|
||||||
|
async def get_event_call(request_info):
|
||||||
|
async def __event_call__(event_data):
|
||||||
|
response = await sio.call(
|
||||||
|
"chat-events",
|
||||||
|
{
|
||||||
|
"chat_id": request_info["chat_id"],
|
||||||
|
"message_id": request_info["message_id"],
|
||||||
|
"data": event_data,
|
||||||
|
},
|
||||||
|
to=request_info["session_id"],
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return __event_call__
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from peewee import *
|
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
|
|
||||||
from apps.webui.internal.wrappers import register_connection
|
from apps.webui.internal.wrappers import register_connection
|
||||||
|
|
||||||
|
from typing import Optional, Any
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, types, Dialect
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
from sqlalchemy.sql.type_api import _T
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
|
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["DB"])
|
log.setLevel(SRC_LOG_LEVELS["DB"])
|
||||||
|
|
||||||
|
|
||||||
class JSONField(TextField):
|
class JSONField(types.TypeDecorator):
|
||||||
|
impl = types.Text
|
||||||
|
cache_ok = True
|
||||||
|
|
||||||
|
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
|
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
|
||||||
|
if value is not None:
|
||||||
|
return json.loads(value)
|
||||||
|
|
||||||
|
def copy(self, **kw: Any) -> Self:
|
||||||
|
return JSONField(self.impl.length)
|
||||||
|
|
||||||
def db_value(self, value):
|
def db_value(self, value):
|
||||||
return json.dumps(value)
|
return json.dumps(value)
|
||||||
|
|
||||||
@@ -30,25 +51,60 @@ else:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# The `register_connection` function encapsulates the logic for setting up
|
# Workaround to handle the peewee migration
|
||||||
# the database connection based on the connection string, while `connect`
|
# This is required to ensure the peewee migration is handled before the alembic migration
|
||||||
# is a Peewee-specific method to manage the connection state and avoid errors
|
def handle_peewee_migration(DATABASE_URL):
|
||||||
# when a connection is already open.
|
try:
|
||||||
try:
|
# Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL
|
||||||
DB = register_connection(DATABASE_URL)
|
db = register_connection(
|
||||||
log.info(f"Connected to a {DB.__class__.__name__} database.")
|
DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@")
|
||||||
except Exception as e:
|
)
|
||||||
log.error(f"Failed to initialize the database connection: {e}")
|
migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations"
|
||||||
raise
|
router = Router(db, logger=log, migrate_dir=migrate_dir)
|
||||||
|
router.run()
|
||||||
|
db.close()
|
||||||
|
|
||||||
router = Router(
|
# check if db connection has been closed
|
||||||
DB,
|
|
||||||
migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
|
except Exception as e:
|
||||||
logger=log,
|
log.error(f"Failed to initialize the database connection: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Properly closing the database connection
|
||||||
|
if db and not db.is_closed():
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
# Assert if db connection has been closed
|
||||||
|
assert db.is_closed(), "Database connection is still open."
|
||||||
|
|
||||||
|
|
||||||
|
handle_peewee_migration(DATABASE_URL)
|
||||||
|
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URL = DATABASE_URL
|
||||||
|
if "sqlite" in SQLALCHEMY_DATABASE_URL:
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
|
||||||
|
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(
|
||||||
|
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
|
||||||
)
|
)
|
||||||
router.run()
|
Base = declarative_base()
|
||||||
try:
|
Session = scoped_session(SessionLocal)
|
||||||
DB.connect(reuse_if_open=True)
|
|
||||||
except OperationalError as e:
|
|
||||||
log.info(f"Failed to connect to database again due to: {e}")
|
# Dependency
|
||||||
pass
|
def get_session():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
get_db = contextmanager(get_session)
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
"""Peewee migrations -- 017_add_user_oauth_sub.py.
|
"""Peewee migrations -- 017_add_user_oauth_sub.py.
|
||||||
|
|
||||||
Some examples (model - class or model name)::
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
> Model = migrator.ModelClass # Return model in current state by name
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
> 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.create_model(Model) # Create a model (could be used as decorator)
|
||||||
@@ -21,7 +18,6 @@ Some examples (model - class or model name)::
|
|||||||
> migrator.drop_index(model, *col_names)
|
> migrator.drop_index(model, *col_names)
|
||||||
> migrator.drop_not_null(model, *field_names)
|
> migrator.drop_not_null(model, *field_names)
|
||||||
> migrator.drop_constraints(model, *constraints)
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# Database Migrations
|
|
||||||
|
|
||||||
This directory contains all the database migrations for the web app.
|
|
||||||
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
|
|
||||||
|
|
||||||
Migrations are automatically ran at app startup.
|
|
||||||
|
|
||||||
## Creating a migration
|
|
||||||
|
|
||||||
Have you made a change to the schema of an existing model?
|
|
||||||
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
|
|
||||||
|
|
||||||
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
|
|
||||||
2. Make your changes to the models.
|
|
||||||
3. From the `backend` directory, run the following command:
|
|
||||||
```bash
|
|
||||||
pw_migrate create --auto --auto-source apps.webui.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
|
|
||||||
```
|
|
||||||
- `$SQLITE_DB` should be the path to the database file.
|
|
||||||
- `$MIGRATION_NAME` should be a descriptive name for the migration.
|
|
||||||
4. The migration file will be created in the `apps/web/internal/migrations` directory.
|
|
||||||
+127
-5
@@ -3,7 +3,7 @@ from fastapi.routing import APIRoute
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from apps.webui.routers import (
|
from apps.webui.routers import (
|
||||||
auths,
|
auths,
|
||||||
users,
|
users,
|
||||||
@@ -19,8 +19,12 @@ from apps.webui.routers import (
|
|||||||
functions,
|
functions,
|
||||||
)
|
)
|
||||||
from apps.webui.models.functions import Functions
|
from apps.webui.models.functions import Functions
|
||||||
|
from apps.webui.models.models import Models
|
||||||
from apps.webui.utils import load_function_module_by_id
|
from apps.webui.utils import load_function_module_by_id
|
||||||
|
|
||||||
from utils.misc import stream_message_template
|
from utils.misc import stream_message_template
|
||||||
|
from utils.task import prompt_template
|
||||||
|
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
WEBUI_BUILD_HASH,
|
WEBUI_BUILD_HASH,
|
||||||
@@ -39,14 +43,18 @@ from config import (
|
|||||||
WEBUI_BANNERS,
|
WEBUI_BANNERS,
|
||||||
ENABLE_COMMUNITY_SHARING,
|
ENABLE_COMMUNITY_SHARING,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
|
OAUTH_USERNAME_CLAIM,
|
||||||
|
OAUTH_PICTURE_CLAIM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from apps.socket.main import get_event_call, get_event_emitter
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Iterator, Generator
|
from typing import Iterator, Generator, Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -74,6 +82,9 @@ app.state.config.BANNERS = WEBUI_BANNERS
|
|||||||
|
|
||||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||||
|
|
||||||
|
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
|
||||||
|
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
|
||||||
|
|
||||||
app.state.MODELS = {}
|
app.state.MODELS = {}
|
||||||
app.state.TOOLS = {}
|
app.state.TOOLS = {}
|
||||||
app.state.FUNCTIONS = {}
|
app.state.FUNCTIONS = {}
|
||||||
@@ -129,7 +140,6 @@ async def get_pipe_models():
|
|||||||
function_module = app.state.FUNCTIONS[pipe.id]
|
function_module = app.state.FUNCTIONS[pipe.id]
|
||||||
|
|
||||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||||
print(f"Getting valves for {pipe.id}")
|
|
||||||
valves = Functions.get_function_valves_by_id(pipe.id)
|
valves = Functions.get_function_valves_by_id(pipe.id)
|
||||||
function_module.valves = function_module.Valves(
|
function_module.valves = function_module.Valves(
|
||||||
**(valves if valves else {})
|
**(valves if valves else {})
|
||||||
@@ -155,6 +165,10 @@ async def get_pipe_models():
|
|||||||
f"{function_module.name}{manifold_pipe_name}"
|
f"{function_module.name}{manifold_pipe_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pipe_flag = {"type": pipe.type}
|
||||||
|
if hasattr(function_module, "ChatValves"):
|
||||||
|
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||||
|
|
||||||
pipe_models.append(
|
pipe_models.append(
|
||||||
{
|
{
|
||||||
"id": manifold_pipe_id,
|
"id": manifold_pipe_id,
|
||||||
@@ -162,10 +176,14 @@ async def get_pipe_models():
|
|||||||
"object": "model",
|
"object": "model",
|
||||||
"created": pipe.created_at,
|
"created": pipe.created_at,
|
||||||
"owned_by": "openai",
|
"owned_by": "openai",
|
||||||
"pipe": {"type": pipe.type},
|
"pipe": pipe_flag,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
pipe_flag = {"type": "pipe"}
|
||||||
|
if hasattr(function_module, "ChatValves"):
|
||||||
|
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||||
|
|
||||||
pipe_models.append(
|
pipe_models.append(
|
||||||
{
|
{
|
||||||
"id": pipe.id,
|
"id": pipe.id,
|
||||||
@@ -173,7 +191,7 @@ async def get_pipe_models():
|
|||||||
"object": "model",
|
"object": "model",
|
||||||
"created": pipe.created_at,
|
"created": pipe.created_at,
|
||||||
"owned_by": "openai",
|
"owned_by": "openai",
|
||||||
"pipe": {"type": "pipe"},
|
"pipe": pipe_flag,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,6 +199,98 @@ async def get_pipe_models():
|
|||||||
|
|
||||||
|
|
||||||
async def generate_function_chat_completion(form_data, user):
|
async def generate_function_chat_completion(form_data, user):
|
||||||
|
model_id = form_data.get("model")
|
||||||
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
|
metadata = None
|
||||||
|
if "metadata" in form_data:
|
||||||
|
metadata = form_data["metadata"]
|
||||||
|
del form_data["metadata"]
|
||||||
|
|
||||||
|
__event_emitter__ = None
|
||||||
|
__event_call__ = None
|
||||||
|
__task__ = None
|
||||||
|
|
||||||
|
if metadata:
|
||||||
|
if (
|
||||||
|
metadata.get("session_id")
|
||||||
|
and metadata.get("chat_id")
|
||||||
|
and metadata.get("message_id")
|
||||||
|
):
|
||||||
|
__event_emitter__ = await get_event_emitter(metadata)
|
||||||
|
__event_call__ = await get_event_call(metadata)
|
||||||
|
|
||||||
|
if metadata.get("task"):
|
||||||
|
__task__ = metadata.get("task")
|
||||||
|
|
||||||
|
if model_info:
|
||||||
|
if model_info.base_model_id:
|
||||||
|
form_data["model"] = model_info.base_model_id
|
||||||
|
|
||||||
|
model_info.params = model_info.params.model_dump()
|
||||||
|
|
||||||
|
if model_info.params:
|
||||||
|
if model_info.params.get("temperature", None) is not None:
|
||||||
|
form_data["temperature"] = float(model_info.params.get("temperature"))
|
||||||
|
|
||||||
|
if model_info.params.get("top_p", None):
|
||||||
|
form_data["top_p"] = int(model_info.params.get("top_p", None))
|
||||||
|
|
||||||
|
if model_info.params.get("max_tokens", None):
|
||||||
|
form_data["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||||
|
|
||||||
|
if model_info.params.get("frequency_penalty", None):
|
||||||
|
form_data["frequency_penalty"] = int(
|
||||||
|
model_info.params.get("frequency_penalty", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
if model_info.params.get("seed", None):
|
||||||
|
form_data["seed"] = model_info.params.get("seed", None)
|
||||||
|
|
||||||
|
if model_info.params.get("stop", None):
|
||||||
|
form_data["stop"] = (
|
||||||
|
[
|
||||||
|
bytes(stop, "utf-8").decode("unicode_escape")
|
||||||
|
for stop in model_info.params["stop"]
|
||||||
|
]
|
||||||
|
if model_info.params.get("stop", None)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
system = model_info.params.get("system", None)
|
||||||
|
if system:
|
||||||
|
system = prompt_template(
|
||||||
|
system,
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"user_name": user.name,
|
||||||
|
"user_location": (
|
||||||
|
user.info.get("location") if user.info else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Check if the payload already has a system message
|
||||||
|
# If not, add a system message to the payload
|
||||||
|
if form_data.get("messages"):
|
||||||
|
for message in form_data["messages"]:
|
||||||
|
if message.get("role") == "system":
|
||||||
|
message["content"] = system + message["content"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
form_data["messages"].insert(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": system,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
async def job():
|
async def job():
|
||||||
pipe_id = form_data["model"]
|
pipe_id = form_data["model"]
|
||||||
if "." in pipe_id:
|
if "." in pipe_id:
|
||||||
@@ -227,6 +337,15 @@ async def generate_function_chat_completion(form_data, user):
|
|||||||
|
|
||||||
params = {**params, "__user__": __user__}
|
params = {**params, "__user__": __user__}
|
||||||
|
|
||||||
|
if "__event_emitter__" in sig.parameters:
|
||||||
|
params = {**params, "__event_emitter__": __event_emitter__}
|
||||||
|
|
||||||
|
if "__event_call__" in sig.parameters:
|
||||||
|
params = {**params, "__event_call__": __event_call__}
|
||||||
|
|
||||||
|
if "__task__" in sig.parameters:
|
||||||
|
params = {**params, "__task__": __task__}
|
||||||
|
|
||||||
if form_data["stream"]:
|
if form_data["stream"]:
|
||||||
|
|
||||||
async def stream_content():
|
async def stream_content():
|
||||||
@@ -259,6 +378,9 @@ async def generate_function_chat_completion(form_data, user):
|
|||||||
if isinstance(line, BaseModel):
|
if isinstance(line, BaseModel):
|
||||||
line = line.model_dump_json()
|
line = line.model_dump_json()
|
||||||
line = f"data: {line}"
|
line = f"data: {line}"
|
||||||
|
if isinstance(line, dict):
|
||||||
|
line = f"data: {json.dumps(line)}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
line = line.decode("utf-8")
|
line = line.decode("utf-8")
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Union, Optional
|
from typing import Optional
|
||||||
import time
|
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
from peewee import *
|
from sqlalchemy import String, Column, Boolean, Text
|
||||||
|
|
||||||
from apps.webui.models.users import UserModel, Users
|
from apps.webui.models.users import UserModel, Users
|
||||||
from utils.utils import verify_password
|
from utils.utils import verify_password
|
||||||
|
|
||||||
from apps.webui.internal.db import DB
|
from apps.webui.internal.db import Base, get_db
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
@@ -20,14 +19,13 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Auth(Model):
|
class Auth(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "auth"
|
||||||
email = CharField()
|
|
||||||
password = TextField()
|
|
||||||
active = BooleanField()
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
email = Column(String)
|
||||||
|
password = Column(Text)
|
||||||
|
active = Column(Boolean)
|
||||||
|
|
||||||
|
|
||||||
class AuthModel(BaseModel):
|
class AuthModel(BaseModel):
|
||||||
@@ -94,9 +92,6 @@ class AddUserForm(SignupForm):
|
|||||||
|
|
||||||
|
|
||||||
class AuthsTable:
|
class AuthsTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Auth])
|
|
||||||
|
|
||||||
def insert_new_auth(
|
def insert_new_auth(
|
||||||
self,
|
self,
|
||||||
@@ -107,36 +102,44 @@ class AuthsTable:
|
|||||||
role: str = "pending",
|
role: str = "pending",
|
||||||
oauth_sub: Optional[str] = None,
|
oauth_sub: Optional[str] = None,
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
log.info("insert_new_auth")
|
with get_db() as db:
|
||||||
|
|
||||||
id = str(uuid.uuid4())
|
log.info("insert_new_auth")
|
||||||
|
|
||||||
auth = AuthModel(
|
id = str(uuid.uuid4())
|
||||||
**{"id": id, "email": email, "password": password, "active": True}
|
|
||||||
)
|
|
||||||
result = Auth.create(**auth.model_dump())
|
|
||||||
|
|
||||||
user = Users.insert_new_user(
|
auth = AuthModel(
|
||||||
id, name, email, profile_image_url, role, oauth_sub
|
**{"id": id, "email": email, "password": password, "active": True}
|
||||||
)
|
)
|
||||||
|
result = Auth(**auth.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
|
||||||
if result and user:
|
user = Users.insert_new_user(
|
||||||
return user
|
id, name, email, profile_image_url, role, oauth_sub
|
||||||
else:
|
)
|
||||||
return None
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
|
||||||
|
if result and user:
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
||||||
log.info(f"authenticate_user: {email}")
|
log.info(f"authenticate_user: {email}")
|
||||||
try:
|
try:
|
||||||
auth = Auth.get(Auth.email == email, Auth.active == True)
|
with get_db() as db:
|
||||||
if auth:
|
|
||||||
if verify_password(password, auth.password):
|
auth = db.query(Auth).filter_by(email=email, active=True).first()
|
||||||
user = Users.get_user_by_id(auth.id)
|
if auth:
|
||||||
return user
|
if verify_password(password, auth.password):
|
||||||
|
user = Users.get_user_by_id(auth.id)
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
return None
|
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -155,46 +158,50 @@ class AuthsTable:
|
|||||||
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
|
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
|
||||||
log.info(f"authenticate_user_by_trusted_header: {email}")
|
log.info(f"authenticate_user_by_trusted_header: {email}")
|
||||||
try:
|
try:
|
||||||
auth = Auth.get(Auth.email == email, Auth.active == True)
|
with get_db() as db:
|
||||||
if auth:
|
auth = db.query(Auth).filter_by(email=email, active=True).first()
|
||||||
user = Users.get_user_by_id(auth.id)
|
if auth:
|
||||||
return user
|
user = Users.get_user_by_id(auth.id)
|
||||||
|
return user
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Auth.update(password=new_password).where(Auth.id == id)
|
with get_db() as db:
|
||||||
result = query.execute()
|
result = (
|
||||||
|
db.query(Auth).filter_by(id=id).update({"password": new_password})
|
||||||
return True if result == 1 else False
|
)
|
||||||
|
db.commit()
|
||||||
|
return True if result == 1 else False
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_email_by_id(self, id: str, email: str) -> bool:
|
def update_email_by_id(self, id: str, email: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Auth.update(email=email).where(Auth.id == id)
|
with get_db() as db:
|
||||||
result = query.execute()
|
result = db.query(Auth).filter_by(id=id).update({"email": email})
|
||||||
|
db.commit()
|
||||||
return True if result == 1 else False
|
return True if result == 1 else False
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_auth_by_id(self, id: str) -> bool:
|
def delete_auth_by_id(self, id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
# Delete User
|
with get_db() as db:
|
||||||
result = Users.delete_user_by_id(id)
|
|
||||||
|
|
||||||
if result:
|
# Delete User
|
||||||
# Delete Auth
|
result = Users.delete_user_by_id(id)
|
||||||
query = Auth.delete().where(Auth.id == id)
|
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
if result:
|
||||||
else:
|
db.query(Auth).filter_by(id=id).delete()
|
||||||
return False
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Auths = AuthsTable(DB)
|
Auths = AuthsTable()
|
||||||
|
|||||||
+203
-184
@@ -1,36 +1,38 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
from peewee import *
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from apps.webui.internal.db import DB
|
from sqlalchemy import Column, String, BigInteger, Boolean, Text
|
||||||
|
|
||||||
|
from apps.webui.internal.db import Base, get_db
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Chat DB Schema
|
# Chat DB Schema
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Chat(Model):
|
class Chat(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "chat"
|
||||||
user_id = CharField()
|
|
||||||
title = TextField()
|
|
||||||
chat = TextField() # Save Chat JSON as Text
|
|
||||||
|
|
||||||
created_at = BigIntegerField()
|
id = Column(String, primary_key=True)
|
||||||
updated_at = BigIntegerField()
|
user_id = Column(String)
|
||||||
|
title = Column(Text)
|
||||||
|
chat = Column(Text) # Save Chat JSON as Text
|
||||||
|
|
||||||
share_id = CharField(null=True, unique=True)
|
created_at = Column(BigInteger)
|
||||||
archived = BooleanField(default=False)
|
updated_at = Column(BigInteger)
|
||||||
|
|
||||||
class Meta:
|
share_id = Column(Text, unique=True, nullable=True)
|
||||||
database = DB
|
archived = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
|
||||||
class ChatModel(BaseModel):
|
class ChatModel(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
title: str
|
title: str
|
||||||
@@ -75,91 +77,104 @@ class ChatTitleIdResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ChatTable:
|
class ChatTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
db.create_tables([Chat])
|
|
||||||
|
|
||||||
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
|
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
|
||||||
id = str(uuid.uuid4())
|
with get_db() as db:
|
||||||
chat = ChatModel(
|
|
||||||
**{
|
|
||||||
"id": id,
|
|
||||||
"user_id": user_id,
|
|
||||||
"title": (
|
|
||||||
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
|
|
||||||
),
|
|
||||||
"chat": json.dumps(form_data.chat),
|
|
||||||
"created_at": int(time.time()),
|
|
||||||
"updated_at": int(time.time()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = Chat.create(**chat.model_dump())
|
id = str(uuid.uuid4())
|
||||||
return chat if result else None
|
chat = ChatModel(
|
||||||
|
**{
|
||||||
|
"id": id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"title": (
|
||||||
|
form_data.chat["title"]
|
||||||
|
if "title" in form_data.chat
|
||||||
|
else "New Chat"
|
||||||
|
),
|
||||||
|
"chat": json.dumps(form_data.chat),
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = Chat(**chat.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
return ChatModel.model_validate(result) if result else None
|
||||||
|
|
||||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
query = Chat.update(
|
with get_db() as db:
|
||||||
chat=json.dumps(chat),
|
|
||||||
title=chat["title"] if "title" in chat else "New Chat",
|
|
||||||
updated_at=int(time.time()),
|
|
||||||
).where(Chat.id == id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
chat = Chat.get(Chat.id == id)
|
chat_obj = db.get(Chat, id)
|
||||||
return ChatModel(**model_to_dict(chat))
|
chat_obj.chat = json.dumps(chat)
|
||||||
except:
|
chat_obj.title = chat["title"] if "title" in chat else "New Chat"
|
||||||
|
chat_obj.updated_at = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat_obj)
|
||||||
|
|
||||||
|
return ChatModel.model_validate(chat_obj)
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||||
# Get the existing chat to share
|
with get_db() as db:
|
||||||
chat = Chat.get(Chat.id == chat_id)
|
|
||||||
# Check if the chat is already shared
|
|
||||||
if chat.share_id:
|
|
||||||
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
|
|
||||||
# Create a new chat with the same data, but with a new ID
|
|
||||||
shared_chat = ChatModel(
|
|
||||||
**{
|
|
||||||
"id": str(uuid.uuid4()),
|
|
||||||
"user_id": f"shared-{chat_id}",
|
|
||||||
"title": chat.title,
|
|
||||||
"chat": chat.chat,
|
|
||||||
"created_at": chat.created_at,
|
|
||||||
"updated_at": int(time.time()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
shared_result = Chat.create(**shared_chat.model_dump())
|
|
||||||
# Update the original chat with the share_id
|
|
||||||
result = (
|
|
||||||
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
|
|
||||||
)
|
|
||||||
|
|
||||||
return shared_chat if (shared_result and result) else None
|
# Get the existing chat to share
|
||||||
|
chat = db.get(Chat, chat_id)
|
||||||
|
# Check if the chat is already shared
|
||||||
|
if chat.share_id:
|
||||||
|
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
|
||||||
|
# Create a new chat with the same data, but with a new ID
|
||||||
|
shared_chat = ChatModel(
|
||||||
|
**{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"user_id": f"shared-{chat_id}",
|
||||||
|
"title": chat.title,
|
||||||
|
"chat": chat.chat,
|
||||||
|
"created_at": chat.created_at,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
shared_result = Chat(**shared_chat.model_dump())
|
||||||
|
db.add(shared_result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(shared_result)
|
||||||
|
|
||||||
|
# Update the original chat with the share_id
|
||||||
|
result = (
|
||||||
|
db.query(Chat)
|
||||||
|
.filter_by(id=chat_id)
|
||||||
|
.update({"share_id": shared_chat.id})
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return shared_chat if (shared_result and result) else None
|
||||||
|
|
||||||
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
print("update_shared_chat_by_id")
|
with get_db() as db:
|
||||||
chat = Chat.get(Chat.id == chat_id)
|
|
||||||
print(chat)
|
|
||||||
|
|
||||||
query = Chat.update(
|
print("update_shared_chat_by_id")
|
||||||
title=chat.title,
|
chat = db.get(Chat, chat_id)
|
||||||
chat=chat.chat,
|
print(chat)
|
||||||
).where(Chat.id == chat.share_id)
|
chat.title = chat.title
|
||||||
|
chat.chat = chat.chat
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat)
|
||||||
|
|
||||||
query.execute()
|
return self.get_chat_by_id(chat.share_id)
|
||||||
|
|
||||||
chat = Chat.get(Chat.id == chat.share_id)
|
|
||||||
return ChatModel(**model_to_dict(chat))
|
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
|
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -167,56 +182,50 @@ class ChatTable:
|
|||||||
self, id: str, share_id: Optional[str]
|
self, id: str, share_id: Optional[str]
|
||||||
) -> Optional[ChatModel]:
|
) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
query = Chat.update(
|
with get_db() as db:
|
||||||
share_id=share_id,
|
|
||||||
).where(Chat.id == id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
chat = Chat.get(Chat.id == id)
|
chat = db.get(Chat, id)
|
||||||
return ChatModel(**model_to_dict(chat))
|
chat.share_id = share_id
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat)
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
|
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = self.get_chat_by_id(id)
|
with get_db() as db:
|
||||||
query = Chat.update(
|
|
||||||
archived=(not chat.archived),
|
|
||||||
).where(Chat.id == id)
|
|
||||||
|
|
||||||
query.execute()
|
chat = db.get(Chat, id)
|
||||||
|
chat.archived = not chat.archived
|
||||||
chat = Chat.get(Chat.id == id)
|
db.commit()
|
||||||
return ChatModel(**model_to_dict(chat))
|
db.refresh(chat)
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
|
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
chats = self.get_chats_by_user_id(user_id)
|
with get_db() as db:
|
||||||
for chat in chats:
|
db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
|
||||||
query = Chat.update(
|
db.commit()
|
||||||
archived=True,
|
return True
|
||||||
).where(Chat.id == chat.id)
|
|
||||||
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
return True
|
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_archived_chat_list_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 [
|
with get_db() as db:
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select()
|
all_chats = (
|
||||||
.where(Chat.archived == True)
|
db.query(Chat)
|
||||||
.where(Chat.user_id == user_id)
|
.filter_by(user_id=user_id, archived=True)
|
||||||
.order_by(Chat.updated_at.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
# .limit(limit)
|
# .limit(limit).offset(skip)
|
||||||
# .offset(skip)
|
.all()
|
||||||
]
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def get_chat_list_by_user_id(
|
def get_chat_list_by_user_id(
|
||||||
self,
|
self,
|
||||||
@@ -225,131 +234,141 @@ class ChatTable:
|
|||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> List[ChatModel]:
|
) -> List[ChatModel]:
|
||||||
if include_archived:
|
with get_db() as db:
|
||||||
return [
|
query = db.query(Chat).filter_by(user_id=user_id)
|
||||||
ChatModel(**model_to_dict(chat))
|
if not include_archived:
|
||||||
for chat in Chat.select()
|
query = query.filter_by(archived=False)
|
||||||
.where(Chat.user_id == user_id)
|
all_chats = (
|
||||||
.order_by(Chat.updated_at.desc())
|
query.order_by(Chat.updated_at.desc())
|
||||||
# .limit(limit)
|
# .limit(limit).offset(skip)
|
||||||
# .offset(skip)
|
.all()
|
||||||
]
|
)
|
||||||
else:
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
return [
|
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select()
|
|
||||||
.where(Chat.archived == False)
|
|
||||||
.where(Chat.user_id == user_id)
|
|
||||||
.order_by(Chat.updated_at.desc())
|
|
||||||
# .limit(limit)
|
|
||||||
# .offset(skip)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_chat_list_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 [
|
with get_db() as db:
|
||||||
ChatModel(**model_to_dict(chat))
|
all_chats = (
|
||||||
for chat in Chat.select()
|
db.query(Chat)
|
||||||
.where(Chat.archived == False)
|
.filter(Chat.id.in_(chat_ids))
|
||||||
.where(Chat.id.in_(chat_ids))
|
.filter_by(archived=False)
|
||||||
.order_by(Chat.updated_at.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
.all()
|
||||||
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = Chat.get(Chat.id == id)
|
with get_db() as db:
|
||||||
return ChatModel(**model_to_dict(chat))
|
|
||||||
|
chat = db.get(Chat, id)
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
|
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = Chat.get(Chat.share_id == id)
|
with get_db() as db:
|
||||||
|
|
||||||
if chat:
|
chat = db.query(Chat).filter_by(share_id=id).first()
|
||||||
chat = Chat.get(Chat.id == id)
|
|
||||||
return ChatModel(**model_to_dict(chat))
|
if chat:
|
||||||
else:
|
return self.get_chat_by_id(id)
|
||||||
return None
|
else:
|
||||||
except:
|
return None
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
|
with get_db() as db:
|
||||||
return ChatModel(**model_to_dict(chat))
|
|
||||||
|
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
|
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select().order_by(Chat.updated_at.desc())
|
all_chats = (
|
||||||
# .limit(limit).offset(skip)
|
db.query(Chat)
|
||||||
]
|
# .limit(limit).offset(skip)
|
||||||
|
.order_by(Chat.updated_at.desc())
|
||||||
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select()
|
all_chats = (
|
||||||
.where(Chat.user_id == user_id)
|
db.query(Chat)
|
||||||
.order_by(Chat.updated_at.desc())
|
.filter_by(user_id=user_id)
|
||||||
# .limit(limit).offset(skip)
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
ChatModel(**model_to_dict(chat))
|
|
||||||
for chat in Chat.select()
|
all_chats = (
|
||||||
.where(Chat.archived == True)
|
db.query(Chat)
|
||||||
.where(Chat.user_id == user_id)
|
.filter_by(user_id=user_id, archived=True)
|
||||||
.order_by(Chat.updated_at.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def delete_chat_by_id(self, id: str) -> bool:
|
def delete_chat_by_id(self, id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Chat.delete().where((Chat.id == id))
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True and self.delete_shared_chat_by_chat_id(id)
|
db.query(Chat).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
except:
|
except:
|
||||||
return False
|
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))
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True and self.delete_shared_chat_by_chat_id(id)
|
db.query(Chat).filter_by(id=id, user_id=user_id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
self.delete_shared_chats_by_user_id(user_id)
|
with get_db() as db:
|
||||||
|
|
||||||
query = Chat.delete().where(Chat.user_id == user_id)
|
self.delete_shared_chats_by_user_id(user_id)
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
db.query(Chat).filter_by(user_id=user_id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
shared_chat_ids = [
|
|
||||||
f"shared-{chat.id}"
|
|
||||||
for chat in Chat.select().where(Chat.user_id == user_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
|
||||||
|
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
|
||||||
|
|
||||||
|
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Chats = ChatTable(DB)
|
Chats = ChatTable()
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
from typing import List, Optional
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from utils.utils import decode_token
|
from sqlalchemy import String, Column, BigInteger, Text
|
||||||
from utils.misc import get_gravatar_url
|
|
||||||
|
|
||||||
from apps.webui.internal.db import DB
|
from apps.webui.internal.db import Base, get_db
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -22,20 +19,21 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Document(Model):
|
class Document(Base):
|
||||||
collection_name = CharField(unique=True)
|
__tablename__ = "document"
|
||||||
name = CharField(unique=True)
|
|
||||||
title = TextField()
|
|
||||||
filename = TextField()
|
|
||||||
content = TextField(null=True)
|
|
||||||
user_id = CharField()
|
|
||||||
timestamp = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
collection_name = Column(String, primary_key=True)
|
||||||
database = DB
|
name = Column(String, unique=True)
|
||||||
|
title = Column(Text)
|
||||||
|
filename = Column(Text)
|
||||||
|
content = Column(Text, nullable=True)
|
||||||
|
user_id = Column(String)
|
||||||
|
timestamp = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class DocumentModel(BaseModel):
|
class DocumentModel(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
collection_name: str
|
collection_name: str
|
||||||
name: str
|
name: str
|
||||||
title: str
|
title: str
|
||||||
@@ -72,57 +70,63 @@ class DocumentForm(DocumentUpdateForm):
|
|||||||
|
|
||||||
|
|
||||||
class DocumentsTable:
|
class DocumentsTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Document])
|
|
||||||
|
|
||||||
def insert_new_doc(
|
def insert_new_doc(
|
||||||
self, user_id: str, form_data: DocumentForm
|
self, user_id: str, form_data: DocumentForm
|
||||||
) -> Optional[DocumentModel]:
|
) -> Optional[DocumentModel]:
|
||||||
document = DocumentModel(
|
with get_db() as db:
|
||||||
**{
|
|
||||||
**form_data.model_dump(),
|
|
||||||
"user_id": user_id,
|
|
||||||
"timestamp": int(time.time()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
document = DocumentModel(
|
||||||
result = Document.create(**document.model_dump())
|
**{
|
||||||
if result:
|
**form_data.model_dump(),
|
||||||
return document
|
"user_id": user_id,
|
||||||
else:
|
"timestamp": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = Document(**document.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return DocumentModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
return None
|
return None
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
|
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
|
||||||
try:
|
try:
|
||||||
document = Document.get(Document.name == name)
|
with get_db() as db:
|
||||||
return DocumentModel(**model_to_dict(document))
|
|
||||||
|
document = db.query(Document).filter_by(name=name).first()
|
||||||
|
return DocumentModel.model_validate(document) if document else None
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_docs(self) -> List[DocumentModel]:
|
def get_docs(self) -> List[DocumentModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
DocumentModel(**model_to_dict(doc))
|
|
||||||
for doc in Document.select()
|
return [
|
||||||
# .limit(limit).offset(skip)
|
DocumentModel.model_validate(doc) for doc in db.query(Document).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_doc_by_name(
|
def update_doc_by_name(
|
||||||
self, name: str, form_data: DocumentUpdateForm
|
self, name: str, form_data: DocumentUpdateForm
|
||||||
) -> Optional[DocumentModel]:
|
) -> Optional[DocumentModel]:
|
||||||
try:
|
try:
|
||||||
query = Document.update(
|
with get_db() as db:
|
||||||
title=form_data.title,
|
|
||||||
name=form_data.name,
|
|
||||||
timestamp=int(time.time()),
|
|
||||||
).where(Document.name == name)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
doc = Document.get(Document.name == form_data.name)
|
db.query(Document).filter_by(name=name).update(
|
||||||
return DocumentModel(**model_to_dict(doc))
|
{
|
||||||
|
"title": form_data.title,
|
||||||
|
"name": form_data.name,
|
||||||
|
"timestamp": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return self.get_doc_by_name(form_data.name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
return None
|
return None
|
||||||
@@ -135,26 +139,29 @@ class DocumentsTable:
|
|||||||
doc_content = json.loads(doc.content if doc.content else "{}")
|
doc_content = json.loads(doc.content if doc.content else "{}")
|
||||||
doc_content = {**doc_content, **updated}
|
doc_content = {**doc_content, **updated}
|
||||||
|
|
||||||
query = Document.update(
|
with get_db() as db:
|
||||||
content=json.dumps(doc_content),
|
|
||||||
timestamp=int(time.time()),
|
|
||||||
).where(Document.name == name)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
doc = Document.get(Document.name == name)
|
db.query(Document).filter_by(name=name).update(
|
||||||
return DocumentModel(**model_to_dict(doc))
|
{
|
||||||
|
"content": json.dumps(doc_content),
|
||||||
|
"timestamp": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return self.get_doc_by_name(name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_doc_by_name(self, name: str) -> bool:
|
def delete_doc_by_name(self, name: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Document.delete().where((Document.name == name))
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
db.query(Document).filter_by(name=name).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Documents = DocumentsTable(DB)
|
Documents = DocumentsTable()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from apps.webui.internal.db import DB, JSONField
|
|
||||||
|
from sqlalchemy import Column, String, BigInteger, Text
|
||||||
|
|
||||||
|
from apps.webui.internal.db import JSONField, Base, get_db
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -18,15 +19,14 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class File(Model):
|
class File(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "file"
|
||||||
user_id = CharField()
|
|
||||||
filename = TextField()
|
|
||||||
meta = JSONField()
|
|
||||||
created_at = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
user_id = Column(String)
|
||||||
|
filename = Column(Text)
|
||||||
|
meta = Column(JSONField)
|
||||||
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class FileModel(BaseModel):
|
class FileModel(BaseModel):
|
||||||
@@ -36,6 +36,8 @@ class FileModel(BaseModel):
|
|||||||
meta: dict
|
meta: dict
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -57,56 +59,68 @@ class FileForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class FilesTable:
|
class FilesTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([File])
|
|
||||||
|
|
||||||
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
|
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
|
||||||
file = FileModel(
|
with get_db() as db:
|
||||||
**{
|
|
||||||
**form_data.model_dump(),
|
|
||||||
"user_id": user_id,
|
|
||||||
"created_at": int(time.time()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
file = FileModel(
|
||||||
result = File.create(**file.model_dump())
|
**{
|
||||||
if result:
|
**form_data.model_dump(),
|
||||||
return file
|
"user_id": user_id,
|
||||||
else:
|
"created_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = File(**file.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return FileModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating tool: {e}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating tool: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_file_by_id(self, id: str) -> Optional[FileModel]:
|
def get_file_by_id(self, id: str) -> Optional[FileModel]:
|
||||||
try:
|
with get_db() as db:
|
||||||
file = File.get(File.id == id)
|
|
||||||
return FileModel(**model_to_dict(file))
|
try:
|
||||||
except:
|
file = db.get(File, id)
|
||||||
return None
|
return FileModel.model_validate(file)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_files(self) -> List[FileModel]:
|
def get_files(self) -> List[FileModel]:
|
||||||
return [FileModel(**model_to_dict(file)) for file in File.select()]
|
with get_db() as db:
|
||||||
|
|
||||||
|
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||||
|
|
||||||
def delete_file_by_id(self, id: str) -> bool:
|
def delete_file_by_id(self, id: str) -> bool:
|
||||||
try:
|
|
||||||
query = File.delete().where((File.id == id))
|
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
with get_db() as db:
|
||||||
except:
|
|
||||||
return False
|
try:
|
||||||
|
db.query(File).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_all_files(self) -> bool:
|
def delete_all_files(self) -> bool:
|
||||||
try:
|
|
||||||
query = File.delete()
|
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
with get_db() as db:
|
||||||
except:
|
|
||||||
return False
|
try:
|
||||||
|
db.query(File).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Files = FilesTable(DB)
|
Files = FilesTable()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from apps.webui.internal.db import DB, JSONField
|
|
||||||
|
from sqlalchemy import Column, String, Text, BigInteger, Boolean
|
||||||
|
|
||||||
|
from apps.webui.internal.db import JSONField, Base, get_db
|
||||||
from apps.webui.models.users import Users
|
from apps.webui.models.users import Users
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -21,21 +22,20 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Function(Model):
|
class Function(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "function"
|
||||||
user_id = CharField()
|
|
||||||
name = TextField()
|
|
||||||
type = TextField()
|
|
||||||
content = TextField()
|
|
||||||
meta = JSONField()
|
|
||||||
valves = JSONField()
|
|
||||||
is_active = BooleanField(default=False)
|
|
||||||
is_global = BooleanField(default=False)
|
|
||||||
updated_at = BigIntegerField()
|
|
||||||
created_at = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
user_id = Column(String)
|
||||||
|
name = Column(Text)
|
||||||
|
type = Column(Text)
|
||||||
|
content = Column(Text)
|
||||||
|
meta = Column(JSONField)
|
||||||
|
valves = Column(JSONField)
|
||||||
|
is_active = Column(Boolean)
|
||||||
|
is_global = Column(Boolean)
|
||||||
|
updated_at = Column(BigInteger)
|
||||||
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class FunctionMeta(BaseModel):
|
class FunctionMeta(BaseModel):
|
||||||
@@ -55,6 +55,8 @@ class FunctionModel(BaseModel):
|
|||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -85,13 +87,11 @@ class FunctionValves(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class FunctionsTable:
|
class FunctionsTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Function])
|
|
||||||
|
|
||||||
def insert_new_function(
|
def insert_new_function(
|
||||||
self, user_id: str, type: str, form_data: FunctionForm
|
self, user_id: str, type: str, form_data: FunctionForm
|
||||||
) -> Optional[FunctionModel]:
|
) -> Optional[FunctionModel]:
|
||||||
|
|
||||||
function = FunctionModel(
|
function = FunctionModel(
|
||||||
**{
|
**{
|
||||||
**form_data.model_dump(),
|
**form_data.model_dump(),
|
||||||
@@ -103,89 +103,111 @@ class FunctionsTable:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = Function.create(**function.model_dump())
|
with get_db() as db:
|
||||||
if result:
|
result = Function(**function.model_dump())
|
||||||
return function
|
db.add(result)
|
||||||
else:
|
db.commit()
|
||||||
return None
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return FunctionModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating tool: {e}")
|
print(f"Error creating tool: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
|
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
|
||||||
try:
|
try:
|
||||||
function = Function.get(Function.id == id)
|
with get_db() as db:
|
||||||
return FunctionModel(**model_to_dict(function))
|
|
||||||
|
function = db.get(Function, id)
|
||||||
|
return FunctionModel.model_validate(function)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_functions(self, active_only=False) -> List[FunctionModel]:
|
def get_functions(self, active_only=False) -> List[FunctionModel]:
|
||||||
if active_only:
|
with get_db() as db:
|
||||||
return [
|
|
||||||
FunctionModel(**model_to_dict(function))
|
if active_only:
|
||||||
for function in Function.select().where(Function.is_active == True)
|
return [
|
||||||
]
|
FunctionModel.model_validate(function)
|
||||||
else:
|
for function in db.query(Function).filter_by(is_active=True).all()
|
||||||
return [
|
]
|
||||||
FunctionModel(**model_to_dict(function))
|
else:
|
||||||
for function in Function.select()
|
return [
|
||||||
]
|
FunctionModel.model_validate(function)
|
||||||
|
for function in db.query(Function).all()
|
||||||
|
]
|
||||||
|
|
||||||
def get_functions_by_type(
|
def get_functions_by_type(
|
||||||
self, type: str, active_only=False
|
self, type: str, active_only=False
|
||||||
) -> List[FunctionModel]:
|
) -> List[FunctionModel]:
|
||||||
if active_only:
|
with get_db() as db:
|
||||||
return [
|
|
||||||
FunctionModel(**model_to_dict(function))
|
if active_only:
|
||||||
for function in Function.select().where(
|
return [
|
||||||
Function.type == type, Function.is_active == True
|
FunctionModel.model_validate(function)
|
||||||
)
|
for function in db.query(Function)
|
||||||
]
|
.filter_by(type=type, is_active=True)
|
||||||
else:
|
.all()
|
||||||
return [
|
]
|
||||||
FunctionModel(**model_to_dict(function))
|
else:
|
||||||
for function in Function.select().where(Function.type == type)
|
return [
|
||||||
]
|
FunctionModel.model_validate(function)
|
||||||
|
for function in db.query(Function).filter_by(type=type).all()
|
||||||
|
]
|
||||||
|
|
||||||
def get_global_filter_functions(self) -> List[FunctionModel]:
|
def get_global_filter_functions(self) -> List[FunctionModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
FunctionModel(**model_to_dict(function))
|
|
||||||
for function in Function.select().where(
|
return [
|
||||||
Function.type == "filter",
|
FunctionModel.model_validate(function)
|
||||||
Function.is_active == True,
|
for function in db.query(Function)
|
||||||
Function.is_global == True,
|
.filter_by(type="filter", is_active=True, is_global=True)
|
||||||
)
|
.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_global_action_functions(self) -> List[FunctionModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
return [
|
||||||
|
FunctionModel.model_validate(function)
|
||||||
|
for function in db.query(Function)
|
||||||
|
.filter_by(type="action", is_active=True, is_global=True)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
|
||||||
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
|
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
|
||||||
try:
|
with get_db() as db:
|
||||||
function = Function.get(Function.id == id)
|
|
||||||
return function.valves if function.valves else {}
|
try:
|
||||||
except Exception as e:
|
function = db.get(Function, id)
|
||||||
print(f"An error occurred: {e}")
|
return function.valves if function.valves else {}
|
||||||
return None
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def update_function_valves_by_id(
|
def update_function_valves_by_id(
|
||||||
self, id: str, valves: dict
|
self, id: str, valves: dict
|
||||||
) -> Optional[FunctionValves]:
|
) -> Optional[FunctionValves]:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Function.update(
|
|
||||||
**{"valves": valves},
|
|
||||||
updated_at=int(time.time()),
|
|
||||||
).where(Function.id == id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
function = Function.get(Function.id == id)
|
try:
|
||||||
return FunctionValves(**model_to_dict(function))
|
function = db.get(Function, id)
|
||||||
except:
|
function.valves = valves
|
||||||
return None
|
function.updated_at = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
db.refresh(function)
|
||||||
|
return self.get_function_by_id(id)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_user_valves_by_id_and_user_id(
|
def get_user_valves_by_id_and_user_id(
|
||||||
self, id: str, user_id: str
|
self, id: str, user_id: str
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
user_settings = user.settings.model_dump()
|
user_settings = user.settings.model_dump() if user.settings else {}
|
||||||
|
|
||||||
# Check if user has "functions" and "valves" settings
|
# Check if user has "functions" and "valves" settings
|
||||||
if "functions" not in user_settings:
|
if "functions" not in user_settings:
|
||||||
@@ -201,9 +223,10 @@ class FunctionsTable:
|
|||||||
def update_user_valves_by_id_and_user_id(
|
def update_user_valves_by_id_and_user_id(
|
||||||
self, id: str, user_id: str, valves: dict
|
self, id: str, user_id: str, valves: dict
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
user_settings = user.settings.model_dump()
|
user_settings = user.settings.model_dump() if user.settings else {}
|
||||||
|
|
||||||
# Check if user has "functions" and "valves" settings
|
# Check if user has "functions" and "valves" settings
|
||||||
if "functions" not in user_settings:
|
if "functions" not in user_settings:
|
||||||
@@ -214,8 +237,7 @@ class FunctionsTable:
|
|||||||
user_settings["functions"]["valves"][id] = valves
|
user_settings["functions"]["valves"][id] = valves
|
||||||
|
|
||||||
# Update the user settings in the database
|
# Update the user settings in the database
|
||||||
query = Users.update_user_by_id(user_id, {"settings": user_settings})
|
Users.update_user_by_id(user_id, {"settings": user_settings})
|
||||||
query.execute()
|
|
||||||
|
|
||||||
return user_settings["functions"]["valves"][id]
|
return user_settings["functions"]["valves"][id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -223,39 +245,44 @@ class FunctionsTable:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
|
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Function.update(
|
|
||||||
**updated,
|
|
||||||
updated_at=int(time.time()),
|
|
||||||
).where(Function.id == id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
function = Function.get(Function.id == id)
|
try:
|
||||||
return FunctionModel(**model_to_dict(function))
|
db.query(Function).filter_by(id=id).update(
|
||||||
except:
|
{
|
||||||
return None
|
**updated,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return self.get_function_by_id(id)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def deactivate_all_functions(self) -> Optional[bool]:
|
def deactivate_all_functions(self) -> Optional[bool]:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Function.update(
|
|
||||||
**{"is_active": False},
|
|
||||||
updated_at=int(time.time()),
|
|
||||||
)
|
|
||||||
|
|
||||||
query.execute()
|
try:
|
||||||
|
db.query(Function).update(
|
||||||
return True
|
{
|
||||||
except:
|
"is_active": False,
|
||||||
return None
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def delete_function_by_id(self, id: str) -> bool:
|
def delete_function_by_id(self, id: str) -> bool:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Function.delete().where((Function.id == id))
|
try:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
db.query(Function).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Functions = FunctionsTable(DB)
|
Functions = FunctionsTable()
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
|
|
||||||
from apps.webui.internal.db import DB
|
from sqlalchemy import Column, String, BigInteger, Text
|
||||||
from apps.webui.models.chats import Chats
|
|
||||||
|
from apps.webui.internal.db import Base, get_db
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
@@ -14,15 +13,14 @@ import uuid
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Memory(Model):
|
class Memory(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "memory"
|
||||||
user_id = CharField()
|
|
||||||
content = TextField()
|
|
||||||
updated_at = BigIntegerField()
|
|
||||||
created_at = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
user_id = Column(String)
|
||||||
|
content = Column(Text)
|
||||||
|
updated_at = Column(BigInteger)
|
||||||
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class MemoryModel(BaseModel):
|
class MemoryModel(BaseModel):
|
||||||
@@ -32,6 +30,8 @@ class MemoryModel(BaseModel):
|
|||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -39,94 +39,110 @@ class MemoryModel(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class MemoriesTable:
|
class MemoriesTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Memory])
|
|
||||||
|
|
||||||
def insert_new_memory(
|
def insert_new_memory(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
content: str,
|
content: str,
|
||||||
) -> Optional[MemoryModel]:
|
) -> Optional[MemoryModel]:
|
||||||
id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
memory = MemoryModel(
|
with get_db() as db:
|
||||||
**{
|
id = str(uuid.uuid4())
|
||||||
"id": id,
|
|
||||||
"user_id": user_id,
|
memory = MemoryModel(
|
||||||
"content": content,
|
**{
|
||||||
"created_at": int(time.time()),
|
"id": id,
|
||||||
"updated_at": int(time.time()),
|
"user_id": user_id,
|
||||||
}
|
"content": content,
|
||||||
)
|
"created_at": int(time.time()),
|
||||||
result = Memory.create(**memory.model_dump())
|
"updated_at": int(time.time()),
|
||||||
if result:
|
}
|
||||||
return memory
|
)
|
||||||
else:
|
result = Memory(**memory.model_dump())
|
||||||
return None
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return MemoryModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_memory_by_id(
|
def update_memory_by_id(
|
||||||
self,
|
self,
|
||||||
id: str,
|
id: str,
|
||||||
content: str,
|
content: str,
|
||||||
) -> Optional[MemoryModel]:
|
) -> Optional[MemoryModel]:
|
||||||
try:
|
with get_db() as db:
|
||||||
memory = Memory.get(Memory.id == id)
|
|
||||||
memory.content = content
|
try:
|
||||||
memory.updated_at = int(time.time())
|
db.query(Memory).filter_by(id=id).update(
|
||||||
memory.save()
|
{"content": content, "updated_at": int(time.time())}
|
||||||
return MemoryModel(**model_to_dict(memory))
|
)
|
||||||
except:
|
db.commit()
|
||||||
return None
|
return self.get_memory_by_id(id)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_memories(self) -> List[MemoryModel]:
|
def get_memories(self) -> List[MemoryModel]:
|
||||||
try:
|
with get_db() as db:
|
||||||
memories = Memory.select()
|
|
||||||
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
|
try:
|
||||||
except:
|
memories = db.query(Memory).all()
|
||||||
return None
|
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
|
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
|
||||||
try:
|
with get_db() as db:
|
||||||
memories = Memory.select().where(Memory.user_id == user_id)
|
|
||||||
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_memory_by_id(self, id) -> Optional[MemoryModel]:
|
try:
|
||||||
try:
|
memories = db.query(Memory).filter_by(user_id=user_id).all()
|
||||||
memory = Memory.get(Memory.id == id)
|
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||||
return MemoryModel(**model_to_dict(memory))
|
except:
|
||||||
except:
|
return None
|
||||||
return None
|
|
||||||
|
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
|
||||||
|
try:
|
||||||
|
memory = db.get(Memory, id)
|
||||||
|
return MemoryModel.model_validate(memory)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def delete_memory_by_id(self, id: str) -> bool:
|
def delete_memory_by_id(self, id: str) -> bool:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Memory.delete().where(Memory.id == id)
|
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
try:
|
||||||
|
db.query(Memory).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
except:
|
return True
|
||||||
return False
|
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_memories_by_user_id(self, user_id: str) -> bool:
|
def delete_memories_by_user_id(self, user_id: str) -> bool:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Memory.delete().where(Memory.user_id == user_id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
return True
|
try:
|
||||||
except:
|
db.query(Memory).filter_by(user_id=user_id).delete()
|
||||||
return False
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||||
try:
|
with get_db() as db:
|
||||||
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
return True
|
try:
|
||||||
except:
|
db.query(Memory).filter_by(id=id, user_id=user_id).delete()
|
||||||
return False
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Memories = MemoriesTable(DB)
|
Memories = MemoriesTable()
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import peewee as pw
|
|
||||||
from peewee import *
|
|
||||||
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from sqlalchemy import String, Column, BigInteger, Text
|
||||||
|
|
||||||
from apps.webui.internal.db import DB, JSONField
|
from apps.webui.internal.db import Base, JSONField, get_db
|
||||||
|
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
@@ -32,7 +29,7 @@ class ModelParams(BaseModel):
|
|||||||
|
|
||||||
# ModelMeta is a model for the data stored in the meta field of the Model table
|
# ModelMeta is a model for the data stored in the meta field of the Model table
|
||||||
class ModelMeta(BaseModel):
|
class ModelMeta(BaseModel):
|
||||||
profile_image_url: Optional[str] = "/favicon.png"
|
profile_image_url: Optional[str] = "/static/favicon.png"
|
||||||
|
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
"""
|
"""
|
||||||
@@ -46,38 +43,37 @@ class ModelMeta(BaseModel):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Model(pw.Model):
|
class Model(Base):
|
||||||
id = pw.TextField(unique=True)
|
__tablename__ = "model"
|
||||||
|
|
||||||
|
id = Column(Text, primary_key=True)
|
||||||
"""
|
"""
|
||||||
The model's id as used in the API. If set to an existing model, it will override the model.
|
The model's id as used in the API. If set to an existing model, it will override the model.
|
||||||
"""
|
"""
|
||||||
user_id = pw.TextField()
|
user_id = Column(Text)
|
||||||
|
|
||||||
base_model_id = pw.TextField(null=True)
|
base_model_id = Column(Text, nullable=True)
|
||||||
"""
|
"""
|
||||||
An optional pointer to the actual model that should be used when proxying requests.
|
An optional pointer to the actual model that should be used when proxying requests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = pw.TextField()
|
name = Column(Text)
|
||||||
"""
|
"""
|
||||||
The human-readable display name of the model.
|
The human-readable display name of the model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = JSONField()
|
params = Column(JSONField)
|
||||||
"""
|
"""
|
||||||
Holds a JSON encoded blob of parameters, see `ModelParams`.
|
Holds a JSON encoded blob of parameters, see `ModelParams`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
meta = JSONField()
|
meta = Column(JSONField)
|
||||||
"""
|
"""
|
||||||
Holds a JSON encoded blob of metadata, see `ModelMeta`.
|
Holds a JSON encoded blob of metadata, see `ModelMeta`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
updated_at = BigIntegerField()
|
updated_at = Column(BigInteger)
|
||||||
created_at = BigIntegerField()
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
database = DB
|
|
||||||
|
|
||||||
|
|
||||||
class ModelModel(BaseModel):
|
class ModelModel(BaseModel):
|
||||||
@@ -92,6 +88,8 @@ class ModelModel(BaseModel):
|
|||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -115,12 +113,6 @@ class ModelForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ModelsTable:
|
class ModelsTable:
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
db: pw.SqliteDatabase | pw.PostgresqlDatabase,
|
|
||||||
):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Model])
|
|
||||||
|
|
||||||
def insert_new_model(
|
def insert_new_model(
|
||||||
self, form_data: ModelForm, user_id: str
|
self, form_data: ModelForm, user_id: str
|
||||||
@@ -134,34 +126,50 @@ class ModelsTable:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = Model.create(**model.model_dump())
|
|
||||||
|
|
||||||
if result:
|
with get_db() as db:
|
||||||
return model
|
|
||||||
else:
|
result = Model(**model.model_dump())
|
||||||
return None
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return ModelModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_all_models(self) -> List[ModelModel]:
|
def get_all_models(self) -> List[ModelModel]:
|
||||||
return [ModelModel(**model_to_dict(model)) for model in Model.select()]
|
with get_db() as db:
|
||||||
|
|
||||||
|
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
|
||||||
|
|
||||||
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||||
try:
|
try:
|
||||||
model = Model.get(Model.id == id)
|
with get_db() as db:
|
||||||
return ModelModel(**model_to_dict(model))
|
|
||||||
|
model = db.get(Model, id)
|
||||||
|
return ModelModel.model_validate(model)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
||||||
try:
|
try:
|
||||||
# update only the fields that are present in the model
|
with get_db() as db:
|
||||||
query = Model.update(**model.model_dump()).where(Model.id == id)
|
# update only the fields that are present in the model
|
||||||
query.execute()
|
result = (
|
||||||
|
db.query(Model)
|
||||||
|
.filter_by(id=id)
|
||||||
|
.update(model.model_dump(exclude={"id"}, exclude_none=True))
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
model = Model.get(Model.id == id)
|
model = db.get(Model, id)
|
||||||
return ModelModel(**model_to_dict(model))
|
db.refresh(model)
|
||||||
|
return ModelModel.model_validate(model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
@@ -169,11 +177,14 @@ class ModelsTable:
|
|||||||
|
|
||||||
def delete_model_by_id(self, id: str) -> bool:
|
def delete_model_by_id(self, id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Model.delete().where(Model.id == id)
|
with get_db() as db:
|
||||||
query.execute()
|
|
||||||
return True
|
db.query(Model).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Models = ModelsTable(DB)
|
Models = ModelsTable()
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
from typing import List, Optional
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from utils.utils import decode_token
|
from sqlalchemy import String, Column, BigInteger, Text
|
||||||
from utils.misc import get_gravatar_url
|
|
||||||
|
|
||||||
from apps.webui.internal.db import DB
|
from apps.webui.internal.db import Base, get_db
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -16,15 +13,14 @@ import json
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Prompt(Model):
|
class Prompt(Base):
|
||||||
command = CharField(unique=True)
|
__tablename__ = "prompt"
|
||||||
user_id = CharField()
|
|
||||||
title = TextField()
|
|
||||||
content = TextField()
|
|
||||||
timestamp = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
command = Column(String, primary_key=True)
|
||||||
database = DB
|
user_id = Column(String)
|
||||||
|
title = Column(Text)
|
||||||
|
content = Column(Text)
|
||||||
|
timestamp = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class PromptModel(BaseModel):
|
class PromptModel(BaseModel):
|
||||||
@@ -34,6 +30,8 @@ class PromptModel(BaseModel):
|
|||||||
content: str
|
content: str
|
||||||
timestamp: int # timestamp in epoch
|
timestamp: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -48,10 +46,6 @@ class PromptForm(BaseModel):
|
|||||||
|
|
||||||
class PromptsTable:
|
class PromptsTable:
|
||||||
|
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Prompt])
|
|
||||||
|
|
||||||
def insert_new_prompt(
|
def insert_new_prompt(
|
||||||
self, user_id: str, form_data: PromptForm
|
self, user_id: str, form_data: PromptForm
|
||||||
) -> Optional[PromptModel]:
|
) -> Optional[PromptModel]:
|
||||||
@@ -66,53 +60,60 @@ class PromptsTable:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = Prompt.create(**prompt.model_dump())
|
with get_db() as db:
|
||||||
if result:
|
|
||||||
return prompt
|
result = Prompt(**prompt.dict())
|
||||||
else:
|
db.add(result)
|
||||||
return None
|
db.commit()
|
||||||
except:
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return PromptModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
|
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
|
||||||
try:
|
try:
|
||||||
prompt = Prompt.get(Prompt.command == command)
|
with get_db() as db:
|
||||||
return PromptModel(**model_to_dict(prompt))
|
|
||||||
|
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||||
|
return PromptModel.model_validate(prompt)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_prompts(self) -> List[PromptModel]:
|
def get_prompts(self) -> List[PromptModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
PromptModel(**model_to_dict(prompt))
|
|
||||||
for prompt in Prompt.select()
|
return [
|
||||||
# .limit(limit).offset(skip)
|
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_prompt_by_command(
|
def update_prompt_by_command(
|
||||||
self, command: str, form_data: PromptForm
|
self, command: str, form_data: PromptForm
|
||||||
) -> Optional[PromptModel]:
|
) -> Optional[PromptModel]:
|
||||||
try:
|
try:
|
||||||
query = Prompt.update(
|
with get_db() as db:
|
||||||
title=form_data.title,
|
|
||||||
content=form_data.content,
|
|
||||||
timestamp=int(time.time()),
|
|
||||||
).where(Prompt.command == command)
|
|
||||||
|
|
||||||
query.execute()
|
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||||
|
prompt.title = form_data.title
|
||||||
prompt = Prompt.get(Prompt.command == command)
|
prompt.content = form_data.content
|
||||||
return PromptModel(**model_to_dict(prompt))
|
prompt.timestamp = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
return PromptModel.model_validate(prompt)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_prompt_by_command(self, command: str) -> bool:
|
def delete_prompt_by_command(self, command: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Prompt.delete().where((Prompt.command == command))
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
|
|
||||||
return True
|
db.query(Prompt).filter_by(command=command).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Prompts = PromptsTable(DB)
|
Prompts = PromptsTable()
|
||||||
|
|||||||
+138
-103
@@ -1,14 +1,14 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from typing import List, Union, Optional
|
from typing import List, Optional
|
||||||
from peewee import *
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from apps.webui.internal.db import DB
|
from sqlalchemy import String, Column, BigInteger, Text
|
||||||
|
|
||||||
|
from apps.webui.internal.db import Base, get_db
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
@@ -20,25 +20,23 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Tag(Model):
|
class Tag(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "tag"
|
||||||
name = CharField()
|
|
||||||
user_id = CharField()
|
|
||||||
data = TextField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
name = Column(String)
|
||||||
|
user_id = Column(String)
|
||||||
|
data = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatIdTag(Model):
|
class ChatIdTag(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "chatidtag"
|
||||||
tag_name = CharField()
|
|
||||||
chat_id = CharField()
|
|
||||||
user_id = CharField()
|
|
||||||
timestamp = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
tag_name = Column(String)
|
||||||
|
chat_id = Column(String)
|
||||||
|
user_id = Column(String)
|
||||||
|
timestamp = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class TagModel(BaseModel):
|
class TagModel(BaseModel):
|
||||||
@@ -47,6 +45,8 @@ class TagModel(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
data: Optional[str] = None
|
data: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatIdTagModel(BaseModel):
|
class ChatIdTagModel(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
@@ -55,6 +55,8 @@ class ChatIdTagModel(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
timestamp: int
|
timestamp: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -75,28 +77,31 @@ class ChatTagsResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TagTable:
|
class TagTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
db.create_tables([Tag, ChatIdTag])
|
|
||||||
|
|
||||||
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
|
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
|
||||||
id = str(uuid.uuid4())
|
with get_db() as db:
|
||||||
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
|
|
||||||
try:
|
id = str(uuid.uuid4())
|
||||||
result = Tag.create(**tag.model_dump())
|
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
|
||||||
if result:
|
try:
|
||||||
return tag
|
result = Tag(**tag.model_dump())
|
||||||
else:
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return TagModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_tag_by_name_and_user_id(
|
def get_tag_by_name_and_user_id(
|
||||||
self, name: str, user_id: str
|
self, name: str, user_id: str
|
||||||
) -> Optional[TagModel]:
|
) -> Optional[TagModel]:
|
||||||
try:
|
try:
|
||||||
tag = Tag.get(Tag.name == name, Tag.user_id == user_id)
|
with get_db() as db:
|
||||||
return TagModel(**model_to_dict(tag))
|
tag = db.query(Tag).filter_by(name=name, user_id=user_id).first()
|
||||||
|
return TagModel.model_validate(tag)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -118,82 +123,110 @@ class TagTable:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = ChatIdTag.create(**chatIdTag.model_dump())
|
with get_db() as db:
|
||||||
if result:
|
result = ChatIdTag(**chatIdTag.model_dump())
|
||||||
return chatIdTag
|
db.add(result)
|
||||||
else:
|
db.commit()
|
||||||
return None
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return ChatIdTagModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
|
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
|
||||||
tag_names = [
|
with get_db() as db:
|
||||||
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
|
tag_names = [
|
||||||
for chat_id_tag in ChatIdTag.select()
|
chat_id_tag.tag_name
|
||||||
.where(ChatIdTag.user_id == user_id)
|
for chat_id_tag in (
|
||||||
.order_by(ChatIdTag.timestamp.desc())
|
db.query(ChatIdTag)
|
||||||
]
|
.filter_by(user_id=user_id)
|
||||||
|
.order_by(ChatIdTag.timestamp.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
TagModel(**model_to_dict(tag))
|
TagModel.model_validate(tag)
|
||||||
for tag in Tag.select()
|
for tag in (
|
||||||
.where(Tag.user_id == user_id)
|
db.query(Tag)
|
||||||
.where(Tag.name.in_(tag_names))
|
.filter_by(user_id=user_id)
|
||||||
]
|
.filter(Tag.name.in_(tag_names))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def get_tags_by_chat_id_and_user_id(
|
def get_tags_by_chat_id_and_user_id(
|
||||||
self, chat_id: str, user_id: str
|
self, chat_id: str, user_id: str
|
||||||
) -> List[TagModel]:
|
) -> List[TagModel]:
|
||||||
tag_names = [
|
with get_db() as db:
|
||||||
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
|
|
||||||
for chat_id_tag in ChatIdTag.select()
|
|
||||||
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id))
|
|
||||||
.order_by(ChatIdTag.timestamp.desc())
|
|
||||||
]
|
|
||||||
|
|
||||||
return [
|
tag_names = [
|
||||||
TagModel(**model_to_dict(tag))
|
chat_id_tag.tag_name
|
||||||
for tag in Tag.select()
|
for chat_id_tag in (
|
||||||
.where(Tag.user_id == user_id)
|
db.query(ChatIdTag)
|
||||||
.where(Tag.name.in_(tag_names))
|
.filter_by(user_id=user_id, chat_id=chat_id)
|
||||||
]
|
.order_by(ChatIdTag.timestamp.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return [
|
||||||
|
TagModel.model_validate(tag)
|
||||||
|
for tag in (
|
||||||
|
db.query(Tag)
|
||||||
|
.filter_by(user_id=user_id)
|
||||||
|
.filter(Tag.name.in_(tag_names))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def get_chat_ids_by_tag_name_and_user_id(
|
def get_chat_ids_by_tag_name_and_user_id(
|
||||||
self, tag_name: str, user_id: str
|
self, tag_name: str, user_id: str
|
||||||
) -> Optional[ChatIdTagModel]:
|
) -> List[ChatIdTagModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
ChatIdTagModel(**model_to_dict(chat_id_tag))
|
|
||||||
for chat_id_tag in ChatIdTag.select()
|
return [
|
||||||
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.tag_name == tag_name))
|
ChatIdTagModel.model_validate(chat_id_tag)
|
||||||
.order_by(ChatIdTag.timestamp.desc())
|
for chat_id_tag in (
|
||||||
]
|
db.query(ChatIdTag)
|
||||||
|
.filter_by(user_id=user_id, tag_name=tag_name)
|
||||||
|
.order_by(ChatIdTag.timestamp.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def count_chat_ids_by_tag_name_and_user_id(
|
def count_chat_ids_by_tag_name_and_user_id(
|
||||||
self, tag_name: str, user_id: str
|
self, tag_name: str, user_id: str
|
||||||
) -> int:
|
) -> int:
|
||||||
return (
|
with get_db() as db:
|
||||||
ChatIdTag.select()
|
|
||||||
.where((ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id))
|
return (
|
||||||
.count()
|
db.query(ChatIdTag)
|
||||||
)
|
.filter_by(tag_name=tag_name, user_id=user_id)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
|
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = ChatIdTag.delete().where(
|
with get_db() as db:
|
||||||
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
|
res = (
|
||||||
)
|
db.query(ChatIdTag)
|
||||||
res = query.execute() # Remove the rows, return number of rows removed.
|
.filter_by(tag_name=tag_name, user_id=user_id)
|
||||||
log.debug(f"res: {res}")
|
.delete()
|
||||||
|
|
||||||
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
|
||||||
if tag_count == 0:
|
|
||||||
# Remove tag item from Tag col as well
|
|
||||||
query = Tag.delete().where(
|
|
||||||
(Tag.name == tag_name) & (Tag.user_id == user_id)
|
|
||||||
)
|
)
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
log.debug(f"res: {res}")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return True
|
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
|
||||||
|
tag_name, user_id
|
||||||
|
)
|
||||||
|
if tag_count == 0:
|
||||||
|
# Remove tag item from Tag col as well
|
||||||
|
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"delete_tag: {e}")
|
log.error(f"delete_tag: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -202,23 +235,25 @@ class TagTable:
|
|||||||
self, tag_name: str, chat_id: str, user_id: str
|
self, tag_name: str, chat_id: str, user_id: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
try:
|
try:
|
||||||
query = ChatIdTag.delete().where(
|
with get_db() as db:
|
||||||
(ChatIdTag.tag_name == tag_name)
|
|
||||||
& (ChatIdTag.chat_id == chat_id)
|
|
||||||
& (ChatIdTag.user_id == user_id)
|
|
||||||
)
|
|
||||||
res = query.execute() # Remove the rows, return number of rows removed.
|
|
||||||
log.debug(f"res: {res}")
|
|
||||||
|
|
||||||
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
res = (
|
||||||
if tag_count == 0:
|
db.query(ChatIdTag)
|
||||||
# Remove tag item from Tag col as well
|
.filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
|
||||||
query = Tag.delete().where(
|
.delete()
|
||||||
(Tag.name == tag_name) & (Tag.user_id == user_id)
|
|
||||||
)
|
)
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
log.debug(f"res: {res}")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return True
|
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
|
||||||
|
tag_name, user_id
|
||||||
|
)
|
||||||
|
if tag_count == 0:
|
||||||
|
# Remove tag item from Tag col as well
|
||||||
|
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"delete_tag: {e}")
|
log.error(f"delete_tag: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -234,4 +269,4 @@ class TagTable:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
Tags = TagTable(DB)
|
Tags = TagTable()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
from typing import List, Optional
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from apps.webui.internal.db import DB, JSONField
|
from sqlalchemy import String, Column, BigInteger, Text
|
||||||
|
|
||||||
|
from apps.webui.internal.db import Base, JSONField, get_db
|
||||||
from apps.webui.models.users import Users
|
from apps.webui.models.users import Users
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -21,19 +21,18 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class Tool(Model):
|
class Tool(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "tool"
|
||||||
user_id = CharField()
|
|
||||||
name = TextField()
|
|
||||||
content = TextField()
|
|
||||||
specs = JSONField()
|
|
||||||
meta = JSONField()
|
|
||||||
valves = JSONField()
|
|
||||||
updated_at = BigIntegerField()
|
|
||||||
created_at = BigIntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
id = Column(String, primary_key=True)
|
||||||
database = DB
|
user_id = Column(String)
|
||||||
|
name = Column(Text)
|
||||||
|
content = Column(Text)
|
||||||
|
specs = Column(JSONField)
|
||||||
|
meta = Column(JSONField)
|
||||||
|
valves = Column(JSONField)
|
||||||
|
updated_at = Column(BigInteger)
|
||||||
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
class ToolMeta(BaseModel):
|
class ToolMeta(BaseModel):
|
||||||
@@ -51,6 +50,8 @@ class ToolModel(BaseModel):
|
|||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -78,61 +79,68 @@ class ToolValves(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ToolsTable:
|
class ToolsTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([Tool])
|
|
||||||
|
|
||||||
def insert_new_tool(
|
def insert_new_tool(
|
||||||
self, user_id: str, form_data: ToolForm, specs: List[dict]
|
self, user_id: str, form_data: ToolForm, specs: List[dict]
|
||||||
) -> Optional[ToolModel]:
|
) -> Optional[ToolModel]:
|
||||||
tool = ToolModel(
|
|
||||||
**{
|
|
||||||
**form_data.model_dump(),
|
|
||||||
"specs": specs,
|
|
||||||
"user_id": user_id,
|
|
||||||
"updated_at": int(time.time()),
|
|
||||||
"created_at": int(time.time()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
with get_db() as db:
|
||||||
result = Tool.create(**tool.model_dump())
|
|
||||||
if result:
|
tool = ToolModel(
|
||||||
return tool
|
**{
|
||||||
else:
|
**form_data.model_dump(),
|
||||||
|
"specs": specs,
|
||||||
|
"user_id": user_id,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = Tool(**tool.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return ToolModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating tool: {e}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating tool: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
||||||
try:
|
try:
|
||||||
tool = Tool.get(Tool.id == id)
|
with get_db() as db:
|
||||||
return ToolModel(**model_to_dict(tool))
|
|
||||||
|
tool = db.get(Tool, id)
|
||||||
|
return ToolModel.model_validate(tool)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_tools(self) -> List[ToolModel]:
|
def get_tools(self) -> List[ToolModel]:
|
||||||
return [ToolModel(**model_to_dict(tool)) for tool in Tool.select()]
|
with get_db() as db:
|
||||||
|
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
|
||||||
|
|
||||||
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
||||||
try:
|
try:
|
||||||
tool = Tool.get(Tool.id == id)
|
with get_db() as db:
|
||||||
return tool.valves if tool.valves else {}
|
|
||||||
|
tool = db.get(Tool, id)
|
||||||
|
return tool.valves if tool.valves else {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
print(f"An error occurred: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
|
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
|
||||||
try:
|
try:
|
||||||
query = Tool.update(
|
with get_db() as db:
|
||||||
**{"valves": valves},
|
|
||||||
updated_at=int(time.time()),
|
|
||||||
).where(Tool.id == id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
tool = Tool.get(Tool.id == id)
|
db.query(Tool).filter_by(id=id).update(
|
||||||
return ToolValves(**model_to_dict(tool))
|
{"valves": valves, "updated_at": int(time.time())}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return self.get_tool_by_id(id)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -141,7 +149,7 @@ class ToolsTable:
|
|||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
try:
|
try:
|
||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
user_settings = user.settings.model_dump()
|
user_settings = user.settings.model_dump() if user.settings else {}
|
||||||
|
|
||||||
# Check if user has "tools" and "valves" settings
|
# Check if user has "tools" and "valves" settings
|
||||||
if "tools" not in user_settings:
|
if "tools" not in user_settings:
|
||||||
@@ -159,7 +167,7 @@ class ToolsTable:
|
|||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
try:
|
try:
|
||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
user_settings = user.settings.model_dump()
|
user_settings = user.settings.model_dump() if user.settings else {}
|
||||||
|
|
||||||
# Check if user has "tools" and "valves" settings
|
# Check if user has "tools" and "valves" settings
|
||||||
if "tools" not in user_settings:
|
if "tools" not in user_settings:
|
||||||
@@ -170,8 +178,7 @@ class ToolsTable:
|
|||||||
user_settings["tools"]["valves"][id] = valves
|
user_settings["tools"]["valves"][id] = valves
|
||||||
|
|
||||||
# Update the user settings in the database
|
# Update the user settings in the database
|
||||||
query = Users.update_user_by_id(user_id, {"settings": user_settings})
|
Users.update_user_by_id(user_id, {"settings": user_settings})
|
||||||
query.execute()
|
|
||||||
|
|
||||||
return user_settings["tools"]["valves"][id]
|
return user_settings["tools"]["valves"][id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -180,25 +187,27 @@ class ToolsTable:
|
|||||||
|
|
||||||
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
||||||
try:
|
try:
|
||||||
query = Tool.update(
|
with get_db() as db:
|
||||||
**updated,
|
db.query(Tool).filter_by(id=id).update(
|
||||||
updated_at=int(time.time()),
|
{**updated, "updated_at": int(time.time())}
|
||||||
).where(Tool.id == id)
|
)
|
||||||
query.execute()
|
db.commit()
|
||||||
|
|
||||||
tool = Tool.get(Tool.id == id)
|
tool = db.query(Tool).get(id)
|
||||||
return ToolModel(**model_to_dict(tool))
|
db.refresh(tool)
|
||||||
|
return ToolModel.model_validate(tool)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_tool_by_id(self, id: str) -> bool:
|
def delete_tool_by_id(self, id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Tool.delete().where((Tool.id == id))
|
with get_db() as db:
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
db.query(Tool).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Tools = ToolsTable(DB)
|
Tools = ToolsTable()
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict, parse_obj_as
|
||||||
from peewee import *
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy import String, Column, BigInteger, Text
|
||||||
|
|
||||||
from utils.misc import get_gravatar_url
|
from utils.misc import get_gravatar_url
|
||||||
|
|
||||||
from apps.webui.internal.db import DB, JSONField
|
from apps.webui.internal.db import Base, JSONField, Session, get_db
|
||||||
from apps.webui.models.chats import Chats
|
from apps.webui.models.chats import Chats
|
||||||
|
|
||||||
####################
|
####################
|
||||||
@@ -13,25 +14,24 @@ from apps.webui.models.chats import Chats
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class User(Model):
|
class User(Base):
|
||||||
id = CharField(unique=True)
|
__tablename__ = "user"
|
||||||
name = CharField()
|
|
||||||
email = CharField()
|
|
||||||
role = CharField()
|
|
||||||
profile_image_url = TextField()
|
|
||||||
|
|
||||||
last_active_at = BigIntegerField()
|
id = Column(String, primary_key=True)
|
||||||
updated_at = BigIntegerField()
|
name = Column(String)
|
||||||
created_at = BigIntegerField()
|
email = Column(String)
|
||||||
|
role = Column(String)
|
||||||
|
profile_image_url = Column(Text)
|
||||||
|
|
||||||
api_key = CharField(null=True, unique=True)
|
last_active_at = Column(BigInteger)
|
||||||
settings = JSONField(null=True)
|
updated_at = Column(BigInteger)
|
||||||
info = JSONField(null=True)
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
oauth_sub = TextField(null=True, unique=True)
|
api_key = Column(String, nullable=True, unique=True)
|
||||||
|
settings = Column(JSONField, nullable=True)
|
||||||
|
info = Column(JSONField, nullable=True)
|
||||||
|
|
||||||
class Meta:
|
oauth_sub = Column(Text, unique=True)
|
||||||
database = DB
|
|
||||||
|
|
||||||
|
|
||||||
class UserSettings(BaseModel):
|
class UserSettings(BaseModel):
|
||||||
@@ -57,6 +57,8 @@ class UserModel(BaseModel):
|
|||||||
|
|
||||||
oauth_sub: Optional[str] = None
|
oauth_sub: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
@@ -76,9 +78,6 @@ class UserUpdateForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class UsersTable:
|
class UsersTable:
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.db.create_tables([User])
|
|
||||||
|
|
||||||
def insert_new_user(
|
def insert_new_user(
|
||||||
self,
|
self,
|
||||||
@@ -89,77 +88,92 @@ class UsersTable:
|
|||||||
role: str = "pending",
|
role: str = "pending",
|
||||||
oauth_sub: Optional[str] = None,
|
oauth_sub: Optional[str] = None,
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
user = UserModel(
|
with get_db() as db:
|
||||||
**{
|
user = UserModel(
|
||||||
"id": id,
|
**{
|
||||||
"name": name,
|
"id": id,
|
||||||
"email": email,
|
"name": name,
|
||||||
"role": role,
|
"email": email,
|
||||||
"profile_image_url": profile_image_url,
|
"role": role,
|
||||||
"last_active_at": int(time.time()),
|
"profile_image_url": profile_image_url,
|
||||||
"created_at": int(time.time()),
|
"last_active_at": int(time.time()),
|
||||||
"updated_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"oauth_sub": oauth_sub,
|
"updated_at": int(time.time()),
|
||||||
}
|
"oauth_sub": oauth_sub,
|
||||||
)
|
}
|
||||||
result = User.create(**user.model_dump())
|
)
|
||||||
if result:
|
result = User(**user.model_dump())
|
||||||
return user
|
db.add(result)
|
||||||
else:
|
db.commit()
|
||||||
return None
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_user_by_id(self, id: str) -> Optional[UserModel]:
|
def get_user_by_id(self, id: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.id == id)
|
with get_db() as db:
|
||||||
return UserModel(**model_to_dict(user))
|
user = db.query(User).filter_by(id=id).first()
|
||||||
except:
|
return UserModel.model_validate(user)
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.api_key == api_key)
|
with get_db() as db:
|
||||||
return UserModel(**model_to_dict(user))
|
|
||||||
|
user = db.query(User).filter_by(api_key=api_key).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.email == email)
|
with get_db() as db:
|
||||||
return UserModel(**model_to_dict(user))
|
|
||||||
|
user = db.query(User).filter_by(email=email).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
|
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.oauth_sub == sub)
|
with get_db() as db:
|
||||||
return UserModel(**model_to_dict(user))
|
|
||||||
|
user = db.query(User).filter_by(oauth_sub=sub).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
|
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
UserModel(**model_to_dict(user))
|
users = (
|
||||||
for user in User.select()
|
db.query(User)
|
||||||
# .limit(limit).offset(skip)
|
# .offset(skip).limit(limit)
|
||||||
]
|
.all()
|
||||||
|
)
|
||||||
|
return [UserModel.model_validate(user) for user in users]
|
||||||
|
|
||||||
def get_num_users(self) -> Optional[int]:
|
def get_num_users(self) -> Optional[int]:
|
||||||
return User.select().count()
|
with get_db() as db:
|
||||||
|
return db.query(User).count()
|
||||||
|
|
||||||
def get_first_user(self) -> UserModel:
|
def get_first_user(self) -> UserModel:
|
||||||
try:
|
try:
|
||||||
user = User.select().order_by(User.created_at).first()
|
with get_db() as db:
|
||||||
return UserModel(**model_to_dict(user))
|
user = db.query(User).order_by(User.created_at).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
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)
|
with get_db() as db:
|
||||||
query.execute()
|
db.query(User).filter_by(id=id).update({"role": role})
|
||||||
|
db.commit()
|
||||||
user = User.get(User.id == id)
|
user = db.query(User).filter_by(id=id).first()
|
||||||
return UserModel(**model_to_dict(user))
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -167,23 +181,28 @@ class UsersTable:
|
|||||||
self, id: str, profile_image_url: str
|
self, id: str, profile_image_url: str
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
query = User.update(profile_image_url=profile_image_url).where(
|
with get_db() as db:
|
||||||
User.id == id
|
db.query(User).filter_by(id=id).update(
|
||||||
)
|
{"profile_image_url": profile_image_url}
|
||||||
query.execute()
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
user = User.get(User.id == id)
|
user = db.query(User).filter_by(id=id).first()
|
||||||
return UserModel(**model_to_dict(user))
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
|
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
query = User.update(last_active_at=int(time.time())).where(User.id == id)
|
with get_db() as db:
|
||||||
query.execute()
|
|
||||||
|
|
||||||
user = User.get(User.id == id)
|
db.query(User).filter_by(id=id).update(
|
||||||
return UserModel(**model_to_dict(user))
|
{"last_active_at": int(time.time())}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
user = db.query(User).filter_by(id=id).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -191,22 +210,25 @@ class UsersTable:
|
|||||||
self, id: str, oauth_sub: str
|
self, id: str, oauth_sub: str
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
query = User.update(oauth_sub=oauth_sub).where(User.id == id)
|
with get_db() as db:
|
||||||
query.execute()
|
db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub})
|
||||||
|
db.commit()
|
||||||
|
|
||||||
user = User.get(User.id == id)
|
user = db.query(User).filter_by(id=id).first()
|
||||||
return UserModel(**model_to_dict(user))
|
return UserModel.model_validate(user)
|
||||||
except:
|
except:
|
||||||
return None
|
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)
|
with get_db() as db:
|
||||||
query.execute()
|
db.query(User).filter_by(id=id).update(updated)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
user = User.get(User.id == id)
|
user = db.query(User).filter_by(id=id).first()
|
||||||
return UserModel(**model_to_dict(user))
|
return UserModel.model_validate(user)
|
||||||
except:
|
# return UserModel(**user.dict())
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_user_by_id(self, id: str) -> bool:
|
def delete_user_by_id(self, id: str) -> bool:
|
||||||
@@ -215,9 +237,10 @@ class UsersTable:
|
|||||||
result = Chats.delete_chats_by_user_id(id)
|
result = Chats.delete_chats_by_user_id(id)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
# Delete User
|
with get_db() as db:
|
||||||
query = User.delete().where(User.id == id)
|
# Delete User
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
db.query(User).filter_by(id=id).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -227,19 +250,20 @@ class UsersTable:
|
|||||||
|
|
||||||
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
|
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
|
||||||
try:
|
try:
|
||||||
query = User.update(api_key=api_key).where(User.id == id)
|
with get_db() as db:
|
||||||
result = query.execute()
|
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
|
||||||
|
db.commit()
|
||||||
return True if result == 1 else False
|
return True if result == 1 else False
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
|
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.id == id)
|
with get_db() as db:
|
||||||
return user.api_key
|
user = db.query(User).filter_by(id=id).first()
|
||||||
except:
|
return user.api_key
|
||||||
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
Users = UsersTable(DB)
|
Users = UsersTable()
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
|
|||||||
|
|
||||||
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
|
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
|
||||||
async def get_user_chat_list_by_user_id(
|
async def get_user_chat_list_by_user_id(
|
||||||
user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
|
user_id: str,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 50,
|
||||||
):
|
):
|
||||||
return Chats.get_chat_list_by_user_id(
|
return Chats.get_chat_list_by_user_id(
|
||||||
user_id, include_archived=True, skip=skip, limit=limit
|
user_id, include_archived=True, skip=skip, limit=limit
|
||||||
@@ -119,7 +122,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/all/archived", response_model=List[ChatResponse])
|
@router.get("/all/archived", response_model=List[ChatResponse])
|
||||||
async def get_user_chats(user=Depends(get_verified_user)):
|
async def get_user_archived_chats(user=Depends(get_verified_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_archived_chats_by_user_id(user.id)
|
for chat in Chats.get_archived_chats_by_user_id(user.id)
|
||||||
@@ -207,7 +210,6 @@ async def get_user_chat_list_by_tag_name(
|
|||||||
form_data: TagNameForm, user=Depends(get_verified_user)
|
form_data: TagNameForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
print(form_data)
|
|
||||||
chat_ids = [
|
chat_ids = [
|
||||||
chat_id_tag.chat_id
|
chat_id_tag.chat_id
|
||||||
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
|
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_
|
|||||||
|
|
||||||
@router.post("/doc/update", response_model=Optional[DocumentResponse])
|
@router.post("/doc/update", response_model=Optional[DocumentResponse])
|
||||||
async def update_doc_by_name(
|
async def update_doc_by_name(
|
||||||
name: str, form_data: DocumentUpdateForm, user=Depends(get_admin_user)
|
name: str,
|
||||||
|
form_data: DocumentUpdateForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
doc = Documents.update_doc_by_name(name, form_data)
|
doc = Documents.update_doc_by_name(name, form_data)
|
||||||
if doc:
|
if doc:
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
def upload_file(
|
def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
|
||||||
file: UploadFile = File(...),
|
|
||||||
user=Depends(get_verified_user),
|
|
||||||
):
|
|
||||||
log.info(f"file.content_type: {file.content_type}")
|
log.info(f"file.content_type: {file.content_type}")
|
||||||
try:
|
try:
|
||||||
unsanitized_filename = file.filename
|
unsanitized_filename = file.filename
|
||||||
@@ -61,6 +58,7 @@ def upload_file(
|
|||||||
|
|
||||||
# replace filename with uuid
|
# replace filename with uuid
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
|
name = filename
|
||||||
filename = f"{id}_{filename}"
|
filename = f"{id}_{filename}"
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
|
|
||||||
@@ -76,6 +74,7 @@ def upload_file(
|
|||||||
"id": id,
|
"id": id,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"meta": {
|
"meta": {
|
||||||
|
"name": name,
|
||||||
"content_type": file.content_type,
|
"content_type": file.content_type,
|
||||||
"size": len(contents),
|
"size": len(contents),
|
||||||
"path": file_path,
|
"path": file_path,
|
||||||
|
|||||||
@@ -233,7 +233,10 @@ async def delete_function_by_id(
|
|||||||
|
|
||||||
# delete the function file
|
# delete the function file
|
||||||
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
|
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
|
||||||
os.remove(function_path)
|
try:
|
||||||
|
os.remove(function_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ class MemoryUpdateModel(BaseModel):
|
|||||||
|
|
||||||
@router.post("/add", response_model=Optional[MemoryModel])
|
@router.post("/add", response_model=Optional[MemoryModel])
|
||||||
async def add_memory(
|
async def add_memory(
|
||||||
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
|
request: Request,
|
||||||
|
form_data: AddMemoryForm,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
memory = Memories.insert_new_memory(user.id, form_data.content)
|
memory = Memories.insert_new_memory(user.id, form_data.content)
|
||||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
|
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import List, Union, Optional
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
|
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
|
||||||
|
|
||||||
from utils.utils import get_verified_user, get_admin_user
|
from utils.utils import get_verified_user, get_admin_user
|
||||||
@@ -29,7 +30,9 @@ async def get_models(user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
@router.post("/add", response_model=Optional[ModelModel])
|
@router.post("/add", response_model=Optional[ModelModel])
|
||||||
async def add_new_model(
|
async def add_new_model(
|
||||||
request: Request, form_data: ModelForm, user=Depends(get_admin_user)
|
request: Request,
|
||||||
|
form_data: ModelForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if form_data.id in request.app.state.MODELS:
|
if form_data.id in request.app.state.MODELS:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -73,7 +76,10 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
@router.post("/update", response_model=Optional[ModelModel])
|
@router.post("/update", response_model=Optional[ModelModel])
|
||||||
async def update_model_by_id(
|
async def update_model_by_id(
|
||||||
request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
form_data: ModelForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
model = Models.get_model_by_id(id)
|
model = Models.get_model_by_id(id)
|
||||||
if model:
|
if model:
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
@router.post("/command/{command}/update", response_model=Optional[PromptModel])
|
@router.post("/command/{command}/update", response_model=Optional[PromptModel])
|
||||||
async def update_prompt_by_command(
|
async def update_prompt_by_command(
|
||||||
command: str, form_data: PromptForm, user=Depends(get_admin_user)
|
command: str,
|
||||||
|
form_data: PromptForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
|
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
|
||||||
if prompt:
|
if prompt:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from fastapi import APIRouter
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
from apps.webui.models.users import Users
|
from apps.webui.models.users import Users
|
||||||
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
||||||
from apps.webui.utils import load_toolkit_module_by_id
|
from apps.webui.utils import load_toolkit_module_by_id
|
||||||
@@ -57,7 +56,9 @@ async def get_toolkits(user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
@router.post("/create", response_model=Optional[ToolResponse])
|
@router.post("/create", response_model=Optional[ToolResponse])
|
||||||
async def create_new_toolkit(
|
async def create_new_toolkit(
|
||||||
request: Request, form_data: ToolForm, user=Depends(get_admin_user)
|
request: Request,
|
||||||
|
form_data: ToolForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if not form_data.id.isidentifier():
|
if not form_data.id.isidentifier():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -131,7 +132,10 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
|
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
|
||||||
async def update_toolkit_by_id(
|
async def update_toolkit_by_id(
|
||||||
request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user)
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
form_data: ToolForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/user/info/update", response_model=Optional[dict])
|
@router.post("/user/info/update", response_model=Optional[dict])
|
||||||
async def update_user_settings_by_session_user(
|
async def update_user_info_by_session_user(
|
||||||
form_data: dict, user=Depends(get_verified_user)
|
form_data: dict, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
user = Users.get_user_by_id(user.id)
|
user = Users.get_user_by_id(user.id)
|
||||||
@@ -205,7 +205,9 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
@router.post("/{user_id}/update", response_model=Optional[UserModel])
|
@router.post("/{user_id}/update", response_model=Optional[UserModel])
|
||||||
async def update_user_by_id(
|
async def update_user_by_id(
|
||||||
user_id: str, form_data: UserUpdateForm, session_user=Depends(get_admin_user)
|
user_id: str,
|
||||||
|
form_data: UserUpdateForm,
|
||||||
|
session_user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -10,7 +9,6 @@ import markdown
|
|||||||
import black
|
import black
|
||||||
|
|
||||||
|
|
||||||
from apps.webui.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
|
||||||
|
|
||||||
@@ -114,13 +112,15 @@ async def download_db(user=Depends(get_admin_user)):
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
)
|
)
|
||||||
if not isinstance(DB, SqliteDatabase):
|
from apps.webui.internal.db import engine
|
||||||
|
|
||||||
|
if engine.name != "sqlite":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
|
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
|
||||||
)
|
)
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
DB.database,
|
engine.url.database,
|
||||||
media_type="application/octet-stream",
|
media_type="application/octet-stream",
|
||||||
filename="webui.db",
|
filename="webui.db",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ def load_function_module_by_id(function_id):
|
|||||||
return module.Pipe(), "pipe", frontmatter
|
return module.Pipe(), "pipe", frontmatter
|
||||||
elif hasattr(module, "Filter"):
|
elif hasattr(module, "Filter"):
|
||||||
return module.Filter(), "filter", frontmatter
|
return module.Filter(), "filter", frontmatter
|
||||||
|
elif hasattr(module, "Action"):
|
||||||
|
return module.Action(), "action", frontmatter
|
||||||
else:
|
else:
|
||||||
raise Exception("No Function class found")
|
raise Exception("No Function class found")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+74
-10
@@ -5,9 +5,8 @@ import importlib.metadata
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
import chromadb
|
import chromadb
|
||||||
from chromadb import Settings
|
from chromadb import Settings
|
||||||
from base64 import b64encode
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import TypeVar, Generic, Union
|
from typing import TypeVar, Generic
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -19,7 +18,6 @@ import markdown
|
|||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from secrets import token_bytes
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -395,6 +393,18 @@ OAUTH_PROVIDER_NAME = PersistentConfig(
|
|||||||
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
|
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OAUTH_USERNAME_CLAIM = PersistentConfig(
|
||||||
|
"OAUTH_USERNAME_CLAIM",
|
||||||
|
"oauth.oidc.username_claim",
|
||||||
|
os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
OAUTH_PICTURE_CLAIM = PersistentConfig(
|
||||||
|
"OAUTH_USERNAME_CLAIM",
|
||||||
|
"oauth.oidc.avatar_claim",
|
||||||
|
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_oauth_providers():
|
def load_oauth_providers():
|
||||||
OAUTH_PROVIDERS.clear()
|
OAUTH_PROVIDERS.clear()
|
||||||
@@ -440,16 +450,27 @@ load_oauth_providers()
|
|||||||
|
|
||||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
|
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
|
||||||
|
|
||||||
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
|
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
|
||||||
|
|
||||||
if frontend_favicon.exists():
|
if frontend_favicon.exists():
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"An error occurred: {e}")
|
logging.error(f"An error occurred: {e}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||||
|
|
||||||
|
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
|
||||||
|
|
||||||
|
if frontend_splash.exists():
|
||||||
|
try:
|
||||||
|
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"An error occurred: {e}")
|
||||||
|
else:
|
||||||
|
logging.warning(f"Frontend splash not found at {frontend_splash}")
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# CUSTOM_NAME
|
# CUSTOM_NAME
|
||||||
####################################
|
####################################
|
||||||
@@ -474,6 +495,19 @@ if CUSTOM_NAME:
|
|||||||
r.raw.decode_content = True
|
r.raw.decode_content = True
|
||||||
shutil.copyfileobj(r.raw, f)
|
shutil.copyfileobj(r.raw, f)
|
||||||
|
|
||||||
|
if "splash" in data:
|
||||||
|
url = (
|
||||||
|
f"https://api.openwebui.com{data['splash']}"
|
||||||
|
if data["splash"][0] == "/"
|
||||||
|
else data["splash"]
|
||||||
|
)
|
||||||
|
|
||||||
|
r = requests.get(url, stream=True)
|
||||||
|
if r.status_code == 200:
|
||||||
|
with open(f"{STATIC_DIR}/splash.png", "wb") as f:
|
||||||
|
r.raw.decode_content = True
|
||||||
|
shutil.copyfileobj(r.raw, f)
|
||||||
|
|
||||||
WEBUI_NAME = data["name"]
|
WEBUI_NAME = data["name"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@@ -674,6 +708,13 @@ ENABLE_SIGNUP = PersistentConfig(
|
|||||||
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DEFAULT_LOCALE = PersistentConfig(
|
||||||
|
"DEFAULT_LOCALE",
|
||||||
|
"ui.default_locale",
|
||||||
|
os.environ.get("DEFAULT_LOCALE", ""),
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_MODELS = PersistentConfig(
|
DEFAULT_MODELS = PersistentConfig(
|
||||||
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
|
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
|
||||||
)
|
)
|
||||||
@@ -762,11 +803,14 @@ class BannerModel(BaseModel):
|
|||||||
timestamp: int
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
WEBUI_BANNERS = PersistentConfig(
|
try:
|
||||||
"WEBUI_BANNERS",
|
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
|
||||||
"ui.banners",
|
banners = [BannerModel(**banner) for banner in banners]
|
||||||
[BannerModel(**banner) for banner in json.loads("[]")],
|
except Exception as e:
|
||||||
)
|
print(f"Error loading WEBUI_BANNERS: {e}")
|
||||||
|
banners = []
|
||||||
|
|
||||||
|
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
|
||||||
|
|
||||||
|
|
||||||
SHOW_ADMIN_DETAILS = PersistentConfig(
|
SHOW_ADMIN_DETAILS = PersistentConfig(
|
||||||
@@ -878,6 +922,22 @@ WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
|
|||||||
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
||||||
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# RAG document content extraction
|
||||||
|
####################################
|
||||||
|
|
||||||
|
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||||
|
"CONTENT_EXTRACTION_ENGINE",
|
||||||
|
"rag.CONTENT_EXTRACTION_ENGINE",
|
||||||
|
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
TIKA_SERVER_URL = PersistentConfig(
|
||||||
|
"TIKA_SERVER_URL",
|
||||||
|
"rag.tika_server_url",
|
||||||
|
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# RAG
|
# RAG
|
||||||
####################################
|
####################################
|
||||||
@@ -1295,3 +1355,7 @@ AUDIO_TTS_VOICE = PersistentConfig(
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
|
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
|
||||||
|
|
||||||
|
# Replace the postgres:// with postgresql://
|
||||||
|
if "postgres://" in DATABASE_URL:
|
||||||
|
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
||||||
|
|||||||
@@ -89,3 +89,14 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
OLLAMA_API_DISABLED = (
|
OLLAMA_API_DISABLED = (
|
||||||
"The Ollama API is disabled. Please enable it to use this feature."
|
"The Ollama API is disabled. Please enable it to use this feature."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TASKS(str, Enum):
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return super().__str__()
|
||||||
|
|
||||||
|
DEFAULT = lambda task="": f"{task if task else 'generation'}"
|
||||||
|
TITLE_GENERATION = "title_generation"
|
||||||
|
EMOJI_GENERATION = "emoji_generation"
|
||||||
|
QUERY_GENERATION = "query_generation"
|
||||||
|
FUNCTION_CALLING = "function_calling"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"ui": {
|
"ui": {
|
||||||
"default_locale": "en-US",
|
"default_locale": "",
|
||||||
"prompt_suggestions": [
|
"prompt_suggestions": [
|
||||||
{
|
{
|
||||||
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||||
|
|||||||
+724
-461
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
|
|
||||||
|
Create new migrations with
|
||||||
|
DATABASE_URL=<replace with actual url> alembic revision --autogenerate -m "a description"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import os
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
from apps.webui.models.auths import Auth
|
||||||
|
from apps.webui.models.chats import Chat
|
||||||
|
from apps.webui.models.documents import Document
|
||||||
|
from apps.webui.models.memories import Memory
|
||||||
|
from apps.webui.models.models import Model
|
||||||
|
from apps.webui.models.prompts import Prompt
|
||||||
|
from apps.webui.models.tags import Tag, ChatIdTag
|
||||||
|
from apps.webui.models.tools import Tool
|
||||||
|
from apps.webui.models.users import User
|
||||||
|
from apps.webui.models.files import File
|
||||||
|
from apps.webui.models.functions import Function
|
||||||
|
|
||||||
|
from config import DATABASE_URL
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name, disable_existing_loggers=False)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
target_metadata = Auth.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
DB_URL = DATABASE_URL
|
||||||
|
|
||||||
|
if DB_URL:
|
||||||
|
config.set_main_option("sqlalchemy.url", DB_URL.replace("%", "%%"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section, {}),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import apps.webui.internal.db
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = ${repr(up_revision)}
|
||||||
|
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import Inspector
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_tables():
|
||||||
|
con = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(con)
|
||||||
|
tables = set(inspector.get_table_names())
|
||||||
|
return tables
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
"""init
|
||||||
|
|
||||||
|
Revision ID: 7e5b5dc7342b
|
||||||
|
Revises:
|
||||||
|
Create Date: 2024-06-24 13:15:33.808998
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import apps.webui.internal.db
|
||||||
|
from migrations.util import get_existing_tables
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "7e5b5dc7342b"
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
existing_tables = set(get_existing_tables())
|
||||||
|
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
if "auth" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"auth",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("email", sa.String(), nullable=True),
|
||||||
|
sa.Column("password", sa.Text(), nullable=True),
|
||||||
|
sa.Column("active", sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "chat" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"chat",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("title", sa.Text(), nullable=True),
|
||||||
|
sa.Column("chat", sa.Text(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("share_id", sa.Text(), nullable=True),
|
||||||
|
sa.Column("archived", sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("share_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "chatidtag" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"chatidtag",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("tag_name", sa.String(), nullable=True),
|
||||||
|
sa.Column("chat_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("timestamp", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "document" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"document",
|
||||||
|
sa.Column("collection_name", sa.String(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(), nullable=True),
|
||||||
|
sa.Column("title", sa.Text(), nullable=True),
|
||||||
|
sa.Column("filename", sa.Text(), nullable=True),
|
||||||
|
sa.Column("content", sa.Text(), nullable=True),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("timestamp", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("collection_name"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "file" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"file",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("filename", sa.Text(), nullable=True),
|
||||||
|
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "function" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"function",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("name", sa.Text(), nullable=True),
|
||||||
|
sa.Column("type", sa.Text(), nullable=True),
|
||||||
|
sa.Column("content", sa.Text(), nullable=True),
|
||||||
|
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("is_global", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "memory" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"memory",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("content", sa.Text(), nullable=True),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "model" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"model",
|
||||||
|
sa.Column("id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=True),
|
||||||
|
sa.Column("base_model_id", sa.Text(), nullable=True),
|
||||||
|
sa.Column("name", sa.Text(), nullable=True),
|
||||||
|
sa.Column("params", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "prompt" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"prompt",
|
||||||
|
sa.Column("command", sa.String(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("title", sa.Text(), nullable=True),
|
||||||
|
sa.Column("content", sa.Text(), nullable=True),
|
||||||
|
sa.Column("timestamp", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("command"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "tag" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"tag",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(), nullable=True),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("data", sa.Text(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "tool" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"tool",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.String(), nullable=True),
|
||||||
|
sa.Column("name", sa.Text(), nullable=True),
|
||||||
|
sa.Column("content", sa.Text(), nullable=True),
|
||||||
|
sa.Column("specs", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if "user" not in existing_tables:
|
||||||
|
op.create_table(
|
||||||
|
"user",
|
||||||
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(), nullable=True),
|
||||||
|
sa.Column("email", sa.String(), nullable=True),
|
||||||
|
sa.Column("role", sa.String(), nullable=True),
|
||||||
|
sa.Column("profile_image_url", sa.Text(), nullable=True),
|
||||||
|
sa.Column("last_active_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("api_key", sa.String(), nullable=True),
|
||||||
|
sa.Column("settings", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("info", apps.webui.internal.db.JSONField(), nullable=True),
|
||||||
|
sa.Column("oauth_sub", sa.Text(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("api_key"),
|
||||||
|
sa.UniqueConstraint("oauth_sub"),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("user")
|
||||||
|
op.drop_table("tool")
|
||||||
|
op.drop_table("tag")
|
||||||
|
op.drop_table("prompt")
|
||||||
|
op.drop_table("model")
|
||||||
|
op.drop_table("memory")
|
||||||
|
op.drop_table("function")
|
||||||
|
op.drop_table("file")
|
||||||
|
op.drop_table("document")
|
||||||
|
op.drop_table("chatidtag")
|
||||||
|
op.drop_table("chat")
|
||||||
|
op.drop_table("auth")
|
||||||
|
# ### end Alembic commands ###
|
||||||
+23
-14
@@ -1,18 +1,20 @@
|
|||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
uvicorn[standard]==0.22.0
|
uvicorn[standard]==0.22.0
|
||||||
pydantic==2.7.1
|
pydantic==2.8.2
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
|
|
||||||
Flask==3.0.3
|
Flask==3.0.3
|
||||||
Flask-Cors==4.0.1
|
Flask-Cors==4.0.1
|
||||||
|
|
||||||
python-socketio==5.11.2
|
python-socketio==5.11.3
|
||||||
python-jose==3.3.0
|
python-jose==3.3.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
|
|
||||||
requests==2.32.2
|
requests==2.32.3
|
||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
peewee==3.17.5
|
sqlalchemy==2.0.31
|
||||||
|
alembic==1.13.2
|
||||||
|
peewee==3.17.6
|
||||||
peewee-migrate==1.12.2
|
peewee-migrate==1.12.2
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
PyMySQL==1.1.1
|
PyMySQL==1.1.1
|
||||||
@@ -29,27 +31,29 @@ APScheduler==3.10.4
|
|||||||
openai
|
openai
|
||||||
anthropic
|
anthropic
|
||||||
google-generativeai==0.5.4
|
google-generativeai==0.5.4
|
||||||
|
tiktoken
|
||||||
|
|
||||||
langchain==0.2.0
|
langchain==0.2.6
|
||||||
langchain-community==0.2.0
|
langchain-community==0.2.6
|
||||||
langchain-chroma==0.1.1
|
langchain-chroma==0.1.2
|
||||||
|
|
||||||
fake-useragent==1.5.1
|
fake-useragent==1.5.1
|
||||||
chromadb==0.5.0
|
chromadb==0.5.4
|
||||||
sentence-transformers==2.7.0
|
sentence-transformers==3.0.1
|
||||||
pypdf==4.2.0
|
pypdf==4.2.0
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
python-pptx==0.6.23
|
python-pptx==0.6.23
|
||||||
unstructured==0.14.0
|
unstructured==0.14.10
|
||||||
Markdown==3.6
|
Markdown==3.6
|
||||||
pypandoc==1.13
|
pypandoc==1.13
|
||||||
pandas==2.2.2
|
pandas==2.2.2
|
||||||
openpyxl==3.1.2
|
openpyxl==3.1.5
|
||||||
pyxlsb==1.0.10
|
pyxlsb==1.0.10
|
||||||
xlrd==2.0.1
|
xlrd==2.0.1
|
||||||
validators==0.28.1
|
validators==0.28.1
|
||||||
|
psutil
|
||||||
|
|
||||||
opencv-python-headless==4.9.0.80
|
opencv-python-headless==4.10.0.84
|
||||||
rapidocr-onnxruntime==1.3.22
|
rapidocr-onnxruntime==1.3.22
|
||||||
|
|
||||||
fpdf2==2.7.9
|
fpdf2==2.7.9
|
||||||
@@ -61,10 +65,15 @@ PyJWT[crypto]==2.8.0
|
|||||||
authlib==1.3.1
|
authlib==1.3.1
|
||||||
|
|
||||||
black==24.4.2
|
black==24.4.2
|
||||||
langfuse==2.33.0
|
langfuse==2.38.0
|
||||||
youtube-transcript-api==0.6.2
|
youtube-transcript-api==0.6.2
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
|
|
||||||
extract_msg
|
extract_msg
|
||||||
pydub
|
pydub
|
||||||
duckduckgo-search~=6.1.5
|
duckduckgo-search~=6.1.12
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
docker~=7.1.0
|
||||||
|
pytest~=8.2.2
|
||||||
|
pytest-docker~=3.1.1
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,202 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||||
|
from test.util.mock_user import mock_webui_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuths(AbstractPostgresTest):
|
||||||
|
BASE_PATH = "/api/v1/auths"
|
||||||
|
|
||||||
|
def setup_class(cls):
|
||||||
|
super().setup_class()
|
||||||
|
from apps.webui.models.users import Users
|
||||||
|
from apps.webui.models.auths import Auths
|
||||||
|
|
||||||
|
cls.users = Users
|
||||||
|
cls.auths = Auths
|
||||||
|
|
||||||
|
def test_get_session_user(self):
|
||||||
|
with mock_webui_user():
|
||||||
|
response = self.fast_api_client.get(self.create_url(""))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"id": "1",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john.doe@openwebui.com",
|
||||||
|
"role": "user",
|
||||||
|
"profile_image_url": "/user.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_update_profile(self):
|
||||||
|
from utils.utils import get_password_hash
|
||||||
|
|
||||||
|
user = self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password=get_password_hash("old_password"),
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="user",
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock_webui_user(id=user.id):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/update/profile"),
|
||||||
|
json={"name": "John Doe 2", "profile_image_url": "/user2.png"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
db_user = self.users.get_user_by_id(user.id)
|
||||||
|
assert db_user.name == "John Doe 2"
|
||||||
|
assert db_user.profile_image_url == "/user2.png"
|
||||||
|
|
||||||
|
def test_update_password(self):
|
||||||
|
from utils.utils import get_password_hash
|
||||||
|
|
||||||
|
user = self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password=get_password_hash("old_password"),
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="user",
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock_webui_user(id=user.id):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/update/password"),
|
||||||
|
json={"password": "old_password", "new_password": "new_password"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
old_auth = self.auths.authenticate_user(
|
||||||
|
"john.doe@openwebui.com", "old_password"
|
||||||
|
)
|
||||||
|
assert old_auth is None
|
||||||
|
new_auth = self.auths.authenticate_user(
|
||||||
|
"john.doe@openwebui.com", "new_password"
|
||||||
|
)
|
||||||
|
assert new_auth is not None
|
||||||
|
|
||||||
|
def test_signin(self):
|
||||||
|
from utils.utils import get_password_hash
|
||||||
|
|
||||||
|
user = self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password=get_password_hash("password"),
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="user",
|
||||||
|
)
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/signin"),
|
||||||
|
json={"email": "john.doe@openwebui.com", "password": "password"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == user.id
|
||||||
|
assert data["name"] == "John Doe"
|
||||||
|
assert data["email"] == "john.doe@openwebui.com"
|
||||||
|
assert data["role"] == "user"
|
||||||
|
assert data["profile_image_url"] == "/user.png"
|
||||||
|
assert data["token"] is not None and len(data["token"]) > 0
|
||||||
|
assert data["token_type"] == "Bearer"
|
||||||
|
|
||||||
|
def test_signup(self):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/signup"),
|
||||||
|
json={
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john.doe@openwebui.com",
|
||||||
|
"password": "password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] is not None and len(data["id"]) > 0
|
||||||
|
assert data["name"] == "John Doe"
|
||||||
|
assert data["email"] == "john.doe@openwebui.com"
|
||||||
|
assert data["role"] in ["admin", "user", "pending"]
|
||||||
|
assert data["profile_image_url"] == "/user.png"
|
||||||
|
assert data["token"] is not None and len(data["token"]) > 0
|
||||||
|
assert data["token_type"] == "Bearer"
|
||||||
|
|
||||||
|
def test_add_user(self):
|
||||||
|
with mock_webui_user():
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/add"),
|
||||||
|
json={
|
||||||
|
"name": "John Doe 2",
|
||||||
|
"email": "john.doe2@openwebui.com",
|
||||||
|
"password": "password2",
|
||||||
|
"role": "admin",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] is not None and len(data["id"]) > 0
|
||||||
|
assert data["name"] == "John Doe 2"
|
||||||
|
assert data["email"] == "john.doe2@openwebui.com"
|
||||||
|
assert data["role"] == "admin"
|
||||||
|
assert data["profile_image_url"] == "/user.png"
|
||||||
|
assert data["token"] is not None and len(data["token"]) > 0
|
||||||
|
assert data["token_type"] == "Bearer"
|
||||||
|
|
||||||
|
def test_get_admin_details(self):
|
||||||
|
self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password="password",
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="admin",
|
||||||
|
)
|
||||||
|
with mock_webui_user():
|
||||||
|
response = self.fast_api_client.get(self.create_url("/admin/details"))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john.doe@openwebui.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create_api_key_(self):
|
||||||
|
user = self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password="password",
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="admin",
|
||||||
|
)
|
||||||
|
with mock_webui_user(id=user.id):
|
||||||
|
response = self.fast_api_client.post(self.create_url("/api_key"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["api_key"] is not None
|
||||||
|
assert len(data["api_key"]) > 0
|
||||||
|
|
||||||
|
def test_delete_api_key(self):
|
||||||
|
user = self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password="password",
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="admin",
|
||||||
|
)
|
||||||
|
self.users.update_user_api_key_by_id(user.id, "abc")
|
||||||
|
with mock_webui_user(id=user.id):
|
||||||
|
response = self.fast_api_client.delete(self.create_url("/api_key"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == True
|
||||||
|
db_user = self.users.get_user_by_id(user.id)
|
||||||
|
assert db_user.api_key is None
|
||||||
|
|
||||||
|
def test_get_api_key(self):
|
||||||
|
user = self.auths.insert_new_auth(
|
||||||
|
email="john.doe@openwebui.com",
|
||||||
|
password="password",
|
||||||
|
name="John Doe",
|
||||||
|
profile_image_url="/user.png",
|
||||||
|
role="admin",
|
||||||
|
)
|
||||||
|
self.users.update_user_api_key_by_id(user.id, "abc")
|
||||||
|
with mock_webui_user(id=user.id):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/api_key"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"api_key": "abc"}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||||
|
from test.util.mock_user import mock_webui_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestChats(AbstractPostgresTest):
|
||||||
|
|
||||||
|
BASE_PATH = "/api/v1/chats"
|
||||||
|
|
||||||
|
def setup_class(cls):
|
||||||
|
super().setup_class()
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
super().setup_method()
|
||||||
|
from apps.webui.models.chats import ChatForm
|
||||||
|
from apps.webui.models.chats import Chats
|
||||||
|
|
||||||
|
self.chats = Chats
|
||||||
|
self.chats.insert_new_chat(
|
||||||
|
"2",
|
||||||
|
ChatForm(
|
||||||
|
**{
|
||||||
|
"chat": {
|
||||||
|
"name": "chat1",
|
||||||
|
"description": "chat1 description",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"history": {"currentId": "1", "messages": []},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_session_user_chat_list(self):
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
first_chat = response.json()[0]
|
||||||
|
assert first_chat["id"] is not None
|
||||||
|
assert first_chat["title"] == "New Chat"
|
||||||
|
assert first_chat["created_at"] is not None
|
||||||
|
assert first_chat["updated_at"] is not None
|
||||||
|
|
||||||
|
def test_delete_all_user_chats(self):
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.delete(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(self.chats.get_chats()) == 0
|
||||||
|
|
||||||
|
def test_get_user_chat_list_by_user_id(self):
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/list/user/2"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
first_chat = response.json()[0]
|
||||||
|
assert first_chat["id"] is not None
|
||||||
|
assert first_chat["title"] == "New Chat"
|
||||||
|
assert first_chat["created_at"] is not None
|
||||||
|
assert first_chat["updated_at"] is not None
|
||||||
|
|
||||||
|
def test_create_new_chat(self):
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/new"),
|
||||||
|
json={
|
||||||
|
"chat": {
|
||||||
|
"name": "chat2",
|
||||||
|
"description": "chat2 description",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["archived"] is False
|
||||||
|
assert data["chat"] == {
|
||||||
|
"name": "chat2",
|
||||||
|
"description": "chat2 description",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
}
|
||||||
|
assert data["user_id"] == "2"
|
||||||
|
assert data["id"] is not None
|
||||||
|
assert data["share_id"] is None
|
||||||
|
assert data["title"] == "New Chat"
|
||||||
|
assert data["updated_at"] is not None
|
||||||
|
assert data["created_at"] is not None
|
||||||
|
assert len(self.chats.get_chats()) == 2
|
||||||
|
|
||||||
|
def test_get_user_chats(self):
|
||||||
|
self.test_get_session_user_chat_list()
|
||||||
|
|
||||||
|
def test_get_user_archived_chats(self):
|
||||||
|
self.chats.archive_all_chats_by_user_id("2")
|
||||||
|
from apps.webui.internal.db import Session
|
||||||
|
|
||||||
|
Session.commit()
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/all/archived"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
first_chat = response.json()[0]
|
||||||
|
assert first_chat["id"] is not None
|
||||||
|
assert first_chat["title"] == "New Chat"
|
||||||
|
assert first_chat["created_at"] is not None
|
||||||
|
assert first_chat["updated_at"] is not None
|
||||||
|
|
||||||
|
def test_get_all_user_chats_in_db(self):
|
||||||
|
with mock_webui_user(id="4"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/all/db"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
|
||||||
|
def test_get_archived_session_user_chat_list(self):
|
||||||
|
self.test_get_user_archived_chats()
|
||||||
|
|
||||||
|
def test_archive_all_chats(self):
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(self.create_url("/archive/all"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(self.chats.get_archived_chats_by_user_id("2")) == 1
|
||||||
|
|
||||||
|
def test_get_shared_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
self.chats.update_chat_share_id_by_id(chat_id, chat_id)
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(f"/share/{chat_id}"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == chat_id
|
||||||
|
assert data["chat"] == {
|
||||||
|
"name": "chat1",
|
||||||
|
"description": "chat1 description",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"history": {"currentId": "1", "messages": []},
|
||||||
|
}
|
||||||
|
assert data["id"] == chat_id
|
||||||
|
assert data["share_id"] == chat_id
|
||||||
|
assert data["title"] == "New Chat"
|
||||||
|
|
||||||
|
def test_get_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(f"/{chat_id}"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == chat_id
|
||||||
|
assert data["chat"] == {
|
||||||
|
"name": "chat1",
|
||||||
|
"description": "chat1 description",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"history": {"currentId": "1", "messages": []},
|
||||||
|
}
|
||||||
|
assert data["share_id"] is None
|
||||||
|
assert data["title"] == "New Chat"
|
||||||
|
assert data["user_id"] == "2"
|
||||||
|
|
||||||
|
def test_update_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url(f"/{chat_id}"),
|
||||||
|
json={
|
||||||
|
"chat": {
|
||||||
|
"name": "chat2",
|
||||||
|
"description": "chat2 description",
|
||||||
|
"tags": ["tag2", "tag4"],
|
||||||
|
"title": "Just another title",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == chat_id
|
||||||
|
assert data["chat"] == {
|
||||||
|
"name": "chat2",
|
||||||
|
"title": "Just another title",
|
||||||
|
"description": "chat2 description",
|
||||||
|
"tags": ["tag2", "tag4"],
|
||||||
|
"history": {"currentId": "1", "messages": []},
|
||||||
|
}
|
||||||
|
assert data["share_id"] is None
|
||||||
|
assert data["title"] == "Just another title"
|
||||||
|
assert data["user_id"] == "2"
|
||||||
|
|
||||||
|
def test_delete_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() is True
|
||||||
|
|
||||||
|
def test_clone_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/clone"))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] != chat_id
|
||||||
|
assert data["chat"] == {
|
||||||
|
"branchPointMessageId": "1",
|
||||||
|
"description": "chat1 description",
|
||||||
|
"history": {"currentId": "1", "messages": []},
|
||||||
|
"name": "chat1",
|
||||||
|
"originalChatId": chat_id,
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"title": "Clone of New Chat",
|
||||||
|
}
|
||||||
|
assert data["share_id"] is None
|
||||||
|
assert data["title"] == "Clone of New Chat"
|
||||||
|
assert data["user_id"] == "2"
|
||||||
|
|
||||||
|
def test_archive_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/archive"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
chat = self.chats.get_chat_by_id(chat_id)
|
||||||
|
assert chat.archived is True
|
||||||
|
|
||||||
|
def test_share_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(self.create_url(f"/{chat_id}/share"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
chat = self.chats.get_chat_by_id(chat_id)
|
||||||
|
assert chat.share_id is not None
|
||||||
|
|
||||||
|
def test_delete_shared_chat_by_id(self):
|
||||||
|
chat_id = self.chats.get_chats()[0].id
|
||||||
|
share_id = str(uuid.uuid4())
|
||||||
|
self.chats.update_chat_share_id_by_id(chat_id, share_id)
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}/share"))
|
||||||
|
assert response.status_code
|
||||||
|
|
||||||
|
chat = self.chats.get_chat_by_id(chat_id)
|
||||||
|
assert chat.share_id is None
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||||
|
from test.util.mock_user import mock_webui_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestDocuments(AbstractPostgresTest):
|
||||||
|
|
||||||
|
BASE_PATH = "/api/v1/documents"
|
||||||
|
|
||||||
|
def setup_class(cls):
|
||||||
|
super().setup_class()
|
||||||
|
from apps.webui.models.documents import Documents
|
||||||
|
|
||||||
|
cls.documents = Documents
|
||||||
|
|
||||||
|
def test_documents(self):
|
||||||
|
# Empty database
|
||||||
|
assert len(self.documents.get_docs()) == 0
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 0
|
||||||
|
|
||||||
|
# Create a new document
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/create"),
|
||||||
|
json={
|
||||||
|
"name": "doc_name",
|
||||||
|
"title": "doc title",
|
||||||
|
"collection_name": "custom collection",
|
||||||
|
"filename": "doc_name.pdf",
|
||||||
|
"content": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["name"] == "doc_name"
|
||||||
|
assert len(self.documents.get_docs()) == 1
|
||||||
|
|
||||||
|
# Get the document
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/doc?name=doc_name"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["collection_name"] == "custom collection"
|
||||||
|
assert data["name"] == "doc_name"
|
||||||
|
assert data["title"] == "doc title"
|
||||||
|
assert data["filename"] == "doc_name.pdf"
|
||||||
|
assert data["content"] == {}
|
||||||
|
|
||||||
|
# Create another document
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/create"),
|
||||||
|
json={
|
||||||
|
"name": "doc_name 2",
|
||||||
|
"title": "doc title 2",
|
||||||
|
"collection_name": "custom collection 2",
|
||||||
|
"filename": "doc_name2.pdf",
|
||||||
|
"content": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["name"] == "doc_name 2"
|
||||||
|
assert len(self.documents.get_docs()) == 2
|
||||||
|
|
||||||
|
# Get all documents
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
|
||||||
|
# Update the first document
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/doc/update?name=doc_name"),
|
||||||
|
json={"name": "doc_name rework", "title": "updated title"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "doc_name rework"
|
||||||
|
assert data["title"] == "updated title"
|
||||||
|
|
||||||
|
# Tag the first document
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/doc/tags"),
|
||||||
|
json={
|
||||||
|
"name": "doc_name rework",
|
||||||
|
"tags": [{"name": "testing-tag"}, {"name": "another-tag"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "doc_name rework"
|
||||||
|
assert data["content"] == {
|
||||||
|
"tags": [{"name": "testing-tag"}, {"name": "another-tag"}]
|
||||||
|
}
|
||||||
|
assert len(self.documents.get_docs()) == 2
|
||||||
|
|
||||||
|
# Delete the first document
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.delete(
|
||||||
|
self.create_url("/doc/delete?name=doc_name rework")
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(self.documents.get_docs()) == 1
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||||
|
from test.util.mock_user import mock_webui_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestModels(AbstractPostgresTest):
|
||||||
|
|
||||||
|
BASE_PATH = "/api/v1/models"
|
||||||
|
|
||||||
|
def setup_class(cls):
|
||||||
|
super().setup_class()
|
||||||
|
from apps.webui.models.models import Model
|
||||||
|
|
||||||
|
cls.models = Model
|
||||||
|
|
||||||
|
def test_models(self):
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 0
|
||||||
|
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/add"),
|
||||||
|
json={
|
||||||
|
"id": "my-model",
|
||||||
|
"base_model_id": "base-model-id",
|
||||||
|
"name": "Hello World",
|
||||||
|
"meta": {
|
||||||
|
"profile_image_url": "/static/favicon.png",
|
||||||
|
"description": "description",
|
||||||
|
"capabilities": None,
|
||||||
|
"model_config": {},
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(
|
||||||
|
self.create_url(query_params={"id": "my-model"})
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()[0]
|
||||||
|
assert data["id"] == "my-model"
|
||||||
|
assert data["name"] == "Hello World"
|
||||||
|
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.delete(
|
||||||
|
self.create_url("/delete?id=my-model")
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 0
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||||
|
from test.util.mock_user import mock_webui_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrompts(AbstractPostgresTest):
|
||||||
|
|
||||||
|
BASE_PATH = "/api/v1/prompts"
|
||||||
|
|
||||||
|
def test_prompts(self):
|
||||||
|
# Get all prompts
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 0
|
||||||
|
|
||||||
|
# Create a two new prompts
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/create"),
|
||||||
|
json={
|
||||||
|
"command": "/my-command",
|
||||||
|
"title": "Hello World",
|
||||||
|
"content": "description",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/create"),
|
||||||
|
json={
|
||||||
|
"command": "/my-command2",
|
||||||
|
"title": "Hello World 2",
|
||||||
|
"content": "description 2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Get all prompts
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
|
||||||
|
# Get prompt by command
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/command/my-command"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["command"] == "/my-command"
|
||||||
|
assert data["title"] == "Hello World"
|
||||||
|
assert data["content"] == "description"
|
||||||
|
assert data["user_id"] == "2"
|
||||||
|
|
||||||
|
# Update prompt
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/command/my-command2/update"),
|
||||||
|
json={
|
||||||
|
"command": "irrelevant for request",
|
||||||
|
"title": "Hello World Updated",
|
||||||
|
"content": "description Updated",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["command"] == "/my-command2"
|
||||||
|
assert data["title"] == "Hello World Updated"
|
||||||
|
assert data["content"] == "description Updated"
|
||||||
|
assert data["user_id"] == "3"
|
||||||
|
|
||||||
|
# Get prompt by command
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/command/my-command2"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["command"] == "/my-command2"
|
||||||
|
assert data["title"] == "Hello World Updated"
|
||||||
|
assert data["content"] == "description Updated"
|
||||||
|
assert data["user_id"] == "3"
|
||||||
|
|
||||||
|
# Delete prompt
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.delete(
|
||||||
|
self.create_url("/command/my-command/delete")
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Get all prompts
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 1
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||||
|
from test.util.mock_user import mock_webui_user
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_by_id(data, param):
|
||||||
|
return next((item for item in data if item["id"] == param), None)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_user(data, id, **kwargs):
|
||||||
|
user = _get_user_by_id(data, id)
|
||||||
|
assert user is not None
|
||||||
|
comparison_data = {
|
||||||
|
"name": f"user {id}",
|
||||||
|
"email": f"user{id}@openwebui.com",
|
||||||
|
"profile_image_url": f"/user{id}.png",
|
||||||
|
"role": "user",
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
for key, value in comparison_data.items():
|
||||||
|
assert user[key] == value
|
||||||
|
|
||||||
|
|
||||||
|
class TestUsers(AbstractPostgresTest):
|
||||||
|
|
||||||
|
BASE_PATH = "/api/v1/users"
|
||||||
|
|
||||||
|
def setup_class(cls):
|
||||||
|
super().setup_class()
|
||||||
|
from apps.webui.models.users import Users
|
||||||
|
|
||||||
|
cls.users = Users
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
super().setup_method()
|
||||||
|
self.users.insert_new_user(
|
||||||
|
id="1",
|
||||||
|
name="user 1",
|
||||||
|
email="user1@openwebui.com",
|
||||||
|
profile_image_url="/user1.png",
|
||||||
|
role="user",
|
||||||
|
)
|
||||||
|
self.users.insert_new_user(
|
||||||
|
id="2",
|
||||||
|
name="user 2",
|
||||||
|
email="user2@openwebui.com",
|
||||||
|
profile_image_url="/user2.png",
|
||||||
|
role="user",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_users(self):
|
||||||
|
# Get all users
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(""))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
data = response.json()
|
||||||
|
_assert_user(data, "1")
|
||||||
|
_assert_user(data, "2")
|
||||||
|
|
||||||
|
# update role
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/update/role"), json={"id": "2", "role": "admin"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
_assert_user([response.json()], "2", role="admin")
|
||||||
|
|
||||||
|
# Get all users
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(""))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
data = response.json()
|
||||||
|
_assert_user(data, "1")
|
||||||
|
_assert_user(data, "2", role="admin")
|
||||||
|
|
||||||
|
# Get (empty) user settings
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/user/settings"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() is None
|
||||||
|
|
||||||
|
# Update user settings
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/user/settings/update"),
|
||||||
|
json={
|
||||||
|
"ui": {"attr1": "value1", "attr2": "value2"},
|
||||||
|
"model_config": {"attr3": "value3", "attr4": "value4"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Get user settings
|
||||||
|
with mock_webui_user(id="2"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/user/settings"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"ui": {"attr1": "value1", "attr2": "value2"},
|
||||||
|
"model_config": {"attr3": "value3", "attr4": "value4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get (empty) user info
|
||||||
|
with mock_webui_user(id="1"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/user/info"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() is None
|
||||||
|
|
||||||
|
# Update user info
|
||||||
|
with mock_webui_user(id="1"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/user/info/update"),
|
||||||
|
json={"attr1": "value1", "attr2": "value2"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
with mock_webui_user(id="1"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/user/info"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"attr1": "value1", "attr2": "value2"}
|
||||||
|
|
||||||
|
# Get user by id
|
||||||
|
with mock_webui_user(id="1"):
|
||||||
|
response = self.fast_api_client.get(self.create_url("/2"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"name": "user 2", "profile_image_url": "/user2.png"}
|
||||||
|
|
||||||
|
# Update user by id
|
||||||
|
with mock_webui_user(id="1"):
|
||||||
|
response = self.fast_api_client.post(
|
||||||
|
self.create_url("/2/update"),
|
||||||
|
json={
|
||||||
|
"name": "user 2 updated",
|
||||||
|
"email": "user2-updated@openwebui.com",
|
||||||
|
"profile_image_url": "/user2-updated.png",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Get all users
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(""))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
data = response.json()
|
||||||
|
_assert_user(data, "1")
|
||||||
|
_assert_user(
|
||||||
|
data,
|
||||||
|
"2",
|
||||||
|
role="admin",
|
||||||
|
name="user 2 updated",
|
||||||
|
email="user2-updated@openwebui.com",
|
||||||
|
profile_image_url="/user2-updated.png",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete user by id
|
||||||
|
with mock_webui_user(id="1"):
|
||||||
|
response = self.fast_api_client.delete(self.create_url("/2"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Get all users
|
||||||
|
with mock_webui_user(id="3"):
|
||||||
|
response = self.fast_api_client.get(self.create_url(""))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
data = response.json()
|
||||||
|
_assert_user(data, "1")
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import docker
|
||||||
|
import pytest
|
||||||
|
from docker import DockerClient
|
||||||
|
from pytest_docker.plugin import get_docker_ip
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import text, create_engine
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_fast_api_client():
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
with TestClient(app) as c:
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractIntegrationTest:
|
||||||
|
BASE_PATH = None
|
||||||
|
|
||||||
|
def create_url(self, path="", query_params=None):
|
||||||
|
if self.BASE_PATH is None:
|
||||||
|
raise Exception("BASE_PATH is not set")
|
||||||
|
parts = self.BASE_PATH.split("/")
|
||||||
|
parts = [part.strip() for part in parts if part.strip() != ""]
|
||||||
|
path_parts = path.split("/")
|
||||||
|
path_parts = [part.strip() for part in path_parts if part.strip() != ""]
|
||||||
|
query_parts = ""
|
||||||
|
if query_params:
|
||||||
|
query_parts = "&".join(
|
||||||
|
[f"{key}={value}" for key, value in query_params.items()]
|
||||||
|
)
|
||||||
|
query_parts = f"?{query_parts}"
|
||||||
|
return "/".join(parts + path_parts) + query_parts
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_class(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractPostgresTest(AbstractIntegrationTest):
|
||||||
|
DOCKER_CONTAINER_NAME = "postgres-test-container-will-get-deleted"
|
||||||
|
docker_client: DockerClient
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_db_url(cls, env_vars_postgres: dict) -> str:
|
||||||
|
host = get_docker_ip()
|
||||||
|
user = env_vars_postgres["POSTGRES_USER"]
|
||||||
|
pw = env_vars_postgres["POSTGRES_PASSWORD"]
|
||||||
|
port = 8081
|
||||||
|
db = env_vars_postgres["POSTGRES_DB"]
|
||||||
|
return f"postgresql://{user}:{pw}@{host}:{port}/{db}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
super().setup_class()
|
||||||
|
try:
|
||||||
|
env_vars_postgres = {
|
||||||
|
"POSTGRES_USER": "user",
|
||||||
|
"POSTGRES_PASSWORD": "example",
|
||||||
|
"POSTGRES_DB": "openwebui",
|
||||||
|
}
|
||||||
|
cls.docker_client = docker.from_env()
|
||||||
|
cls.docker_client.containers.run(
|
||||||
|
"postgres:16.2",
|
||||||
|
detach=True,
|
||||||
|
environment=env_vars_postgres,
|
||||||
|
name=cls.DOCKER_CONTAINER_NAME,
|
||||||
|
ports={5432: ("0.0.0.0", 8081)},
|
||||||
|
command="postgres -c log_statement=all",
|
||||||
|
)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
database_url = cls._create_db_url(env_vars_postgres)
|
||||||
|
os.environ["DATABASE_URL"] = database_url
|
||||||
|
retries = 10
|
||||||
|
db = None
|
||||||
|
while retries > 0:
|
||||||
|
try:
|
||||||
|
from config import BACKEND_DIR
|
||||||
|
|
||||||
|
db = create_engine(database_url, pool_pre_ping=True)
|
||||||
|
db = db.connect()
|
||||||
|
log.info("postgres is ready!")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
time.sleep(3)
|
||||||
|
retries -= 1
|
||||||
|
|
||||||
|
if db:
|
||||||
|
# import must be after setting env!
|
||||||
|
cls.fast_api_client = get_fast_api_client()
|
||||||
|
db.close()
|
||||||
|
else:
|
||||||
|
raise Exception("Could not connect to Postgres")
|
||||||
|
except Exception as ex:
|
||||||
|
log.error(ex)
|
||||||
|
cls.teardown_class()
|
||||||
|
pytest.fail(f"Could not setup test environment: {ex}")
|
||||||
|
|
||||||
|
def _check_db_connection(self):
|
||||||
|
from apps.webui.internal.db import Session
|
||||||
|
|
||||||
|
retries = 10
|
||||||
|
while retries > 0:
|
||||||
|
try:
|
||||||
|
Session.execute(text("SELECT 1"))
|
||||||
|
Session.commit()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
Session.rollback()
|
||||||
|
log.warning(e)
|
||||||
|
time.sleep(3)
|
||||||
|
retries -= 1
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
super().setup_method()
|
||||||
|
self._check_db_connection()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_class(cls) -> None:
|
||||||
|
super().teardown_class()
|
||||||
|
cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True)
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
from apps.webui.internal.db import Session
|
||||||
|
|
||||||
|
# rollback everything not yet committed
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
|
# truncate all tables
|
||||||
|
tables = [
|
||||||
|
"auth",
|
||||||
|
"chat",
|
||||||
|
"chatidtag",
|
||||||
|
"document",
|
||||||
|
"memory",
|
||||||
|
"model",
|
||||||
|
"prompt",
|
||||||
|
"tag",
|
||||||
|
'"user"',
|
||||||
|
]
|
||||||
|
for table in tables:
|
||||||
|
Session.execute(text(f"TRUNCATE TABLE {table}"))
|
||||||
|
Session.commit()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_webui_user(**kwargs):
|
||||||
|
from apps.webui.main import app
|
||||||
|
|
||||||
|
with mock_user(app, **kwargs):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_user(app: FastAPI, **kwargs):
|
||||||
|
from utils.utils import (
|
||||||
|
get_current_user,
|
||||||
|
get_verified_user,
|
||||||
|
get_admin_user,
|
||||||
|
get_current_user_by_api_key,
|
||||||
|
)
|
||||||
|
from apps.webui.models.users import User
|
||||||
|
|
||||||
|
def create_user():
|
||||||
|
user_parameters = {
|
||||||
|
"id": "1",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john.doe@openwebui.com",
|
||||||
|
"role": "user",
|
||||||
|
"profile_image_url": "/user.png",
|
||||||
|
"last_active_at": 1627351200,
|
||||||
|
"updated_at": 1627351200,
|
||||||
|
"created_at": 162735120,
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
return User(**user_parameters)
|
||||||
|
|
||||||
|
app.dependency_overrides = {
|
||||||
|
get_current_user: create_user,
|
||||||
|
get_verified_user: create_user,
|
||||||
|
get_admin_user: create_user,
|
||||||
|
get_current_user_by_api_key: create_user,
|
||||||
|
}
|
||||||
|
yield
|
||||||
|
app.dependency_overrides = {}
|
||||||
+14
-6
@@ -8,14 +8,22 @@ import uuid
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
def get_last_user_message(messages: List[dict]) -> str:
|
def get_last_user_message_item(messages: List[dict]) -> str:
|
||||||
for message in reversed(messages):
|
for message in reversed(messages):
|
||||||
if message["role"] == "user":
|
if message["role"] == "user":
|
||||||
if isinstance(message["content"], list):
|
return message
|
||||||
for item in message["content"]:
|
return None
|
||||||
if item["type"] == "text":
|
|
||||||
return item["text"]
|
|
||||||
return message["content"]
|
def get_last_user_message(messages: List[dict]) -> str:
|
||||||
|
message = get_last_user_message_item(messages)
|
||||||
|
|
||||||
|
if message is not None:
|
||||||
|
if isinstance(message["content"], list):
|
||||||
|
for item in message["content"]:
|
||||||
|
if item["type"] == "text":
|
||||||
|
return item["text"]
|
||||||
|
return message["content"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ def get_tools_specs(tools) -> List[dict]:
|
|||||||
for param_name, param_annotation in get_type_hints(
|
for param_name, param_annotation in get_type_hints(
|
||||||
function
|
function
|
||||||
).items()
|
).items()
|
||||||
if param_name != "return" and param_name != "__user__"
|
if param_name != "return"
|
||||||
|
and not (
|
||||||
|
param_name.startswith("__") and param_name.endswith("__")
|
||||||
|
)
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi import HTTPException, status, Depends, Request
|
from fastapi import HTTPException, status, Depends, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from apps.webui.models.users import Users
|
from apps.webui.models.users import Users
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.3.7",
|
"version": "0.3.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.3.7",
|
"version": "0.3.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.3.7",
|
"version": "0.3.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# all-features: false
|
# all-features: false
|
||||||
# with-sources: false
|
# with-sources: false
|
||||||
# generate-hashes: false
|
# generate-hashes: false
|
||||||
|
# universal: false
|
||||||
|
|
||||||
-e file:.
|
-e file:.
|
||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
@@ -31,7 +32,7 @@ asgiref==3.8.1
|
|||||||
# via opentelemetry-instrumentation-asgi
|
# via opentelemetry-instrumentation-asgi
|
||||||
attrs==23.2.0
|
attrs==23.2.0
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
authlib==1.3.0
|
authlib==1.3.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
av==11.0.0
|
av==11.0.0
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
@@ -398,7 +399,6 @@ pandas==2.2.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via passlib
|
|
||||||
pathspec==0.12.1
|
pathspec==0.12.1
|
||||||
# via black
|
# via black
|
||||||
pcodedmp==1.2.6
|
pcodedmp==1.2.6
|
||||||
@@ -457,7 +457,6 @@ pygments==2.18.0
|
|||||||
# via rich
|
# via rich
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via pyjwt
|
|
||||||
pymysql==1.1.0
|
pymysql==1.1.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pypandoc==1.13
|
pypandoc==1.13
|
||||||
@@ -559,6 +558,9 @@ scipy==1.13.0
|
|||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
sentence-transformers==2.7.0
|
sentence-transformers==2.7.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
|
setuptools==69.5.1
|
||||||
|
# via ctranslate2
|
||||||
|
# via opentelemetry-instrumentation
|
||||||
shapely==2.0.4
|
shapely==2.0.4
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
@@ -653,7 +655,6 @@ uvicorn==0.22.0
|
|||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via uvicorn
|
|
||||||
uvloop==0.19.0
|
uvloop==0.19.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
validators==0.28.1
|
validators==0.28.1
|
||||||
@@ -681,6 +682,3 @@ youtube-transcript-api==0.6.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
zipp==3.18.1
|
zipp==3.18.1
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
setuptools==69.5.1
|
|
||||||
# via ctranslate2
|
|
||||||
# via opentelemetry-instrumentation
|
|
||||||
|
|||||||
+5
-7
@@ -7,6 +7,7 @@
|
|||||||
# all-features: false
|
# all-features: false
|
||||||
# with-sources: false
|
# with-sources: false
|
||||||
# generate-hashes: false
|
# generate-hashes: false
|
||||||
|
# universal: false
|
||||||
|
|
||||||
-e file:.
|
-e file:.
|
||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
@@ -31,7 +32,7 @@ asgiref==3.8.1
|
|||||||
# via opentelemetry-instrumentation-asgi
|
# via opentelemetry-instrumentation-asgi
|
||||||
attrs==23.2.0
|
attrs==23.2.0
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
authlib==1.3.0
|
authlib==1.3.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
av==11.0.0
|
av==11.0.0
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
@@ -398,7 +399,6 @@ pandas==2.2.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via passlib
|
|
||||||
pathspec==0.12.1
|
pathspec==0.12.1
|
||||||
# via black
|
# via black
|
||||||
pcodedmp==1.2.6
|
pcodedmp==1.2.6
|
||||||
@@ -457,7 +457,6 @@ pygments==2.18.0
|
|||||||
# via rich
|
# via rich
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via pyjwt
|
|
||||||
pymysql==1.1.0
|
pymysql==1.1.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pypandoc==1.13
|
pypandoc==1.13
|
||||||
@@ -559,6 +558,9 @@ scipy==1.13.0
|
|||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
sentence-transformers==2.7.0
|
sentence-transformers==2.7.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
|
setuptools==69.5.1
|
||||||
|
# via ctranslate2
|
||||||
|
# via opentelemetry-instrumentation
|
||||||
shapely==2.0.4
|
shapely==2.0.4
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
@@ -653,7 +655,6 @@ uvicorn==0.22.0
|
|||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via uvicorn
|
|
||||||
uvloop==0.19.0
|
uvloop==0.19.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
validators==0.28.1
|
validators==0.28.1
|
||||||
@@ -681,6 +682,3 @@ youtube-transcript-api==0.6.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
zipp==3.18.1
|
zipp==3.18.1
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
setuptools==69.5.1
|
|
||||||
# via ctranslate2
|
|
||||||
# via opentelemetry-instrumentation
|
|
||||||
|
|||||||
+16
-2
@@ -1,6 +1,12 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Arimo';
|
font-family: 'Inter';
|
||||||
src: url('/assets/fonts/Arimo-Variable.ttf');
|
src: url('/assets/fonts/Inter-Variable.ttf');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Archivo';
|
||||||
|
src: url('/assets/fonts/Archivo-Variable.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +38,10 @@ math {
|
|||||||
@apply underline;
|
@apply underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-primary {
|
||||||
|
font-family: 'Archivo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
}
|
}
|
||||||
@@ -140,3 +150,7 @@ input[type='number'] {
|
|||||||
.cm-editor.cm-focused {
|
.cm-editor.cm-focused {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme~='dark'] {
|
||||||
|
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
|
||||||
|
}
|
||||||
|
|||||||
+12
-7
@@ -23,6 +23,8 @@
|
|||||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||||
(() => {
|
(() => {
|
||||||
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
|
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-800', '#101010');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-850', '#050505');
|
||||||
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||||
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
|
|
||||||
%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>
|
||||||
|
|
||||||
@@ -80,13 +83,13 @@
|
|||||||
id="logo"
|
id="logo"
|
||||||
style="
|
style="
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 6rem;
|
width: auto;
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
top: 41%;
|
top: 44%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -3rem;
|
transform: translateX(-50%);
|
||||||
"
|
"
|
||||||
src="/logo.svg"
|
src="/static/splash.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -96,7 +99,7 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
||||||
width: 24rem;
|
width: 24rem;
|
||||||
margin-left: -12rem;
|
transform: translateX(-50%);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -105,8 +108,8 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
id="logo-her"
|
id="logo-her"
|
||||||
style="width: 13rem; height: 13rem"
|
style="width: auto; height: 13rem"
|
||||||
src="/logo.svg"
|
src="/static/splash.png"
|
||||||
class="animate-pulse-fast"
|
class="animate-pulse-fast"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -152,6 +155,7 @@
|
|||||||
#splash-screen {
|
#splash-screen {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark #splash-screen {
|
html.dark #splash-screen {
|
||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
@@ -208,6 +212,7 @@
|
|||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse-fast {
|
.animate-pulse-fast {
|
||||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,45 @@ export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ChatActionForm = {
|
||||||
|
model: string;
|
||||||
|
messages: string[];
|
||||||
|
chat_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chatAction = async (token: string, action_id: string, body: ChatActionForm) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/chat/actions/${action_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
} else {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getTaskConfig = async (token: string = '') => {
|
export const getTaskConfig = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ type ChunkConfigForm = {
|
|||||||
chunk_overlap: number;
|
chunk_overlap: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ContentExtractConfigForm = {
|
||||||
|
engine: string;
|
||||||
|
tika_server_url: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
type YoutubeConfigForm = {
|
type YoutubeConfigForm = {
|
||||||
language: string[];
|
language: string[];
|
||||||
translation?: string | null;
|
translation?: string | null;
|
||||||
@@ -40,6 +45,7 @@ type YoutubeConfigForm = {
|
|||||||
type RAGConfigForm = {
|
type RAGConfigForm = {
|
||||||
pdf_extract_images?: boolean;
|
pdf_extract_images?: boolean;
|
||||||
chunk?: ChunkConfigForm;
|
chunk?: ChunkConfigForm;
|
||||||
|
content_extraction?: ContentExtractConfigForm;
|
||||||
web_loader_ssl_verification?: boolean;
|
web_loader_ssl_verification?: boolean;
|
||||||
youtube?: YoutubeConfigForm;
|
youtube?: YoutubeConfigForm;
|
||||||
};
|
};
|
||||||
@@ -419,7 +425,7 @@ export const resetUploadDir = async (token: string) => {
|
|||||||
export const resetVectorDB = async (token: string) => {
|
export const resetVectorDB = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${RAG_API_BASE_URL}/reset`, {
|
const res = await fetch(`${RAG_API_BASE_URL}/reset/db`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
|
<div class="px-5 pt-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-semibold">
|
||||||
{$i18n.t('What’s New in')}
|
{$i18n.t('What’s New in')}
|
||||||
{$WEBUI_NAME}
|
{$WEBUI_NAME}
|
||||||
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
{#if changelog}
|
{#if changelog}
|
||||||
{#each Object.keys(changelog) as version}
|
{#each Object.keys(changelog) as version}
|
||||||
<div class=" mb-3 pr-2">
|
<div class=" mb-3 pr-2">
|
||||||
<div class="font-bold text-xl mb-1 dark:text-white">
|
<div class="font-semibold text-xl mb-1 dark:text-white">
|
||||||
v{version} - {changelog[version].date}
|
v{version} - {changelog[version].date}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div
|
<div
|
||||||
class="font-bold uppercase text-xs {section === 'added'
|
class="font-semibold uppercase text-xs {section === 'added'
|
||||||
? 'text-white bg-blue-600'
|
? 'text-white bg-blue-600'
|
||||||
: section === 'fixed'
|
: section === 'fixed'
|
||||||
? 'text-white bg-green-600'
|
? 'text-white bg-green-600'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, tick } from 'svelte';
|
import { getContext, tick, onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import Database from './Settings/Database.svelte';
|
import Database from './Settings/Database.svelte';
|
||||||
@@ -21,17 +21,31 @@
|
|||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let selectedTab = 'general';
|
let selectedTab = 'general';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const containerElement = document.getElementById('admin-settings-tabs-container');
|
||||||
|
|
||||||
|
if (containerElement) {
|
||||||
|
containerElement.addEventListener('wheel', function (event) {
|
||||||
|
if (event.deltaY !== 0) {
|
||||||
|
// Adjust horizontal scroll position based on vertical scroll
|
||||||
|
containerElement.scrollLeft += event.deltaY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
|
||||||
<div
|
<div
|
||||||
|
id="admin-settings-tabs-container"
|
||||||
class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
|
class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
||||||
'general'
|
'general'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'general';
|
selectedTab = 'general';
|
||||||
}}
|
}}
|
||||||
@@ -56,8 +70,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'users'
|
'users'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'users';
|
selectedTab = 'users';
|
||||||
}}
|
}}
|
||||||
@@ -80,8 +94,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'connections'
|
'connections'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'connections';
|
selectedTab = 'connections';
|
||||||
}}
|
}}
|
||||||
@@ -104,8 +118,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'models'
|
'models'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'models';
|
selectedTab = 'models';
|
||||||
}}
|
}}
|
||||||
@@ -130,8 +144,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'documents'
|
'documents'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'documents';
|
selectedTab = 'documents';
|
||||||
}}
|
}}
|
||||||
@@ -160,8 +174,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'web'
|
'web'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'web';
|
selectedTab = 'web';
|
||||||
}}
|
}}
|
||||||
@@ -184,8 +198,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'interface'
|
'interface'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'interface';
|
selectedTab = 'interface';
|
||||||
}}
|
}}
|
||||||
@@ -210,8 +224,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'audio'
|
'audio'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'audio';
|
selectedTab = 'audio';
|
||||||
}}
|
}}
|
||||||
@@ -237,8 +251,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'images'
|
'images'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'images';
|
selectedTab = 'images';
|
||||||
}}
|
}}
|
||||||
@@ -263,8 +277,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'pipelines'
|
'pipelines'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'pipelines';
|
selectedTab = 'pipelines';
|
||||||
}}
|
}}
|
||||||
@@ -293,8 +307,8 @@
|
|||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'db'
|
'db'
|
||||||
? 'bg-gray-200 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'db';
|
selectedTab = 'db';
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={STT_OPENAI_API_BASE_URL}
|
bind:value={STT_OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="model-list"
|
list="model-list"
|
||||||
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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={STT_MODEL}
|
bind:value={STT_MODEL}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
/>
|
/>
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={TTS_OPENAI_API_BASE_URL}
|
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={TTS_VOICE}
|
bind:value={TTS_VOICE}
|
||||||
>
|
>
|
||||||
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="voice-list"
|
list="voice-list"
|
||||||
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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={TTS_VOICE}
|
bind:value={TTS_VOICE}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
/>
|
/>
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="model-list"
|
list="model-list"
|
||||||
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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={TTS_MODEL}
|
bind:value={TTS_MODEL}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -200,7 +200,7 @@
|
|||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
|
class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
|
||||||
? 'pr-8'
|
? 'pr-8'
|
||||||
: ''} text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
: ''} text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
{#each OLLAMA_BASE_URLS as url, idx}
|
{#each OLLAMA_BASE_URLS as url, idx}
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
let embeddingModel = '';
|
let embeddingModel = '';
|
||||||
let rerankingModel = '';
|
let rerankingModel = '';
|
||||||
|
|
||||||
|
let contentExtractionEngine = 'default';
|
||||||
|
let tikaServerUrl = '';
|
||||||
|
let showTikaServerUrl = false;
|
||||||
|
|
||||||
let chunkSize = 0;
|
let chunkSize = 0;
|
||||||
let chunkOverlap = 0;
|
let chunkOverlap = 0;
|
||||||
let pdfExtractImages = true;
|
let pdfExtractImages = true;
|
||||||
@@ -163,11 +167,20 @@
|
|||||||
rerankingModelUpdateHandler();
|
rerankingModelUpdateHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contentExtractionEngine === 'tika' && tikaServerUrl === '') {
|
||||||
|
toast.error($i18n.t('Tika Server URL required.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await updateRAGConfig(localStorage.token, {
|
const res = await updateRAGConfig(localStorage.token, {
|
||||||
pdf_extract_images: pdfExtractImages,
|
pdf_extract_images: pdfExtractImages,
|
||||||
chunk: {
|
chunk: {
|
||||||
chunk_overlap: chunkOverlap,
|
chunk_overlap: chunkOverlap,
|
||||||
chunk_size: chunkSize
|
chunk_size: chunkSize
|
||||||
|
},
|
||||||
|
content_extraction: {
|
||||||
|
engine: contentExtractionEngine,
|
||||||
|
tika_server_url: tikaServerUrl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,6 +226,10 @@
|
|||||||
|
|
||||||
chunkSize = res.chunk.chunk_size;
|
chunkSize = res.chunk.chunk_size;
|
||||||
chunkOverlap = res.chunk.chunk_overlap;
|
chunkOverlap = res.chunk.chunk_overlap;
|
||||||
|
|
||||||
|
contentExtractionEngine = res.content_extraction.engine;
|
||||||
|
tikaServerUrl = res.content_extraction.tika_server_url;
|
||||||
|
showTikaServerUrl = contentExtractionEngine === 'tika';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -262,7 +279,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
|
class=" self-center text-xs p-1 px-3 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
|
||||||
? ' cursor-not-allowed'
|
? ' cursor-not-allowed'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -335,7 +352,7 @@
|
|||||||
{#if embeddingEngine === 'openai'}
|
{#if embeddingEngine === 'openai'}
|
||||||
<div class="my-0.5 flex gap-2">
|
<div class="my-0.5 flex gap-2">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={OpenAIUrl}
|
bind:value={OpenAIUrl}
|
||||||
required
|
required
|
||||||
@@ -388,7 +405,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-1" />
|
<hr class="dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="space-y-2" />
|
<div class="space-y-2" />
|
||||||
<div>
|
<div>
|
||||||
@@ -398,7 +415,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={embeddingModel}
|
bind:value={embeddingModel}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
required
|
required
|
||||||
@@ -407,7 +424,7 @@
|
|||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
|
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,7 +433,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
|
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
|
||||||
model: embeddingModel.slice(-40)
|
model: embeddingModel.slice(-40)
|
||||||
})}
|
})}
|
||||||
@@ -426,7 +443,7 @@
|
|||||||
|
|
||||||
{#if embeddingEngine === ''}
|
{#if embeddingEngine === ''}
|
||||||
<button
|
<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"
|
class="px-2.5 bg-gray-50 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={() => {
|
on:click={() => {
|
||||||
embeddingModelUpdateHandler();
|
embeddingModelUpdateHandler();
|
||||||
}}
|
}}
|
||||||
@@ -495,7 +512,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
|
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
|
||||||
model: 'BAAI/bge-reranker-v2-m3'
|
model: 'BAAI/bge-reranker-v2-m3'
|
||||||
})}
|
})}
|
||||||
@@ -503,7 +520,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
class="px-2.5 bg-gray-50 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={() => {
|
on:click={() => {
|
||||||
rerankingModelUpdateHandler();
|
rerankingModelUpdateHandler();
|
||||||
}}
|
}}
|
||||||
@@ -562,6 +579,39 @@
|
|||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<div class="text-sm font-medium">{$i18n.t('Content Extraction')}</div>
|
||||||
|
|
||||||
|
<div class="flex w-full justify-between mt-2">
|
||||||
|
<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
|
bind:value={contentExtractionEngine}
|
||||||
|
on:change={(e) => {
|
||||||
|
showTikaServerUrl = e.target.value === 'tika';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">{$i18n.t('Default')} </option>
|
||||||
|
<option value="tika">{$i18n.t('Tika')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showTikaServerUrl}
|
||||||
|
<div class="flex w-full mt-2">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Enter Tika Server URL')}
|
||||||
|
bind:value={tikaServerUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
||||||
|
|
||||||
@@ -571,7 +621,7 @@
|
|||||||
|
|
||||||
<div class="self-center p-3">
|
<div class="self-center p-3">
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Top K')}
|
placeholder={$i18n.t('Enter Top K')}
|
||||||
bind:value={querySettings.k}
|
bind:value={querySettings.k}
|
||||||
@@ -589,7 +639,7 @@
|
|||||||
|
|
||||||
<div class="self-center p-3">
|
<div class="self-center p-3">
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
placeholder={$i18n.t('Enter Score')}
|
placeholder={$i18n.t('Enter Score')}
|
||||||
@@ -617,7 +667,7 @@
|
|||||||
<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
|
||||||
bind:value={querySettings.template}
|
bind:value={querySettings.template}
|
||||||
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="4"
|
rows="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -633,7 +683,7 @@
|
|||||||
<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
|
<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Chunk Size')}
|
placeholder={$i18n.t('Enter Chunk Size')}
|
||||||
bind:value={chunkSize}
|
bind:value={chunkSize}
|
||||||
@@ -650,7 +700,7 @@
|
|||||||
|
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Chunk Overlap')}
|
placeholder={$i18n.t('Enter Chunk Overlap')}
|
||||||
bind:value={chunkOverlap}
|
bind:value={chunkOverlap}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
<div class="flex mt-2 space-x-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||||
bind:value={adminConfig.JWT_EXPIRES_IN}
|
bind:value={adminConfig.JWT_EXPIRES_IN}
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
<div class="flex mt-2 space-x-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={`https://example.com/webhook`}
|
placeholder={`https://example.com/webhook`}
|
||||||
bind:value={webhookUrl}
|
bind:value={webhookUrl}
|
||||||
|
|||||||
@@ -240,13 +240,13 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
bind:value={AUTOMATIC1111_BASE_URL}
|
bind:value={AUTOMATIC1111_BASE_URL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
updateUrlHandler();
|
updateUrlHandler();
|
||||||
@@ -282,6 +282,7 @@
|
|||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
|
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
|
||||||
bind:value={AUTOMATIC1111_API_AUTH}
|
bind:value={AUTOMATIC1111_API_AUTH}
|
||||||
|
required={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
@@ -299,13 +300,13 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
bind:value={COMFYUI_BASE_URL}
|
bind:value={COMFYUI_BASE_URL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
updateUrlHandler();
|
updateUrlHandler();
|
||||||
@@ -331,7 +332,7 @@
|
|||||||
|
|
||||||
<div class="flex gap-2 mb-1">
|
<div class="flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={OPENAI_API_BASE_URL}
|
bind:value={OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -354,7 +355,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="model-list"
|
list="model-list"
|
||||||
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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
/>
|
/>
|
||||||
@@ -368,7 +369,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
required
|
required
|
||||||
@@ -391,7 +392,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||||
bind:value={imageSize}
|
bind:value={imageSize}
|
||||||
/>
|
/>
|
||||||
@@ -404,7 +405,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||||
bind:value={steps}
|
bind:value={steps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={taskConfig.TASK_MODEL}
|
bind:value={taskConfig.TASK_MODEL}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||||
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="6"
|
rows="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
||||||
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="6"
|
rows="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
|
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,24 +273,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
||||||
{#each promptSuggestions as prompt, promptIdx}
|
{#each promptSuggestions as prompt, promptIdx}
|
||||||
<div class=" flex dark:bg-gray-850 rounded-xl py-1.5">
|
<div
|
||||||
|
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
|
||||||
|
>
|
||||||
<div class="flex flex-col flex-1 pl-1">
|
<div class="flex flex-col flex-1 pl-1">
|
||||||
<div class="flex border-b dark:border-gray-800 w-full">
|
<div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
|
||||||
<input
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||||
bind:value={prompt.title[0]}
|
bind:value={prompt.title[0]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||||
bind:value={prompt.title[1]}
|
bind:value={prompt.title[1]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||||
bind:value={prompt.content}
|
bind:value={prompt.content}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -158,12 +158,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
|
const [res, controller] = await pullModel(
|
||||||
(error) => {
|
localStorage.token,
|
||||||
toast.error(error);
|
sanitizedModelTag,
|
||||||
return null;
|
selectedOllamaUrlIdx
|
||||||
}
|
).catch((error) => {
|
||||||
);
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
const reader = res.body
|
const reader = res.body
|
||||||
@@ -570,12 +572,12 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex-1 pb-1">
|
<div class="flex-1 pb-1">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedOllamaUrlIdx}
|
bind:value={selectedOllamaUrlIdx}
|
||||||
placeholder={$i18n.t('Select an Ollama instance')}
|
placeholder={$i18n.t('Select an Ollama instance')}
|
||||||
>
|
>
|
||||||
{#each OLLAMA_URLS as url, idx}
|
{#each OLLAMA_URLS as url, idx}
|
||||||
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
|
<option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -584,7 +586,7 @@
|
|||||||
<div class="flex w-full justify-end">
|
<div class="flex w-full justify-end">
|
||||||
<Tooltip content="Update All Models" placement="top">
|
<Tooltip content="Update All Models" placement="top">
|
||||||
<button
|
<button
|
||||||
class="p-2.5 flex gap-2 items-center 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"
|
class="p-2.5 flex gap-2 items-center bg-gray-50 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={() => {
|
on:click={() => {
|
||||||
updateModelsHandler();
|
updateModelsHandler();
|
||||||
}}
|
}}
|
||||||
@@ -619,7 +621,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||||
modelTag: 'mistral:7b'
|
modelTag: 'mistral:7b'
|
||||||
})}
|
})}
|
||||||
@@ -627,7 +629,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
class="px-2.5 bg-gray-50 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={() => {
|
on:click={() => {
|
||||||
pullModelHandler();
|
pullModelHandler();
|
||||||
}}
|
}}
|
||||||
@@ -753,7 +755,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={deleteModelTag}
|
bind:value={deleteModelTag}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
@@ -761,7 +763,7 @@
|
|||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
||||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.name} class="bg-gray-50 dark:bg-gray-700"
|
||||||
>{model.name +
|
>{model.name +
|
||||||
' (' +
|
' (' +
|
||||||
(model.ollama.size / 1024 ** 3).toFixed(1) +
|
(model.ollama.size / 1024 ** 3).toFixed(1) +
|
||||||
@@ -771,7 +773,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
class="px-2.5 bg-gray-50 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={() => {
|
on:click={() => {
|
||||||
showModelDeleteConfirm = true;
|
showModelDeleteConfirm = true;
|
||||||
}}
|
}}
|
||||||
@@ -797,7 +799,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2 flex flex-col gap-2">
|
<div class="flex-1 mr-2 flex flex-col gap-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||||
modelTag: 'my-modelfile'
|
modelTag: 'my-modelfile'
|
||||||
})}
|
})}
|
||||||
@@ -807,7 +809,7 @@
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={createModelContent}
|
bind:value={createModelContent}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
|
||||||
rows="6"
|
rows="6"
|
||||||
placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
|
placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
|
||||||
disabled={createModelLoading}
|
disabled={createModelLoading}
|
||||||
@@ -816,7 +818,7 @@
|
|||||||
|
|
||||||
<div class="flex self-start">
|
<div class="flex self-start">
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-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 disabled:cursor-not-allowed"
|
class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
createModelHandler();
|
createModelHandler();
|
||||||
}}
|
}}
|
||||||
@@ -925,7 +927,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
|
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
modelUploadInputElement.click();
|
modelUploadInputElement.click();
|
||||||
}}
|
}}
|
||||||
@@ -940,7 +942,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||||
''
|
''
|
||||||
? 'mr-2'
|
? 'mr-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
@@ -955,7 +957,7 @@
|
|||||||
|
|
||||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||||
<button
|
<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 disabled:cursor-not-allowed transition"
|
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={modelTransferring}
|
disabled={modelTransferring}
|
||||||
>
|
>
|
||||||
@@ -1014,7 +1016,7 @@
|
|||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={modelFileContent}
|
bind:value={modelFileContent}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="6"
|
rows="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
} from '$lib/apis';
|
} from '$lib/apis';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
|
||||||
const i18n: Writable<i18nType> = getContext('i18n');
|
const i18n: Writable<i18nType> = getContext('i18n');
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedPipelinesUrlIdx}
|
bind:value={selectedPipelinesUrlIdx}
|
||||||
placeholder={$i18n.t('Select a pipeline url')}
|
placeholder={$i18n.t('Select a pipeline url')}
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
@@ -327,7 +328,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter Github Raw URL')}
|
placeholder={$i18n.t('Enter Github Raw URL')}
|
||||||
bind:value={pipelineDownloadUrl}
|
bind:value={pipelineDownloadUrl}
|
||||||
/>
|
/>
|
||||||
@@ -411,7 +412,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedPipelineIdx}
|
bind:value={selectedPipelineIdx}
|
||||||
placeholder={$i18n.t('Select a pipeline')}
|
placeholder={$i18n.t('Select a pipeline')}
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
@@ -476,15 +477,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (valves[property] ?? null) !== null}
|
{#if (valves[property] ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<!-- {valves[property]} -->
|
||||||
|
<div class="flex mt-0.5 mb-1.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
{#if valves_spec.properties[property]?.enum ?? null}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
<select
|
||||||
type="text"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={valves_spec.properties[property].title}
|
bind:value={valves[property]}
|
||||||
bind:value={valves[property]}
|
>
|
||||||
autocomplete="off"
|
{#each valves_spec.properties[property].enum as option}
|
||||||
/>
|
<option value={option} selected={option === valves[property]}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else if (valves_spec.properties[property]?.type ?? null) === 'boolean'}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{valves[property] ? 'Enabled' : 'Disabled'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" pr-2">
|
||||||
|
<Switch bind:state={valves[property]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="text"
|
||||||
|
placeholder={valves_spec.properties[property].title}
|
||||||
|
bind:value={valves[property]}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
|
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={defaultModelId}
|
bind:value={defaultModelId}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={modelId}
|
bind:value={modelId}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||||
bind:value={webConfig.search.searxng_query_url}
|
bind:value={webConfig.search.searxng_query_url}
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
||||||
bind:value={webConfig.search.google_pse_engine_id}
|
bind:value={webConfig.search.google_pse_engine_id}
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Search Result Count')}
|
placeholder={$i18n.t('Search Result Count')}
|
||||||
bind:value={webConfig.search.result_count}
|
bind:value={webConfig.search.result_count}
|
||||||
required
|
required
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Concurrent Requests')}
|
placeholder={$i18n.t('Concurrent Requests')}
|
||||||
bind:value={webConfig.search.concurrent_requests}
|
bind:value={webConfig.search.concurrent_requests}
|
||||||
required
|
required
|
||||||
@@ -267,7 +267,7 @@
|
|||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter language codes')}
|
placeholder={$i18n.t('Enter language codes')}
|
||||||
bind:value={youtubeLanguage}
|
bind:value={youtubeLanguage}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||||
import { queryMemory } from '$lib/apis/memories';
|
import { queryMemory } from '$lib/apis/memories';
|
||||||
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
||||||
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
|
import { chatCompleted, generateTitle, generateSearchQuery, chatAction } from '$lib/apis';
|
||||||
|
|
||||||
import Banner from '../common/Banner.svelte';
|
import Banner from '../common/Banner.svelte';
|
||||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||||
@@ -60,19 +60,28 @@
|
|||||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||||
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
import ChatControls from './ChatControls.svelte';
|
||||||
|
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||||
|
|
||||||
const i18n: Writable<i18nType> = getContext('i18n');
|
const i18n: Writable<i18nType> = getContext('i18n');
|
||||||
|
|
||||||
export let chatIdProp = '';
|
export let chatIdProp = '';
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
const eventTarget = new EventTarget();
|
const eventTarget = new EventTarget();
|
||||||
|
|
||||||
|
let showControls = false;
|
||||||
let stopResponseFlag = false;
|
let stopResponseFlag = false;
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let processing = '';
|
let processing = '';
|
||||||
let messagesContainerElement: HTMLDivElement;
|
let messagesContainerElement: HTMLDivElement;
|
||||||
|
|
||||||
|
let showEventConfirmation = false;
|
||||||
|
let eventConfirmationTitle = '';
|
||||||
|
let eventConfirmationMessage = '';
|
||||||
|
let eventConfirmationInput = false;
|
||||||
|
let eventConfirmationInputPlaceholder = '';
|
||||||
|
let eventCallback = null;
|
||||||
|
|
||||||
let showModelSelector = true;
|
let showModelSelector = true;
|
||||||
|
|
||||||
let selectedModels = [''];
|
let selectedModels = [''];
|
||||||
@@ -89,6 +98,8 @@
|
|||||||
|
|
||||||
let title = '';
|
let title = '';
|
||||||
let prompt = '';
|
let prompt = '';
|
||||||
|
|
||||||
|
let chatFiles = [];
|
||||||
let files = [];
|
let files = [];
|
||||||
let messages = [];
|
let messages = [];
|
||||||
let history = {
|
let history = {
|
||||||
@@ -96,6 +107,9 @@
|
|||||||
currentId: null
|
currentId: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let params = {};
|
||||||
|
let valves = {};
|
||||||
|
|
||||||
$: if (history.currentId !== null) {
|
$: if (history.currentId !== null) {
|
||||||
let _messages = [];
|
let _messages = [];
|
||||||
|
|
||||||
@@ -126,6 +140,56 @@
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chatEventHandler = async (event, cb) => {
|
||||||
|
if (event.chat_id === $chatId) {
|
||||||
|
await tick();
|
||||||
|
console.log(event);
|
||||||
|
let message = history.messages[event.message_id];
|
||||||
|
|
||||||
|
const type = event?.data?.type ?? null;
|
||||||
|
const data = event?.data?.data ?? null;
|
||||||
|
|
||||||
|
if (type === 'status') {
|
||||||
|
if (message?.statusHistory) {
|
||||||
|
message.statusHistory.push(data);
|
||||||
|
} else {
|
||||||
|
message.statusHistory = [data];
|
||||||
|
}
|
||||||
|
} else if (type === 'citation') {
|
||||||
|
if (message?.citations) {
|
||||||
|
message.citations.push(data);
|
||||||
|
} else {
|
||||||
|
message.citations = [data];
|
||||||
|
}
|
||||||
|
} else if (type === 'message') {
|
||||||
|
message.content += data.content;
|
||||||
|
} else if (type === 'replace') {
|
||||||
|
message.content = data.content;
|
||||||
|
} else if (type === 'confirmation') {
|
||||||
|
eventCallback = cb;
|
||||||
|
|
||||||
|
eventConfirmationInput = false;
|
||||||
|
showEventConfirmation = true;
|
||||||
|
|
||||||
|
eventConfirmationTitle = data.title;
|
||||||
|
eventConfirmationMessage = data.message;
|
||||||
|
} else if (type === 'input') {
|
||||||
|
eventCallback = cb;
|
||||||
|
|
||||||
|
eventConfirmationInput = true;
|
||||||
|
showEventConfirmation = true;
|
||||||
|
|
||||||
|
eventConfirmationTitle = data.title;
|
||||||
|
eventConfirmationMessage = data.message;
|
||||||
|
eventConfirmationInputPlaceholder = data.placeholder;
|
||||||
|
} else {
|
||||||
|
console.log('Unknown message type', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = messages;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const onMessageHandler = async (event) => {
|
const onMessageHandler = async (event) => {
|
||||||
if (event.origin === window.origin) {
|
if (event.origin === window.origin) {
|
||||||
@@ -163,6 +227,8 @@
|
|||||||
};
|
};
|
||||||
window.addEventListener('message', onMessageHandler);
|
window.addEventListener('message', onMessageHandler);
|
||||||
|
|
||||||
|
$socket.on('chat-events', chatEventHandler);
|
||||||
|
|
||||||
if (!$chatId) {
|
if (!$chatId) {
|
||||||
chatId.subscribe(async (value) => {
|
chatId.subscribe(async (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -177,6 +243,8 @@
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', onMessageHandler);
|
window.removeEventListener('message', onMessageHandler);
|
||||||
|
|
||||||
|
$socket.off('chat-events');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +264,7 @@
|
|||||||
messages: {},
|
messages: {},
|
||||||
currentId: null
|
currentId: null
|
||||||
};
|
};
|
||||||
|
params = {};
|
||||||
|
|
||||||
if ($page.url.searchParams.get('models')) {
|
if ($page.url.searchParams.get('models')) {
|
||||||
selectedModels = $page.url.searchParams.get('models')?.split(',');
|
selectedModels = $page.url.searchParams.get('models')?.split(',');
|
||||||
@@ -265,11 +334,8 @@
|
|||||||
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
await settings.set({
|
params = chatContent?.params ?? {};
|
||||||
...$settings,
|
chatFiles = chatContent?.files ?? {};
|
||||||
system: chatContent.system ?? $settings.system,
|
|
||||||
params: chatContent.options ?? $settings.params
|
|
||||||
});
|
|
||||||
|
|
||||||
autoScroll = true;
|
autoScroll = true;
|
||||||
await tick();
|
await tick();
|
||||||
@@ -302,7 +368,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatCompletedHandler = async (modelId, messages) => {
|
const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
|
||||||
await mermaid.run({
|
await mermaid.run({
|
||||||
querySelector: '.mermaid'
|
querySelector: '.mermaid'
|
||||||
});
|
});
|
||||||
@@ -316,7 +382,9 @@
|
|||||||
info: m.info ? m.info : undefined,
|
info: m.info ? m.info : undefined,
|
||||||
timestamp: m.timestamp
|
timestamp: m.timestamp
|
||||||
})),
|
})),
|
||||||
chat_id: $chatId
|
chat_id: chatId,
|
||||||
|
session_id: $socket?.id,
|
||||||
|
id: responseMessageId
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
messages.at(-1).error = { content: error };
|
messages.at(-1).error = { content: error };
|
||||||
@@ -336,6 +404,65 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($chatId == chatId) {
|
||||||
|
if ($settings.saveChatHistory ?? true) {
|
||||||
|
chat = await updateChatById(localStorage.token, chatId, {
|
||||||
|
models: selectedModels,
|
||||||
|
messages: messages,
|
||||||
|
history: history,
|
||||||
|
params: params,
|
||||||
|
files: chatFiles
|
||||||
|
});
|
||||||
|
await chats.set(await getChatList(localStorage.token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId) => {
|
||||||
|
const res = await chatAction(localStorage.token, actionId, {
|
||||||
|
model: modelId,
|
||||||
|
messages: messages.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
content: m.content,
|
||||||
|
info: m.info ? m.info : undefined,
|
||||||
|
timestamp: m.timestamp
|
||||||
|
})),
|
||||||
|
chat_id: chatId,
|
||||||
|
session_id: $socket?.id,
|
||||||
|
id: responseMessageId
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
messages.at(-1).error = { content: error };
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res !== null) {
|
||||||
|
// Update chat history with the new messages
|
||||||
|
for (const message of res.messages) {
|
||||||
|
history.messages[message.id] = {
|
||||||
|
...history.messages[message.id],
|
||||||
|
...(history.messages[message.id].content !== message.content
|
||||||
|
? { originalContent: history.messages[message.id].content }
|
||||||
|
: {}),
|
||||||
|
...message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($chatId == chatId) {
|
||||||
|
if ($settings.saveChatHistory ?? true) {
|
||||||
|
chat = await updateChatById(localStorage.token, chatId, {
|
||||||
|
models: selectedModels,
|
||||||
|
messages: messages,
|
||||||
|
history: history,
|
||||||
|
params: params,
|
||||||
|
files: chatFiles
|
||||||
|
});
|
||||||
|
await chats.set(await getChatList(localStorage.token));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
|
const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
|
||||||
@@ -392,6 +519,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _files = JSON.parse(JSON.stringify(files));
|
const _files = JSON.parse(JSON.stringify(files));
|
||||||
|
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
|
||||||
|
chatFiles = chatFiles.filter(
|
||||||
|
// Remove duplicates
|
||||||
|
(item, index, array) =>
|
||||||
|
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||||
|
);
|
||||||
|
|
||||||
files = [];
|
files = [];
|
||||||
|
|
||||||
prompt = '';
|
prompt = '';
|
||||||
@@ -480,9 +614,7 @@
|
|||||||
title: $i18n.t('New Chat'),
|
title: $i18n.t('New Chat'),
|
||||||
models: selectedModels,
|
models: selectedModels,
|
||||||
system: $settings.system ?? undefined,
|
system: $settings.system ?? undefined,
|
||||||
options: {
|
params: params,
|
||||||
...($settings.params ?? {})
|
|
||||||
},
|
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history,
|
history: history,
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -580,11 +712,11 @@
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
const messagesBody = [
|
const messagesBody = [
|
||||||
$settings.system || (responseMessage?.userContext ?? null)
|
params?.system || $settings.system || (responseMessage?.userContext ?? null)
|
||||||
? {
|
? {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `${promptTemplate(
|
content: `${promptTemplate(
|
||||||
$settings?.system ?? '',
|
params?.system ?? $settings?.system ?? '',
|
||||||
$user.name,
|
$user.name,
|
||||||
$settings?.userLocation
|
$settings?.userLocation
|
||||||
? await getAndUpdateUserLocation(localStorage.token)
|
? await getAndUpdateUserLocation(localStorage.token)
|
||||||
@@ -634,25 +766,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let files = [];
|
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||||
if (model?.info?.meta?.knowledge ?? false) {
|
if (model?.info?.meta?.knowledge ?? false) {
|
||||||
files = model.info.meta.knowledge;
|
files.push(...model.info.meta.knowledge);
|
||||||
}
|
}
|
||||||
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
|
|
||||||
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
...(lastUserMessage?.files?.filter((item) =>
|
|
||||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
|
||||||
) ?? []),
|
|
||||||
...(responseMessage?.files?.filter((item) =>
|
|
||||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
|
||||||
) ?? [])
|
|
||||||
].filter(
|
|
||||||
// Remove duplicates
|
|
||||||
(item, index, array) =>
|
|
||||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
|
||||||
);
|
|
||||||
|
|
||||||
eventTarget.dispatchEvent(
|
eventTarget.dispatchEvent(
|
||||||
new CustomEvent('chat:start', {
|
new CustomEvent('chat:start', {
|
||||||
@@ -665,25 +782,29 @@
|
|||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
||||||
|
stream: true,
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: messagesBody,
|
messages: messagesBody,
|
||||||
options: {
|
options: {
|
||||||
...($settings.params ?? {}),
|
...(params ?? $settings.params ?? {}),
|
||||||
stop:
|
stop:
|
||||||
$settings?.params?.stop ?? undefined
|
params?.stop ?? $settings?.params?.stop ?? undefined
|
||||||
? $settings.params.stop.map((str) =>
|
? (params?.stop ?? $settings.params.stop).map((str) =>
|
||||||
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
num_predict: $settings?.params?.max_tokens ?? undefined,
|
num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
|
||||||
repeat_penalty: $settings?.params?.frequency_penalty ?? undefined
|
repeat_penalty:
|
||||||
|
params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined
|
||||||
},
|
},
|
||||||
format: $settings.requestFormat ?? undefined,
|
format: $settings.requestFormat ?? undefined,
|
||||||
keep_alive: $settings.keepAlive ?? undefined,
|
keep_alive: $settings.keepAlive ?? undefined,
|
||||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||||
files: files.length > 0 ? files : undefined,
|
files: files.length > 0 ? files : undefined,
|
||||||
citations: files.length > 0 ? true : undefined,
|
...(Object.keys(valves).length ? { valves } : {}),
|
||||||
chat_id: $chatId
|
session_id: $socket?.id,
|
||||||
|
chat_id: $chatId,
|
||||||
|
id: responseMessageId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
@@ -704,7 +825,7 @@
|
|||||||
controller.abort('User: Stop Response');
|
controller.abort('User: Stop Response');
|
||||||
} else {
|
} else {
|
||||||
const messages = createMessagesList(responseMessageId);
|
const messages = createMessagesList(responseMessageId);
|
||||||
await chatCompletedHandler(model.id, messages);
|
await chatCompletedHandler(_chatId, model.id, responseMessageId, messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
_response = responseMessage.content;
|
_response = responseMessage.content;
|
||||||
@@ -811,7 +932,9 @@
|
|||||||
chat = await updateChatById(localStorage.token, _chatId, {
|
chat = await updateChatById(localStorage.token, _chatId, {
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history,
|
history: history,
|
||||||
models: selectedModels
|
models: selectedModels,
|
||||||
|
params: params,
|
||||||
|
files: chatFiles
|
||||||
});
|
});
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
}
|
}
|
||||||
@@ -865,7 +988,7 @@
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length == 2 && messages.at(1).content !== '') {
|
if (messages.length == 2 && messages.at(1).content !== '' && selectedModels[0] === model.id) {
|
||||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||||
const _title = await generateChatTitle(userPrompt);
|
const _title = await generateChatTitle(userPrompt);
|
||||||
await setChatTitle(_chatId, _title);
|
await setChatTitle(_chatId, _title);
|
||||||
@@ -878,24 +1001,10 @@
|
|||||||
let _response = null;
|
let _response = null;
|
||||||
const responseMessage = history.messages[responseMessageId];
|
const responseMessage = history.messages[responseMessageId];
|
||||||
|
|
||||||
let files = [];
|
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||||
if (model?.info?.meta?.knowledge ?? false) {
|
if (model?.info?.meta?.knowledge ?? false) {
|
||||||
files = model.info.meta.knowledge;
|
files.push(...model.info.meta.knowledge);
|
||||||
}
|
}
|
||||||
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
...(lastUserMessage?.files?.filter((item) =>
|
|
||||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
|
||||||
) ?? []),
|
|
||||||
...(responseMessage?.files?.filter((item) =>
|
|
||||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
|
||||||
) ?? [])
|
|
||||||
].filter(
|
|
||||||
// Remove duplicates
|
|
||||||
(item, index, array) =>
|
|
||||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
|
||||||
);
|
|
||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
@@ -912,8 +1021,8 @@
|
|||||||
const [res, controller] = await generateOpenAIChatCompletion(
|
const [res, controller] = await generateOpenAIChatCompletion(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
{
|
{
|
||||||
model: model.id,
|
|
||||||
stream: true,
|
stream: true,
|
||||||
|
model: model.id,
|
||||||
stream_options:
|
stream_options:
|
||||||
model.info?.meta?.capabilities?.usage ?? false
|
model.info?.meta?.capabilities?.usage ?? false
|
||||||
? {
|
? {
|
||||||
@@ -921,11 +1030,11 @@
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
messages: [
|
messages: [
|
||||||
$settings.system || (responseMessage?.userContext ?? null)
|
params?.system || $settings.system || (responseMessage?.userContext ?? null)
|
||||||
? {
|
? {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `${promptTemplate(
|
content: `${promptTemplate(
|
||||||
$settings?.system ?? '',
|
params?.system ?? $settings?.system ?? '',
|
||||||
$user.name,
|
$user.name,
|
||||||
$settings?.userLocation
|
$settings?.userLocation
|
||||||
? await getAndUpdateUserLocation(localStorage.token)
|
? await getAndUpdateUserLocation(localStorage.token)
|
||||||
@@ -970,22 +1079,24 @@
|
|||||||
: message?.raContent ?? message.content
|
: message?.raContent ?? message.content
|
||||||
})
|
})
|
||||||
})),
|
})),
|
||||||
seed: $settings?.params?.seed ?? undefined,
|
seed: params?.seed ?? $settings?.params?.seed ?? undefined,
|
||||||
stop:
|
stop:
|
||||||
$settings?.params?.stop ?? undefined
|
params?.stop ?? $settings?.params?.stop ?? undefined
|
||||||
? $settings.params.stop.map((str) =>
|
? (params?.stop ?? $settings.params.stop).map((str) =>
|
||||||
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
temperature: $settings?.params?.temperature ?? undefined,
|
temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
|
||||||
top_p: $settings?.params?.top_p ?? undefined,
|
top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
|
||||||
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
|
frequency_penalty:
|
||||||
max_tokens: $settings?.params?.max_tokens ?? undefined,
|
params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined,
|
||||||
|
max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
|
||||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||||
files: files.length > 0 ? files : undefined,
|
files: files.length > 0 ? files : undefined,
|
||||||
citations: files.length > 0 ? true : undefined,
|
...(Object.keys(valves).length ? { valves } : {}),
|
||||||
|
session_id: $socket?.id,
|
||||||
chat_id: $chatId
|
chat_id: $chatId,
|
||||||
|
id: responseMessageId
|
||||||
},
|
},
|
||||||
`${WEBUI_BASE_URL}/api`
|
`${WEBUI_BASE_URL}/api`
|
||||||
);
|
);
|
||||||
@@ -1014,7 +1125,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const messages = createMessagesList(responseMessageId);
|
const messages = createMessagesList(responseMessageId);
|
||||||
|
|
||||||
await chatCompletedHandler(model.id, messages);
|
await chatCompletedHandler(_chatId, model.id, responseMessageId, messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
_response = responseMessage.content;
|
_response = responseMessage.content;
|
||||||
@@ -1086,7 +1197,9 @@
|
|||||||
chat = await updateChatById(localStorage.token, _chatId, {
|
chat = await updateChatById(localStorage.token, _chatId, {
|
||||||
models: selectedModels,
|
models: selectedModels,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history
|
history: history,
|
||||||
|
params: params,
|
||||||
|
files: chatFiles
|
||||||
});
|
});
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
}
|
}
|
||||||
@@ -1124,7 +1237,7 @@
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length == 2) {
|
if (messages.length == 2 && selectedModels[0] === model.id) {
|
||||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||||
|
|
||||||
const _title = await generateChatTitle(userPrompt);
|
const _title = await generateChatTitle(userPrompt);
|
||||||
@@ -1353,6 +1466,24 @@
|
|||||||
|
|
||||||
<audio id="audioElement" src="" style="display: none;" />
|
<audio id="audioElement" src="" style="display: none;" />
|
||||||
|
|
||||||
|
<EventConfirmDialog
|
||||||
|
bind:show={showEventConfirmation}
|
||||||
|
title={eventConfirmationTitle}
|
||||||
|
message={eventConfirmationMessage}
|
||||||
|
input={eventConfirmationInput}
|
||||||
|
inputPlaceholder={eventConfirmationInputPlaceholder}
|
||||||
|
on:confirm={(e) => {
|
||||||
|
if (e.detail) {
|
||||||
|
eventCallback(e.detail);
|
||||||
|
} else {
|
||||||
|
eventCallback(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:cancel={() => {
|
||||||
|
eventCallback(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if $showCallOverlay}
|
{#if $showCallOverlay}
|
||||||
<CallOverlay
|
<CallOverlay
|
||||||
{submitPrompt}
|
{submitPrompt}
|
||||||
@@ -1387,6 +1518,7 @@
|
|||||||
{title}
|
{title}
|
||||||
bind:selectedModels
|
bind:selectedModels
|
||||||
bind:showModelSelector
|
bind:showModelSelector
|
||||||
|
bind:showControls
|
||||||
shareEnabled={messages.length > 0}
|
shareEnabled={messages.length > 0}
|
||||||
{chat}
|
{chat}
|
||||||
{initNewChat}
|
{initNewChat}
|
||||||
@@ -1396,7 +1528,7 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute top-[4.25rem] w-full {$showSidebar
|
class="absolute top-[4.25rem] w-full {$showSidebar
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
? 'md:max-w-[calc(100%-260px)]'
|
||||||
: ''} z-20"
|
: ''} {showControls ? 'lg:pr-[24rem]' : ''} z-20"
|
||||||
>
|
>
|
||||||
<div class=" flex flex-col gap-1 w-full">
|
<div class=" flex flex-col gap-1 w-full">
|
||||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
||||||
@@ -1423,7 +1555,9 @@
|
|||||||
|
|
||||||
<div class="flex flex-col flex-auto z-10">
|
<div class="flex flex-col flex-auto z-10">
|
||||||
<div
|
<div
|
||||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10"
|
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {showControls
|
||||||
|
? 'lg:pr-[24rem]'
|
||||||
|
: ''}"
|
||||||
id="messages-container"
|
id="messages-container"
|
||||||
bind:this={messagesContainerElement}
|
bind:this={messagesContainerElement}
|
||||||
on:scroll={(e) => {
|
on:scroll={(e) => {
|
||||||
@@ -1445,29 +1579,47 @@
|
|||||||
{sendPrompt}
|
{sendPrompt}
|
||||||
{continueGeneration}
|
{continueGeneration}
|
||||||
{regenerateResponse}
|
{regenerateResponse}
|
||||||
|
{chatActionHandler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MessageInput
|
|
||||||
bind:files
|
<div class={showControls ? 'lg:pr-[24rem]' : ''}>
|
||||||
bind:prompt
|
<MessageInput
|
||||||
bind:autoScroll
|
bind:files
|
||||||
bind:selectedToolIds
|
bind:prompt
|
||||||
bind:webSearchEnabled
|
bind:autoScroll
|
||||||
bind:atSelectedModel
|
bind:selectedToolIds
|
||||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
bind:webSearchEnabled
|
||||||
const model = $models.find((m) => m.id === e);
|
bind:atSelectedModel
|
||||||
if (model?.info?.meta?.toolIds ?? false) {
|
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
||||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
const model = $models.find((m) => m.id === e);
|
||||||
}
|
if (model?.info?.meta?.toolIds ?? false) {
|
||||||
return a;
|
return [...new Set([...a, ...model.info.meta.toolIds])];
|
||||||
}, [])}
|
}
|
||||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
return a;
|
||||||
{selectedModels}
|
}, [])}
|
||||||
{messages}
|
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||||
{submitPrompt}
|
{selectedModels}
|
||||||
{stopResponse}
|
{messages}
|
||||||
/>
|
{submitPrompt}
|
||||||
|
{stopResponse}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ChatControls
|
||||||
|
models={selectedModelIds.reduce((a, e, i, arr) => {
|
||||||
|
const model = $models.find((m) => m.id === e);
|
||||||
|
if (model) {
|
||||||
|
return [...a, model];
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}, [])}
|
||||||
|
bind:show={showControls}
|
||||||
|
bind:chatFiles
|
||||||
|
bind:params
|
||||||
|
bind:valves
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import Controls from './Controls/Controls.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
|
||||||
|
export let models = [];
|
||||||
|
|
||||||
|
export let chatId = null;
|
||||||
|
|
||||||
|
export let chatFiles = [];
|
||||||
|
export let valves = {};
|
||||||
|
export let params = {};
|
||||||
|
|
||||||
|
let largeScreen = false;
|
||||||
|
onMount(() => {
|
||||||
|
// listen to resize 1024px
|
||||||
|
const mediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||||
|
|
||||||
|
const handleMediaQuery = (e) => {
|
||||||
|
if (e.matches) {
|
||||||
|
largeScreen = true;
|
||||||
|
} else {
|
||||||
|
largeScreen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleMediaQuery);
|
||||||
|
|
||||||
|
handleMediaQuery(mediaQuery);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener('change', handleMediaQuery);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if largeScreen}
|
||||||
|
{#if show}
|
||||||
|
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
|
||||||
|
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
|
||||||
|
<div
|
||||||
|
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||||
|
>
|
||||||
|
<Controls
|
||||||
|
on:close={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
{models}
|
||||||
|
bind:chatFiles
|
||||||
|
bind:valves
|
||||||
|
bind:params
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<Modal bind:show>
|
||||||
|
<div class=" px-6 py-4 h-full">
|
||||||
|
<Controls
|
||||||
|
on:close={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
{models}
|
||||||
|
bind:chatFiles
|
||||||
|
bind:valves
|
||||||
|
bind:params
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher, getContext } from 'svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
|
||||||
|
import Valves from '$lib/components/common/Valves.svelte';
|
||||||
|
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||||
|
|
||||||
|
export let models = [];
|
||||||
|
|
||||||
|
export let chatFiles = [];
|
||||||
|
export let valves = {};
|
||||||
|
export let params = {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class=" dark:text-white">
|
||||||
|
<div class=" flex justify-between dark:text-gray-100 mb-2">
|
||||||
|
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Controls')}</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('close');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" dark:text-gray-200 text-sm font-primary">
|
||||||
|
{#if chatFiles.length > 0}
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 font-medium">{$i18n.t('Files')}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each chatFiles as file}
|
||||||
|
<FileItem
|
||||||
|
className="w-full"
|
||||||
|
url={`${file?.url}`}
|
||||||
|
name={file.name}
|
||||||
|
type={file.type}
|
||||||
|
dismissible={true}
|
||||||
|
on:dismiss={() => {
|
||||||
|
// Remove the file from the chatFiles array
|
||||||
|
chatFiles = chatFiles.filter((f) => f.id !== file.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-2 border-gray-100 dark:border-gray-800" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if models.length === 1 && models[0]?.pipe?.valves_spec}
|
||||||
|
<div>
|
||||||
|
<div class=" font-medium">{$i18n.t('Valves')}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-2 border-gray-100 dark:border-gray-800" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 font-medium">{$i18n.t('System Prompt')}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
bind:value={params.system}
|
||||||
|
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
|
||||||
|
rows="3"
|
||||||
|
placeholder={$i18n.t('Enter system prompt')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-2 border-gray-100 dark:border-gray-800" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 font-medium">{$i18n.t('Advanced Params')}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AdvancedParams bind:params />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -40,6 +40,8 @@
|
|||||||
import Headphone from '../icons/Headphone.svelte';
|
import Headphone from '../icons/Headphone.svelte';
|
||||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||||
import { transcribeAudio } from '$lib/apis/audio';
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
|
import FileItem from '../common/FileItem.svelte';
|
||||||
|
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -297,26 +299,9 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if dragged}
|
<FilesOverlay show={dragged} />
|
||||||
<div
|
|
||||||
class="fixed {$showSidebar
|
|
||||||
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
|
|
||||||
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
|
|
||||||
id="dropzone"
|
|
||||||
role="region"
|
|
||||||
aria-label="Drag and Drop Container"
|
|
||||||
>
|
|
||||||
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
|
||||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
|
||||||
<div class="max-w-md">
|
|
||||||
<AddFilesPlaceholder />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full font-primary">
|
||||||
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
|
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||||
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
|
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -500,10 +485,10 @@
|
|||||||
dir={$settings?.chatDirection ?? 'LTR'}
|
dir={$settings?.chatDirection ?? 'LTR'}
|
||||||
>
|
>
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
|
<div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
|
||||||
{#each files as file, fileIdx}
|
{#each files as file, fileIdx}
|
||||||
<div class=" relative group">
|
{#if file.type === 'image'}
|
||||||
{#if file.type === 'image'}
|
<div class=" relative group">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img
|
<img
|
||||||
src={file.url}
|
src={file.url}
|
||||||
@@ -534,137 +519,39 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if ['doc', 'file'].includes(file.type)}
|
<div class=" absolute -top-1 -right-1">
|
||||||
<div
|
<button
|
||||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
|
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||||
>
|
type="button"
|
||||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
on:click={() => {
|
||||||
{#if file.status === 'processed'}
|
files.splice(fileIdx, 1);
|
||||||
<svg
|
files = files;
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
}}
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
fill="currentColor"
|
|
||||||
class="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
class=" w-6 h-6 translate-y-[0.5px]"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_qM83 {
|
|
||||||
animation: spinner_8HQG 1.05s infinite;
|
|
||||||
}
|
|
||||||
.spinner_oXPr {
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
}
|
|
||||||
.spinner_ZTLf {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
@keyframes spinner_8HQG {
|
|
||||||
0%,
|
|
||||||
57.14% {
|
|
||||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
|
||||||
transform: translate(0);
|
|
||||||
}
|
|
||||||
28.57% {
|
|
||||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
|
||||||
transform: translateY(-6px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><circle
|
|
||||||
class="spinner_qM83"
|
|
||||||
cx="4"
|
|
||||||
cy="12"
|
|
||||||
r="2.5"
|
|
||||||
/><circle
|
|
||||||
class="spinner_qM83 spinner_oXPr"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="2.5"
|
|
||||||
/><circle
|
|
||||||
class="spinner_qM83 spinner_ZTLf"
|
|
||||||
cx="20"
|
|
||||||
cy="12"
|
|
||||||
r="2.5"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
|
||||||
{file.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if file.type === 'collection'}
|
|
||||||
<div
|
|
||||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
|
|
||||||
>
|
|
||||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-6 h-6"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
|
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"
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
|
||||||
{file?.title ?? `#${file.name}`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class=" absolute -top-1 -right-1">
|
|
||||||
<button
|
|
||||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
files.splice(fileIdx, 1);
|
|
||||||
files = files;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<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>
|
||||||
</div>
|
{:else}
|
||||||
|
<FileItem
|
||||||
|
name={file.name}
|
||||||
|
type={file.type}
|
||||||
|
dismissible={true}
|
||||||
|
on:dismiss={() => {
|
||||||
|
files.splice(fileIdx, 1);
|
||||||
|
files = files;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -662,10 +662,11 @@
|
|||||||
: rmsLevel * 100 > 1
|
: rmsLevel * 100 > 1
|
||||||
? 'size-14'
|
? 'size-14'
|
||||||
: 'size-12'} transition-all rounded-full {(model?.info?.meta
|
: 'size-12'} transition-all rounded-full {(model?.info?.meta
|
||||||
?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
|
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
|
||||||
? ' bg-cover bg-center bg-no-repeat'
|
? ' bg-cover bg-center bg-no-repeat'
|
||||||
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
|
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
|
||||||
style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
|
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
|
||||||
|
'/static/favicon.png'
|
||||||
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
|
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
|
||||||
: ''}
|
: ''}
|
||||||
/>
|
/>
|
||||||
@@ -743,10 +744,11 @@
|
|||||||
: rmsLevel * 100 > 1
|
: rmsLevel * 100 > 1
|
||||||
? 'size-[11.5rem]'
|
? 'size-[11.5rem]'
|
||||||
: 'size-44'} transition-all rounded-full {(model?.info?.meta
|
: 'size-44'} transition-all rounded-full {(model?.info?.meta
|
||||||
?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
|
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
|
||||||
? ' bg-cover bg-center bg-no-repeat'
|
? ' bg-cover bg-center bg-no-repeat'
|
||||||
: 'bg-black dark:bg-white'} "
|
: 'bg-black dark:bg-white'} "
|
||||||
style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
|
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
|
||||||
|
'/static/favicon.png'
|
||||||
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
|
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
|
||||||
: ''}
|
: ''}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
.substring(1)
|
.substring(1)
|
||||||
.startsWith('https://youtu.be'))}
|
.startsWith('https://youtu.be'))}
|
||||||
<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-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black line-clamp-1">
|
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{: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-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black line-clamp-1">
|
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { showSidebar } from '$lib/stores';
|
||||||
|
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
let overlayElement = null;
|
||||||
|
|
||||||
|
$: if (show && overlayElement) {
|
||||||
|
document.body.appendChild(overlayElement);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else if (overlayElement) {
|
||||||
|
document.body.removeChild(overlayElement);
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<div
|
||||||
|
bind:this={overlayElement}
|
||||||
|
class="fixed {$showSidebar
|
||||||
|
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
|
||||||
|
: 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none"
|
||||||
|
id="dropzone"
|
||||||
|
role="region"
|
||||||
|
aria-label="Drag and Drop Container"
|
||||||
|
>
|
||||||
|
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||||
|
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<AddFilesPlaceholder />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
export let sendPrompt: Function;
|
export let sendPrompt: Function;
|
||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
export let regenerateResponse: Function;
|
export let regenerateResponse: Function;
|
||||||
|
export let chatActionHandler: Function;
|
||||||
|
|
||||||
export let user = $_user;
|
export let user = $_user;
|
||||||
export let prompt;
|
export let prompt;
|
||||||
@@ -335,6 +336,9 @@
|
|||||||
copyToClipboard={copyToClipboardWithToast}
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
{continueGeneration}
|
{continueGeneration}
|
||||||
{regenerateResponse}
|
{regenerateResponse}
|
||||||
|
on:action={async (e) => {
|
||||||
|
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||||
|
}}
|
||||||
on:save={async (e) => {
|
on:save={async (e) => {
|
||||||
console.log('save', e);
|
console.log('save', e);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount, tick } from 'svelte';
|
import { getContext, onMount, tick } from 'svelte';
|
||||||
|
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -54,9 +53,25 @@
|
|||||||
<div class="text-sm font-medium dark:text-gray-300">
|
<div class="text-sm font-medium dark:text-gray-300">
|
||||||
{$i18n.t('Source')}
|
{$i18n.t('Source')}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm dark:text-gray-400">
|
|
||||||
{document.source?.name ?? $i18n.t('No source available')}
|
{#if document.source?.name}
|
||||||
</div>
|
<div class="text-sm dark:text-gray-400">
|
||||||
|
<a
|
||||||
|
href={document?.metadata?.file_id
|
||||||
|
? `/api/v1/files/${document?.metadata?.file_id}/content`
|
||||||
|
: document.source.name.includes('http')
|
||||||
|
? document.source.name
|
||||||
|
: `#`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{document?.metadata?.name ?? document.source.name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-sm dark:text-gray-400">
|
||||||
|
{$i18n.t('No source available')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" text-sm font-medium dark:text-gray-300">
|
<div class=" text-sm font-medium dark:text-gray-300">
|
||||||
|
|||||||
@@ -250,7 +250,8 @@ __builtins__.input = input`);
|
|||||||
stderr ||
|
stderr ||
|
||||||
result) &&
|
result) &&
|
||||||
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
||||||
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
|
class="language-{lang} rounded-t-none whitespace-pre"
|
||||||
|
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
|
||||||
></pre>
|
></pre>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -100,64 +100,68 @@
|
|||||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||||
id="responses-container-{parentMessage.id}"
|
id="responses-container-{parentMessage.id}"
|
||||||
>
|
>
|
||||||
{#each Object.keys(groupedMessages) as model}
|
{#key currentMessageId}
|
||||||
{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
|
{#each Object.keys(groupedMessages) as model}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
{@const message = groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
||||||
currentMessageId
|
currentMessageId
|
||||||
].model === model
|
].model === model
|
||||||
? 'border-gray-100 dark:border-gray-850 border-[1.5px]'
|
? 'border-gray-100 dark:border-gray-800 border-[1.5px]'
|
||||||
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
if (currentMessageId != message.id) {
|
||||||
|
currentMessageId = message.id;
|
||||||
|
let messageId = message.id;
|
||||||
|
console.log(messageId);
|
||||||
|
|
||||||
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
//
|
||||||
|
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
while (messageChildrenIds.length !== 0) {
|
||||||
|
messageId = messageChildrenIds.at(-1);
|
||||||
|
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(messageId);
|
history.currentId = messageId;
|
||||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
dispatch('change');
|
||||||
|
}
|
||||||
while (messageChildrenIds.length !== 0) {
|
|
||||||
messageId = messageChildrenIds.at(-1);
|
|
||||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.currentId = messageId;
|
|
||||||
dispatch('change');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ResponseMessage
|
|
||||||
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
|
||||||
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
|
||||||
isLastMessage={true}
|
|
||||||
{updateChatMessages}
|
|
||||||
{confirmEditResponseMessage}
|
|
||||||
showPreviousMessage={() => showPreviousMessage(model)}
|
|
||||||
showNextMessage={() => showNextMessage(model)}
|
|
||||||
{readOnly}
|
|
||||||
{rateMessage}
|
|
||||||
{copyToClipboard}
|
|
||||||
{continueGeneration}
|
|
||||||
regenerateResponse={async (message) => {
|
|
||||||
regenerateResponse(message);
|
|
||||||
await tick();
|
|
||||||
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
|
||||||
}}
|
}}
|
||||||
on:save={async (e) => {
|
>
|
||||||
console.log('save', e);
|
<ResponseMessage
|
||||||
|
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||||
|
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
||||||
|
isLastMessage={true}
|
||||||
|
{updateChatMessages}
|
||||||
|
{confirmEditResponseMessage}
|
||||||
|
showPreviousMessage={() => showPreviousMessage(model)}
|
||||||
|
showNextMessage={() => showNextMessage(model)}
|
||||||
|
{readOnly}
|
||||||
|
{rateMessage}
|
||||||
|
{copyToClipboard}
|
||||||
|
{continueGeneration}
|
||||||
|
regenerateResponse={async (message) => {
|
||||||
|
regenerateResponse(message);
|
||||||
|
await tick();
|
||||||
|
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
||||||
|
}}
|
||||||
|
on:save={async (e) => {
|
||||||
|
console.log('save', e);
|
||||||
|
|
||||||
const message = e.detail;
|
const message = e.detail;
|
||||||
history.messages[message.id] = message;
|
history.messages[message.id] = message;
|
||||||
await updateChatById(localStorage.token, chatId, {
|
await updateChatById(localStorage.token, chatId, {
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history
|
history: history
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<div class=" self-center font-bold mb-0.5 line-clamp-1 contents">
|
<div class=" self-center font-semibold mb-0.5 line-clamp-1 contents">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
|
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4 font-primary"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1">
|
<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1 font-p">
|
||||||
{$i18n.t('How can I help you today?')}
|
{$i18n.t('How can I help you today?')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
|
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
|
||||||
<Suggestions
|
<Suggestions
|
||||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||||
$config.default_prompt_suggestions}
|
$config.default_prompt_suggestions}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
|
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
|
import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
|
||||||
|
import Sparkles from '$lib/components/icons/Sparkles.svelte';
|
||||||
|
|
||||||
export let message;
|
export let message;
|
||||||
export let siblings;
|
export let siblings;
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
export let copyToClipboard: Function;
|
export let copyToClipboard: Function;
|
||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
export let regenerateResponse: Function;
|
export let regenerateResponse: Function;
|
||||||
|
export let chatActionHandler: Function;
|
||||||
|
|
||||||
let model = null;
|
let model = null;
|
||||||
$: model = $models.find((m) => m.id === message.model);
|
$: model = $models.find((m) => m.id === message.model);
|
||||||
@@ -152,7 +154,10 @@
|
|||||||
}
|
}
|
||||||
tooltipInstance = tippy(`#info-${message.id}`, {
|
tooltipInstance = tippy(`#info-${message.id}`, {
|
||||||
content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
|
content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
|
||||||
allowHTML: true
|
allowHTML: true,
|
||||||
|
theme: 'dark',
|
||||||
|
arrow: false,
|
||||||
|
offset: [0, 4]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1017,6 +1022,33 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{#each model?.actions ?? [] as action}
|
||||||
|
<Tooltip content={action.name} placement="bottom">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="{isLastMessage
|
||||||
|
? 'visible'
|
||||||
|
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('action', action.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if action.icon_url}
|
||||||
|
<img
|
||||||
|
src={action.icon_url}
|
||||||
|
class="w-4 h-4 {action.icon_url.includes('svg')
|
||||||
|
? 'dark:invert-[80%]'
|
||||||
|
: ''}"
|
||||||
|
style="fill: currentColor;"
|
||||||
|
alt={action.name}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Sparkles strokeWidth="2.1" className="size-4" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,32 +2,25 @@
|
|||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||||
import { Collapsible } from 'bits-ui';
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
|
|
||||||
export let status = { urls: [], query: '' };
|
export let status = { urls: [], query: '' };
|
||||||
let state = false;
|
let state = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collapsible.Root class="w-full space-y-1" bind:open={state}>
|
<Collapsible bind:open={state} className="w-full space-y-1">
|
||||||
<Collapsible.Trigger>
|
<div
|
||||||
<div
|
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
{#if state}
|
|
||||||
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
|
|
||||||
{:else}
|
|
||||||
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
|
|
||||||
<Collapsible.Content
|
|
||||||
class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
|
|
||||||
transition={slide}
|
|
||||||
>
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
{#if state}
|
||||||
|
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content">
|
||||||
{#if status?.query}
|
{#if status?.query}
|
||||||
<a
|
<a
|
||||||
href="https://www.google.com/search?q={status.query}"
|
href="https://www.google.com/search?q={status.query}"
|
||||||
@@ -93,5 +86,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</Collapsible.Content>
|
</div>
|
||||||
</Collapsible.Root>
|
</Collapsible>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { user as _user } from '$lib/stores';
|
import { user as _user } from '$lib/stores';
|
||||||
import { getFileContentById } from '$lib/apis/files';
|
import { getFileContentById } from '$lib/apis/files';
|
||||||
|
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -99,106 +100,11 @@
|
|||||||
{#if file.type === 'image'}
|
{#if file.type === 'image'}
|
||||||
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
|
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
|
||||||
{:else if file.type === 'file'}
|
{:else if file.type === 'file'}
|
||||||
<button
|
<FileItem url={`${file?.url}/content`} name={file.name} type={$i18n.t('File')} />
|
||||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
|
|
||||||
type="button"
|
|
||||||
on:click={async () => {
|
|
||||||
if (file?.url) {
|
|
||||||
window.open(`${file?.url}/content`, '_blank').focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
|
||||||
{file.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{:else if file.type === 'doc'}
|
{:else if file.type === 'doc'}
|
||||||
<button
|
<FileItem url={`${file?.url}`} name={file.name} type={$i18n.t('Document')} />
|
||||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
if (file?.url) {
|
|
||||||
window.open(file?.url, '_blank').focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
|
||||||
{file.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{:else if file.type === 'collection'}
|
{:else if file.type === 'collection'}
|
||||||
<button
|
<FileItem name={file?.title ?? `#${file.name}`} type={$i18n.t('Collection')} />
|
||||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
|
||||||
{file?.title ?? `#${file.name}`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col w-full items-center md:items-start">
|
<div class="flex flex-col w-full items-start">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex w-full max-w-fit">
|
<div class="flex w-full max-w-fit">
|
||||||
<div class="overflow-hidden w-full">
|
<div class="overflow-hidden w-full">
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showSetDefault && !$mobile}
|
{#if showSetDefault && !$mobile}
|
||||||
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500 font-primary">
|
||||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
}}
|
}}
|
||||||
closeFocus={false}
|
closeFocus={false}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
|
<DropdownMenu.Trigger class="relative w-full font-primary" aria-label={placeholder}>
|
||||||
<div
|
<div
|
||||||
class="flex w-full 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"
|
||||||
>
|
>
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class=" z-40 {$mobile
|
class=" z-40 {$mobile
|
||||||
? `w-full`
|
? `w-full`
|
||||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none "
|
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-none"
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
<div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
|
<div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
|
||||||
{#each item.model?.info?.meta.tags as tag}
|
{#each item.model?.info?.meta.tags as tag}
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -299,7 +299,7 @@
|
|||||||
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
|
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
|
||||||
{#each item.model?.info?.meta.tags as tag}
|
{#each item.model?.info?.meta.tags as tag}
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user