Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13b0e7d64a | ||
|
|
c8badfe21f | ||
|
|
2a1b9cae91 | ||
|
|
5ba7bbdd98 | ||
|
|
9435d2044a | ||
|
|
609a42c29c | ||
|
|
6a1e7ab038 | ||
|
|
53daa15b9a | ||
|
|
55097410f6 | ||
|
|
04e2b6e2bd | ||
|
|
9f0c9d973c | ||
|
|
1597e33a74 | ||
|
|
0b218bbb72 | ||
|
|
7b21b718fe | ||
|
|
6e5b557a1f | ||
|
|
6aefc79807 | ||
|
|
ec9e0dadea | ||
|
|
0470146d7b | ||
|
|
a518d50477 | ||
|
|
29f904db45 | ||
|
|
038fc48ac0 | ||
|
|
6f72def1ac | ||
|
|
0ec1f9e331 | ||
|
|
9682806476 | ||
|
|
fc6fa7887b | ||
|
|
e1e69cfbcb | ||
|
|
7a8f8960c5 | ||
|
|
30c44d431b | ||
|
|
b177976f29 | ||
|
|
af6b92a6fa | ||
|
|
a1888b3757 | ||
|
|
c75e77cdde | ||
|
|
6447b484c1 | ||
|
|
0c5236ac27 | ||
|
|
7ef5aa520c | ||
|
|
d620bcf516 | ||
|
|
b0c2e607b8 | ||
|
|
56e2d579f2 | ||
|
|
70f580ec45 | ||
|
|
eae35dddc2 | ||
|
|
bb979c9a78 | ||
|
|
4f959c31de | ||
|
|
3bbe065db1 | ||
|
|
41460e1335 | ||
|
|
ffdb44f887 | ||
|
|
549d3b4d10 | ||
|
|
879f85802a | ||
|
|
d652a1bbb1 | ||
|
|
9c2429ff97 | ||
|
|
d0645d3c4f | ||
|
|
748be3e637 | ||
|
|
153ba168a0 | ||
|
|
c70b18b2ef | ||
|
|
af6420e06d | ||
|
|
547611b703 | ||
|
|
1b2ae7bb77 | ||
|
|
5cd8011d53 | ||
|
|
208833d9f2 | ||
|
|
6863028540 | ||
|
|
5e97a52cbe | ||
|
|
af865ab035 | ||
|
|
01f622866f | ||
|
|
e9d14601a1 | ||
|
|
92e77d7b33 | ||
|
|
3b370bbcb3 | ||
|
|
20b1753a91 | ||
|
|
b2379c7104 | ||
|
|
e491e96f88 | ||
|
|
8afc8c5714 | ||
|
|
d388c20373 | ||
|
|
13403cd7dc | ||
|
|
52fd701f2e | ||
|
|
204a4fbe7a | ||
|
|
549817627f | ||
|
|
fa4d1d42a5 | ||
|
|
309cd645f1 | ||
|
|
a725801e55 | ||
|
|
8cdf9814bd | ||
|
|
e6bbce439d | ||
|
|
8d257ed596 | ||
|
|
a7063a598d | ||
|
|
367fa039a0 | ||
|
|
71d88fe35d | ||
|
|
958fe9639a | ||
|
|
670f28d694 | ||
|
|
3715994c25 | ||
|
|
d72d5d0e8e | ||
|
|
dbe463a53d | ||
|
|
678dd780ee | ||
|
|
44c870447f | ||
|
|
70a8f6e707 | ||
|
|
2fb4d3356c | ||
|
|
590fd129c8 | ||
|
|
99d10d1189 | ||
|
|
ad9a7cb1e2 | ||
|
|
8187922ef1 | ||
|
|
c869652ef4 | ||
|
|
240a30147d | ||
|
|
91fdb86fcc | ||
|
|
0e5a56e2cf | ||
|
|
40ecc2563a | ||
|
|
7e473f194d | ||
|
|
f62281a0c7 | ||
|
|
0c231fd387 | ||
|
|
7f3ebcaa91 | ||
|
|
1f8d08eaa2 | ||
|
|
5904ef86f2 | ||
|
|
d692649bac | ||
|
|
0d019a00c9 | ||
|
|
e6b3de310b | ||
|
|
d19b96d0c2 | ||
|
|
7cbc94592e | ||
|
|
9747f1e841 | ||
|
|
42bc24a646 | ||
|
|
35115957d8 | ||
|
|
b193eb1d82 | ||
|
|
4105c9735e | ||
|
|
ed205d82e8 | ||
|
|
fc31267a54 | ||
|
|
44c781f414 | ||
|
|
ff90b125ee | ||
|
|
831fe9f509 | ||
|
|
a140d319fe | ||
|
|
9581bf5dde | ||
|
|
76dcabdce6 | ||
|
|
dc262a5f79 | ||
|
|
db5830dc28 | ||
|
|
ecf3d567cf | ||
|
|
e8375e9acd | ||
|
|
8c84c74197 | ||
|
|
f7d1225c23 | ||
|
|
3dc90b854c | ||
|
|
ac3c657315 | ||
|
|
37b117a84c | ||
|
|
ab6346ea1c | ||
|
|
b289eef523 | ||
|
|
6e6f9862e6 | ||
|
|
7d42a79177 | ||
|
|
5315fb201d | ||
|
|
6a012d290e | ||
|
|
05bbca5b07 | ||
|
|
5afe0be5b4 | ||
|
|
d8aa2cc05f | ||
|
|
7b071d403f | ||
|
|
d5b1e5e0c0 | ||
|
|
cec5fdd144 | ||
|
|
d8498aa2b0 | ||
|
|
e593d3aee3 | ||
|
|
323cfaf2d7 | ||
|
|
637f0c6fb2 | ||
|
|
2756403102 | ||
|
|
92861d095a | ||
|
|
bf6c6627e8 | ||
|
|
b217cb76f0 | ||
|
|
b414fde6ca | ||
|
|
d8c39569be | ||
|
|
5ce15bb0df | ||
|
|
88086935af | ||
|
|
ace3552e15 | ||
|
|
003ceff7e4 | ||
|
|
209ccdf668 | ||
|
|
e5dd7e65d4 | ||
|
|
b2999ad590 | ||
|
|
1871a7a2fa | ||
|
|
49677e9c9d | ||
|
|
73954f4a63 | ||
|
|
a084938d9c | ||
|
|
1bf042ac84 | ||
|
|
a2f9f7c975 | ||
|
|
565f40c642 | ||
|
|
4441338574 | ||
|
|
389d650ee3 | ||
|
|
080d45239a | ||
|
|
91851114e4 | ||
|
|
5b11099a0b | ||
|
|
2996d1e096 | ||
|
|
be82175f86 | ||
|
|
05977bd1c7 | ||
|
|
8074289a8f | ||
|
|
f8ba0334e8 | ||
|
|
7c9bed83d2 | ||
|
|
482a7723b2 | ||
|
|
948d29b676 | ||
|
|
3fa6c41303 | ||
|
|
220a4bb535 | ||
|
|
287559e834 | ||
|
|
cdac0cd1df | ||
|
|
3653126179 | ||
|
|
2eb662df85 | ||
|
|
cdcf214455 | ||
|
|
a75a9c953a | ||
|
|
800ba206f7 | ||
|
|
8d5aa98297 | ||
|
|
283234d51d | ||
|
|
f9e1a933a9 | ||
|
|
ebd8ef1a9e | ||
|
|
ed691e26a6 | ||
|
|
067d76fece | ||
|
|
534c18c94c | ||
|
|
af9e0cc33a | ||
|
|
6847c2fc8c | ||
|
|
028eb7b351 | ||
|
|
12c21fac22 | ||
|
|
774defd184 | ||
|
|
6657f28694 | ||
|
|
9d7507236f | ||
|
|
8f49429228 | ||
|
|
f21e9dbd9a | ||
|
|
d3146d20ad | ||
|
|
24a177a149 | ||
|
|
a58dfccb7d | ||
|
|
b35293228e | ||
|
|
963e250654 | ||
|
|
ca8b766c09 | ||
|
|
21eca7f1c1 | ||
|
|
8062866973 | ||
|
|
99530358fd | ||
|
|
621393ca62 | ||
|
|
0c6284be02 | ||
|
|
9d5e3e2a91 | ||
|
|
7f260938db | ||
|
|
c416444e24 | ||
|
|
b559bc84a7 | ||
|
|
3f53abb233 | ||
|
|
67efd0dd39 | ||
|
|
dcd32faa83 | ||
|
|
8fcb5cee66 | ||
|
|
13b757d847 | ||
|
|
c6d7f24542 | ||
|
|
4c1a9380e6 | ||
|
|
ad6e8edcd3 | ||
|
|
95c742ba9b | ||
|
|
546d442626 | ||
|
|
ed8c0b23a0 | ||
|
|
c8f44b73f1 | ||
|
|
bf6b149b8b | ||
|
|
f334c834e5 | ||
|
|
7c850bb78a | ||
|
|
a280fed4be | ||
|
|
cf9b5241ae | ||
|
|
dfa25af271 | ||
|
|
b538c5a206 | ||
|
|
4fa3fcbb3b | ||
|
|
b049ae269b | ||
|
|
e5bf27e716 | ||
|
|
a9a6ed8b71 | ||
|
|
e6c64282fc | ||
|
|
64b41655bb | ||
|
|
6ac40552b4 | ||
|
|
039c5c540b | ||
|
|
c9ed934d0b | ||
|
|
06a64219bc | ||
|
|
ef36b21684 | ||
|
|
c44e51ae58 | ||
|
|
2c4bc7a2b2 | ||
|
|
62dc486c85 | ||
|
|
d11961626c | ||
|
|
519375b4c0 | ||
|
|
6ecb7c80f1 | ||
|
|
380cb93e46 | ||
|
|
36e7662706 | ||
|
|
49199819db | ||
|
|
aa8d26496c | ||
|
|
4619e32b6a | ||
|
|
0352e956b6 | ||
|
|
f999956997 | ||
|
|
bfd268c5e6 | ||
|
|
b7ad47017d | ||
|
|
502b722902 | ||
|
|
c89b34fd75 | ||
|
|
b9b1fdd1a1 | ||
|
|
2e0fa1c6a0 | ||
|
|
f8726719ef | ||
|
|
3569fe9c73 | ||
|
|
4ffcabfb3f | ||
|
|
a7ed74744f | ||
|
|
ae0bb8f1eb | ||
|
|
034411e47e | ||
|
|
baf58ef396 | ||
|
|
006fc3495e | ||
|
|
29a3b82336 | ||
|
|
22a5e196c9 | ||
|
|
deec41d29a | ||
|
|
3978efd710 | ||
|
|
7a80e732df | ||
|
|
6681df29d2 | ||
|
|
9d58bb1c66 | ||
|
|
acbdb05bd0 | ||
|
|
25f5259e84 | ||
|
|
0ced153f86 | ||
|
|
faca8c8b53 | ||
|
|
4ad8181088 | ||
|
|
4f3ff4a69d | ||
|
|
fc0f2f2b3a | ||
|
|
b4cd084117 | ||
|
|
c1fd55bb04 | ||
|
|
77b2d2dbee | ||
|
|
f92aed1b91 | ||
|
|
90a6be8001 | ||
|
|
2e4de209fe | ||
|
|
287147687e | ||
|
|
a53f5d01aa | ||
|
|
1aaa2e8219 | ||
|
|
02b104f56b | ||
|
|
e691839771 | ||
|
|
4c22d130d9 | ||
|
|
065d23d0b1 | ||
|
|
4b0a232ae1 | ||
|
|
81130c205c | ||
|
|
36b94ca5f5 | ||
|
|
9e4326a582 | ||
|
|
8193c2f847 | ||
|
|
fbef731a04 | ||
|
|
dbf88a2eca | ||
|
|
413a9031eb | ||
|
|
23e69bcdb4 | ||
|
|
edff071cd2 | ||
|
|
3b487cfae9 | ||
|
|
a8d2072e9f | ||
|
|
260e42a691 | ||
|
|
596a9c60d7 | ||
|
|
a47e4a1148 | ||
|
|
312c8b9e7a | ||
|
|
1d99508943 | ||
|
|
b42fda0971 | ||
|
|
cf377d2e1c | ||
|
|
ec9d0b8a6d | ||
|
|
14cac8ed22 | ||
|
|
4ecf9dd62d | ||
|
|
b56dcf155c | ||
|
|
63ffdb38aa | ||
|
|
f9289d3079 | ||
|
|
709b56bc40 | ||
|
|
eb38d382ba | ||
|
|
5beaa5d892 | ||
|
|
01b24f6282 | ||
|
|
cbe3694b81 | ||
|
|
f531a51e91 | ||
|
|
a0667dfd1b | ||
|
|
2b78e613a4 | ||
|
|
b34ab10b5e | ||
|
|
f07cc37939 | ||
|
|
957b0ecd15 | ||
|
|
659bc246c9 | ||
|
|
1502af7e94 | ||
|
|
26c5a533ec | ||
|
|
4274016491 | ||
|
|
c6c298b412 | ||
|
|
63eda0fe42 | ||
|
|
c3693c91b3 | ||
|
|
e426067e46 | ||
|
|
2e482eca6d | ||
|
|
fdea1afc7f | ||
|
|
41fecb800d | ||
|
|
52cf06b36c | ||
|
|
ebce006974 | ||
|
|
ff60695a0f | ||
|
|
3aa9685f5a | ||
|
|
6cecb964a9 | ||
|
|
0216ee101c | ||
|
|
d3c6bfb764 | ||
|
|
ea6a5121b3 | ||
|
|
d6a4fe00b3 | ||
|
|
36318e8a1b | ||
|
|
79562a2531 | ||
|
|
d2664ab2a6 | ||
|
|
e664a429a1 | ||
|
|
b09bd1ed34 | ||
|
|
f83c80aaec | ||
|
|
bbc8adca94 | ||
|
|
20c71d127b | ||
|
|
e35df904a0 | ||
|
|
62682a8c4c | ||
|
|
253c02cc3c | ||
|
|
175d1cc8f2 | ||
|
|
23c9122458 | ||
|
|
b42d2886a2 | ||
|
|
c3f90ba347 | ||
|
|
c6eba8c0a1 | ||
|
|
51bdb4cbea | ||
|
|
376b2d0297 | ||
|
|
7e03624408 | ||
|
|
133f223df6 | ||
|
|
8dc73fdbdb | ||
|
|
8ec3ed1830 | ||
|
|
8b71ce23bb | ||
|
|
82079e644a | ||
|
|
1e7a364c68 | ||
|
|
c74e7df6a0 | ||
|
|
29d8f8d91b | ||
|
|
0d1553562d | ||
|
|
8e3312c032 | ||
|
|
878d12da24 | ||
|
|
c1dfafcc9e | ||
|
|
6668d79519 | ||
|
|
775a3dc359 | ||
|
|
98f935b8f7 | ||
|
|
86359ef374 | ||
|
|
ef8fbaae28 | ||
|
|
9e48aeca45 | ||
|
|
9518111ed3 | ||
|
|
4fea55bac3 | ||
|
|
027dd848eb | ||
|
|
98928fc5f0 | ||
|
|
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
|
||||||
|
|||||||
@@ -8,36 +8,43 @@ assignees: ''
|
|||||||
|
|
||||||
# Bug Report
|
# Bug Report
|
||||||
|
|
||||||
## Description
|
## Installation Method
|
||||||
|
|
||||||
**Bug Summary:**
|
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
|
||||||
[Provide a brief but clear summary of the bug]
|
|
||||||
|
|
||||||
**Steps to Reproduce:**
|
|
||||||
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
|
||||||
|
|
||||||
**Expected Behavior:**
|
|
||||||
[Describe what you expected to happen.]
|
|
||||||
|
|
||||||
**Actual Behavior:**
|
|
||||||
[Describe what actually happened.]
|
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
- **Open WebUI Version:** [e.g., 0.1.120]
|
- **Open WebUI Version:** [e.g., v0.3.11]
|
||||||
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
|
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
|
||||||
|
|
||||||
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
||||||
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
||||||
|
|
||||||
## Reproduction Details
|
|
||||||
|
|
||||||
**Confirmation:**
|
**Confirmation:**
|
||||||
|
|
||||||
- [ ] I have read and followed all the instructions provided in the README.md.
|
- [ ] I have read and followed all the instructions provided in the README.md.
|
||||||
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
||||||
- [ ] I have included the browser console logs.
|
- [ ] I have included the browser console logs.
|
||||||
- [ ] I have included the Docker container logs.
|
- [ ] I have included the Docker container logs.
|
||||||
|
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
|
||||||
|
|
||||||
|
## Expected Behavior:
|
||||||
|
|
||||||
|
[Describe what you expected to happen.]
|
||||||
|
|
||||||
|
## Actual Behavior:
|
||||||
|
|
||||||
|
[Describe what actually happened.]
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
**Bug Summary:**
|
||||||
|
[Provide a brief but clear summary of the bug]
|
||||||
|
|
||||||
|
## Reproduction Details
|
||||||
|
|
||||||
|
**Steps to Reproduce:**
|
||||||
|
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
||||||
|
|
||||||
## Logs and Screenshots
|
## Logs and Screenshots
|
||||||
|
|
||||||
@@ -47,13 +54,9 @@ assignees: ''
|
|||||||
**Docker Container Logs:**
|
**Docker Container Logs:**
|
||||||
[Include relevant Docker container logs, if applicable]
|
[Include relevant Docker container logs, if applicable]
|
||||||
|
|
||||||
**Screenshots (if applicable):**
|
**Screenshots/Screen Recordings (if applicable):**
|
||||||
[Attach any relevant screenshots to help illustrate the issue]
|
[Attach any relevant screenshots to help illustrate the issue]
|
||||||
|
|
||||||
## Installation Method
|
|
||||||
|
|
||||||
[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.]
|
|
||||||
|
|
||||||
## Additional Information
|
## Additional Information
|
||||||
|
|
||||||
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
|
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ jobs:
|
|||||||
name: Run Cypress Integration Tests
|
name: Run Cypress Integration Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Maximize build space
|
||||||
|
uses: AdityaGarg8/remove-unwanted-software@v4.1
|
||||||
|
with:
|
||||||
|
remove-android: 'true'
|
||||||
|
remove-haskell: 'true'
|
||||||
|
remove-codeql: 'true'
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -26,6 +33,10 @@ jobs:
|
|||||||
--file docker-compose.a1111-test.yaml \
|
--file docker-compose.a1111-test.yaml \
|
||||||
up --detach --build
|
up --detach --build
|
||||||
|
|
||||||
|
- name: Delete Docker build cache
|
||||||
|
run: |
|
||||||
|
docker builder prune --all --force
|
||||||
|
|
||||||
- name: Wait for Ollama to be up
|
- name: Wait for Ollama to be up
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +54,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 +78,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 +159,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 +204,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 +216,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
|
||||||
|
|||||||
+144
@@ -5,6 +5,150 @@ 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.13] - 2024-08-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🎨 Enhanced Markdown Rendering**: Significant improvements in rendering markdown, ensuring smooth and reliable display of LaTeX and Mermaid charts, enhancing user experience with more robust visual content.
|
||||||
|
- **🔄 Auto-Install Tools & Functions Python Dependencies**: For 'Tools' and 'Functions', Open WebUI now automatically install extra python requirements specified in the frontmatter, streamlining setup processes and customization.
|
||||||
|
- **🌀 OAuth Email Claim Customization**: Introduced an 'OAUTH_EMAIL_CLAIM' variable to allow customization of the default "email" claim within OAuth configurations, providing greater flexibility in authentication processes.
|
||||||
|
- **📶 Websocket Reconnection**: Enhanced reliability with the capability to automatically reconnect when a websocket is closed, ensuring consistent and stable communication.
|
||||||
|
- **🤳 Haptic Feedback on Support Devices**: Android devices now support haptic feedback for an immersive tactile experience during certain interactions.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🛠️ ComfyUI Performance Improvement**: Addressed an issue causing FastAPI to stall when ComfyUI image generation was active; now runs in a separate thread to prevent UI unresponsiveness.
|
||||||
|
- **🔀 Session Handling**: Fixed an issue mandating session_id on client-side to ensure smoother session management and transitions.
|
||||||
|
- **🖋️ Minor Bug Fixes and Format Corrections**: Various minor fixes including typo corrections, backend formatting improvements, and test amendments enhancing overall system stability and performance.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🚀 Migration to SvelteKit 2**: Upgraded the underlying framework to SvelteKit version 2, offering enhanced speed, better code structure, and improved deployment capabilities.
|
||||||
|
- **🧹 General Cleanup and Refactoring**: Performed broad cleanup and refactoring across the platform, improving code efficiency and maintaining high standards of code health.
|
||||||
|
- **🚧 Integration Testing Improvements**: Modified how Cypress integration tests detect chat messages and updated sharing tests for better reliability and accuracy.
|
||||||
|
- **📁 Standardized '.safetensors' File Extension**: Renamed the '.sft' file extension to '.safetensors' for ComfyUI workflows, standardizing file formats across the platform.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **🗑️ Deprecated Frontend Functions**: Removed frontend functions that were migrated to backend to declutter the codebase and reduce redundancy.
|
||||||
|
|
||||||
|
## [0.3.12] - 2024-08-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔄 Sidebar Infinite Scroll**: Added an infinite scroll feature in the sidebar for more efficient chat navigation, reducing load times and enhancing user experience.
|
||||||
|
- **🚀 Enhanced Markdown Rendering**: Support for rendering all code blocks and making images clickable for preview; codespan styling is also enhanced to improve readability and user interaction.
|
||||||
|
- **🔒 Admin Shared Chat Visibility**: Admins no longer have default visibility over shared chats when ENABLE_ADMIN_CHAT_ACCESS is set to false, tightening security and privacy settings for users.
|
||||||
|
- **🌍 Language Updates**: Added Malay (Bahasa Malaysia) translation and updated Catalan and Traditional Chinese translations to improve accessibility for more users.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **📊 Markdown Rendering Issues**: Resolved issues with markdown rendering to ensure consistent and correct display across components.
|
||||||
|
- **🛠️ Styling Issues**: Multiple fixes applied to styling throughout the application, improving the overall visual experience and interface consistency.
|
||||||
|
- **🗃️ Modal Handling**: Fixed an issue where modals were not closing correctly in various model chat scenarios, enhancing usability and interface reliability.
|
||||||
|
- **📄 Missing OpenAI Usage Information**: Resolved issues where usage statistics for OpenAI services were not being correctly displayed, ensuring users have access to crucial data for managing and monitoring their API consumption.
|
||||||
|
- **🔧 Non-Streaming Support for Functions Plugin**: Fixed a functionality issue with the Functions plugin where non-streaming operations were not functioning as intended, restoring full capabilities for async and sync integration within the platform.
|
||||||
|
- **🔄 Environment Variable Type Correction (COMFYUI_FLUX_FP8_CLIP)**: Corrected the data type of the 'COMFYUI_FLUX_FP8_CLIP' environment variable from string to boolean, ensuring environment settings apply correctly and enhance configuration management.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔧 Backend Dependency Updates**: Updated several backend dependencies such as boto3, pypdf, python-pptx, validators, and black, ensuring up-to-date security and performance optimizations.
|
||||||
|
|
||||||
|
## [0.3.11] - 2024-08-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📊 Model Information Display**: Added visuals for model selection, including images next to model names for more intuitive navigation.
|
||||||
|
- **🗣 ElevenLabs Voice Adaptations**: Voice enhancements including support for ElevenLabs voice ID by name for personalized vocal interactions.
|
||||||
|
- **⌨️ Arrow Keys Model Selection**: Users can now use arrow keys for quicker model selection, enhancing accessibility.
|
||||||
|
- **🔍 Fuzzy Search in Model Selector**: Enhanced model selector with fuzzy search to locate models swiftly, including descriptions.
|
||||||
|
- **🕹️ ComfyUI Flux Image Generation**: Added support for the new Flux image gen model; introduces environment controls like weight precision and CLIP model options in Settings.
|
||||||
|
- **💾 Display File Size for Uploads**: Enhanced file interface now displays file size, preparing for upcoming upload restrictions.
|
||||||
|
- **🎚️ Advanced Params "Min P"**: Added 'Min P' parameter in the advanced settings for customized model precision control.
|
||||||
|
- **🔒 Enhanced OAuth**: Introduced custom redirect URI support for OAuth behind reverse proxies, enabling safer authentication processes.
|
||||||
|
- **🖥 Enhanced Latex Rendering**: Adjustments made to latex rendering processes, now accurately detecting and presenting latex inputs from text.
|
||||||
|
- **🌐 Internationalization**: Enhanced with new Romanian and updated Vietnamese and Ukrainian translations, helping broaden accessibility for international users.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Tags Handling in Document Upload**: Tags are now properly sent to the upload document handler, resolving issues with missing metadata.
|
||||||
|
- **🖥️ Sensitive Input Fields**: Corrected browser misinterpretation of secure input fields, preventing misclassification as password fields.
|
||||||
|
- **📂 Static Path Resolution in PDF Generation**: Fixed static paths that adjust dynamically to prevent issues across various environments.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🎨 UI/UX Styling Enhancements**: Multiple minor styling updates for a cleaner and more intuitive user interface.
|
||||||
|
- **🚧 Refactoring Various Components**: Numerous refactoring changes across styling, file handling, and function simplifications for clarity and performance.
|
||||||
|
- **🎛️ User Valves Management**: Moved user valves from settings to direct chat controls for more user-friendly access during interactions.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **⚙️ Health Check Logging**: Removed verbose logging from the health checking processes to declutter logs and improve backend performance.
|
||||||
|
|
||||||
|
## [0.3.10] - 2024-07-17
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔄 Improved File Upload**: Addressed the issue where file uploads lacked animation.
|
||||||
|
- **💬 Chat Continuity**: Fixed a problem where existing chats were not functioning properly in some instances.
|
||||||
|
- **🗂️ Chat File Reset**: Resolved the issue of chat files not resetting for new conversations, now ensuring a clean slate for each chat session.
|
||||||
|
- **📁 Document Workspace Uploads**: Corrected the handling of document uploads in the workspace using the Files API.
|
||||||
|
|
||||||
|
## [0.3.9] - 2024-07-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed.
|
||||||
|
- **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon.
|
||||||
|
- **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access.
|
||||||
|
- **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly.
|
||||||
|
- **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface.
|
||||||
|
- **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings.
|
||||||
|
- **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly.
|
||||||
|
- **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required.
|
||||||
|
- **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat.
|
||||||
|
- **✅ Boolean Values in Params**: Added support for boolean values in parameters.
|
||||||
|
- **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **⬆️ Dependency Updates**
|
||||||
|
- Upgraded 'pydantic' from version 2.7.1 to 2.8.2.
|
||||||
|
- Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31.
|
||||||
|
- Upgraded 'unstructured' from version 0.14.9 to 0.14.10.
|
||||||
|
- Upgraded 'chromadb' from version 0.5.3 to 0.5.4.
|
||||||
|
|
||||||
|
## [0.3.8] - 2024-07-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions.
|
||||||
|
- **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible.
|
||||||
|
- **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities.
|
||||||
|
- **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication.
|
||||||
|
- **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website.
|
||||||
|
- **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation.
|
||||||
|
- **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal.
|
||||||
|
- **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance.
|
||||||
|
- **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality.
|
||||||
|
- **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
|
||||||
|
- **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version.
|
||||||
|
- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
|
||||||
|
- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
|
||||||
|
- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
|
||||||
|
- **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title".
|
||||||
|
|
||||||
## [0.3.7] - 2024-06-29
|
## [0.3.7] - 2024-06-29
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -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
|
||||||
@@ -149,7 +151,7 @@ COPY --chown=$UID:$GID ./backend .
|
|||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
|
HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
|
||||||
|
|
||||||
USER $UID:$GID
|
USER $UID:$GID
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
+175
-45
@@ -10,13 +10,12 @@ from fastapi import (
|
|||||||
File,
|
File,
|
||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -32,6 +31,7 @@ from utils.utils import (
|
|||||||
)
|
)
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
@@ -44,6 +44,7 @@ from config import (
|
|||||||
AUDIO_STT_OPENAI_API_KEY,
|
AUDIO_STT_OPENAI_API_KEY,
|
||||||
AUDIO_TTS_OPENAI_API_BASE_URL,
|
AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||||
AUDIO_TTS_OPENAI_API_KEY,
|
AUDIO_TTS_OPENAI_API_KEY,
|
||||||
|
AUDIO_TTS_API_KEY,
|
||||||
AUDIO_STT_ENGINE,
|
AUDIO_STT_ENGINE,
|
||||||
AUDIO_STT_MODEL,
|
AUDIO_STT_MODEL,
|
||||||
AUDIO_TTS_ENGINE,
|
AUDIO_TTS_ENGINE,
|
||||||
@@ -76,6 +77,7 @@ app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
|||||||
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||||
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||||
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||||
|
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
|
||||||
|
|
||||||
# setting device type for whisper model
|
# setting device type for whisper model
|
||||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||||
@@ -88,6 +90,7 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|||||||
class TTSConfigForm(BaseModel):
|
class TTSConfigForm(BaseModel):
|
||||||
OPENAI_API_BASE_URL: str
|
OPENAI_API_BASE_URL: str
|
||||||
OPENAI_API_KEY: str
|
OPENAI_API_KEY: str
|
||||||
|
API_KEY: str
|
||||||
ENGINE: str
|
ENGINE: str
|
||||||
MODEL: str
|
MODEL: str
|
||||||
VOICE: str
|
VOICE: str
|
||||||
@@ -138,6 +141,7 @@ async def get_audio_config(user=Depends(get_admin_user)):
|
|||||||
"tts": {
|
"tts": {
|
||||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||||
|
"API_KEY": app.state.config.TTS_API_KEY,
|
||||||
"ENGINE": app.state.config.TTS_ENGINE,
|
"ENGINE": app.state.config.TTS_ENGINE,
|
||||||
"MODEL": app.state.config.TTS_MODEL,
|
"MODEL": app.state.config.TTS_MODEL,
|
||||||
"VOICE": app.state.config.TTS_VOICE,
|
"VOICE": app.state.config.TTS_VOICE,
|
||||||
@@ -157,6 +161,7 @@ async def update_audio_config(
|
|||||||
):
|
):
|
||||||
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
||||||
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
|
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
|
||||||
|
app.state.config.TTS_API_KEY = form_data.tts.API_KEY
|
||||||
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
|
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
|
||||||
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
||||||
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
||||||
@@ -170,6 +175,7 @@ async def update_audio_config(
|
|||||||
"tts": {
|
"tts": {
|
||||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||||
|
"API_KEY": app.state.config.TTS_API_KEY,
|
||||||
"ENGINE": app.state.config.TTS_ENGINE,
|
"ENGINE": app.state.config.TTS_ENGINE,
|
||||||
"MODEL": app.state.config.TTS_MODEL,
|
"MODEL": app.state.config.TTS_MODEL,
|
||||||
"VOICE": app.state.config.TTS_VOICE,
|
"VOICE": app.state.config.TTS_VOICE,
|
||||||
@@ -195,55 +201,111 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
if file_path.is_file():
|
if file_path.is_file():
|
||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
|
|
||||||
headers = {}
|
if app.state.config.TTS_ENGINE == "openai":
|
||||||
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
headers = {}
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body = body.decode("utf-8")
|
body = body.decode("utf-8")
|
||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
body["model"] = app.state.config.TTS_MODEL
|
body["model"] = app.state.config.TTS_MODEL
|
||||||
body = json.dumps(body).encode("utf-8")
|
body = json.dumps(body).encode("utf-8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||||
data=body,
|
data=body,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
# Save the streaming content to a file
|
# Save the streaming content to a file
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
for chunk in r.iter_content(chunk_size=8192):
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
|
||||||
with open(file_body_path, "w") as f:
|
with open(file_body_path, "w") as f:
|
||||||
json.dump(json.loads(body.decode("utf-8")), f)
|
json.dump(json.loads(body.decode("utf-8")), f)
|
||||||
|
|
||||||
# Return the saved file
|
# Return the saved file
|
||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(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:
|
||||||
try:
|
try:
|
||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']['message']}"
|
error_detail = f"External: {res['error']['message']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=r.status_code if r != None else 500,
|
status_code=r.status_code if r != None else 500,
|
||||||
detail=error_detail,
|
detail=error_detail,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||||
|
payload = None
|
||||||
|
try:
|
||||||
|
payload = json.loads(body.decode("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid JSON payload")
|
||||||
|
|
||||||
|
voice_id = payload.get("voice", "")
|
||||||
|
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "audio/mpeg",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"text": payload["input"],
|
||||||
|
"model_id": app.state.config.TTS_MODEL,
|
||||||
|
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(url, json=data, headers=headers)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# Save the streaming content to a file
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
with open(file_body_path, "w") as f:
|
||||||
|
json.dump(json.loads(body.decode("utf-8")), f)
|
||||||
|
|
||||||
|
# Return the saved file
|
||||||
|
return FileResponse(file_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']['message']}"
|
||||||
|
except Exception:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=r.status_code if r != None else 500,
|
||||||
|
detail=error_detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/transcriptions")
|
@app.post("/transcriptions")
|
||||||
@@ -277,6 +339,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,
|
||||||
@@ -289,7 +353,7 @@ def transcribe(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
model = WhisperModel(**whisper_kwargs)
|
model = WhisperModel(**whisper_kwargs)
|
||||||
except:
|
except Exception:
|
||||||
log.warning(
|
log.warning(
|
||||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||||
)
|
)
|
||||||
@@ -357,7 +421,7 @@ def transcribe(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']['message']}"
|
error_detail = f"External: {res['error']['message']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -372,3 +436,69 @@ def transcribe(
|
|||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_models() -> list[dict]:
|
||||||
|
if app.state.config.TTS_ENGINE == "openai":
|
||||||
|
return [{"id": "tts-1"}, {"id": "tts-1-hd"}]
|
||||||
|
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||||
|
headers = {
|
||||||
|
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
"https://api.elevenlabs.io/v1/models", headers=headers
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
models = response.json()
|
||||||
|
return [
|
||||||
|
{"name": model["name"], "id": model["model_id"]} for model in models
|
||||||
|
]
|
||||||
|
except requests.RequestException as e:
|
||||||
|
log.error(f"Error fetching voices: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/models")
|
||||||
|
async def get_models(user=Depends(get_verified_user)):
|
||||||
|
return {"models": get_available_models()}
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_voices() -> list[dict]:
|
||||||
|
if app.state.config.TTS_ENGINE == "openai":
|
||||||
|
return [
|
||||||
|
{"name": "alloy", "id": "alloy"},
|
||||||
|
{"name": "echo", "id": "echo"},
|
||||||
|
{"name": "fable", "id": "fable"},
|
||||||
|
{"name": "onyx", "id": "onyx"},
|
||||||
|
{"name": "nova", "id": "nova"},
|
||||||
|
{"name": "shimmer", "id": "shimmer"},
|
||||||
|
]
|
||||||
|
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||||
|
headers = {
|
||||||
|
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
"https://api.elevenlabs.io/v1/voices", headers=headers
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
voices_data = response.json()
|
||||||
|
|
||||||
|
voices = []
|
||||||
|
for voice in voices_data.get("voices", []):
|
||||||
|
voices.append({"name": voice["name"], "id": voice["voice_id"]})
|
||||||
|
return voices
|
||||||
|
except requests.RequestException as e:
|
||||||
|
log.error(f"Error fetching voices: {str(e)}")
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/voices")
|
||||||
|
async def get_voices(user=Depends(get_verified_user)):
|
||||||
|
return {"voices": get_available_voices()}
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -43,6 +42,9 @@ from config import (
|
|||||||
COMFYUI_SAMPLER,
|
COMFYUI_SAMPLER,
|
||||||
COMFYUI_SCHEDULER,
|
COMFYUI_SCHEDULER,
|
||||||
COMFYUI_SD3,
|
COMFYUI_SD3,
|
||||||
|
COMFYUI_FLUX,
|
||||||
|
COMFYUI_FLUX_WEIGHT_DTYPE,
|
||||||
|
COMFYUI_FLUX_FP8_CLIP,
|
||||||
IMAGES_OPENAI_API_BASE_URL,
|
IMAGES_OPENAI_API_BASE_URL,
|
||||||
IMAGES_OPENAI_API_KEY,
|
IMAGES_OPENAI_API_KEY,
|
||||||
IMAGE_GENERATION_MODEL,
|
IMAGE_GENERATION_MODEL,
|
||||||
@@ -86,10 +88,13 @@ app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
|
|||||||
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
|
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
|
||||||
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
|
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
|
||||||
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
|
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
|
||||||
|
app.state.config.COMFYUI_FLUX = COMFYUI_FLUX
|
||||||
|
app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE = COMFYUI_FLUX_WEIGHT_DTYPE
|
||||||
|
app.state.config.COMFYUI_FLUX_FP8_CLIP = COMFYUI_FLUX_FP8_CLIP
|
||||||
|
|
||||||
|
|
||||||
def get_automatic1111_api_auth():
|
def get_automatic1111_api_auth():
|
||||||
if app.state.config.AUTOMATIC1111_API_AUTH == None:
|
if app.state.config.AUTOMATIC1111_API_AUTH is None:
|
||||||
return ""
|
return ""
|
||||||
else:
|
else:
|
||||||
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
|
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
|
||||||
@@ -140,28 +145,30 @@ async def get_engine_url(user=Depends(get_admin_user)):
|
|||||||
async def update_engine_url(
|
async def update_engine_url(
|
||||||
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
if form_data.AUTOMATIC1111_BASE_URL == None:
|
if form_data.AUTOMATIC1111_BASE_URL is None:
|
||||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
else:
|
else:
|
||||||
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
||||||
try:
|
try:
|
||||||
r = requests.head(url)
|
r = requests.head(url)
|
||||||
|
r.raise_for_status()
|
||||||
app.state.config.AUTOMATIC1111_BASE_URL = url
|
app.state.config.AUTOMATIC1111_BASE_URL = url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
|
||||||
|
|
||||||
if form_data.COMFYUI_BASE_URL == None:
|
if form_data.COMFYUI_BASE_URL is None:
|
||||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
else:
|
else:
|
||||||
url = form_data.COMFYUI_BASE_URL.strip("/")
|
url = form_data.COMFYUI_BASE_URL.strip("/")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.head(url)
|
r = requests.head(url)
|
||||||
|
r.raise_for_status()
|
||||||
app.state.config.COMFYUI_BASE_URL = url
|
app.state.config.COMFYUI_BASE_URL = url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
|
||||||
|
|
||||||
if form_data.AUTOMATIC1111_API_AUTH == None:
|
if form_data.AUTOMATIC1111_API_AUTH is None:
|
||||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||||
else:
|
else:
|
||||||
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
|
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
|
||||||
@@ -422,7 +429,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),
|
||||||
):
|
):
|
||||||
@@ -498,9 +505,18 @@ def generate_image(
|
|||||||
if app.state.config.COMFYUI_SD3 is not None:
|
if app.state.config.COMFYUI_SD3 is not None:
|
||||||
data["sd3"] = app.state.config.COMFYUI_SD3
|
data["sd3"] = app.state.config.COMFYUI_SD3
|
||||||
|
|
||||||
|
if app.state.config.COMFYUI_FLUX is not None:
|
||||||
|
data["flux"] = app.state.config.COMFYUI_FLUX
|
||||||
|
|
||||||
|
if app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE is not None:
|
||||||
|
data["flux_weight_dtype"] = app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE
|
||||||
|
|
||||||
|
if app.state.config.COMFYUI_FLUX_FP8_CLIP is not None:
|
||||||
|
data["flux_fp8_clip"] = app.state.config.COMFYUI_FLUX_FP8_CLIP
|
||||||
|
|
||||||
data = ImageGenerationPayload(**data)
|
data = ImageGenerationPayload(**data)
|
||||||
|
|
||||||
res = comfyui_generate_image(
|
res = await comfyui_generate_image(
|
||||||
app.state.config.MODEL,
|
app.state.config.MODEL,
|
||||||
data,
|
data,
|
||||||
user.id,
|
user.id,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import asyncio
|
||||||
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
||||||
import uuid
|
|
||||||
import json
|
import json
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -125,6 +125,135 @@ COMFYUI_DEFAULT_PROMPT = """
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
FLUX_DEFAULT_PROMPT = """
|
||||||
|
{
|
||||||
|
"5": {
|
||||||
|
"inputs": {
|
||||||
|
"width": 1024,
|
||||||
|
"height": 1024,
|
||||||
|
"batch_size": 1
|
||||||
|
},
|
||||||
|
"class_type": "EmptyLatentImage"
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"inputs": {
|
||||||
|
"text": "Input Text Here",
|
||||||
|
"clip": [
|
||||||
|
"11",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"inputs": {
|
||||||
|
"samples": [
|
||||||
|
"13",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"10",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "VAEDecode"
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"inputs": {
|
||||||
|
"filename_prefix": "ComfyUI",
|
||||||
|
"images": [
|
||||||
|
"8",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "SaveImage"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"inputs": {
|
||||||
|
"vae_name": "ae.safetensors"
|
||||||
|
},
|
||||||
|
"class_type": "VAELoader"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"inputs": {
|
||||||
|
"clip_name1": "clip_l.safetensors",
|
||||||
|
"clip_name2": "t5xxl_fp16.safetensors",
|
||||||
|
"type": "flux"
|
||||||
|
},
|
||||||
|
"class_type": "DualCLIPLoader"
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"inputs": {
|
||||||
|
"unet_name": "flux1-dev.safetensors",
|
||||||
|
"weight_dtype": "default"
|
||||||
|
},
|
||||||
|
"class_type": "UNETLoader"
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"inputs": {
|
||||||
|
"noise": [
|
||||||
|
"25",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"guider": [
|
||||||
|
"22",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"sampler": [
|
||||||
|
"16",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"sigmas": [
|
||||||
|
"17",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent_image": [
|
||||||
|
"5",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "SamplerCustomAdvanced"
|
||||||
|
},
|
||||||
|
"16": {
|
||||||
|
"inputs": {
|
||||||
|
"sampler_name": "euler"
|
||||||
|
},
|
||||||
|
"class_type": "KSamplerSelect"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"inputs": {
|
||||||
|
"scheduler": "simple",
|
||||||
|
"steps": 20,
|
||||||
|
"denoise": 1,
|
||||||
|
"model": [
|
||||||
|
"12",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "BasicScheduler"
|
||||||
|
},
|
||||||
|
"22": {
|
||||||
|
"inputs": {
|
||||||
|
"model": [
|
||||||
|
"12",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"conditioning": [
|
||||||
|
"6",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "BasicGuider"
|
||||||
|
},
|
||||||
|
"25": {
|
||||||
|
"inputs": {
|
||||||
|
"noise_seed": 778937779713005
|
||||||
|
},
|
||||||
|
"class_type": "RandomNoise"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def queue_prompt(prompt, client_id, base_url):
|
def queue_prompt(prompt, client_id, base_url):
|
||||||
log.info("queue_prompt")
|
log.info("queue_prompt")
|
||||||
@@ -194,9 +323,12 @@ class ImageGenerationPayload(BaseModel):
|
|||||||
sampler: Optional[str] = None
|
sampler: Optional[str] = None
|
||||||
scheduler: Optional[str] = None
|
scheduler: Optional[str] = None
|
||||||
sd3: Optional[bool] = None
|
sd3: Optional[bool] = None
|
||||||
|
flux: Optional[bool] = None
|
||||||
|
flux_weight_dtype: Optional[str] = None
|
||||||
|
flux_fp8_clip: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
def comfyui_generate_image(
|
async def comfyui_generate_image(
|
||||||
model: str, payload: ImageGenerationPayload, client_id, base_url
|
model: str, payload: ImageGenerationPayload, client_id, base_url
|
||||||
):
|
):
|
||||||
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
@@ -215,21 +347,46 @@ def comfyui_generate_image(
|
|||||||
if payload.sd3:
|
if payload.sd3:
|
||||||
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
|
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
|
||||||
|
|
||||||
|
if payload.steps:
|
||||||
|
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
||||||
|
|
||||||
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
||||||
|
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
||||||
|
comfyui_prompt["3"]["inputs"]["seed"] = (
|
||||||
|
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||||
|
)
|
||||||
|
|
||||||
|
# as Flux uses a completely different workflow, we must treat it specially
|
||||||
|
if payload.flux:
|
||||||
|
comfyui_prompt = json.loads(FLUX_DEFAULT_PROMPT)
|
||||||
|
comfyui_prompt["12"]["inputs"]["unet_name"] = model
|
||||||
|
comfyui_prompt["25"]["inputs"]["noise_seed"] = (
|
||||||
|
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||||
|
)
|
||||||
|
|
||||||
|
if payload.sampler:
|
||||||
|
comfyui_prompt["16"]["inputs"]["sampler_name"] = payload.sampler
|
||||||
|
|
||||||
|
if payload.steps:
|
||||||
|
comfyui_prompt["17"]["inputs"]["steps"] = payload.steps
|
||||||
|
|
||||||
|
if payload.scheduler:
|
||||||
|
comfyui_prompt["17"]["inputs"]["scheduler"] = payload.scheduler
|
||||||
|
|
||||||
|
if payload.flux_weight_dtype:
|
||||||
|
comfyui_prompt["12"]["inputs"]["weight_dtype"] = payload.flux_weight_dtype
|
||||||
|
|
||||||
|
if payload.flux_fp8_clip:
|
||||||
|
comfyui_prompt["11"]["inputs"][
|
||||||
|
"clip_name2"
|
||||||
|
] = "t5xxl_fp8_e4m3fn.safetensors"
|
||||||
|
|
||||||
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
||||||
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
||||||
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
||||||
|
|
||||||
# set the text prompt for our positive CLIPTextEncode
|
# set the text prompt for our positive CLIPTextEncode
|
||||||
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
||||||
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
|
||||||
|
|
||||||
if payload.steps:
|
|
||||||
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
|
||||||
|
|
||||||
comfyui_prompt["3"]["inputs"]["seed"] = (
|
|
||||||
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ws = websocket.WebSocket()
|
ws = websocket.WebSocket()
|
||||||
@@ -240,7 +397,9 @@ def comfyui_generate_image(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
images = await asyncio.to_thread(
|
||||||
|
get_images, ws, comfyui_prompt, client_id, base_url
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error while receiving images: {e}")
|
log.exception(f"Error while receiving images: {e}")
|
||||||
images = None
|
images = None
|
||||||
|
|||||||
+117
-270
@@ -1,47 +1,36 @@
|
|||||||
from fastapi import (
|
from fastapi import (
|
||||||
FastAPI,
|
FastAPI,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
|
||||||
HTTPException,
|
HTTPException,
|
||||||
Depends,
|
Depends,
|
||||||
status,
|
|
||||||
UploadFile,
|
UploadFile,
|
||||||
File,
|
File,
|
||||||
BackgroundTasks,
|
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from fastapi.concurrency import run_in_threadpool
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import copy
|
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import uuid
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from typing import Optional, List, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from starlette.background import BackgroundTask
|
from starlette.background import BackgroundTask
|
||||||
|
|
||||||
from apps.webui.models.models import Models
|
from apps.webui.models.models import Models
|
||||||
from apps.webui.models.users import Users
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
decode_token,
|
|
||||||
get_current_user,
|
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
from utils.task import prompt_template
|
|
||||||
|
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
@@ -53,7 +42,12 @@ from config import (
|
|||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
)
|
)
|
||||||
from utils.misc import calculate_sha256, add_or_update_system_message
|
from utils.misc import (
|
||||||
|
calculate_sha256,
|
||||||
|
apply_model_params_to_body_ollama,
|
||||||
|
apply_model_params_to_body_openai,
|
||||||
|
apply_model_system_prompt_to_body,
|
||||||
|
)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
||||||
@@ -120,7 +114,7 @@ async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
|
|
||||||
class UrlUpdateForm(BaseModel):
|
class UrlUpdateForm(BaseModel):
|
||||||
urls: List[str]
|
urls: list[str]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/urls/update")
|
@app.post("/urls/update")
|
||||||
@@ -153,7 +147,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 +156,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:
|
||||||
@@ -175,7 +177,7 @@ async def post_streaming_url(url: str, payload: str):
|
|||||||
res = await r.json()
|
res = await r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -230,7 +232,7 @@ async def get_all_models():
|
|||||||
async def get_ollama_tags(
|
async def get_ollama_tags(
|
||||||
url_idx: Optional[int] = None, user=Depends(get_verified_user)
|
url_idx: Optional[int] = None, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
|
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
@@ -261,7 +263,7 @@ async def get_ollama_tags(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -274,8 +276,7 @@ async def get_ollama_tags(
|
|||||||
@app.get("/api/version/{url_idx}")
|
@app.get("/api/version/{url_idx}")
|
||||||
async def get_ollama_versions(url_idx: Optional[int] = None):
|
async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||||
if app.state.config.ENABLE_OLLAMA_API:
|
if app.state.config.ENABLE_OLLAMA_API:
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
|
|
||||||
# returns lowest version
|
# returns lowest version
|
||||||
tasks = [
|
tasks = [
|
||||||
fetch_url(f"{url}/api/version")
|
fetch_url(f"{url}/api/version")
|
||||||
@@ -315,7 +316,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -338,8 +339,6 @@ async def pull_model(
|
|||||||
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}")
|
||||||
|
|
||||||
r = None
|
|
||||||
|
|
||||||
# Admin should be able to pull models from any source
|
# Admin should be able to pull models from any source
|
||||||
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
|
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
|
||||||
|
|
||||||
@@ -359,7 +358,7 @@ async def push_model(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
if form_data.name in app.state.MODELS:
|
if form_data.name in app.state.MODELS:
|
||||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
||||||
else:
|
else:
|
||||||
@@ -409,7 +408,7 @@ async def copy_model(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
if form_data.source in app.state.MODELS:
|
if form_data.source in app.state.MODELS:
|
||||||
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
||||||
else:
|
else:
|
||||||
@@ -420,13 +419,13 @@ async def copy_model(
|
|||||||
|
|
||||||
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}")
|
||||||
|
r = requests.request(
|
||||||
|
method="POST",
|
||||||
|
url=f"{url}/api/copy",
|
||||||
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
|
||||||
method="POST",
|
|
||||||
url=f"{url}/api/copy",
|
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
log.debug(f"r.text: {r.text}")
|
log.debug(f"r.text: {r.text}")
|
||||||
@@ -440,7 +439,7 @@ async def copy_model(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -456,7 +455,7 @@ async def delete_model(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
if form_data.name in app.state.MODELS:
|
if form_data.name in app.state.MODELS:
|
||||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
||||||
else:
|
else:
|
||||||
@@ -468,12 +467,12 @@ async def delete_model(
|
|||||||
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}")
|
||||||
|
|
||||||
|
r = requests.request(
|
||||||
|
method="DELETE",
|
||||||
|
url=f"{url}/api/delete",
|
||||||
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
|
||||||
method="DELETE",
|
|
||||||
url=f"{url}/api/delete",
|
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
log.debug(f"r.text: {r.text}")
|
log.debug(f"r.text: {r.text}")
|
||||||
@@ -487,7 +486,7 @@ async def delete_model(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -508,12 +507,12 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
|
|||||||
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}")
|
||||||
|
|
||||||
|
r = requests.request(
|
||||||
|
method="POST",
|
||||||
|
url=f"{url}/api/show",
|
||||||
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
|
||||||
method="POST",
|
|
||||||
url=f"{url}/api/show",
|
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
@@ -525,7 +524,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -548,7 +547,7 @@ async def generate_embeddings(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
|
|
||||||
if ":" not in model:
|
if ":" not in model:
|
||||||
@@ -565,12 +564,12 @@ async def generate_embeddings(
|
|||||||
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}")
|
||||||
|
|
||||||
|
r = requests.request(
|
||||||
|
method="POST",
|
||||||
|
url=f"{url}/api/embeddings",
|
||||||
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
|
||||||
method="POST",
|
|
||||||
url=f"{url}/api/embeddings",
|
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
@@ -582,7 +581,7 @@ async def generate_embeddings(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -595,10 +594,9 @@ def generate_ollama_embeddings(
|
|||||||
form_data: GenerateEmbeddingsForm,
|
form_data: GenerateEmbeddingsForm,
|
||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
log.info(f"generate_ollama_embeddings {form_data}")
|
log.info(f"generate_ollama_embeddings {form_data}")
|
||||||
|
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
|
|
||||||
if ":" not in model:
|
if ":" not in model:
|
||||||
@@ -615,12 +613,12 @@ def generate_ollama_embeddings(
|
|||||||
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}")
|
||||||
|
|
||||||
|
r = requests.request(
|
||||||
|
method="POST",
|
||||||
|
url=f"{url}/api/embeddings",
|
||||||
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
|
||||||
method="POST",
|
|
||||||
url=f"{url}/api/embeddings",
|
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
@@ -630,7 +628,7 @@ def generate_ollama_embeddings(
|
|||||||
if "embedding" in data:
|
if "embedding" in data:
|
||||||
return data["embedding"]
|
return data["embedding"]
|
||||||
else:
|
else:
|
||||||
raise "Something went wrong :/"
|
raise Exception("Something went wrong :/")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
@@ -639,16 +637,16 @@ def generate_ollama_embeddings(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise error_detail
|
raise Exception(error_detail)
|
||||||
|
|
||||||
|
|
||||||
class GenerateCompletionForm(BaseModel):
|
class GenerateCompletionForm(BaseModel):
|
||||||
model: str
|
model: str
|
||||||
prompt: str
|
prompt: str
|
||||||
images: Optional[List[str]] = None
|
images: Optional[list[str]] = None
|
||||||
format: Optional[str] = None
|
format: Optional[str] = None
|
||||||
options: Optional[dict] = None
|
options: Optional[dict] = None
|
||||||
system: Optional[str] = None
|
system: Optional[str] = None
|
||||||
@@ -666,8 +664,7 @@ async def generate_completion(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
|
if url_idx is None:
|
||||||
if url_idx == None:
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
|
|
||||||
if ":" not in model:
|
if ":" not in model:
|
||||||
@@ -692,12 +689,12 @@ async def generate_completion(
|
|||||||
class ChatMessage(BaseModel):
|
class ChatMessage(BaseModel):
|
||||||
role: str
|
role: str
|
||||||
content: str
|
content: str
|
||||||
images: Optional[List[str]] = None
|
images: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
class GenerateChatCompletionForm(BaseModel):
|
class GenerateChatCompletionForm(BaseModel):
|
||||||
model: str
|
model: str
|
||||||
messages: List[ChatMessage]
|
messages: list[ChatMessage]
|
||||||
format: Optional[str] = None
|
format: Optional[str] = None
|
||||||
options: Optional[dict] = None
|
options: Optional[dict] = None
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
@@ -705,6 +702,18 @@ class GenerateChatCompletionForm(BaseModel):
|
|||||||
keep_alive: Optional[Union[int, str]] = None
|
keep_alive: Optional[Union[int, str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ollama_url(url_idx: Optional[int], model: str):
|
||||||
|
if url_idx is None:
|
||||||
|
if model not in app.state.MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
|
||||||
|
)
|
||||||
|
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||||
|
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/chat")
|
@app.post("/api/chat")
|
||||||
@app.post("/api/chat/{url_idx}")
|
@app.post("/api/chat/{url_idx}")
|
||||||
async def generate_chat_completion(
|
async def generate_chat_completion(
|
||||||
@@ -712,16 +721,13 @@ async def generate_chat_completion(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
|
log.debug(f"{form_data.model_dump_json(exclude_none=True).encode()}=")
|
||||||
log.debug(
|
|
||||||
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
|
||||||
form_data.model_dump_json(exclude_none=True).encode()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -730,127 +736,21 @@ async def generate_chat_completion(
|
|||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
||||||
model_info.params = model_info.params.model_dump()
|
params = model_info.params.model_dump()
|
||||||
|
|
||||||
if model_info.params:
|
if params:
|
||||||
payload["options"] = {}
|
if payload.get("options") is None:
|
||||||
|
payload["options"] = {}
|
||||||
|
|
||||||
if model_info.params.get("mirostat", None):
|
payload["options"] = apply_model_params_to_body_ollama(
|
||||||
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
params, payload["options"]
|
||||||
|
|
||||||
if model_info.params.get("mirostat_eta", None):
|
|
||||||
payload["options"]["mirostat_eta"] = model_info.params.get(
|
|
||||||
"mirostat_eta", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("mirostat_tau", None):
|
|
||||||
|
|
||||||
payload["options"]["mirostat_tau"] = model_info.params.get(
|
|
||||||
"mirostat_tau", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("num_ctx", None):
|
|
||||||
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
|
||||||
|
|
||||||
if model_info.params.get("num_batch", None):
|
|
||||||
payload["options"]["num_batch"] = model_info.params.get(
|
|
||||||
"num_batch", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if 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):
|
|
||||||
payload["options"]["repeat_last_n"] = model_info.params.get(
|
|
||||||
"repeat_last_n", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("frequency_penalty", None):
|
|
||||||
payload["options"]["repeat_penalty"] = model_info.params.get(
|
|
||||||
"frequency_penalty", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("temperature", None) is not None:
|
|
||||||
payload["options"]["temperature"] = model_info.params.get(
|
|
||||||
"temperature", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("seed", None):
|
|
||||||
payload["options"]["seed"] = model_info.params.get("seed", None)
|
|
||||||
|
|
||||||
if model_info.params.get("stop", None):
|
|
||||||
payload["options"]["stop"] = (
|
|
||||||
[
|
|
||||||
bytes(stop, "utf-8").decode("unicode_escape")
|
|
||||||
for stop in model_info.params["stop"]
|
|
||||||
]
|
|
||||||
if model_info.params.get("stop", None)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("tfs_z", None):
|
|
||||||
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
|
||||||
|
|
||||||
if model_info.params.get("max_tokens", None):
|
|
||||||
payload["options"]["num_predict"] = model_info.params.get(
|
|
||||||
"max_tokens", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("top_k", None):
|
|
||||||
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
|
||||||
|
|
||||||
if model_info.params.get("top_p", None):
|
|
||||||
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
|
||||||
|
|
||||||
if model_info.params.get("use_mmap", None):
|
|
||||||
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
|
||||||
|
|
||||||
if model_info.params.get("use_mlock", None):
|
|
||||||
payload["options"]["use_mlock"] = model_info.params.get(
|
|
||||||
"use_mlock", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("num_thread", None):
|
|
||||||
payload["options"]["num_thread"] = model_info.params.get(
|
|
||||||
"num_thread", None
|
|
||||||
)
|
|
||||||
|
|
||||||
system = model_info.params.get("system", None)
|
|
||||||
if system:
|
|
||||||
# Check if the payload already has a system message
|
|
||||||
# If not, add a system message to the payload
|
|
||||||
system = prompt_template(
|
|
||||||
system,
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
"user_name": user.name,
|
|
||||||
"user_location": (
|
|
||||||
user.info.get("location") if user.info else None
|
|
||||||
),
|
|
||||||
}
|
|
||||||
if user
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||||
|
|
||||||
if payload.get("messages"):
|
if ":" not in payload["model"]:
|
||||||
payload["messages"] = add_or_update_system_message(
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
system, payload["messages"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if url_idx == None:
|
url = get_ollama_url(url_idx, payload["model"])
|
||||||
if ":" not in payload["model"]:
|
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
|
||||||
|
|
||||||
if payload["model"] in app.state.MODELS:
|
|
||||||
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
|
||||||
)
|
|
||||||
|
|
||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
|
||||||
log.info(f"url: {url}")
|
log.info(f"url: {url}")
|
||||||
log.debug(payload)
|
log.debug(payload)
|
||||||
|
|
||||||
@@ -872,7 +772,7 @@ class OpenAIChatMessage(BaseModel):
|
|||||||
|
|
||||||
class OpenAIChatCompletionForm(BaseModel):
|
class OpenAIChatCompletionForm(BaseModel):
|
||||||
model: str
|
model: str
|
||||||
messages: List[OpenAIChatMessage]
|
messages: list[OpenAIChatMessage]
|
||||||
|
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
@@ -884,86 +784,35 @@ async def generate_openai_chat_completion(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
form_data = OpenAIChatCompletionForm(**form_data)
|
completion_form = OpenAIChatCompletionForm(**form_data)
|
||||||
|
payload = {**completion_form.model_dump(exclude_none=True, exclude=["metadata"])}
|
||||||
|
if "metadata" in payload:
|
||||||
|
del payload["metadata"]
|
||||||
|
|
||||||
payload = {
|
model_id = completion_form.model
|
||||||
**form_data.model_dump(exclude_none=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
model_id = form_data.model
|
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
if model_info:
|
if model_info:
|
||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
||||||
model_info.params = model_info.params.model_dump()
|
params = model_info.params.model_dump()
|
||||||
|
|
||||||
if model_info.params:
|
if params:
|
||||||
payload["temperature"] = model_info.params.get("temperature", None)
|
payload = apply_model_params_to_body_openai(params, payload)
|
||||||
payload["top_p"] = model_info.params.get("top_p", None)
|
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||||
payload["max_tokens"] = model_info.params.get("max_tokens", None)
|
|
||||||
payload["frequency_penalty"] = model_info.params.get(
|
|
||||||
"frequency_penalty", None
|
|
||||||
)
|
|
||||||
payload["seed"] = model_info.params.get("seed", None)
|
|
||||||
payload["stop"] = (
|
|
||||||
[
|
|
||||||
bytes(stop, "utf-8").decode("unicode_escape")
|
|
||||||
for stop in model_info.params["stop"]
|
|
||||||
]
|
|
||||||
if model_info.params.get("stop", None)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
system = model_info.params.get("system", None)
|
if ":" not in payload["model"]:
|
||||||
|
payload["model"] = f"{payload['model']}:latest"
|
||||||
|
|
||||||
if system:
|
url = get_ollama_url(url_idx, payload["model"])
|
||||||
system = prompt_template(
|
|
||||||
system,
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
"user_name": user.name,
|
|
||||||
"user_location": (
|
|
||||||
user.info.get("location") if user.info else None
|
|
||||||
),
|
|
||||||
}
|
|
||||||
if user
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# Check if the payload already has a system message
|
|
||||||
# If not, add a system message to the payload
|
|
||||||
if payload.get("messages"):
|
|
||||||
for message in payload["messages"]:
|
|
||||||
if message.get("role") == "system":
|
|
||||||
message["content"] = system + message["content"]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
payload["messages"].insert(
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": system,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if url_idx == None:
|
|
||||||
if ":" not in payload["model"]:
|
|
||||||
payload["model"] = f"{payload['model']}:latest"
|
|
||||||
|
|
||||||
if payload["model"] in app.state.MODELS:
|
|
||||||
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
|
||||||
)
|
|
||||||
|
|
||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
|
||||||
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")
|
||||||
@@ -972,7 +821,7 @@ async def get_openai_models(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
|
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
@@ -1027,7 +876,7 @@ async def get_openai_models(
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"Ollama: {res['error']}"
|
error_detail = f"Ollama: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"Ollama: {e}"
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -1053,7 +902,6 @@ def parse_huggingface_url(hf_url):
|
|||||||
path_components = parsed_url.path.split("/")
|
path_components = parsed_url.path.split("/")
|
||||||
|
|
||||||
# Extract the desired output
|
# Extract the desired output
|
||||||
user_repo = "/".join(path_components[1:3])
|
|
||||||
model_file = path_components[-1]
|
model_file = path_components[-1]
|
||||||
|
|
||||||
return model_file
|
return model_file
|
||||||
@@ -1118,7 +966,6 @@ async def download_model(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
|
|
||||||
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
||||||
|
|
||||||
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
||||||
@@ -1127,7 +974,7 @@ async def download_model(
|
|||||||
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
url_idx = 0
|
url_idx = 0
|
||||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
|
||||||
@@ -1150,7 +997,7 @@ def upload_model(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
url_idx = 0
|
url_idx = 0
|
||||||
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
|
||||||
|
|||||||
+84
-140
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
from fastapi import FastAPI, Request, HTTPException, Depends
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
from fastapi.responses import StreamingResponse, FileResponse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -12,19 +12,20 @@ from pydantic import BaseModel
|
|||||||
from starlette.background import BackgroundTask
|
from starlette.background import BackgroundTask
|
||||||
|
|
||||||
from apps.webui.models.models import Models
|
from apps.webui.models.models import Models
|
||||||
from apps.webui.models.users import Users
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
decode_token,
|
|
||||||
get_verified_user,
|
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
from utils.task import prompt_template
|
from utils.misc import (
|
||||||
|
apply_model_params_to_body_openai,
|
||||||
|
apply_model_system_prompt_to_body,
|
||||||
|
)
|
||||||
|
|
||||||
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,
|
||||||
@@ -32,7 +33,7 @@ from config import (
|
|||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
)
|
)
|
||||||
from typing import List, Optional
|
from typing import Optional, Literal, overload
|
||||||
|
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -67,8 +68,6 @@ app.state.MODELS = {}
|
|||||||
async def check_url(request: Request, call_next):
|
async def check_url(request: Request, call_next):
|
||||||
if len(app.state.MODELS) == 0:
|
if len(app.state.MODELS) == 0:
|
||||||
await get_all_models()
|
await get_all_models()
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
return response
|
return response
|
||||||
@@ -90,11 +89,11 @@ async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user
|
|||||||
|
|
||||||
|
|
||||||
class UrlsUpdateForm(BaseModel):
|
class UrlsUpdateForm(BaseModel):
|
||||||
urls: List[str]
|
urls: list[str]
|
||||||
|
|
||||||
|
|
||||||
class KeysUpdateForm(BaseModel):
|
class KeysUpdateForm(BaseModel):
|
||||||
keys: List[str]
|
keys: list[str]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/urls")
|
@app.get("/urls")
|
||||||
@@ -173,7 +172,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']}"
|
error_detail = f"External: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -232,64 +231,68 @@ def merge_models_lists(model_lists):
|
|||||||
return merged_list
|
return merged_list
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models(raw: bool = False):
|
def is_openai_api_disabled():
|
||||||
|
api_keys = app.state.config.OPENAI_API_KEYS
|
||||||
|
no_keys = len(api_keys) == 1 and api_keys[0] == ""
|
||||||
|
return no_keys or not app.state.config.ENABLE_OPENAI_API
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_models_raw() -> list:
|
||||||
|
if is_openai_api_disabled():
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Check if API KEYS length is same than API URLS length
|
||||||
|
num_urls = len(app.state.config.OPENAI_API_BASE_URLS)
|
||||||
|
num_keys = len(app.state.config.OPENAI_API_KEYS)
|
||||||
|
|
||||||
|
if num_keys != num_urls:
|
||||||
|
# if there are more keys than urls, remove the extra keys
|
||||||
|
if num_keys > num_urls:
|
||||||
|
new_keys = app.state.config.OPENAI_API_KEYS[:num_urls]
|
||||||
|
app.state.config.OPENAI_API_KEYS = new_keys
|
||||||
|
# if there are more urls than keys, add empty keys
|
||||||
|
else:
|
||||||
|
app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys)
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||||
|
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||||
|
]
|
||||||
|
|
||||||
|
responses = await asyncio.gather(*tasks)
|
||||||
|
log.debug(f"get_all_models:responses() {responses}")
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def get_all_models(raw: Literal[True]) -> list: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_models(raw=False) -> dict[str, list] | list:
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
|
if is_openai_api_disabled():
|
||||||
|
return [] if raw else {"data": []}
|
||||||
|
|
||||||
if (
|
responses = await get_all_models_raw()
|
||||||
len(app.state.config.OPENAI_API_KEYS) == 1
|
if raw:
|
||||||
and app.state.config.OPENAI_API_KEYS[0] == ""
|
return responses
|
||||||
) or not app.state.config.ENABLE_OPENAI_API:
|
|
||||||
models = {"data": []}
|
|
||||||
else:
|
|
||||||
# Check if API KEYS length is same than API URLS length
|
|
||||||
if len(app.state.config.OPENAI_API_KEYS) != len(
|
|
||||||
app.state.config.OPENAI_API_BASE_URLS
|
|
||||||
):
|
|
||||||
# if there are more keys than urls, remove the extra keys
|
|
||||||
if len(app.state.config.OPENAI_API_KEYS) > len(
|
|
||||||
app.state.config.OPENAI_API_BASE_URLS
|
|
||||||
):
|
|
||||||
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
|
|
||||||
: len(app.state.config.OPENAI_API_BASE_URLS)
|
|
||||||
]
|
|
||||||
# if there are more urls than keys, add empty keys
|
|
||||||
else:
|
|
||||||
app.state.config.OPENAI_API_KEYS += [
|
|
||||||
""
|
|
||||||
for _ in range(
|
|
||||||
len(app.state.config.OPENAI_API_BASE_URLS)
|
|
||||||
- len(app.state.config.OPENAI_API_KEYS)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks = [
|
def extract_data(response):
|
||||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
if response and "data" in response:
|
||||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
return response["data"]
|
||||||
]
|
if isinstance(response, list):
|
||||||
|
return response
|
||||||
|
return None
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
models = {"data": merge_models_lists(map(extract_data, responses))}
|
||||||
log.debug(f"get_all_models:responses() {responses}")
|
|
||||||
|
|
||||||
if raw:
|
log.debug(f"models: {models}")
|
||||||
return responses
|
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||||
|
|
||||||
models = {
|
|
||||||
"data": merge_models_lists(
|
|
||||||
list(
|
|
||||||
map(
|
|
||||||
lambda response: (
|
|
||||||
response["data"]
|
|
||||||
if (response and "data" in response)
|
|
||||||
else (response if isinstance(response, list) else None)
|
|
||||||
),
|
|
||||||
responses,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(f"models: {models}")
|
|
||||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
|
||||||
|
|
||||||
return models
|
return models
|
||||||
|
|
||||||
@@ -297,7 +300,7 @@ async def get_all_models(raw: bool = False):
|
|||||||
@app.get("/models")
|
@app.get("/models")
|
||||||
@app.get("/models/{url_idx}")
|
@app.get("/models/{url_idx}")
|
||||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
||||||
if url_idx == None:
|
if url_idx is None:
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
if app.state.config.ENABLE_MODEL_FILTER:
|
if app.state.config.ENABLE_MODEL_FILTER:
|
||||||
if user.role == "user":
|
if user.role == "user":
|
||||||
@@ -338,7 +341,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
|||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']}"
|
error_detail = f"External: {res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -357,6 +360,9 @@ 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)
|
||||||
|
|
||||||
@@ -364,69 +370,9 @@ async def generate_chat_completion(
|
|||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
||||||
model_info.params = model_info.params.model_dump()
|
params = model_info.params.model_dump()
|
||||||
|
payload = apply_model_params_to_body_openai(params, payload)
|
||||||
if model_info.params:
|
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||||
if model_info.params.get("temperature", None) is not None:
|
|
||||||
payload["temperature"] = float(model_info.params.get("temperature"))
|
|
||||||
|
|
||||||
if model_info.params.get("top_p", None):
|
|
||||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
|
||||||
|
|
||||||
if model_info.params.get("max_tokens", None):
|
|
||||||
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
|
||||||
|
|
||||||
if model_info.params.get("frequency_penalty", None):
|
|
||||||
payload["frequency_penalty"] = int(
|
|
||||||
model_info.params.get("frequency_penalty", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("seed", None):
|
|
||||||
payload["seed"] = model_info.params.get("seed", None)
|
|
||||||
|
|
||||||
if model_info.params.get("stop", None):
|
|
||||||
payload["stop"] = (
|
|
||||||
[
|
|
||||||
bytes(stop, "utf-8").decode("unicode_escape")
|
|
||||||
for stop in model_info.params["stop"]
|
|
||||||
]
|
|
||||||
if model_info.params.get("stop", None)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
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 payload.get("messages"):
|
|
||||||
for message in payload["messages"]:
|
|
||||||
if message.get("role") == "system":
|
|
||||||
message["content"] = system + message["content"]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
payload["messages"].insert(
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": system,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
model = app.state.MODELS[payload.get("model")]
|
model = app.state.MODELS[payload.get("model")]
|
||||||
idx = model["urlIdx"]
|
idx = model["urlIdx"]
|
||||||
@@ -439,13 +385,6 @@ async def generate_chat_completion(
|
|||||||
"role": user.role,
|
"role": user.role,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
|
||||||
# This is a workaround until OpenAI fixes the issue with this model
|
|
||||||
if payload.get("model") == "gpt-4-vision-preview":
|
|
||||||
if "max_tokens" not in payload:
|
|
||||||
payload["max_tokens"] = 4000
|
|
||||||
log.debug("Modified payload:", payload)
|
|
||||||
|
|
||||||
# Convert the modified body back to JSON
|
# Convert the modified body back to JSON
|
||||||
payload = json.dumps(payload)
|
payload = json.dumps(payload)
|
||||||
|
|
||||||
@@ -457,13 +396,18 @@ async def generate_chat_completion(
|
|||||||
headers = {}
|
headers = {}
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
|
||||||
|
headers["HTTP-Referer"] = "https://openwebui.com/"
|
||||||
|
headers["X-Title"] = "Open WebUI"
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
session = None
|
session = None
|
||||||
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",
|
||||||
@@ -496,7 +440,7 @@ async def generate_chat_completion(
|
|||||||
print(res)
|
print(res)
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||||
finally:
|
finally:
|
||||||
@@ -559,7 +503,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
print(res)
|
print(res)
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
except:
|
except Exception:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+148
-56
@@ -13,7 +13,7 @@ import os, shutil, logging, re
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Union, Sequence, Iterator, Any
|
from typing import Union, Sequence, Iterator, Any
|
||||||
|
|
||||||
from chromadb.utils.batch_utils import create_batches
|
from chromadb.utils.batch_utils import create_batches
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
@@ -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,
|
||||||
@@ -369,7 +376,7 @@ async def update_reranking_config(
|
|||||||
try:
|
try:
|
||||||
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
|
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
|
||||||
|
|
||||||
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
|
update_reranking_model(app.state.config.RAG_RERANKING_MODEL, True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
@@ -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,13 +428,18 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ContentExtractionConfig(BaseModel):
|
||||||
|
engine: str = ""
|
||||||
|
tika_server_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ChunkParamUpdateForm(BaseModel):
|
class ChunkParamUpdateForm(BaseModel):
|
||||||
chunk_size: int
|
chunk_size: int
|
||||||
chunk_overlap: int
|
chunk_overlap: int
|
||||||
|
|
||||||
|
|
||||||
class YoutubeLoaderConfig(BaseModel):
|
class YoutubeLoaderConfig(BaseModel):
|
||||||
language: List[str]
|
language: list[str]
|
||||||
translation: Optional[str] = None
|
translation: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -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,
|
||||||
@@ -616,7 +642,7 @@ def query_doc_handler(
|
|||||||
|
|
||||||
|
|
||||||
class QueryCollectionsForm(BaseModel):
|
class QueryCollectionsForm(BaseModel):
|
||||||
collection_names: List[str]
|
collection_names: list[str]
|
||||||
query: str
|
query: str
|
||||||
k: Optional[int] = None
|
k: Optional[int] = None
|
||||||
r: Optional[float] = None
|
r: Optional[float] = None
|
||||||
@@ -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
|
||||||
@@ -1033,49 +1099,67 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
"vue",
|
"vue",
|
||||||
"svelte",
|
"svelte",
|
||||||
"msg",
|
"msg",
|
||||||
|
"ex",
|
||||||
|
"exs",
|
||||||
|
"erl",
|
||||||
|
"tsx",
|
||||||
|
"jsx",
|
||||||
|
"hs",
|
||||||
|
"lhs",
|
||||||
]
|
]
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -1101,7 +1185,7 @@ def store_doc(
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
f = open(file_path, "rb")
|
f = open(file_path, "rb")
|
||||||
if collection_name == None:
|
if collection_name is None:
|
||||||
collection_name = calculate_sha256(f)[:63]
|
collection_name = calculate_sha256(f)[:63]
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
@@ -1154,7 +1238,7 @@ def process_doc(
|
|||||||
f = open(file_path, "rb")
|
f = open(file_path, "rb")
|
||||||
|
|
||||||
collection_name = form_data.collection_name
|
collection_name = form_data.collection_name
|
||||||
if collection_name == None:
|
if collection_name is None:
|
||||||
collection_name = calculate_sha256(f)[:63]
|
collection_name = calculate_sha256(f)[:63]
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
@@ -1164,13 +1248,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(
|
||||||
@@ -1204,7 +1296,7 @@ def store_text(
|
|||||||
):
|
):
|
||||||
|
|
||||||
collection_name = form_data.collection_name
|
collection_name = form_data.collection_name
|
||||||
if collection_name == None:
|
if collection_name is None:
|
||||||
collection_name = calculate_sha256_string(form_data.content)
|
collection_name = calculate_sha256_string(form_data.content)
|
||||||
|
|
||||||
result = store_text_in_vector_db(
|
result = store_text_in_vector_db(
|
||||||
@@ -1247,7 +1339,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
|||||||
sanitized_filename = sanitize_filename(filename)
|
sanitized_filename = sanitize_filename(filename)
|
||||||
doc = Documents.get_doc_by_name(sanitized_filename)
|
doc = Documents.get_doc_by_name(sanitized_filename)
|
||||||
|
|
||||||
if doc == None:
|
if doc is None:
|
||||||
doc = Documents.insert_new_doc(
|
doc = Documents.insert_new_doc(
|
||||||
user.id,
|
user.id,
|
||||||
DocumentForm(
|
DocumentForm(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||||
@@ -10,7 +10,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
|||||||
|
|
||||||
|
|
||||||
def search_brave(
|
def search_brave(
|
||||||
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
|
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
|
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||||
from duckduckgo_search import DDGS
|
from duckduckgo_search import DDGS
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
@@ -9,7 +9,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
|||||||
|
|
||||||
|
|
||||||
def search_duckduckgo(
|
def search_duckduckgo(
|
||||||
query: str, count: int, filter_list: Optional[List[str]] = None
|
query: str, count: int, filter_list: Optional[list[str]] = None
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""
|
"""
|
||||||
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
||||||
@@ -18,7 +18,7 @@ def search_duckduckgo(
|
|||||||
count (int): The number of results to return
|
count (int): The number of results to return
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[SearchResult]: A list of search results
|
list[SearchResult]: A list of search results
|
||||||
"""
|
"""
|
||||||
# Use the DDGS context manager to create a DDGS object
|
# Use the DDGS context manager to create a DDGS object
|
||||||
with DDGS() as ddgs:
|
with DDGS() as ddgs:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||||
@@ -15,7 +15,7 @@ def search_google_pse(
|
|||||||
search_engine_id: str,
|
search_engine_id: str,
|
||||||
query: str,
|
query: str,
|
||||||
count: int,
|
count: int,
|
||||||
filter_list: Optional[List[str]] = None,
|
filter_list: Optional[list[str]] = None,
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def search_jina(query: str, count: int) -> list[SearchResult]:
|
|||||||
count (int): The number of results to return
|
count (int): The number of results to return
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[SearchResult]: A list of search results
|
list[SearchResult]: A list of search results
|
||||||
"""
|
"""
|
||||||
jina_search_endpoint = "https://s.jina.ai/"
|
jina_search_endpoint = "https://s.jina.ai/"
|
||||||
headers = {
|
headers = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
@@ -14,9 +14,9 @@ def search_searxng(
|
|||||||
query_url: str,
|
query_url: str,
|
||||||
query: str,
|
query: str,
|
||||||
count: int,
|
count: int,
|
||||||
filter_list: Optional[List[str]] = None,
|
filter_list: Optional[list[str]] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> List[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""
|
"""
|
||||||
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ def search_searxng(
|
|||||||
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
||||||
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
|
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
|
||||||
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
||||||
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
categories: (Optional[list[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
||||||
|
|
||||||
Raise:
|
Raise:
|
||||||
requests.exceptions.RequestException: If a request error occurs during the search process.
|
requests.exceptions.RequestException: If a request error occurs during the search process.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||||
@@ -11,7 +11,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
|||||||
|
|
||||||
|
|
||||||
def search_serper(
|
def search_serper(
|
||||||
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
|
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ def search_serply(
|
|||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
device_type: str = "desktop",
|
device_type: str = "desktop",
|
||||||
proxy_location: str = "US",
|
proxy_location: str = "US",
|
||||||
filter_list: Optional[List[str]] = None,
|
filter_list: Optional[list[str]] = None,
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||||
@@ -14,7 +14,7 @@ def search_serpstack(
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
query: str,
|
query: str,
|
||||||
count: int,
|
count: int,
|
||||||
filter_list: Optional[List[str]] = None,
|
filter_list: Optional[list[str]] = None,
|
||||||
https_enabled: bool = True,
|
https_enabled: bool = True,
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
|
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
|
|||||||
query (str): The query to search for
|
query (str): The query to search for
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[SearchResult]: A list of search results
|
list[SearchResult]: A list of search results
|
||||||
"""
|
"""
|
||||||
url = "https://api.tavily.com/search"
|
url = "https://api.tavily.com/search"
|
||||||
data = {"query": query, "api_key": api_key}
|
data = {"query": query, "api_key": api_key}
|
||||||
|
|||||||
+15
-15
@@ -2,7 +2,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from typing import List, Union
|
from typing import Union
|
||||||
|
|
||||||
from apps.ollama.main import (
|
from apps.ollama.main import (
|
||||||
generate_ollama_embeddings,
|
generate_ollama_embeddings,
|
||||||
@@ -142,7 +142,7 @@ def merge_and_sort_query_results(query_results, k, reverse=False):
|
|||||||
|
|
||||||
|
|
||||||
def query_collection(
|
def query_collection(
|
||||||
collection_names: List[str],
|
collection_names: list[str],
|
||||||
query: str,
|
query: str,
|
||||||
embedding_function,
|
embedding_function,
|
||||||
k: int,
|
k: int,
|
||||||
@@ -157,13 +157,13 @@ def query_collection(
|
|||||||
embedding_function=embedding_function,
|
embedding_function=embedding_function,
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return merge_and_sort_query_results(results, k=k)
|
return merge_and_sort_query_results(results, k=k)
|
||||||
|
|
||||||
|
|
||||||
def query_collection_with_hybrid_search(
|
def query_collection_with_hybrid_search(
|
||||||
collection_names: List[str],
|
collection_names: list[str],
|
||||||
query: str,
|
query: str,
|
||||||
embedding_function,
|
embedding_function,
|
||||||
k: int,
|
k: int,
|
||||||
@@ -182,7 +182,7 @@ def query_collection_with_hybrid_search(
|
|||||||
r=r,
|
r=r,
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return merge_and_sort_query_results(results, k=k, reverse=True)
|
return merge_and_sort_query_results(results, k=k, reverse=True)
|
||||||
|
|
||||||
@@ -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):
|
||||||
@@ -411,7 +411,7 @@ class ChromaRetriever(BaseRetriever):
|
|||||||
query: str,
|
query: str,
|
||||||
*,
|
*,
|
||||||
run_manager: CallbackManagerForRetrieverRun,
|
run_manager: CallbackManagerForRetrieverRun,
|
||||||
) -> List[Document]:
|
) -> list[Document]:
|
||||||
query_embeddings = self.embedding_function(query)
|
query_embeddings = self.embedding_function(query)
|
||||||
|
|
||||||
results = self.collection.query(
|
results = self.collection.query(
|
||||||
@@ -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]
|
||||||
|
|||||||
+46
-14
@@ -44,24 +44,26 @@ async def user_join(sid, data):
|
|||||||
print("user-join", sid, data)
|
print("user-join", sid, data)
|
||||||
|
|
||||||
auth = data["auth"] if "auth" in data else None
|
auth = data["auth"] if "auth" in data else None
|
||||||
|
if not auth or "token" not in auth:
|
||||||
|
return
|
||||||
|
|
||||||
if auth and "token" in auth:
|
data = decode_token(auth["token"])
|
||||||
data = decode_token(auth["token"])
|
if data is None or "id" not in data:
|
||||||
|
return
|
||||||
|
|
||||||
if data is not None and "id" in data:
|
user = Users.get_user_by_id(data["id"])
|
||||||
user = Users.get_user_by_id(data["id"])
|
if not user:
|
||||||
|
return
|
||||||
|
|
||||||
if user:
|
SESSION_POOL[sid] = user.id
|
||||||
|
if user.id in USER_POOL:
|
||||||
|
USER_POOL[user.id].append(sid)
|
||||||
|
else:
|
||||||
|
USER_POOL[user.id] = [sid]
|
||||||
|
|
||||||
SESSION_POOL[sid] = user.id
|
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||||
if user.id in USER_POOL:
|
|
||||||
USER_POOL[user.id].append(sid)
|
|
||||||
else:
|
|
||||||
USER_POOL[user.id] = [sid]
|
|
||||||
|
|
||||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||||
|
|
||||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
|
||||||
|
|
||||||
|
|
||||||
@sio.on("user-count")
|
@sio.on("user-count")
|
||||||
@@ -80,7 +82,6 @@ def get_models_in_use():
|
|||||||
|
|
||||||
@sio.on("usage")
|
@sio.on("usage")
|
||||||
async def usage(sid, data):
|
async def usage(sid, data):
|
||||||
|
|
||||||
model_id = data["model"]
|
model_id = data["model"]
|
||||||
|
|
||||||
# Cancel previous callback if there is one
|
# Cancel previous callback if there is one
|
||||||
@@ -137,3 +138,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")
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_emitter(request_info):
|
||||||
|
async def __event_emitter__(event_data):
|
||||||
|
await sio.emit(
|
||||||
|
"chat-events",
|
||||||
|
{
|
||||||
|
"chat_id": request_info["chat_id"],
|
||||||
|
"message_id": request_info["message_id"],
|
||||||
|
"data": event_data,
|
||||||
|
},
|
||||||
|
to=request_info["session_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return __event_emitter__
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_call(request_info):
|
||||||
|
async def __event_call__(event_data):
|
||||||
|
response = await sio.call(
|
||||||
|
"chat-events",
|
||||||
|
{
|
||||||
|
"chat_id": request_info["chat_id"],
|
||||||
|
"message_id": request_info["message_id"],
|
||||||
|
"data": event_data,
|
||||||
|
},
|
||||||
|
to=request_info["session_id"],
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return __event_call__
|
||||||
|
|||||||
@@ -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.
|
|
||||||
+218
-188
@@ -1,9 +1,6 @@
|
|||||||
from fastapi import FastAPI, Depends
|
from fastapi import FastAPI
|
||||||
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 apps.webui.routers import (
|
from apps.webui.routers import (
|
||||||
auths,
|
auths,
|
||||||
users,
|
users,
|
||||||
@@ -19,11 +16,18 @@ 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 (
|
||||||
|
openai_chat_chunk_message_template,
|
||||||
|
openai_chat_completion_message_template,
|
||||||
|
apply_model_params_to_body_openai,
|
||||||
|
apply_model_system_prompt_to_body,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
WEBUI_BUILD_HASH,
|
|
||||||
SHOW_ADMIN_DETAILS,
|
SHOW_ADMIN_DETAILS,
|
||||||
ADMIN_EMAIL,
|
ADMIN_EMAIL,
|
||||||
WEBUI_AUTH,
|
WEBUI_AUTH,
|
||||||
@@ -31,6 +35,7 @@ from config import (
|
|||||||
DEFAULT_PROMPT_SUGGESTIONS,
|
DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
DEFAULT_USER_ROLE,
|
DEFAULT_USER_ROLE,
|
||||||
ENABLE_SIGNUP,
|
ENABLE_SIGNUP,
|
||||||
|
ENABLE_LOGIN_FORM,
|
||||||
USER_PERMISSIONS,
|
USER_PERMISSIONS,
|
||||||
WEBHOOK_URL,
|
WEBHOOK_URL,
|
||||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||||
@@ -39,14 +44,17 @@ from config import (
|
|||||||
WEBUI_BANNERS,
|
WEBUI_BANNERS,
|
||||||
ENABLE_COMMUNITY_SHARING,
|
ENABLE_COMMUNITY_SHARING,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
|
OAUTH_USERNAME_CLAIM,
|
||||||
|
OAUTH_PICTURE_CLAIM,
|
||||||
|
OAUTH_EMAIL_CLAIM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from apps.socket.main import get_event_call, get_event_emitter
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Iterator, Generator
|
from typing import Iterator, Generator, AsyncGenerator
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -56,6 +64,7 @@ origins = ["*"]
|
|||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
|
|
||||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||||
|
app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
|
||||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
||||||
@@ -74,6 +83,10 @@ 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.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
|
||||||
|
|
||||||
app.state.MODELS = {}
|
app.state.MODELS = {}
|
||||||
app.state.TOOLS = {}
|
app.state.TOOLS = {}
|
||||||
app.state.FUNCTIONS = {}
|
app.state.FUNCTIONS = {}
|
||||||
@@ -114,58 +127,63 @@ async def get_status():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_function_module(pipe_id: str):
|
||||||
|
# Check if function is already loaded
|
||||||
|
if pipe_id not in app.state.FUNCTIONS:
|
||||||
|
function_module, _, _ = load_function_module_by_id(pipe_id)
|
||||||
|
app.state.FUNCTIONS[pipe_id] = function_module
|
||||||
|
else:
|
||||||
|
function_module = app.state.FUNCTIONS[pipe_id]
|
||||||
|
|
||||||
|
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||||
|
valves = Functions.get_function_valves_by_id(pipe_id)
|
||||||
|
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
||||||
|
return function_module
|
||||||
|
|
||||||
|
|
||||||
async def get_pipe_models():
|
async def get_pipe_models():
|
||||||
pipes = Functions.get_functions_by_type("pipe", active_only=True)
|
pipes = Functions.get_functions_by_type("pipe", active_only=True)
|
||||||
pipe_models = []
|
pipe_models = []
|
||||||
|
|
||||||
for pipe in pipes:
|
for pipe in pipes:
|
||||||
# Check if function is already loaded
|
function_module = get_function_module(pipe.id)
|
||||||
if pipe.id not in app.state.FUNCTIONS:
|
|
||||||
function_module, function_type, frontmatter = load_function_module_by_id(
|
|
||||||
pipe.id
|
|
||||||
)
|
|
||||||
app.state.FUNCTIONS[pipe.id] = function_module
|
|
||||||
else:
|
|
||||||
function_module = app.state.FUNCTIONS[pipe.id]
|
|
||||||
|
|
||||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
|
||||||
print(f"Getting valves for {pipe.id}")
|
|
||||||
valves = Functions.get_function_valves_by_id(pipe.id)
|
|
||||||
function_module.valves = function_module.Valves(
|
|
||||||
**(valves if valves else {})
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if function is a manifold
|
# Check if function is a manifold
|
||||||
if hasattr(function_module, "type"):
|
if hasattr(function_module, "pipes"):
|
||||||
if function_module.type == "manifold":
|
manifold_pipes = []
|
||||||
manifold_pipes = []
|
|
||||||
|
|
||||||
# Check if pipes is a function or a list
|
# Check if pipes is a function or a list
|
||||||
if callable(function_module.pipes):
|
if callable(function_module.pipes):
|
||||||
manifold_pipes = function_module.pipes()
|
manifold_pipes = function_module.pipes()
|
||||||
else:
|
else:
|
||||||
manifold_pipes = function_module.pipes
|
manifold_pipes = function_module.pipes
|
||||||
|
|
||||||
for p in manifold_pipes:
|
for p in manifold_pipes:
|
||||||
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
|
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
|
||||||
manifold_pipe_name = p["name"]
|
manifold_pipe_name = p["name"]
|
||||||
|
|
||||||
if hasattr(function_module, "name"):
|
if hasattr(function_module, "name"):
|
||||||
manifold_pipe_name = (
|
manifold_pipe_name = f"{function_module.name}{manifold_pipe_name}"
|
||||||
f"{function_module.name}{manifold_pipe_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
pipe_models.append(
|
pipe_flag = {"type": pipe.type}
|
||||||
{
|
if hasattr(function_module, "ChatValves"):
|
||||||
"id": manifold_pipe_id,
|
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||||
"name": manifold_pipe_name,
|
|
||||||
"object": "model",
|
pipe_models.append(
|
||||||
"created": pipe.created_at,
|
{
|
||||||
"owned_by": "openai",
|
"id": manifold_pipe_id,
|
||||||
"pipe": {"type": pipe.type},
|
"name": manifold_pipe_name,
|
||||||
}
|
"object": "model",
|
||||||
)
|
"created": pipe.created_at,
|
||||||
|
"owned_by": "openai",
|
||||||
|
"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,165 +191,177 @@ 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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return pipe_models
|
return pipe_models
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_pipe(pipe, params):
|
||||||
|
if inspect.iscoroutinefunction(pipe):
|
||||||
|
return await pipe(**params)
|
||||||
|
else:
|
||||||
|
return pipe(**params)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
|
||||||
|
if isinstance(res, str):
|
||||||
|
return res
|
||||||
|
if isinstance(res, Generator):
|
||||||
|
return "".join(map(str, res))
|
||||||
|
if isinstance(res, AsyncGenerator):
|
||||||
|
return "".join([str(stream) async for stream in res])
|
||||||
|
|
||||||
|
|
||||||
|
def process_line(form_data: dict, line):
|
||||||
|
if isinstance(line, BaseModel):
|
||||||
|
line = line.model_dump_json()
|
||||||
|
line = f"data: {line}"
|
||||||
|
if isinstance(line, dict):
|
||||||
|
line = f"data: {json.dumps(line)}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
line = line.decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if line.startswith("data:"):
|
||||||
|
return f"{line}\n\n"
|
||||||
|
else:
|
||||||
|
line = openai_chat_chunk_message_template(form_data["model"], line)
|
||||||
|
return f"data: {json.dumps(line)}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def get_pipe_id(form_data: dict) -> str:
|
||||||
|
pipe_id = form_data["model"]
|
||||||
|
if "." in pipe_id:
|
||||||
|
pipe_id, _ = pipe_id.split(".", 1)
|
||||||
|
print(pipe_id)
|
||||||
|
return pipe_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_function_params(function_module, form_data, user, extra_params={}):
|
||||||
|
pipe_id = get_pipe_id(form_data)
|
||||||
|
# Get the signature of the function
|
||||||
|
sig = inspect.signature(function_module.pipe)
|
||||||
|
params = {"body": form_data}
|
||||||
|
|
||||||
|
for key, value in extra_params.items():
|
||||||
|
if key in sig.parameters:
|
||||||
|
params[key] = value
|
||||||
|
|
||||||
|
if "__user__" in sig.parameters:
|
||||||
|
__user__ = {
|
||||||
|
"id": user.id,
|
||||||
|
"email": user.email,
|
||||||
|
"name": user.name,
|
||||||
|
"role": user.role,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(function_module, "UserValves"):
|
||||||
|
__user__["valves"] = function_module.UserValves(
|
||||||
|
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
params["__user__"] = __user__
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
async def generate_function_chat_completion(form_data, user):
|
async def generate_function_chat_completion(form_data, user):
|
||||||
async def job():
|
model_id = form_data.get("model")
|
||||||
pipe_id = form_data["model"]
|
model_info = Models.get_model_by_id(model_id)
|
||||||
if "." in pipe_id:
|
metadata = form_data.pop("metadata", None)
|
||||||
pipe_id, sub_pipe_id = pipe_id.split(".", 1)
|
|
||||||
print(pipe_id)
|
|
||||||
|
|
||||||
# Check if function is already loaded
|
__event_emitter__ = None
|
||||||
if pipe_id not in app.state.FUNCTIONS:
|
__event_call__ = None
|
||||||
function_module, function_type, frontmatter = load_function_module_by_id(
|
__task__ = None
|
||||||
pipe_id
|
|
||||||
)
|
|
||||||
app.state.FUNCTIONS[pipe_id] = function_module
|
|
||||||
else:
|
|
||||||
function_module = app.state.FUNCTIONS[pipe_id]
|
|
||||||
|
|
||||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
if metadata:
|
||||||
|
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
|
||||||
|
__event_emitter__ = get_event_emitter(metadata)
|
||||||
|
__event_call__ = get_event_call(metadata)
|
||||||
|
__task__ = metadata.get("task", None)
|
||||||
|
|
||||||
valves = Functions.get_function_valves_by_id(pipe_id)
|
if model_info:
|
||||||
function_module.valves = function_module.Valves(
|
if model_info.base_model_id:
|
||||||
**(valves if valves else {})
|
form_data["model"] = model_info.base_model_id
|
||||||
)
|
|
||||||
|
|
||||||
pipe = function_module.pipe
|
params = model_info.params.model_dump()
|
||||||
|
form_data = apply_model_params_to_body_openai(params, form_data)
|
||||||
|
form_data = apply_model_system_prompt_to_body(params, form_data, user)
|
||||||
|
|
||||||
# Get the signature of the function
|
pipe_id = get_pipe_id(form_data)
|
||||||
sig = inspect.signature(pipe)
|
function_module = get_function_module(pipe_id)
|
||||||
params = {"body": form_data}
|
|
||||||
|
|
||||||
if "__user__" in sig.parameters:
|
pipe = function_module.pipe
|
||||||
__user__ = {
|
params = get_function_params(
|
||||||
"id": user.id,
|
function_module,
|
||||||
"email": user.email,
|
form_data,
|
||||||
"name": user.name,
|
user,
|
||||||
"role": user.role,
|
{
|
||||||
}
|
"__event_emitter__": __event_emitter__,
|
||||||
|
"__event_call__": __event_call__,
|
||||||
|
"__task__": __task__,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if form_data["stream"]:
|
||||||
|
|
||||||
|
async def stream_content():
|
||||||
try:
|
try:
|
||||||
if hasattr(function_module, "UserValves"):
|
res = await execute_pipe(pipe, params)
|
||||||
__user__["valves"] = function_module.UserValves(
|
|
||||||
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
params = {**params, "__user__": __user__}
|
# Directly return if the response is a StreamingResponse
|
||||||
|
if isinstance(res, StreamingResponse):
|
||||||
if form_data["stream"]:
|
async for data in res.body_iterator:
|
||||||
|
yield data
|
||||||
async def stream_content():
|
return
|
||||||
try:
|
if isinstance(res, dict):
|
||||||
if inspect.iscoroutinefunction(pipe):
|
yield f"data: {json.dumps(res)}\n\n"
|
||||||
res = await pipe(**params)
|
|
||||||
else:
|
|
||||||
res = pipe(**params)
|
|
||||||
|
|
||||||
# Directly return if the response is a StreamingResponse
|
|
||||||
if isinstance(res, StreamingResponse):
|
|
||||||
async for data in res.body_iterator:
|
|
||||||
yield data
|
|
||||||
return
|
|
||||||
if isinstance(res, dict):
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
return
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(res, str):
|
|
||||||
message = stream_message_template(form_data["model"], res)
|
|
||||||
yield f"data: {json.dumps(message)}\n\n"
|
|
||||||
|
|
||||||
if isinstance(res, Iterator):
|
|
||||||
for line in res:
|
|
||||||
if isinstance(line, BaseModel):
|
|
||||||
line = line.model_dump_json()
|
|
||||||
line = f"data: {line}"
|
|
||||||
try:
|
|
||||||
line = line.decode("utf-8")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if line.startswith("data:"):
|
|
||||||
yield f"{line}\n\n"
|
|
||||||
else:
|
|
||||||
line = stream_message_template(form_data["model"], line)
|
|
||||||
yield f"data: {json.dumps(line)}\n\n"
|
|
||||||
|
|
||||||
if isinstance(res, str) or isinstance(res, Generator):
|
|
||||||
finish_message = {
|
|
||||||
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
|
|
||||||
"object": "chat.completion.chunk",
|
|
||||||
"created": int(time.time()),
|
|
||||||
"model": form_data["model"],
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"delta": {},
|
|
||||||
"logprobs": None,
|
|
||||||
"finish_reason": "stop",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
yield f"data: {json.dumps(finish_message)}\n\n"
|
|
||||||
yield f"data: [DONE]"
|
|
||||||
|
|
||||||
return StreamingResponse(stream_content(), media_type="text/event-stream")
|
|
||||||
else:
|
|
||||||
|
|
||||||
try:
|
|
||||||
if inspect.iscoroutinefunction(pipe):
|
|
||||||
res = await pipe(**params)
|
|
||||||
else:
|
|
||||||
res = pipe(**params)
|
|
||||||
|
|
||||||
if isinstance(res, StreamingResponse):
|
|
||||||
return res
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return {"error": {"detail": str(e)}}
|
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(res, dict):
|
if isinstance(res, str):
|
||||||
return res
|
message = openai_chat_chunk_message_template(form_data["model"], res)
|
||||||
elif isinstance(res, BaseModel):
|
yield f"data: {json.dumps(message)}\n\n"
|
||||||
return res.model_dump()
|
|
||||||
else:
|
|
||||||
message = ""
|
|
||||||
if isinstance(res, str):
|
|
||||||
message = res
|
|
||||||
if isinstance(res, Generator):
|
|
||||||
for stream in res:
|
|
||||||
message = f"{message}{stream}"
|
|
||||||
|
|
||||||
return {
|
if isinstance(res, Iterator):
|
||||||
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
|
for line in res:
|
||||||
"object": "chat.completion",
|
yield process_line(form_data, line)
|
||||||
"created": int(time.time()),
|
|
||||||
"model": form_data["model"],
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"message": {
|
|
||||||
"role": "assistant",
|
|
||||||
"content": message,
|
|
||||||
},
|
|
||||||
"logprobs": None,
|
|
||||||
"finish_reason": "stop",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return await job()
|
if isinstance(res, AsyncGenerator):
|
||||||
|
async for line in res:
|
||||||
|
yield process_line(form_data, line)
|
||||||
|
|
||||||
|
if isinstance(res, str) or isinstance(res, Generator):
|
||||||
|
finish_message = openai_chat_chunk_message_template(
|
||||||
|
form_data["model"], ""
|
||||||
|
)
|
||||||
|
finish_message["choices"][0]["finish_reason"] = "stop"
|
||||||
|
yield f"data: {json.dumps(finish_message)}\n\n"
|
||||||
|
yield "data: [DONE]"
|
||||||
|
|
||||||
|
return StreamingResponse(stream_content(), media_type="text/event-stream")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
res = await execute_pipe(pipe, params)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return {"error": {"detail": str(e)}}
|
||||||
|
|
||||||
|
if isinstance(res, StreamingResponse) or isinstance(res, dict):
|
||||||
|
return res
|
||||||
|
if isinstance(res, BaseModel):
|
||||||
|
return res.model_dump()
|
||||||
|
|
||||||
|
message = await get_message_content(res)
|
||||||
|
return openai_chat_completion_message_template(form_data["model"], message)
|
||||||
|
|||||||
@@ -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,37 +102,45 @@ 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:
|
except Exception:
|
||||||
return None
|
|
||||||
except:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||||
@@ -149,52 +152,56 @@ class AuthsTable:
|
|||||||
try:
|
try:
|
||||||
user = Users.get_user_by_api_key(api_key)
|
user = Users.get_user_by_api_key(api_key)
|
||||||
return user if user else None
|
return user if user else None
|
||||||
except:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
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)
|
||||||
except:
|
return user
|
||||||
|
except Exception:
|
||||||
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
|
)
|
||||||
except:
|
db.commit()
|
||||||
|
return True if result == 1 else False
|
||||||
|
except Exception:
|
||||||
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 Exception:
|
||||||
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()
|
||||||
except:
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Auths = AuthsTable(DB)
|
Auths = AuthsTable()
|
||||||
|
|||||||
+253
-201
@@ -1,36 +1,38 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from typing import List, Union, Optional
|
from typing import 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,148 +77,155 @@ 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)
|
||||||
|
except Exception:
|
||||||
chat = Chat.get(Chat.id == chat.share_id)
|
|
||||||
return ChatModel(**model_to_dict(chat))
|
|
||||||
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()
|
||||||
except:
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_chat_share_id_by_id(
|
def update_chat_share_id_by_id(
|
||||||
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
|
||||||
except:
|
db.commit()
|
||||||
|
db.refresh(chat)
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
|
except Exception:
|
||||||
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)
|
||||||
except:
|
return ChatModel.model_validate(chat)
|
||||||
|
except Exception:
|
||||||
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)
|
except Exception:
|
||||||
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
return True
|
|
||||||
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,
|
||||||
@@ -224,132 +233,175 @@ class ChatTable:
|
|||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> List[ChatModel]:
|
) -> list[ChatModel]:
|
||||||
if include_archived:
|
with get_db() as db:
|
||||||
|
query = db.query(Chat).filter_by(user_id=user_id)
|
||||||
|
if not include_archived:
|
||||||
|
query = query.filter_by(archived=False)
|
||||||
|
all_chats = (
|
||||||
|
query.order_by(Chat.updated_at.desc())
|
||||||
|
# .limit(limit).offset(skip)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
|
def get_chat_title_id_list_by_user_id(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
include_archived: bool = False,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = -1,
|
||||||
|
) -> list[ChatTitleIdResponse]:
|
||||||
|
with get_db() as db:
|
||||||
|
query = db.query(Chat).filter_by(user_id=user_id)
|
||||||
|
if not include_archived:
|
||||||
|
query = query.filter_by(archived=False)
|
||||||
|
|
||||||
|
all_chats = (
|
||||||
|
query.order_by(Chat.updated_at.desc())
|
||||||
|
# limit cols
|
||||||
|
.with_entities(Chat.id, Chat.title, Chat.updated_at, Chat.created_at)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(skip)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
# result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
|
||||||
return [
|
return [
|
||||||
ChatModel(**model_to_dict(chat))
|
ChatTitleIdResponse.model_validate(
|
||||||
for chat in Chat.select()
|
{
|
||||||
.where(Chat.user_id == user_id)
|
"id": chat[0],
|
||||||
.order_by(Chat.updated_at.desc())
|
"title": chat[1],
|
||||||
# .limit(limit)
|
"updated_at": chat[2],
|
||||||
# .offset(skip)
|
"created_at": chat[3],
|
||||||
]
|
}
|
||||||
else:
|
)
|
||||||
return [
|
for chat in all_chats
|
||||||
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))
|
|
||||||
except:
|
chat = db.get(Chat, id)
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
|
except Exception:
|
||||||
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))
|
|
||||||
except:
|
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
|
||||||
|
return ChatModel.model_validate(chat)
|
||||||
|
except Exception:
|
||||||
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())
|
|
||||||
# .limit(limit).offset(skip)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
all_chats = (
|
||||||
return [
|
db.query(Chat)
|
||||||
ChatModel(**model_to_dict(chat))
|
# .limit(limit).offset(skip)
|
||||||
for chat in Chat.select()
|
.order_by(Chat.updated_at.desc())
|
||||||
.where(Chat.user_id == user_id)
|
)
|
||||||
.order_by(Chat.updated_at.desc())
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
# .limit(limit).offset(skip)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_archived_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.archived == True)
|
db.query(Chat)
|
||||||
.where(Chat.user_id == user_id)
|
.filter_by(user_id=user_id)
|
||||||
.order_by(Chat.updated_at.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
|
def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
|
||||||
|
all_chats = (
|
||||||
|
db.query(Chat)
|
||||||
|
.filter_by(user_id=user_id, archived=True)
|
||||||
|
.order_by(Chat.updated_at.desc())
|
||||||
|
)
|
||||||
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def delete_chat_by_id(self, id: str) -> bool:
|
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()
|
||||||
except:
|
db.commit()
|
||||||
|
|
||||||
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
|
except Exception:
|
||||||
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()
|
||||||
except:
|
db.commit()
|
||||||
|
|
||||||
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
|
except Exception:
|
||||||
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()
|
||||||
except:
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
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()
|
||||||
except:
|
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
|
||||||
|
|
||||||
|
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
return False
|
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 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 Exception:
|
||||||
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))
|
|
||||||
except:
|
document = db.query(Document).filter_by(name=name).first()
|
||||||
|
return DocumentModel.model_validate(document) if document else None
|
||||||
|
except Exception:
|
||||||
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()
|
||||||
except:
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
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 typing import Union, 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 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))
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_files(self) -> List[FileModel]:
|
try:
|
||||||
return [FileModel(**model_to_dict(file)) for file in File.select()]
|
file = db.get(File, id)
|
||||||
|
return FileModel.model_validate(file)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_files(self) -> list[FileModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
|
||||||
|
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||||
|
|
||||||
def delete_file_by_id(self, id: str) -> bool:
|
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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Files = FilesTable(DB)
|
Files = FilesTable()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from peewee import *
|
from typing import Union, 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 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))
|
|
||||||
except:
|
function = db.get(Function, id)
|
||||||
|
return FunctionModel.model_validate(function)
|
||||||
|
except Exception:
|
||||||
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:
|
||||||
|
|
||||||
|
if active_only:
|
||||||
|
return [
|
||||||
|
FunctionModel.model_validate(function)
|
||||||
|
for function in db.query(Function)
|
||||||
|
.filter_by(type=type, is_active=True)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
FunctionModel.model_validate(function)
|
||||||
|
for function in db.query(Function).filter_by(type=type).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_global_filter_functions(self) -> list[FunctionModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
|
||||||
return [
|
return [
|
||||||
FunctionModel(**model_to_dict(function))
|
FunctionModel.model_validate(function)
|
||||||
for function in Function.select().where(
|
for function in db.query(Function)
|
||||||
Function.type == type, Function.is_active == True
|
.filter_by(type="filter", is_active=True, is_global=True)
|
||||||
)
|
.all()
|
||||||
]
|
|
||||||
else:
|
|
||||||
return [
|
|
||||||
FunctionModel(**model_to_dict(function))
|
|
||||||
for function in Function.select().where(Function.type == type)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_global_filter_functions(self) -> List[FunctionModel]:
|
def get_global_action_functions(self) -> list[FunctionModel]:
|
||||||
return [
|
with get_db() as db:
|
||||||
FunctionModel(**model_to_dict(function))
|
return [
|
||||||
for function in Function.select().where(
|
FunctionModel.model_validate(function)
|
||||||
Function.type == "filter",
|
for function in db.query(Function)
|
||||||
Function.is_active == True,
|
.filter_by(type="action", is_active=True, is_global=True)
|
||||||
Function.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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
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 Exception:
|
||||||
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 typing import Union, Optional
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
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
|
|
||||||
memory.updated_at = int(time.time())
|
|
||||||
memory.save()
|
|
||||||
return MemoryModel(**model_to_dict(memory))
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_memories(self) -> List[MemoryModel]:
|
try:
|
||||||
try:
|
db.query(Memory).filter_by(id=id).update(
|
||||||
memories = Memory.select()
|
{"content": content, "updated_at": int(time.time())}
|
||||||
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
|
)
|
||||||
except:
|
db.commit()
|
||||||
return None
|
return self.get_memory_by_id(id)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
|
def get_memories(self) -> 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).all()
|
||||||
memory = Memory.get(Memory.id == id)
|
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||||
return MemoryModel(**model_to_dict(memory))
|
except Exception:
|
||||||
except:
|
return None
|
||||||
return None
|
|
||||||
|
def get_memories_by_user_id(self, user_id: str) -> list[MemoryModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
|
||||||
|
try:
|
||||||
|
memories = db.query(Memory).filter_by(user_id=user_id).all()
|
||||||
|
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
|
||||||
|
try:
|
||||||
|
memory = db.get(Memory, id)
|
||||||
|
return MemoryModel.model_validate(memory)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def delete_memory_by_id(self, id: str) -> bool:
|
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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Memories = MemoriesTable(DB)
|
Memories = MemoriesTable()
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
import peewee as pw
|
|
||||||
from peewee import *
|
|
||||||
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from sqlalchemy import Column, BigInteger, Text
|
||||||
|
|
||||||
from apps.webui.internal.db import DB, JSONField
|
from apps.webui.internal.db import Base, JSONField, get_db
|
||||||
|
|
||||||
from typing import List, Union, Optional
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@@ -32,7 +27,7 @@ class ModelParams(BaseModel):
|
|||||||
|
|
||||||
# ModelMeta is a model for the data stored in the meta field of the Model table
|
# 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 +41,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 +86,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,13 +111,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
|
||||||
) -> Optional[ModelModel]:
|
) -> Optional[ModelModel]:
|
||||||
@@ -134,34 +123,46 @@ class ModelsTable:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = Model.create(**model.model_dump())
|
with get_db() as db:
|
||||||
|
result = Model(**model.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return model
|
return ModelModel.model_validate(result)
|
||||||
else:
|
else:
|
||||||
return None
|
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)
|
||||||
except:
|
return ModelModel.model_validate(model)
|
||||||
|
except Exception:
|
||||||
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 +170,13 @@ 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()
|
db.query(Model).filter_by(id=id).delete()
|
||||||
return True
|
db.commit()
|
||||||
except:
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
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 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))
|
|
||||||
except:
|
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||||
|
return PromptModel.model_validate(prompt)
|
||||||
|
except Exception:
|
||||||
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())
|
||||||
except:
|
db.commit()
|
||||||
|
return PromptModel.model_validate(prompt)
|
||||||
|
except Exception:
|
||||||
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()
|
||||||
except:
|
db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Prompts = PromptsTable(DB)
|
Prompts = PromptsTable()
|
||||||
|
|||||||
+144
-109
@@ -1,14 +1,14 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from typing import List, Union, Optional
|
from typing import 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
|
||||||
@@ -67,36 +69,39 @@ class ChatIdTagForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TagChatIdsResponse(BaseModel):
|
class TagChatIdsResponse(BaseModel):
|
||||||
chat_ids: List[str]
|
chat_ids: list[str]
|
||||||
|
|
||||||
|
|
||||||
class ChatTagsResponse(BaseModel):
|
class ChatTagsResponse(BaseModel):
|
||||||
tags: List[str]
|
tags: list[str]
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ class TagTable:
|
|||||||
self, user_id: str, form_data: ChatIdTagForm
|
self, user_id: str, form_data: ChatIdTagForm
|
||||||
) -> Optional[ChatIdTagModel]:
|
) -> Optional[ChatIdTagModel]:
|
||||||
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
|
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
|
||||||
if tag == None:
|
if tag is None:
|
||||||
tag = self.insert_new_tag(form_data.tag_name, user_id)
|
tag = self.insert_new_tag(form_data.tag_name, user_id)
|
||||||
|
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
@@ -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)
|
||||||
except:
|
if result:
|
||||||
|
return ChatIdTagModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
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 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):
|
||||||
@@ -46,11 +45,13 @@ class ToolModel(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
name: str
|
name: str
|
||||||
content: str
|
content: str
|
||||||
specs: List[dict]
|
specs: list[dict]
|
||||||
meta: ToolMeta
|
meta: ToolMeta
|
||||||
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,62 +79,69 @@ 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))
|
|
||||||
except:
|
tool = db.get(Tool, id)
|
||||||
|
return ToolModel.model_validate(tool)
|
||||||
|
except Exception:
|
||||||
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())}
|
||||||
except:
|
)
|
||||||
|
db.commit()
|
||||||
|
return self.get_tool_by_id(id)
|
||||||
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user_valves_by_id_and_user_id(
|
def get_user_valves_by_id_and_user_id(
|
||||||
@@ -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)
|
||||||
except:
|
return ToolModel.model_validate(tool)
|
||||||
|
except Exception:
|
||||||
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 Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
Tools = ToolsTable(DB)
|
Tools = ToolsTable()
|
||||||
|
|||||||
+128
-104
@@ -1,11 +1,12 @@
|
|||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict, parse_obj_as
|
||||||
from peewee import *
|
from typing import Union, Optional
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
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,124 +88,147 @@ 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))
|
|
||||||
except:
|
user = db.query(User).filter_by(api_key=api_key).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
|
except Exception:
|
||||||
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))
|
|
||||||
except:
|
user = db.query(User).filter_by(email=email).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
|
except Exception:
|
||||||
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))
|
|
||||||
except:
|
user = db.query(User).filter_by(oauth_sub=sub).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
|
except Exception:
|
||||||
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()
|
||||||
except:
|
return UserModel.model_validate(user)
|
||||||
|
except Exception:
|
||||||
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 Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_profile_image_url_by_id(
|
def update_user_profile_image_url_by_id(
|
||||||
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 Exception:
|
||||||
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())}
|
||||||
except:
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
user = db.query(User).filter_by(id=id).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_oauth_sub_by_id(
|
def update_user_oauth_sub_by_id(
|
||||||
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 Exception:
|
||||||
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,31 +237,33 @@ 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:
|
||||||
return False
|
return False
|
||||||
except:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
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 Exception:
|
||||||
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()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import Depends, Request, HTTPException, status
|
from fastapi import Depends, Request, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
from utils.utils import get_verified_user, get_admin_user
|
from utils.utils import get_verified_user, get_admin_user
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -28,7 +28,7 @@ from apps.webui.models.tags import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
|
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_CHAT_ACCESS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
@@ -40,12 +40,18 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[ChatTitleIdResponse])
|
@router.get("/", response_model=list[ChatTitleIdResponse])
|
||||||
@router.get("/list", response_model=List[ChatTitleIdResponse])
|
@router.get("/list", response_model=list[ChatTitleIdResponse])
|
||||||
async def get_session_user_chat_list(
|
async def get_session_user_chat_list(
|
||||||
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
|
user=Depends(get_verified_user), page: Optional[int] = None
|
||||||
):
|
):
|
||||||
return Chats.get_chat_list_by_user_id(user.id, skip, limit)
|
if page is not None:
|
||||||
|
limit = 60
|
||||||
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
|
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
|
||||||
|
else:
|
||||||
|
return Chats.get_chat_title_id_list_by_user_id(user.id)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -74,10 +80,18 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
|
@router.get("/list/user/{user_id}", response_model=list[ChatTitleIdResponse])
|
||||||
async def get_user_chat_list_by_user_id(
|
async def get_user_chat_list_by_user_id(
|
||||||
user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
|
user_id: str,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 50,
|
||||||
):
|
):
|
||||||
|
if not ENABLE_ADMIN_CHAT_ACCESS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
return Chats.get_chat_list_by_user_id(
|
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
|
||||||
)
|
)
|
||||||
@@ -105,7 +119,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all", response_model=List[ChatResponse])
|
@router.get("/all", response_model=list[ChatResponse])
|
||||||
async def get_user_chats(user=Depends(get_verified_user)):
|
async def get_user_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)})
|
||||||
@@ -118,8 +132,8 @@ 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)
|
||||||
@@ -131,7 +145,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all/db", response_model=List[ChatResponse])
|
@router.get("/all/db", response_model=list[ChatResponse])
|
||||||
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||||
if not ENABLE_ADMIN_EXPORT:
|
if not ENABLE_ADMIN_EXPORT:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -149,7 +163,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/archived", response_model=List[ChatTitleIdResponse])
|
@router.get("/archived", response_model=list[ChatTitleIdResponse])
|
||||||
async def get_archived_session_user_chat_list(
|
async def get_archived_session_user_chat_list(
|
||||||
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
|
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
|
||||||
):
|
):
|
||||||
@@ -178,9 +192,9 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if user.role == "user":
|
if user.role == "user" or (user.role == "admin" and not ENABLE_ADMIN_CHAT_ACCESS):
|
||||||
chat = Chats.get_chat_by_share_id(share_id)
|
chat = Chats.get_chat_by_share_id(share_id)
|
||||||
elif user.role == "admin":
|
elif user.role == "admin" and ENABLE_ADMIN_CHAT_ACCESS:
|
||||||
chat = Chats.get_chat_by_id(share_id)
|
chat = Chats.get_chat_by_id(share_id)
|
||||||
|
|
||||||
if chat:
|
if chat:
|
||||||
@@ -202,12 +216,11 @@ class TagNameForm(BaseModel):
|
|||||||
limit: Optional[int] = 50
|
limit: Optional[int] = 50
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tags", response_model=List[ChatTitleIdResponse])
|
@router.post("/tags", response_model=list[ChatTitleIdResponse])
|
||||||
async def get_user_chat_list_by_tag_name(
|
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(
|
||||||
@@ -228,7 +241,7 @@ async def get_user_chat_list_by_tag_name(
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tags/all", response_model=List[TagModel])
|
@router.get("/tags/all", response_model=list[TagModel])
|
||||||
async def get_all_tags(user=Depends(get_verified_user)):
|
async def get_all_tags(user=Depends(get_verified_user)):
|
||||||
try:
|
try:
|
||||||
tags = Tags.get_tags_by_user_id(user.id)
|
tags = Tags.get_tags_by_user_id(user.id)
|
||||||
@@ -404,7 +417,7 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}/tags", response_model=List[TagModel])
|
@router.get("/{id}/tags", response_model=list[TagModel])
|
||||||
async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
|
async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
|
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import Response, Request
|
from fastapi import Response, Request
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, FastAPI, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union
|
from typing import Union
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -29,12 +29,12 @@ class SetDefaultModelsForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PromptSuggestion(BaseModel):
|
class PromptSuggestion(BaseModel):
|
||||||
title: List[str]
|
title: list[str]
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
|
|
||||||
class SetDefaultSuggestionsForm(BaseModel):
|
class SetDefaultSuggestionsForm(BaseModel):
|
||||||
suggestions: List[PromptSuggestion]
|
suggestions: list[PromptSuggestion]
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -50,7 +50,7 @@ async def set_global_default_models(
|
|||||||
return request.app.state.config.DEFAULT_MODELS
|
return request.app.state.config.DEFAULT_MODELS
|
||||||
|
|
||||||
|
|
||||||
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
@router.post("/default/suggestions", response_model=list[PromptSuggestion])
|
||||||
async def set_global_default_suggestions(
|
async def set_global_default_suggestions(
|
||||||
request: Request,
|
request: Request,
|
||||||
form_data: SetDefaultSuggestionsForm,
|
form_data: SetDefaultSuggestionsForm,
|
||||||
@@ -67,10 +67,10 @@ async def set_global_default_suggestions(
|
|||||||
|
|
||||||
|
|
||||||
class SetBannersForm(BaseModel):
|
class SetBannersForm(BaseModel):
|
||||||
banners: List[BannerModel]
|
banners: list[BannerModel]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/banners", response_model=List[BannerModel])
|
@router.post("/banners", response_model=list[BannerModel])
|
||||||
async def set_banners(
|
async def set_banners(
|
||||||
request: Request,
|
request: Request,
|
||||||
form_data: SetBannersForm,
|
form_data: SetBannersForm,
|
||||||
@@ -81,7 +81,7 @@ async def set_banners(
|
|||||||
return request.app.state.config.BANNERS
|
return request.app.state.config.BANNERS
|
||||||
|
|
||||||
|
|
||||||
@router.get("/banners", response_model=List[BannerModel])
|
@router.get("/banners", response_model=list[BannerModel])
|
||||||
async def get_banners(
|
async def get_banners(
|
||||||
request: Request,
|
request: Request,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, FastAPI, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -24,7 +24,7 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[DocumentResponse])
|
@router.get("/", response_model=list[DocumentResponse])
|
||||||
async def get_documents(user=Depends(get_verified_user)):
|
async def get_documents(user=Depends(get_verified_user)):
|
||||||
docs = [
|
docs = [
|
||||||
DocumentResponse(
|
DocumentResponse(
|
||||||
@@ -46,7 +46,7 @@ async def get_documents(user=Depends(get_verified_user)):
|
|||||||
@router.post("/create", response_model=Optional[DocumentResponse])
|
@router.post("/create", response_model=Optional[DocumentResponse])
|
||||||
async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
|
async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
|
||||||
doc = Documents.get_doc_by_name(form_data.name)
|
doc = Documents.get_doc_by_name(form_data.name)
|
||||||
if doc == None:
|
if doc is None:
|
||||||
doc = Documents.insert_new_doc(user.id, form_data)
|
doc = Documents.insert_new_doc(user.id, form_data)
|
||||||
|
|
||||||
if doc:
|
if doc:
|
||||||
@@ -102,7 +102,7 @@ class TagItem(BaseModel):
|
|||||||
|
|
||||||
class TagDocumentForm(BaseModel):
|
class TagDocumentForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
tags: List[dict]
|
tags: list[dict]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
|
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from fastapi import (
|
|||||||
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
@@ -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,
|
||||||
@@ -105,7 +104,7 @@ def upload_file(
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[FileModel])
|
@router.get("/", response_model=list[FileModel])
|
||||||
async def list_files(user=Depends(get_verified_user)):
|
async def list_files(user=Depends(get_verified_user)):
|
||||||
files = Files.get_files()
|
files = Files.get_files()
|
||||||
return files
|
return files
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -30,7 +30,7 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[FunctionResponse])
|
@router.get("/", response_model=list[FunctionResponse])
|
||||||
async def get_functions(user=Depends(get_verified_user)):
|
async def get_functions(user=Depends(get_verified_user)):
|
||||||
return Functions.get_functions()
|
return Functions.get_functions()
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ async def get_functions(user=Depends(get_verified_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/export", response_model=List[FunctionModel])
|
@router.get("/export", response_model=list[FunctionModel])
|
||||||
async def get_functions(user=Depends(get_admin_user)):
|
async def get_functions(user=Depends(get_admin_user)):
|
||||||
return Functions.get_functions()
|
return Functions.get_functions()
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ async def create_new_function(
|
|||||||
form_data.id = form_data.id.lower()
|
form_data.id = form_data.id.lower()
|
||||||
|
|
||||||
function = Functions.get_function_by_id(form_data.id)
|
function = Functions.get_function_by_id(form_data.id)
|
||||||
if function == None:
|
if function is None:
|
||||||
function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
|
function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
|
||||||
try:
|
try:
|
||||||
with open(function_path, "w") as function_file:
|
with open(function_path, "w") as function_file:
|
||||||
@@ -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 Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import Response, Request
|
from fastapi import Response, Request
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, FastAPI, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -30,7 +30,7 @@ async def get_embeddings(request: Request):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[MemoryModel])
|
@router.get("/", response_model=list[MemoryModel])
|
||||||
async def get_memories(user=Depends(get_verified_user)):
|
async def get_memories(user=Depends(get_verified_user)):
|
||||||
return Memories.get_memories_by_user_id(user.id)
|
return Memories.get_memories_by_user_id(user.id)
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import 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
|
||||||
@@ -17,7 +18,7 @@ router = APIRouter()
|
|||||||
###########################
|
###########################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[ModelResponse])
|
@router.get("/", response_model=list[ModelResponse])
|
||||||
async def get_models(user=Depends(get_verified_user)):
|
async def get_models(user=Depends(get_verified_user)):
|
||||||
return Models.get_all_models()
|
return Models.get_all_models()
|
||||||
|
|
||||||
@@ -29,7 +30,9 @@ async def get_models(user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
@router.post("/add", response_model=Optional[ModelModel])
|
@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:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, FastAPI, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[PromptModel])
|
@router.get("/", response_model=list[PromptModel])
|
||||||
async def get_prompts(user=Depends(get_verified_user)):
|
async def get_prompts(user=Depends(get_verified_user)):
|
||||||
return Prompts.get_prompts()
|
return Prompts.get_prompts()
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ async def get_prompts(user=Depends(get_verified_user)):
|
|||||||
@router.post("/create", response_model=Optional[PromptModel])
|
@router.post("/create", response_model=Optional[PromptModel])
|
||||||
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
|
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
|
||||||
prompt = Prompts.get_prompt_by_command(form_data.command)
|
prompt = Prompts.get_prompt_by_command(form_data.command)
|
||||||
if prompt == None:
|
if prompt is None:
|
||||||
prompt = Prompts.insert_new_prompt(user.id, form_data)
|
prompt = Prompts.insert_new_prompt(user.id, form_data)
|
||||||
|
|
||||||
if prompt:
|
if prompt:
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
from fastapi import Depends, HTTPException, status, Request
|
||||||
from datetime import datetime, timedelta
|
from typing import Optional
|
||||||
from typing import List, Union, Optional
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
from apps.webui.models.users import Users
|
|
||||||
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
from apps.webui.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
|
||||||
|
|
||||||
@@ -15,7 +10,6 @@ from utils.utils import get_admin_user, get_verified_user
|
|||||||
from utils.tools import get_tools_specs
|
from utils.tools import get_tools_specs
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from importlib import util
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -33,7 +27,7 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[ToolResponse])
|
@router.get("/", response_model=list[ToolResponse])
|
||||||
async def get_toolkits(user=Depends(get_verified_user)):
|
async def get_toolkits(user=Depends(get_verified_user)):
|
||||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||||
return toolkits
|
return toolkits
|
||||||
@@ -44,7 +38,7 @@ async def get_toolkits(user=Depends(get_verified_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/export", response_model=List[ToolModel])
|
@router.get("/export", response_model=list[ToolModel])
|
||||||
async def get_toolkits(user=Depends(get_admin_user)):
|
async def get_toolkits(user=Depends(get_admin_user)):
|
||||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||||
return toolkits
|
return toolkits
|
||||||
@@ -57,7 +51,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(
|
||||||
@@ -68,7 +64,7 @@ async def create_new_toolkit(
|
|||||||
form_data.id = form_data.id.lower()
|
form_data.id = form_data.id.lower()
|
||||||
|
|
||||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||||
if toolkit == None:
|
if toolkit is None:
|
||||||
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||||
try:
|
try:
|
||||||
with open(toolkit_path, "w") as tool_file:
|
with open(toolkit_path, "w") as tool_file:
|
||||||
@@ -97,7 +93,7 @@ async def create_new_toolkit(
|
|||||||
print(e)
|
print(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -131,7 +127,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")
|
||||||
|
|
||||||
@@ -166,7 +165,7 @@ async def update_toolkit_by_id(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -206,7 +205,7 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -229,7 +228,7 @@ async def get_toolkit_valves_spec_by_id(
|
|||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
toolkit_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = toolkit_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "Valves"):
|
if hasattr(toolkit_module, "Valves"):
|
||||||
@@ -257,7 +256,7 @@ async def update_toolkit_valves_by_id(
|
|||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
toolkit_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = toolkit_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "Valves"):
|
if hasattr(toolkit_module, "Valves"):
|
||||||
@@ -272,7 +271,7 @@ async def update_toolkit_valves_by_id(
|
|||||||
print(e)
|
print(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -302,7 +301,7 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -320,7 +319,7 @@ async def get_toolkit_user_valves_spec_by_id(
|
|||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
toolkit_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = toolkit_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "UserValves"):
|
if hasattr(toolkit_module, "UserValves"):
|
||||||
@@ -344,7 +343,7 @@ async def update_toolkit_user_valves_by_id(
|
|||||||
if id in request.app.state.TOOLS:
|
if id in request.app.state.TOOLS:
|
||||||
toolkit_module = request.app.state.TOOLS[id]
|
toolkit_module = request.app.state.TOOLS[id]
|
||||||
else:
|
else:
|
||||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||||
request.app.state.TOOLS[id] = toolkit_module
|
request.app.state.TOOLS[id] = toolkit_module
|
||||||
|
|
||||||
if hasattr(toolkit_module, "UserValves"):
|
if hasattr(toolkit_module, "UserValves"):
|
||||||
@@ -361,7 +360,7 @@ async def update_toolkit_user_valves_by_id(
|
|||||||
print(e)
|
print(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import Response, Request
|
from fastapi import Response, Request
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, FastAPI, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -39,7 +39,7 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[UserModel])
|
@router.get("/", response_model=list[UserModel])
|
||||||
async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)):
|
async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)):
|
||||||
return Users.get_users(skip, limit)
|
return Users.get_users(skip, limit)
|
||||||
|
|
||||||
@@ -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,8 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import site
|
||||||
|
|
||||||
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,13 +12,12 @@ 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
|
||||||
|
|
||||||
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
|
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from typing import List
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ async def get_html_from_markdown(
|
|||||||
|
|
||||||
class ChatForm(BaseModel):
|
class ChatForm(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
messages: List[dict]
|
messages: list[dict]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pdf")
|
@router.post("/pdf")
|
||||||
@@ -66,8 +67,18 @@ async def download_chat_as_pdf(
|
|||||||
pdf = FPDF()
|
pdf = FPDF()
|
||||||
pdf.add_page()
|
pdf.add_page()
|
||||||
|
|
||||||
STATIC_DIR = "./static"
|
# When running in docker, workdir is /app/backend, so fonts is in /app/backend/static/fonts
|
||||||
FONTS_DIR = f"{STATIC_DIR}/fonts"
|
FONTS_DIR = Path("./static/fonts")
|
||||||
|
|
||||||
|
# Non Docker Installation
|
||||||
|
|
||||||
|
# When running using `pip install` the static directory is in the site packages.
|
||||||
|
if not FONTS_DIR.exists():
|
||||||
|
FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
|
||||||
|
# When running using `pip install -e .` the static directory is in the site packages.
|
||||||
|
# This path only works if `open-webui serve` is run from the root of this project.
|
||||||
|
if not FONTS_DIR.exists():
|
||||||
|
FONTS_DIR = Path("./backend/static/fonts")
|
||||||
|
|
||||||
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
|
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
|
||||||
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
|
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
|
||||||
@@ -114,13 +125,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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from importlib import util
|
from importlib import util
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from config import TOOLS_DIR, FUNCTIONS_DIR
|
from config import TOOLS_DIR, FUNCTIONS_DIR
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ def load_toolkit_module_by_id(toolkit_id):
|
|||||||
frontmatter = extract_frontmatter(toolkit_path)
|
frontmatter = extract_frontmatter(toolkit_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
install_frontmatter_requirements(frontmatter.get("requirements", ""))
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
print(f"Loaded module: {module.__name__}")
|
print(f"Loaded module: {module.__name__}")
|
||||||
if hasattr(module, "Tools"):
|
if hasattr(module, "Tools"):
|
||||||
@@ -73,12 +76,15 @@ def load_function_module_by_id(function_id):
|
|||||||
frontmatter = extract_frontmatter(function_path)
|
frontmatter = extract_frontmatter(function_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
install_frontmatter_requirements(frontmatter.get("requirements", ""))
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
print(f"Loaded module: {module.__name__}")
|
print(f"Loaded module: {module.__name__}")
|
||||||
if hasattr(module, "Pipe"):
|
if hasattr(module, "Pipe"):
|
||||||
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:
|
||||||
@@ -86,3 +92,13 @@ def load_function_module_by_id(function_id):
|
|||||||
# Move the file to the error folder
|
# Move the file to the error folder
|
||||||
os.rename(function_path, f"{function_path}.error")
|
os.rename(function_path, f"{function_path}.error")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def install_frontmatter_requirements(requirements):
|
||||||
|
if requirements:
|
||||||
|
req_list = [req.strip() for req in requirements.split(",")]
|
||||||
|
for req in req_list:
|
||||||
|
print(f"Installing requirement: {req}")
|
||||||
|
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
|
||||||
|
else:
|
||||||
|
print("No requirements found in frontmatter.")
|
||||||
|
|||||||
+152
-18
@@ -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
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -79,6 +77,16 @@ for source in log_sources:
|
|||||||
|
|
||||||
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointFilter(logging.Filter):
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
return record.getMessage().find("/health") == -1
|
||||||
|
|
||||||
|
|
||||||
|
# Filter out /endpoint
|
||||||
|
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
|
||||||
|
|
||||||
|
|
||||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||||
if WEBUI_NAME != "Open WebUI":
|
if WEBUI_NAME != "Open WebUI":
|
||||||
WEBUI_NAME += " (Open WebUI)"
|
WEBUI_NAME += " (Open WebUI)"
|
||||||
@@ -96,7 +104,7 @@ ENV = os.environ.get("ENV", "dev")
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
|
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
|
||||||
except:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
|
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
|
||||||
except importlib.metadata.PackageNotFoundError:
|
except importlib.metadata.PackageNotFoundError:
|
||||||
@@ -129,7 +137,7 @@ try:
|
|||||||
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
|
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
|
||||||
changelog_content = file.read()
|
changelog_content = file.read()
|
||||||
|
|
||||||
except:
|
except Exception:
|
||||||
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
|
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
|
||||||
|
|
||||||
|
|
||||||
@@ -194,12 +202,12 @@ if RESET_CONFIG_ON_START:
|
|||||||
os.remove(f"{DATA_DIR}/config.json")
|
os.remove(f"{DATA_DIR}/config.json")
|
||||||
with open(f"{DATA_DIR}/config.json", "w") as f:
|
with open(f"{DATA_DIR}/config.json", "w") as f:
|
||||||
f.write("{}")
|
f.write("{}")
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
|
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
|
||||||
except:
|
except Exception:
|
||||||
CONFIG_DATA = {}
|
CONFIG_DATA = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -341,6 +349,12 @@ GOOGLE_OAUTH_SCOPE = PersistentConfig(
|
|||||||
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
|
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GOOGLE_REDIRECT_URI = PersistentConfig(
|
||||||
|
"GOOGLE_REDIRECT_URI",
|
||||||
|
"oauth.google.redirect_uri",
|
||||||
|
os.environ.get("GOOGLE_REDIRECT_URI", ""),
|
||||||
|
)
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = PersistentConfig(
|
MICROSOFT_CLIENT_ID = PersistentConfig(
|
||||||
"MICROSOFT_CLIENT_ID",
|
"MICROSOFT_CLIENT_ID",
|
||||||
"oauth.microsoft.client_id",
|
"oauth.microsoft.client_id",
|
||||||
@@ -365,6 +379,12 @@ MICROSOFT_OAUTH_SCOPE = PersistentConfig(
|
|||||||
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
|
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MICROSOFT_REDIRECT_URI = PersistentConfig(
|
||||||
|
"MICROSOFT_REDIRECT_URI",
|
||||||
|
"oauth.microsoft.redirect_uri",
|
||||||
|
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_CLIENT_ID = PersistentConfig(
|
OAUTH_CLIENT_ID = PersistentConfig(
|
||||||
"OAUTH_CLIENT_ID",
|
"OAUTH_CLIENT_ID",
|
||||||
"oauth.oidc.client_id",
|
"oauth.oidc.client_id",
|
||||||
@@ -383,6 +403,12 @@ OPENID_PROVIDER_URL = PersistentConfig(
|
|||||||
os.environ.get("OPENID_PROVIDER_URL", ""),
|
os.environ.get("OPENID_PROVIDER_URL", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OPENID_REDIRECT_URI = PersistentConfig(
|
||||||
|
"OPENID_REDIRECT_URI",
|
||||||
|
"oauth.oidc.redirect_uri",
|
||||||
|
os.environ.get("OPENID_REDIRECT_URI", ""),
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_SCOPES = PersistentConfig(
|
OAUTH_SCOPES = PersistentConfig(
|
||||||
"OAUTH_SCOPES",
|
"OAUTH_SCOPES",
|
||||||
"oauth.oidc.scopes",
|
"oauth.oidc.scopes",
|
||||||
@@ -395,6 +421,24 @@ 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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
OAUTH_EMAIL_CLAIM = PersistentConfig(
|
||||||
|
"OAUTH_EMAIL_CLAIM",
|
||||||
|
"oauth.oidc.email_claim",
|
||||||
|
os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_oauth_providers():
|
def load_oauth_providers():
|
||||||
OAUTH_PROVIDERS.clear()
|
OAUTH_PROVIDERS.clear()
|
||||||
@@ -404,6 +448,7 @@ def load_oauth_providers():
|
|||||||
"client_secret": GOOGLE_CLIENT_SECRET.value,
|
"client_secret": GOOGLE_CLIENT_SECRET.value,
|
||||||
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
||||||
"scope": GOOGLE_OAUTH_SCOPE.value,
|
"scope": GOOGLE_OAUTH_SCOPE.value,
|
||||||
|
"redirect_uri": GOOGLE_REDIRECT_URI.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -416,6 +461,7 @@ def load_oauth_providers():
|
|||||||
"client_secret": MICROSOFT_CLIENT_SECRET.value,
|
"client_secret": MICROSOFT_CLIENT_SECRET.value,
|
||||||
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
|
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
|
||||||
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
||||||
|
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -429,6 +475,7 @@ def load_oauth_providers():
|
|||||||
"server_metadata_url": OPENID_PROVIDER_URL.value,
|
"server_metadata_url": OPENID_PROVIDER_URL.value,
|
||||||
"scope": OAUTH_SCOPES.value,
|
"scope": OAUTH_SCOPES.value,
|
||||||
"name": OAUTH_PROVIDER_NAME.value,
|
"name": OAUTH_PROVIDER_NAME.value,
|
||||||
|
"redirect_uri": OPENID_REDIRECT_URI.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -440,16 +487,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 +532,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)
|
||||||
@@ -576,7 +647,7 @@ if AIOHTTP_CLIENT_TIMEOUT == "":
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
||||||
except:
|
except Exception:
|
||||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||||
|
|
||||||
|
|
||||||
@@ -656,7 +727,7 @@ try:
|
|||||||
OPENAI_API_KEY = OPENAI_API_KEYS.value[
|
OPENAI_API_KEY = OPENAI_API_KEYS.value[
|
||||||
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
|
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
|
||||||
]
|
]
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||||
@@ -674,6 +745,19 @@ ENABLE_SIGNUP = PersistentConfig(
|
|||||||
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENABLE_LOGIN_FORM = PersistentConfig(
|
||||||
|
"ENABLE_LOGIN_FORM",
|
||||||
|
"ui.ENABLE_LOGIN_FORM",
|
||||||
|
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_LOCALE = PersistentConfig(
|
||||||
|
"DEFAULT_LOCALE",
|
||||||
|
"ui.default_locale",
|
||||||
|
os.environ.get("DEFAULT_LOCALE", ""),
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_MODELS = PersistentConfig(
|
DEFAULT_MODELS = PersistentConfig(
|
||||||
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
|
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
|
||||||
)
|
)
|
||||||
@@ -746,6 +830,10 @@ WEBHOOK_URL = PersistentConfig(
|
|||||||
|
|
||||||
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||||
|
|
||||||
|
ENABLE_ADMIN_CHAT_ACCESS = (
|
||||||
|
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
||||||
"ENABLE_COMMUNITY_SHARING",
|
"ENABLE_COMMUNITY_SHARING",
|
||||||
"ui.enable_community_sharing",
|
"ui.enable_community_sharing",
|
||||||
@@ -762,11 +850,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 +969,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
|
||||||
####################################
|
####################################
|
||||||
@@ -936,7 +1043,7 @@ RAG_EMBEDDING_MODEL = PersistentConfig(
|
|||||||
"rag.embedding_model",
|
"rag.embedding_model",
|
||||||
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
|
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
|
||||||
)
|
)
|
||||||
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
|
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}")
|
||||||
|
|
||||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
||||||
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||||
@@ -958,7 +1065,7 @@ RAG_RERANKING_MODEL = PersistentConfig(
|
|||||||
os.environ.get("RAG_RERANKING_MODEL", ""),
|
os.environ.get("RAG_RERANKING_MODEL", ""),
|
||||||
)
|
)
|
||||||
if RAG_RERANKING_MODEL.value != "":
|
if RAG_RERANKING_MODEL.value != "":
|
||||||
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
|
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
|
||||||
|
|
||||||
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
||||||
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||||
@@ -1205,6 +1312,24 @@ COMFYUI_SD3 = PersistentConfig(
|
|||||||
os.environ.get("COMFYUI_SD3", "").lower() == "true",
|
os.environ.get("COMFYUI_SD3", "").lower() == "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
COMFYUI_FLUX = PersistentConfig(
|
||||||
|
"COMFYUI_FLUX",
|
||||||
|
"image_generation.comfyui.flux",
|
||||||
|
os.environ.get("COMFYUI_FLUX", "").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
COMFYUI_FLUX_WEIGHT_DTYPE = PersistentConfig(
|
||||||
|
"COMFYUI_FLUX_WEIGHT_DTYPE",
|
||||||
|
"image_generation.comfyui.flux_weight_dtype",
|
||||||
|
os.getenv("COMFYUI_FLUX_WEIGHT_DTYPE", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
COMFYUI_FLUX_FP8_CLIP = PersistentConfig(
|
||||||
|
"COMFYUI_FLUX_FP8_CLIP",
|
||||||
|
"image_generation.comfyui.flux_fp8_clip",
|
||||||
|
os.environ.get("COMFYUI_FLUX_FP8_CLIP", "").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
"IMAGES_OPENAI_API_BASE_URL",
|
"IMAGES_OPENAI_API_BASE_URL",
|
||||||
"image_generation.openai.api_base_url",
|
"image_generation.openai.api_base_url",
|
||||||
@@ -1269,6 +1394,11 @@ AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
|||||||
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUDIO_TTS_API_KEY = PersistentConfig(
|
||||||
|
"AUDIO_TTS_API_KEY",
|
||||||
|
"audio.tts.api_key",
|
||||||
|
os.getenv("AUDIO_TTS_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
AUDIO_TTS_ENGINE = PersistentConfig(
|
AUDIO_TTS_ENGINE = PersistentConfig(
|
||||||
"AUDIO_TTS_ENGINE",
|
"AUDIO_TTS_ENGINE",
|
||||||
@@ -1295,3 +1425,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"],
|
||||||
|
|||||||
+752
-485
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 ###
|
||||||
+34
-24
@@ -1,26 +1,29 @@
|
|||||||
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.10.2
|
||||||
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
|
||||||
bcrypt==4.1.3
|
bcrypt==4.1.3
|
||||||
SQLAlchemy
|
|
||||||
pymongo
|
pymongo
|
||||||
redis
|
redis
|
||||||
boto3==1.34.110
|
boto3==1.34.153
|
||||||
|
|
||||||
argon2-cffi==23.1.0
|
argon2-cffi==23.1.0
|
||||||
APScheduler==3.10.4
|
APScheduler==3.10.4
|
||||||
@@ -28,43 +31,50 @@ APScheduler==3.10.4
|
|||||||
# AI libraries
|
# AI libraries
|
||||||
openai
|
openai
|
||||||
anthropic
|
anthropic
|
||||||
google-generativeai==0.5.4
|
google-generativeai==0.7.2
|
||||||
|
tiktoken
|
||||||
|
|
||||||
langchain==0.2.0
|
langchain==0.2.12
|
||||||
langchain-community==0.2.0
|
langchain-community==0.2.10
|
||||||
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.5
|
||||||
sentence-transformers==2.7.0
|
sentence-transformers==3.0.1
|
||||||
pypdf==4.2.0
|
pypdf==4.3.1
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
python-pptx==0.6.23
|
python-pptx==1.0.0
|
||||||
unstructured==0.14.0
|
unstructured==0.15.0
|
||||||
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.33.0
|
||||||
|
psutil
|
||||||
|
|
||||||
opencv-python-headless==4.9.0.80
|
opencv-python-headless==4.10.0.84
|
||||||
rapidocr-onnxruntime==1.3.22
|
rapidocr-onnxruntime==1.3.24
|
||||||
|
|
||||||
fpdf2==2.7.9
|
fpdf2==2.7.9
|
||||||
rank-bm25==0.2.2
|
rank-bm25==0.2.2
|
||||||
|
|
||||||
faster-whisper==1.0.2
|
faster-whisper==1.0.2
|
||||||
|
|
||||||
PyJWT[crypto]==2.8.0
|
PyJWT[crypto]==2.9.0
|
||||||
authlib==1.3.1
|
authlib==1.3.1
|
||||||
|
|
||||||
black==24.4.2
|
black==24.8.0
|
||||||
langfuse==2.33.0
|
langfuse==2.43.3
|
||||||
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.2.1
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
docker~=7.1.0
|
||||||
|
pytest~=8.3.2
|
||||||
|
pytest-docker~=3.1.1
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
|
|||||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Check if SPACE_ID is set, if so, configure for space
|
# Check if SPACE_ID is set, if so, configure for space
|
||||||
if [ -n "$SPACE_ID" ]; then
|
if [ -n "$SPACE_ID" ]; then
|
||||||
echo "Configuring for HuggingFace Space deployment"
|
echo "Configuring for HuggingFace Space deployment"
|
||||||
|
|||||||
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 = {}
|
||||||
+147
-32
@@ -1,51 +1,77 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, Callable
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from utils.task import prompt_template
|
||||||
|
|
||||||
def get_last_user_message(messages: List[dict]) -> str:
|
|
||||||
|
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
|
||||||
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"]:
|
|
||||||
if item["type"] == "text":
|
|
||||||
return item["text"]
|
|
||||||
return message["content"]
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_last_assistant_message(messages: List[dict]) -> str:
|
def get_content_from_message(message: dict) -> Optional[str]:
|
||||||
|
if isinstance(message["content"], list):
|
||||||
|
for item in message["content"]:
|
||||||
|
if item["type"] == "text":
|
||||||
|
return item["text"]
|
||||||
|
else:
|
||||||
|
return message["content"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_user_message(messages: list[dict]) -> Optional[str]:
|
||||||
|
message = get_last_user_message_item(messages)
|
||||||
|
if message is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return get_content_from_message(message)
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_assistant_message(messages: list[dict]) -> Optional[str]:
|
||||||
for message in reversed(messages):
|
for message in reversed(messages):
|
||||||
if message["role"] == "assistant":
|
if message["role"] == "assistant":
|
||||||
if isinstance(message["content"], list):
|
return get_content_from_message(message)
|
||||||
for item in message["content"]:
|
|
||||||
if item["type"] == "text":
|
|
||||||
return item["text"]
|
|
||||||
return message["content"]
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_system_message(messages: List[dict]) -> dict:
|
def get_system_message(messages: list[dict]) -> Optional[dict]:
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message["role"] == "system":
|
if message["role"] == "system":
|
||||||
return message
|
return message
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def remove_system_message(messages: List[dict]) -> List[dict]:
|
def remove_system_message(messages: list[dict]) -> list[dict]:
|
||||||
return [message for message in messages if message["role"] != "system"]
|
return [message for message in messages if message["role"] != "system"]
|
||||||
|
|
||||||
|
|
||||||
def pop_system_message(messages: List[dict]) -> Tuple[dict, List[dict]]:
|
def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]]:
|
||||||
return get_system_message(messages), remove_system_message(messages)
|
return get_system_message(messages), remove_system_message(messages)
|
||||||
|
|
||||||
|
|
||||||
def add_or_update_system_message(content: str, messages: List[dict]):
|
def prepend_to_first_user_message_content(
|
||||||
|
content: str, messages: list[dict]
|
||||||
|
) -> list[dict]:
|
||||||
|
for message in messages:
|
||||||
|
if message["role"] == "user":
|
||||||
|
if isinstance(message["content"], list):
|
||||||
|
for item in message["content"]:
|
||||||
|
if item["type"] == "text":
|
||||||
|
item["text"] = f"{content}\n{item['text']}"
|
||||||
|
else:
|
||||||
|
message["content"] = f"{content}\n{message['content']}"
|
||||||
|
break
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def add_or_update_system_message(content: str, messages: list[dict]):
|
||||||
"""
|
"""
|
||||||
Adds a new system message at the beginning of the messages list
|
Adds a new system message at the beginning of the messages list
|
||||||
or updates the existing system message at the beginning.
|
or updates the existing system message at the beginning.
|
||||||
@@ -64,23 +90,112 @@ def add_or_update_system_message(content: str, messages: List[dict]):
|
|||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
def stream_message_template(model: str, message: str):
|
def openai_chat_message_template(model: str):
|
||||||
return {
|
return {
|
||||||
"id": f"{model}-{str(uuid.uuid4())}",
|
"id": f"{model}-{str(uuid.uuid4())}",
|
||||||
"object": "chat.completion.chunk",
|
|
||||||
"created": int(time.time()),
|
"created": int(time.time()),
|
||||||
"model": model,
|
"model": model,
|
||||||
"choices": [
|
"choices": [{"index": 0, "logprobs": None, "finish_reason": None}],
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"delta": {"content": message},
|
|
||||||
"logprobs": None,
|
|
||||||
"finish_reason": None,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
|
||||||
|
template = openai_chat_message_template(model)
|
||||||
|
template["object"] = "chat.completion.chunk"
|
||||||
|
template["choices"][0]["delta"] = {"content": message}
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def openai_chat_completion_message_template(model: str, message: str) -> dict:
|
||||||
|
template = openai_chat_message_template(model)
|
||||||
|
template["object"] = "chat.completion"
|
||||||
|
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||||
|
template["choices"][0]["finish_reason"] = "stop"
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
# inplace function: form_data is modified
|
||||||
|
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||||
|
system = params.get("system", None)
|
||||||
|
if not system:
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
if user:
|
||||||
|
template_params = {
|
||||||
|
"user_name": user.name,
|
||||||
|
"user_location": user.info.get("location") if user.info else None,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
template_params = {}
|
||||||
|
system = prompt_template(system, **template_params)
|
||||||
|
form_data["messages"] = add_or_update_system_message(
|
||||||
|
system, form_data.get("messages", [])
|
||||||
|
)
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
|
# inplace function: form_data is modified
|
||||||
|
def apply_model_params_to_body(
|
||||||
|
params: dict, form_data: dict, mappings: dict[str, Callable]
|
||||||
|
) -> dict:
|
||||||
|
if not params:
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
for key, cast_func in mappings.items():
|
||||||
|
if (value := params.get(key)) is not None:
|
||||||
|
form_data[key] = cast_func(value)
|
||||||
|
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
|
# inplace function: form_data is modified
|
||||||
|
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
||||||
|
mappings = {
|
||||||
|
"temperature": float,
|
||||||
|
"top_p": int,
|
||||||
|
"max_tokens": int,
|
||||||
|
"frequency_penalty": int,
|
||||||
|
"seed": lambda x: x,
|
||||||
|
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||||
|
}
|
||||||
|
return apply_model_params_to_body(params, form_data, mappings)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
|
||||||
|
opts = [
|
||||||
|
"temperature",
|
||||||
|
"top_p",
|
||||||
|
"seed",
|
||||||
|
"mirostat",
|
||||||
|
"mirostat_eta",
|
||||||
|
"mirostat_tau",
|
||||||
|
"num_ctx",
|
||||||
|
"num_batch",
|
||||||
|
"num_keep",
|
||||||
|
"repeat_last_n",
|
||||||
|
"tfs_z",
|
||||||
|
"top_k",
|
||||||
|
"min_p",
|
||||||
|
"use_mmap",
|
||||||
|
"use_mlock",
|
||||||
|
"num_thread",
|
||||||
|
"num_gpu",
|
||||||
|
]
|
||||||
|
mappings = {i: lambda x: x for i in opts}
|
||||||
|
form_data = apply_model_params_to_body(params, form_data, mappings)
|
||||||
|
|
||||||
|
name_differences = {
|
||||||
|
"max_tokens": "num_predict",
|
||||||
|
"frequency_penalty": "repeat_penalty",
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in name_differences.items():
|
||||||
|
if (param := params.get(key, None)) is not None:
|
||||||
|
form_data[value] = param
|
||||||
|
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
def get_gravatar_url(email):
|
def get_gravatar_url(email):
|
||||||
# Trim leading and trailing whitespace from
|
# Trim leading and trailing whitespace from
|
||||||
# an email address and force all characters
|
# an email address and force all characters
|
||||||
@@ -151,7 +266,7 @@ def extract_folders_after_data_docs(path):
|
|||||||
tags = []
|
tags = []
|
||||||
|
|
||||||
folders = parts[index_docs:-1]
|
folders = parts[index_docs:-1]
|
||||||
for idx, part in enumerate(folders):
|
for idx, _ in enumerate(folders):
|
||||||
tags.append("/".join(folders[: idx + 1]))
|
tags.append("/".join(folders[: idx + 1]))
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
@@ -247,11 +362,11 @@ def parse_ollama_modelfile(model_text):
|
|||||||
value = param_match.group(1)
|
value = param_match.group(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if param_type == int:
|
if param_type is int:
|
||||||
value = int(value)
|
value = int(value)
|
||||||
elif param_type == float:
|
elif param_type is float:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
elif param_type == bool:
|
elif param_type is bool:
|
||||||
value = value.lower() == "true"
|
value = value.lower() == "true"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Optional
|
|||||||
|
|
||||||
|
|
||||||
def prompt_template(
|
def prompt_template(
|
||||||
template: str, user_name: str = None, user_location: str = None
|
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
# Get the current date
|
# Get the current date
|
||||||
current_date = datetime.now()
|
current_date = datetime.now()
|
||||||
@@ -83,7 +83,6 @@ def title_generation_template(
|
|||||||
def search_query_generation_template(
|
def search_query_generation_template(
|
||||||
template: str, prompt: str, user: Optional[dict] = None
|
template: str, prompt: str, user: Optional[dict] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
||||||
def replacement_function(match):
|
def replacement_function(match):
|
||||||
full_match = match.group(0)
|
full_match = match.group(0)
|
||||||
start_length = match.group(1)
|
start_length = match.group(1)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import inspect
|
import inspect
|
||||||
from typing import get_type_hints, List, Dict, Any
|
from typing import get_type_hints
|
||||||
|
|
||||||
|
|
||||||
def doc_to_dict(docstring):
|
def doc_to_dict(docstring):
|
||||||
@@ -16,7 +16,7 @@ def doc_to_dict(docstring):
|
|||||||
return ret_dict
|
return ret_dict
|
||||||
|
|
||||||
|
|
||||||
def get_tools_specs(tools) -> List[dict]:
|
def get_tools_specs(tools) -> list[dict]:
|
||||||
function_list = [
|
function_list = [
|
||||||
{"name": func, "function": getattr(tools, func)}
|
{"name": func, "function": getattr(tools, func)}
|
||||||
for func in dir(tools)
|
for func in dir(tools)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ from fastapi import HTTPException, status, Depends, Request
|
|||||||
|
|
||||||
from apps.webui.models.users import Users
|
from apps.webui.models.users import Users
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import requests
|
|
||||||
import jwt
|
import jwt
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
@@ -53,7 +51,7 @@ def decode_token(token: str) -> Optional[dict]:
|
|||||||
try:
|
try:
|
||||||
decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM])
|
decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM])
|
||||||
return decoded
|
return decoded
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ def get_http_authorization_cred(auth_header: str):
|
|||||||
try:
|
try:
|
||||||
scheme, credentials = auth_header.split(" ")
|
scheme, credentials = auth_header.split(" ")
|
||||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||||
except:
|
except Exception:
|
||||||
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
|
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ def get_current_user(
|
|||||||
|
|
||||||
# auth by jwt token
|
# auth by jwt token
|
||||||
data = decode_token(token)
|
data = decode_token(token)
|
||||||
if data != None and "id" in data:
|
if data is not None and "id" in data:
|
||||||
user = Users.get_user_by_id(data["id"])
|
user = Users.get_user_by_id(data["id"])
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pellentesque elit eget gravida cum sociis natoque. Morbi tristique senectus et netus et malesuada. Sapien nec sagittis aliquam malesuada bibendum. Amet consectetur adipiscing elit duis tristique sollicitudin. Non pulvinar neque laoreet suspendisse interdum consectetur libero. Arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque. Nec feugiat nisl pretium fusce id velit. Imperdiet proin fermentum leo vel. Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Pellentesque sit amet porttitor eget dolor morbi non arcu risus. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Et ultrices neque ornare aenean euismod.
|
||||||
|
|
||||||
|
Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Viverra nibh cras pulvinar mattis nunc. Lacinia at quis risus sed vulputate. Ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus. Bibendum arcu vitae elementum curabitur vitae nunc. Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet commodo. Velit egestas dui id ornare arcu odio ut. Et malesuada fames ac turpis egestas integer eget aliquet. Lacus suspendisse faucibus interdum posuere lorem ipsum dolor sit. Morbi tristique senectus et netus. Pretium viverra suspendisse potenti nullam ac tortor vitae. Parturient montes nascetur ridiculus mus mauris vitae. Quis viverra nibh cras pulvinar mattis nunc sed blandit libero. Euismod nisi porta lorem mollis aliquam ut porttitor leo. Mauris in aliquam sem fringilla ut morbi. Faucibus pulvinar elementum integer enim neque. Neque sodales ut etiam sit. Consectetur a erat nam at.
|
||||||
|
|
||||||
|
Sed nisi lacus sed viverra tellus in hac habitasse. Proin sagittis nisl rhoncus mattis rhoncus. Risus commodo viverra maecenas accumsan lacus. Morbi quis commodo odio aenean sed adipiscing. Mollis nunc sed id semper risus in. Ultricies mi eget mauris pharetra et ultrices neque. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Eget magna fermentum iaculis eu non diam phasellus. Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Id donec ultrices tincidunt arcu non sodales. Sed cras ornare arcu dui vivamus arcu felis bibendum ut. Urna duis convallis convallis tellus id interdum velit. Rhoncus mattis rhoncus urna neque viverra justo nec. Purus semper eget duis at tellus at urna condimentum. Et odio pellentesque diam volutpat commodo sed egestas. Blandit volutpat maecenas volutpat blandit. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Est ullamcorper eget nulla facilisi etiam dignissim.
|
||||||
|
|
||||||
|
Justo nec ultrices dui sapien eget mi proin sed. Purus gravida quis blandit turpis cursus in hac. Placerat orci nulla pellentesque dignissim enim sit. Morbi tristique senectus et netus et malesuada fames ac. Consequat mauris nunc congue nisi. Eu lobortis elementum nibh tellus molestie nunc non blandit. Viverra justo nec ultrices dui. Morbi non arcu risus quis. Elementum sagittis vitae et leo duis. Lectus mauris ultrices eros in cursus. Neque laoreet suspendisse interdum consectetur.
|
||||||
|
|
||||||
|
Facilisis gravida neque convallis a cras. Nisl rhoncus mattis rhoncus urna neque viverra justo. Faucibus purus in massa tempor. Lacus laoreet non curabitur gravida arcu ac tortor. Tincidunt eget nullam non nisi est sit amet. Ornare lectus sit amet est placerat in egestas. Sollicitudin tempor id eu nisl nunc mi. Scelerisque viverra mauris in aliquam sem fringilla ut. Ullamcorper sit amet risus nullam. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar. Velit euismod in pellentesque massa placerat duis ultricies lacus. Pharetra magna ac placerat vestibulum lectus mauris ultrices eros in. Lorem ipsum dolor sit amet. Sit amet mauris commodo quis imperdiet. Quam pellentesque nec nam aliquam sem et tortor. Amet nisl purus in mollis nunc. Sed risus pretium quam vulputate dignissim suspendisse in est. Nisl condimentum id venenatis a condimentum. Velit euismod in pellentesque massa. Quam id leo in vitae turpis massa sed.
|
||||||
+18
-13
@@ -38,9 +38,10 @@ describe('Settings', () => {
|
|||||||
// User's message should be visible
|
// User's message should be visible
|
||||||
cy.get('.chat-user').should('exist');
|
cy.get('.chat-user').should('exist');
|
||||||
// Wait for the response
|
// Wait for the response
|
||||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
// .chat-assistant is created after the first token is received
|
||||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
|
||||||
.should('exist');
|
// Generation Info is created after the stop token is received
|
||||||
|
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('user can share chat', () => {
|
it('user can share chat', () => {
|
||||||
@@ -57,21 +58,24 @@ describe('Settings', () => {
|
|||||||
// User's message should be visible
|
// User's message should be visible
|
||||||
cy.get('.chat-user').should('exist');
|
cy.get('.chat-user').should('exist');
|
||||||
// Wait for the response
|
// Wait for the response
|
||||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
// .chat-assistant is created after the first token is received
|
||||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
|
||||||
.should('exist');
|
// Generation Info is created after the stop token is received
|
||||||
|
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
|
||||||
// spy on requests
|
// spy on requests
|
||||||
const spy = cy.spy();
|
const spy = cy.spy();
|
||||||
cy.intercept('GET', '/api/v1/chats/*', spy);
|
cy.intercept('POST', '/api/v1/chats/**/share', spy);
|
||||||
// Open context menu
|
// Open context menu
|
||||||
cy.get('#chat-context-menu-button').click();
|
cy.get('#chat-context-menu-button').click();
|
||||||
// Click share button
|
// Click share button
|
||||||
cy.get('#chat-share-button').click();
|
cy.get('#chat-share-button').click();
|
||||||
// Check if the share dialog is visible
|
// Check if the share dialog is visible
|
||||||
cy.get('#copy-and-share-chat-button').should('exist');
|
cy.get('#copy-and-share-chat-button').should('exist');
|
||||||
cy.wrap({}, { timeout: 5000 }).should(() => {
|
// Click the copy button
|
||||||
// Check if the request was made twice (once for to replace chat object and once more due to change event)
|
cy.get('#copy-and-share-chat-button').click();
|
||||||
expect(spy).to.be.callCount(2);
|
cy.wrap({}, { timeout: 5_000 }).should(() => {
|
||||||
|
// Check if the share request was made
|
||||||
|
expect(spy).to.be.callCount(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,9 +93,10 @@ describe('Settings', () => {
|
|||||||
// User's message should be visible
|
// User's message should be visible
|
||||||
cy.get('.chat-user').should('exist');
|
cy.get('.chat-user').should('exist');
|
||||||
// Wait for the response
|
// Wait for the response
|
||||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
// .chat-assistant is created after the first token is received
|
||||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
|
||||||
.should('exist');
|
// Generation Info is created after the stop token is received
|
||||||
|
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
|
||||||
// Click on the generate image button
|
// Click on the generate image button
|
||||||
cy.get('[aria-label="Generate Image"]').click();
|
cy.get('[aria-label="Generate Image"]').click();
|
||||||
// Wait for image to be visible
|
// Wait for image to be visible
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../support/index.d.ts" />
|
||||||
|
|
||||||
|
describe('Documents', () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.uploadTestDocument(timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.deleteTestDocument(timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Login as the admin user
|
||||||
|
cy.loginAdmin();
|
||||||
|
// Visit the home page
|
||||||
|
cy.visit('/workspace/documents');
|
||||||
|
cy.get('button').contains('#cypress-test').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can see documents', () => {
|
||||||
|
cy.get('div').contains(`document-test-initial-${timestamp}.txt`).should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can see edit button', () => {
|
||||||
|
cy.get('div')
|
||||||
|
.contains(`document-test-initial-${timestamp}.txt`)
|
||||||
|
.get("button[aria-label='Edit Doc']")
|
||||||
|
.should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can see delete button', () => {
|
||||||
|
cy.get('div')
|
||||||
|
.contains(`document-test-initial-${timestamp}.txt`)
|
||||||
|
.get("button[aria-label='Delete Doc']")
|
||||||
|
.should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can see upload button', () => {
|
||||||
|
cy.get("button[aria-label='Add Docs']").should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../support/index.d.ts" />
|
||||||
|
|
||||||
export const adminUser = {
|
export const adminUser = {
|
||||||
name: 'Admin User',
|
name: 'Admin User',
|
||||||
@@ -10,6 +12,9 @@ const login = (email: string, password: string) => {
|
|||||||
return cy.session(
|
return cy.session(
|
||||||
email,
|
email,
|
||||||
() => {
|
() => {
|
||||||
|
// Make sure to test against us english to have stable tests,
|
||||||
|
// regardless on local language preferences
|
||||||
|
localStorage.setItem('locale', 'en-US');
|
||||||
// Visit auth page
|
// Visit auth page
|
||||||
cy.visit('/auth');
|
cy.visit('/auth');
|
||||||
// Fill out the form
|
// Fill out the form
|
||||||
@@ -68,6 +73,50 @@ Cypress.Commands.add('register', (name, email, password) => register(name, email
|
|||||||
Cypress.Commands.add('registerAdmin', () => registerAdmin());
|
Cypress.Commands.add('registerAdmin', () => registerAdmin());
|
||||||
Cypress.Commands.add('loginAdmin', () => loginAdmin());
|
Cypress.Commands.add('loginAdmin', () => loginAdmin());
|
||||||
|
|
||||||
|
Cypress.Commands.add('uploadTestDocument', (suffix: any) => {
|
||||||
|
// Login as admin
|
||||||
|
cy.loginAdmin();
|
||||||
|
// upload example document
|
||||||
|
cy.visit('/workspace/documents');
|
||||||
|
// Create a document
|
||||||
|
cy.get("button[aria-label='Add Docs']").click();
|
||||||
|
cy.readFile('cypress/data/example-doc.txt').then((text) => {
|
||||||
|
// select file
|
||||||
|
cy.get('#upload-doc-input').selectFile(
|
||||||
|
{
|
||||||
|
contents: Cypress.Buffer.from(text + Date.now()),
|
||||||
|
fileName: `document-test-initial-${suffix}.txt`,
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
lastModified: Date.now()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
force: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// open tag input
|
||||||
|
cy.get("button[aria-label='Add Tag']").click();
|
||||||
|
cy.get("input[placeholder='Add a tag']").type('cypress-test');
|
||||||
|
cy.get("button[aria-label='Save Tag']").click();
|
||||||
|
|
||||||
|
// submit to upload
|
||||||
|
cy.get("button[type='submit']").click();
|
||||||
|
|
||||||
|
// wait for upload to finish
|
||||||
|
cy.get('button').contains('#cypress-test').should('exist');
|
||||||
|
cy.get('div').contains(`document-test-initial-${suffix}.txt`).should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('deleteTestDocument', (suffix: any) => {
|
||||||
|
cy.loginAdmin();
|
||||||
|
cy.visit('/workspace/documents');
|
||||||
|
// clean up uploaded documents
|
||||||
|
cy.get('div')
|
||||||
|
.contains(`document-test-initial-${suffix}.txt`)
|
||||||
|
.find("button[aria-label='Delete Doc']")
|
||||||
|
.click();
|
||||||
|
});
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.registerAdmin();
|
cy.registerAdmin();
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+2
@@ -7,5 +7,7 @@ declare namespace Cypress {
|
|||||||
register(name: string, email: string, password: string): Chainable<Element>;
|
register(name: string, email: string, password: string): Chainable<Element>;
|
||||||
registerAdmin(): Chainable<Element>;
|
registerAdmin(): Chainable<Element>;
|
||||||
loginAdmin(): Chainable<Element>;
|
loginAdmin(): Chainable<Element>;
|
||||||
|
uploadTestDocument(suffix: any): Chainable<Element>;
|
||||||
|
deleteTestDocument(suffix: any): Chainable<Element>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Noticed something off? Have an idea? Check our [Issues tab](https://github.com/o
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
|
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
|
||||||
>
|
|
||||||
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
|
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
|
||||||
|
|
||||||
### 🧭 Scope of Support
|
### 🧭 Scope of Support
|
||||||
|
|||||||
+19
-3
@@ -11,10 +11,26 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord.
|
We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines:
|
||||||
|
|
||||||
|
1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted.
|
||||||
|
|
||||||
|
2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts.
|
||||||
|
|
||||||
|
3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded.
|
||||||
|
|
||||||
|
4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
|
||||||
|
|
||||||
|
5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security.
|
||||||
|
|
||||||
|
Submissions that do not meet these criteria will be closed, and repeat offenders may face a ban from future submissions. We aim to create a respectful and constructive reporting environment, where high-quality submissions foster better security for everyone.
|
||||||
|
|
||||||
## Product Security
|
## Product Security
|
||||||
|
|
||||||
We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques.
|
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
|
||||||
|
|
||||||
We are planning on implementing SAST and SCA scans in our project soon.
|
For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Last updated on **2024-08-06**._
|
||||||
|
|||||||
Generated
+394
-597
File diff suppressed because it is too large
Load Diff
+23
-16
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.3.7",
|
"version": "0.3.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
@@ -20,30 +20,31 @@
|
|||||||
"pyodide:fetch": "node scripts/prepare-pyodide.js"
|
"pyodide:fetch": "node scripts/prepare-pyodide.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "3.2.2",
|
||||||
"@sveltejs/adapter-static": "^2.0.3",
|
"@sveltejs/adapter-static": "^3.0.2",
|
||||||
"@sveltejs/kit": "^1.30.0",
|
"@sveltejs/kit": "^2.5.20",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/parser": "^6.17.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cypress": "^13.8.1",
|
"cypress": "^13.8.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-cypress": "^3.0.2",
|
"eslint-plugin-cypress": "^3.4.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.43.0",
|
||||||
"i18next-parser": "^8.13.0",
|
"i18next-parser": "^9.0.1",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"svelte": "^4.0.5",
|
"svelte": "^4.2.18",
|
||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.8.5",
|
||||||
"svelte-confetti": "^1.3.2",
|
"svelte-confetti": "^1.3.2",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^4.4.2",
|
"vite": "^5.3.5",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@pyscript/core": "^0.4.32",
|
"@pyscript/core": "^0.4.32",
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^2.0.0",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.19.7",
|
"bits-ui": "^0.19.7",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"eventsource-parser": "^1.1.2",
|
"eventsource-parser": "^1.1.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
@@ -68,6 +70,7 @@
|
|||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
|
"marked-katex-extension": "^5.1.1",
|
||||||
"mermaid": "^10.9.1",
|
"mermaid": "^10.9.1",
|
||||||
"pyodide": "^0.26.1",
|
"pyodide": "^0.26.1",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
@@ -76,5 +79,9 @@
|
|||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.13.0 <=21.x.x",
|
||||||
|
"npm": ">=6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-24
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "open-webui"
|
name = "open-webui"
|
||||||
description = "Open WebUI (Formerly Ollama WebUI)"
|
description = "Open WebUI"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
|
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
|
||||||
]
|
]
|
||||||
@@ -8,67 +8,82 @@ license = { file = "LICENSE" }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"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.10.2",
|
||||||
"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.0",
|
"PyMySQL==1.1.1",
|
||||||
"bcrypt==4.1.3",
|
"bcrypt==4.1.3",
|
||||||
|
|
||||||
"boto3==1.34.110",
|
"pymongo",
|
||||||
|
"redis",
|
||||||
|
"boto3==1.34.153",
|
||||||
|
|
||||||
"argon2-cffi==23.1.0",
|
"argon2-cffi==23.1.0",
|
||||||
"APScheduler==3.10.4",
|
"APScheduler==3.10.4",
|
||||||
"google-generativeai==0.5.4",
|
|
||||||
|
|
||||||
"langchain==0.2.0",
|
"openai",
|
||||||
"langchain-community==0.2.0",
|
"anthropic",
|
||||||
"langchain-chroma==0.1.1",
|
"google-generativeai==0.7.2",
|
||||||
|
"tiktoken",
|
||||||
|
|
||||||
|
"langchain==0.2.12",
|
||||||
|
"langchain-community==0.2.10",
|
||||||
|
"langchain-chroma==0.1.2",
|
||||||
|
|
||||||
"fake-useragent==1.5.1",
|
"fake-useragent==1.5.1",
|
||||||
"chromadb==0.5.0",
|
"chromadb==0.5.5",
|
||||||
"sentence-transformers==2.7.0",
|
"sentence-transformers==3.0.1",
|
||||||
"pypdf==4.2.0",
|
"pypdf==4.3.1",
|
||||||
"docx2txt==0.8",
|
"docx2txt==0.8",
|
||||||
"unstructured==0.14.0",
|
"python-pptx==1.0.0",
|
||||||
|
"unstructured==0.15.0",
|
||||||
"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.33.0",
|
||||||
|
"psutil",
|
||||||
|
|
||||||
"opencv-python-headless==4.9.0.80",
|
"opencv-python-headless==4.10.0.84",
|
||||||
"rapidocr-onnxruntime==1.3.22",
|
"rapidocr-onnxruntime==1.3.24",
|
||||||
|
|
||||||
"fpdf2==2.7.9",
|
"fpdf2==2.7.9",
|
||||||
"rank-bm25==0.2.2",
|
"rank-bm25==0.2.2",
|
||||||
|
|
||||||
"faster-whisper==1.0.2",
|
"faster-whisper==1.0.2",
|
||||||
|
|
||||||
"PyJWT[crypto]==2.8.0",
|
"PyJWT[crypto]==2.9.0",
|
||||||
"authlib==1.3.1",
|
"authlib==1.3.1",
|
||||||
|
|
||||||
"black==24.4.2",
|
"black==24.8.0",
|
||||||
"langfuse==2.33.0",
|
"langfuse==2.43.3",
|
||||||
"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.2.1",
|
||||||
|
|
||||||
|
"docker~=7.1.0",
|
||||||
|
"pytest~=8.2.2",
|
||||||
|
"pytest-docker~=3.1.1"
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.11, < 3.12.0a1"
|
requires-python = ">= 3.11, < 3.12.0a1"
|
||||||
|
|||||||
+108
-44
@@ -7,18 +7,28 @@
|
|||||||
# 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
|
aiohappyeyeballs==2.3.5
|
||||||
|
# via aiohttp
|
||||||
|
aiohttp==3.10.2
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via open-webui
|
# via open-webui
|
||||||
aiosignal==1.3.1
|
aiosignal==1.3.1
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
|
alembic==1.13.2
|
||||||
|
# via open-webui
|
||||||
annotated-types==0.6.0
|
annotated-types==0.6.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
anyio==4.3.0
|
anthropic==0.32.0
|
||||||
|
# via open-webui
|
||||||
|
anyio==4.4.0
|
||||||
|
# via anthropic
|
||||||
# via httpx
|
# via httpx
|
||||||
|
# via langfuse
|
||||||
|
# via openai
|
||||||
# via starlette
|
# via starlette
|
||||||
# via watchfiles
|
# via watchfiles
|
||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
@@ -31,7 +41,8 @@ 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
|
# via pytest-docker
|
||||||
|
authlib==1.3.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
av==11.0.0
|
av==11.0.0
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
@@ -48,13 +59,13 @@ beautifulsoup4==4.12.3
|
|||||||
# via unstructured
|
# via unstructured
|
||||||
bidict==0.23.1
|
bidict==0.23.1
|
||||||
# via python-socketio
|
# via python-socketio
|
||||||
black==24.4.2
|
black==24.8.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
blinker==1.8.2
|
blinker==1.8.2
|
||||||
# via flask
|
# via flask
|
||||||
boto3==1.34.110
|
boto3==1.34.153
|
||||||
# via open-webui
|
# via open-webui
|
||||||
botocore==1.34.110
|
botocore==1.34.155
|
||||||
# via boto3
|
# via boto3
|
||||||
# via s3transfer
|
# via s3transfer
|
||||||
build==1.2.1
|
build==1.2.1
|
||||||
@@ -75,9 +86,9 @@ chardet==5.2.0
|
|||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
# via requests
|
# via requests
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
chroma-hnswlib==0.7.3
|
chroma-hnswlib==0.7.6
|
||||||
# via chromadb
|
# via chromadb
|
||||||
chromadb==0.5.0
|
chromadb==0.5.5
|
||||||
# via langchain-chroma
|
# via langchain-chroma
|
||||||
# via open-webui
|
# via open-webui
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
@@ -101,7 +112,6 @@ cryptography==42.0.7
|
|||||||
ctranslate2==4.2.1
|
ctranslate2==4.2.1
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
dataclasses-json==0.6.6
|
dataclasses-json==0.6.6
|
||||||
# via langchain
|
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via unstructured
|
# via unstructured
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
@@ -112,11 +122,17 @@ defusedxml==0.7.1
|
|||||||
deprecated==1.2.14
|
deprecated==1.2.14
|
||||||
# via opentelemetry-api
|
# via opentelemetry-api
|
||||||
# via opentelemetry-exporter-otlp-proto-grpc
|
# via opentelemetry-exporter-otlp-proto-grpc
|
||||||
|
distro==1.9.0
|
||||||
|
# via anthropic
|
||||||
|
# via openai
|
||||||
dnspython==2.6.1
|
dnspython==2.6.1
|
||||||
# via email-validator
|
# via email-validator
|
||||||
|
# via pymongo
|
||||||
|
docker==7.1.0
|
||||||
|
# via open-webui
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
# via open-webui
|
# via open-webui
|
||||||
duckduckgo-search==6.1.5
|
duckduckgo-search==6.2.6
|
||||||
# via open-webui
|
# via open-webui
|
||||||
easygui==0.98.3
|
easygui==0.98.3
|
||||||
# via oletools
|
# via oletools
|
||||||
@@ -165,7 +181,7 @@ frozenlist==1.4.1
|
|||||||
fsspec==2024.3.1
|
fsspec==2024.3.1
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
# via torch
|
# via torch
|
||||||
google-ai-generativelanguage==0.6.4
|
google-ai-generativelanguage==0.6.6
|
||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
google-api-core==2.19.0
|
google-api-core==2.19.0
|
||||||
# via google-ai-generativelanguage
|
# via google-ai-generativelanguage
|
||||||
@@ -182,7 +198,7 @@ google-auth==2.29.0
|
|||||||
# via kubernetes
|
# via kubernetes
|
||||||
google-auth-httplib2==0.2.0
|
google-auth-httplib2==0.2.0
|
||||||
# via google-api-python-client
|
# via google-api-python-client
|
||||||
google-generativeai==0.5.4
|
google-generativeai==0.7.2
|
||||||
# via open-webui
|
# via open-webui
|
||||||
googleapis-common-protos==1.63.0
|
googleapis-common-protos==1.63.0
|
||||||
# via google-api-core
|
# via google-api-core
|
||||||
@@ -207,8 +223,11 @@ httplib2==0.22.0
|
|||||||
httptools==0.6.1
|
httptools==0.6.1
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
|
# via anthropic
|
||||||
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via langfuse
|
# via langfuse
|
||||||
|
# via openai
|
||||||
huggingface-hub==0.23.0
|
huggingface-hub==0.23.0
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
@@ -228,12 +247,16 @@ importlib-metadata==7.0.0
|
|||||||
# via opentelemetry-api
|
# via opentelemetry-api
|
||||||
importlib-resources==6.4.0
|
importlib-resources==6.4.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
|
iniconfig==2.0.0
|
||||||
|
# via pytest
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
# via flask
|
# via flask
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via flask
|
# via flask
|
||||||
# via torch
|
# via torch
|
||||||
|
jiter==0.5.0
|
||||||
|
# via anthropic
|
||||||
jmespath==1.0.1
|
jmespath==1.0.1
|
||||||
# via boto3
|
# via boto3
|
||||||
# via botocore
|
# via botocore
|
||||||
@@ -248,14 +271,14 @@ jsonpointer==2.4
|
|||||||
# via jsonpatch
|
# via jsonpatch
|
||||||
kubernetes==29.0.0
|
kubernetes==29.0.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
langchain==0.2.0
|
langchain==0.2.12
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langchain-chroma==0.1.1
|
langchain-chroma==0.1.2
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langchain-community==0.2.0
|
langchain-community==0.2.10
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langchain-core==0.2.1
|
langchain-core==0.2.28
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-chroma
|
# via langchain-chroma
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
@@ -264,22 +287,26 @@ langchain-text-splitters==0.2.0
|
|||||||
# via langchain
|
# via langchain
|
||||||
langdetect==1.0.9
|
langdetect==1.0.9
|
||||||
# via unstructured
|
# via unstructured
|
||||||
langfuse==2.33.0
|
langfuse==2.43.3
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langsmith==0.1.57
|
langsmith==0.1.96
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via langchain-core
|
# via langchain-core
|
||||||
lark==1.1.8
|
lark==1.1.8
|
||||||
# via rtfde
|
# via rtfde
|
||||||
lxml==5.2.2
|
lxml==5.2.2
|
||||||
|
# via python-pptx
|
||||||
# via unstructured
|
# via unstructured
|
||||||
|
mako==1.3.5
|
||||||
|
# via alembic
|
||||||
markdown==3.6
|
markdown==3.6
|
||||||
# via open-webui
|
# via open-webui
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
# via rich
|
# via rich
|
||||||
markupsafe==2.1.5
|
markupsafe==2.1.5
|
||||||
# via jinja2
|
# via jinja2
|
||||||
|
# via mako
|
||||||
# via werkzeug
|
# via werkzeug
|
||||||
marshmallow==3.21.2
|
marshmallow==3.21.2
|
||||||
# via dataclasses-json
|
# via dataclasses-json
|
||||||
@@ -338,11 +365,13 @@ onnxruntime==1.17.3
|
|||||||
# via chromadb
|
# via chromadb
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
|
openai==1.38.0
|
||||||
|
# via open-webui
|
||||||
opencv-python==4.9.0.80
|
opencv-python==4.9.0.80
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
opencv-python-headless==4.9.0.80
|
opencv-python-headless==4.10.0.84
|
||||||
# via open-webui
|
# via open-webui
|
||||||
openpyxl==3.1.2
|
openpyxl==3.1.5
|
||||||
# via open-webui
|
# via open-webui
|
||||||
opentelemetry-api==1.24.0
|
opentelemetry-api==1.24.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
@@ -379,7 +408,6 @@ ordered-set==4.1.0
|
|||||||
# via deepdiff
|
# via deepdiff
|
||||||
orjson==3.10.3
|
orjson==3.10.3
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via duckduckgo-search
|
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via langsmith
|
# via langsmith
|
||||||
overrides==7.7.0
|
overrides==7.7.0
|
||||||
@@ -392,30 +420,35 @@ packaging==23.2
|
|||||||
# via langfuse
|
# via langfuse
|
||||||
# via marshmallow
|
# via marshmallow
|
||||||
# via onnxruntime
|
# via onnxruntime
|
||||||
|
# via pytest
|
||||||
# via transformers
|
# via transformers
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
pandas==2.2.2
|
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
|
||||||
# via oletools
|
# via oletools
|
||||||
peewee==3.17.5
|
peewee==3.17.6
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via peewee-migrate
|
# via peewee-migrate
|
||||||
peewee-migrate==1.12.2
|
peewee-migrate==1.12.2
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pillow==10.3.0
|
pillow==10.3.0
|
||||||
# via fpdf2
|
# via fpdf2
|
||||||
|
# via python-pptx
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
platformdirs==4.2.1
|
platformdirs==4.2.1
|
||||||
# via black
|
# via black
|
||||||
|
pluggy==1.5.0
|
||||||
|
# via pytest
|
||||||
posthog==3.5.0
|
posthog==3.5.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
|
primp==0.5.5
|
||||||
|
# via duckduckgo-search
|
||||||
proto-plus==1.23.0
|
proto-plus==1.23.0
|
||||||
# via google-ai-generativelanguage
|
# via google-ai-generativelanguage
|
||||||
# via google-api-core
|
# via google-api-core
|
||||||
@@ -428,6 +461,9 @@ protobuf==4.25.3
|
|||||||
# via onnxruntime
|
# via onnxruntime
|
||||||
# via opentelemetry-proto
|
# via opentelemetry-proto
|
||||||
# via proto-plus
|
# via proto-plus
|
||||||
|
psutil==6.0.0
|
||||||
|
# via open-webui
|
||||||
|
# via unstructured
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pyasn1==0.6.0
|
pyasn1==0.6.0
|
||||||
@@ -440,7 +476,8 @@ pyclipper==1.3.0.post5
|
|||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.7.1
|
pydantic==2.8.2
|
||||||
|
# via anthropic
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
@@ -449,31 +486,36 @@ pydantic==2.7.1
|
|||||||
# via langfuse
|
# via langfuse
|
||||||
# via langsmith
|
# via langsmith
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pydantic-core==2.18.2
|
# via openai
|
||||||
|
pydantic-core==2.20.1
|
||||||
# via pydantic
|
# via pydantic
|
||||||
pydub==0.25.1
|
pydub==0.25.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pygments==2.18.0
|
pygments==2.18.0
|
||||||
# via rich
|
# via rich
|
||||||
pyjwt==2.8.0
|
pyjwt==2.9.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via pyjwt
|
pymongo==4.8.0
|
||||||
pymysql==1.1.0
|
# via open-webui
|
||||||
|
pymysql==1.1.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pypandoc==1.13
|
pypandoc==1.13
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
# via httplib2
|
# via httplib2
|
||||||
# via oletools
|
# via oletools
|
||||||
pypdf==4.2.0
|
pypdf==4.3.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
pypika==0.48.9
|
pypika==0.48.9
|
||||||
# via chromadb
|
# via chromadb
|
||||||
pyproject-hooks==1.1.0
|
pyproject-hooks==1.1.0
|
||||||
# via build
|
# via build
|
||||||
pyreqwest-impersonate==0.4.7
|
pytest==8.2.2
|
||||||
# via duckduckgo-search
|
# via open-webui
|
||||||
|
# via pytest-docker
|
||||||
|
pytest-docker==3.1.1
|
||||||
|
# via open-webui
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via botocore
|
# via botocore
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
@@ -493,7 +535,9 @@ python-magic==0.4.27
|
|||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via open-webui
|
# via open-webui
|
||||||
python-socketio==5.11.2
|
python-pptx==1.0.0
|
||||||
|
# via open-webui
|
||||||
|
python-socketio==5.11.3
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
@@ -517,15 +561,18 @@ rank-bm25==0.2.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
rapidfuzz==3.9.0
|
rapidfuzz==3.9.0
|
||||||
# via unstructured
|
# via unstructured
|
||||||
rapidocr-onnxruntime==1.3.22
|
rapidocr-onnxruntime==1.3.24
|
||||||
# via open-webui
|
# via open-webui
|
||||||
red-black-tree-mod==1.20
|
red-black-tree-mod==1.20
|
||||||
# via extract-msg
|
# via extract-msg
|
||||||
|
redis==5.0.8
|
||||||
|
# via open-webui
|
||||||
regex==2024.5.10
|
regex==2024.5.10
|
||||||
# via nltk
|
# via nltk
|
||||||
|
# via tiktoken
|
||||||
# via transformers
|
# via transformers
|
||||||
requests==2.32.2
|
requests==2.32.3
|
||||||
# via chromadb
|
# via docker
|
||||||
# via google-api-core
|
# via google-api-core
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
@@ -535,6 +582,7 @@ requests==2.32.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
# via posthog
|
# via posthog
|
||||||
# via requests-oauthlib
|
# via requests-oauthlib
|
||||||
|
# via tiktoken
|
||||||
# via transformers
|
# via transformers
|
||||||
# via unstructured
|
# via unstructured
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
@@ -557,9 +605,12 @@ scikit-learn==1.4.2
|
|||||||
scipy==1.13.0
|
scipy==1.13.0
|
||||||
# via scikit-learn
|
# via scikit-learn
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
sentence-transformers==2.7.0
|
sentence-transformers==3.0.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
shapely==2.0.4
|
setuptools==69.5.1
|
||||||
|
# via ctranslate2
|
||||||
|
# via opentelemetry-instrumentation
|
||||||
|
shapely==2.0.5
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
# via typer
|
# via typer
|
||||||
@@ -575,13 +626,17 @@ six==1.16.0
|
|||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
|
# via anthropic
|
||||||
# via anyio
|
# via anyio
|
||||||
# via httpx
|
# via httpx
|
||||||
|
# via openai
|
||||||
soupsieve==2.5
|
soupsieve==2.5
|
||||||
# via beautifulsoup4
|
# via beautifulsoup4
|
||||||
sqlalchemy==2.0.30
|
sqlalchemy==2.0.31
|
||||||
|
# via alembic
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
|
# via open-webui
|
||||||
starlette==0.37.2
|
starlette==0.37.2
|
||||||
# via fastapi
|
# via fastapi
|
||||||
sympy==1.12
|
sympy==1.12
|
||||||
@@ -596,7 +651,10 @@ tenacity==8.3.0
|
|||||||
# via langchain-core
|
# via langchain-core
|
||||||
threadpoolctl==3.5.0
|
threadpoolctl==3.5.0
|
||||||
# via scikit-learn
|
# via scikit-learn
|
||||||
|
tiktoken==0.7.0
|
||||||
|
# via open-webui
|
||||||
tokenizers==0.15.2
|
tokenizers==0.15.2
|
||||||
|
# via anthropic
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
# via transformers
|
# via transformers
|
||||||
@@ -607,21 +665,28 @@ tqdm==4.66.4
|
|||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
# via nltk
|
# via nltk
|
||||||
|
# via openai
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
# via transformers
|
# via transformers
|
||||||
|
# via unstructured
|
||||||
transformers==4.39.3
|
transformers==4.39.3
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
typer==0.12.3
|
typer==0.12.3
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi-cli
|
# via fastapi-cli
|
||||||
typing-extensions==4.11.0
|
typing-extensions==4.11.0
|
||||||
|
# via alembic
|
||||||
|
# via anthropic
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
|
# via langchain-core
|
||||||
|
# via openai
|
||||||
# via opentelemetry-sdk
|
# via opentelemetry-sdk
|
||||||
# via pydantic
|
# via pydantic
|
||||||
# via pydantic-core
|
# via pydantic-core
|
||||||
|
# via python-pptx
|
||||||
# via sqlalchemy
|
# via sqlalchemy
|
||||||
# via torch
|
# via torch
|
||||||
# via typer
|
# via typer
|
||||||
@@ -638,7 +703,7 @@ tzlocal==5.2
|
|||||||
# via extract-msg
|
# via extract-msg
|
||||||
ujson==5.10.0
|
ujson==5.10.0
|
||||||
# via fastapi
|
# via fastapi
|
||||||
unstructured==0.14.0
|
unstructured==0.15.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
unstructured-client==0.22.0
|
unstructured-client==0.22.0
|
||||||
# via unstructured
|
# via unstructured
|
||||||
@@ -646,6 +711,7 @@ uritemplate==4.1.1
|
|||||||
# via google-api-python-client
|
# via google-api-python-client
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
# via botocore
|
# via botocore
|
||||||
|
# via docker
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
# via requests
|
# via requests
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
@@ -653,10 +719,9 @@ 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.33.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
watchfiles==0.21.0
|
watchfiles==0.21.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
@@ -675,12 +740,11 @@ wsproto==1.2.0
|
|||||||
# via simple-websocket
|
# via simple-websocket
|
||||||
xlrd==2.0.1
|
xlrd==2.0.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
|
xlsxwriter==3.2.0
|
||||||
|
# via python-pptx
|
||||||
yarl==1.9.4
|
yarl==1.9.4
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
youtube-transcript-api==0.6.2
|
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
|
|
||||||
|
|||||||
+108
-44
@@ -7,18 +7,28 @@
|
|||||||
# 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
|
aiohappyeyeballs==2.3.5
|
||||||
|
# via aiohttp
|
||||||
|
aiohttp==3.10.2
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via open-webui
|
# via open-webui
|
||||||
aiosignal==1.3.1
|
aiosignal==1.3.1
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
|
alembic==1.13.2
|
||||||
|
# via open-webui
|
||||||
annotated-types==0.6.0
|
annotated-types==0.6.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
anyio==4.3.0
|
anthropic==0.32.0
|
||||||
|
# via open-webui
|
||||||
|
anyio==4.4.0
|
||||||
|
# via anthropic
|
||||||
# via httpx
|
# via httpx
|
||||||
|
# via langfuse
|
||||||
|
# via openai
|
||||||
# via starlette
|
# via starlette
|
||||||
# via watchfiles
|
# via watchfiles
|
||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
@@ -31,7 +41,8 @@ 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
|
# via pytest-docker
|
||||||
|
authlib==1.3.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
av==11.0.0
|
av==11.0.0
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
@@ -48,13 +59,13 @@ beautifulsoup4==4.12.3
|
|||||||
# via unstructured
|
# via unstructured
|
||||||
bidict==0.23.1
|
bidict==0.23.1
|
||||||
# via python-socketio
|
# via python-socketio
|
||||||
black==24.4.2
|
black==24.8.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
blinker==1.8.2
|
blinker==1.8.2
|
||||||
# via flask
|
# via flask
|
||||||
boto3==1.34.110
|
boto3==1.34.153
|
||||||
# via open-webui
|
# via open-webui
|
||||||
botocore==1.34.110
|
botocore==1.34.155
|
||||||
# via boto3
|
# via boto3
|
||||||
# via s3transfer
|
# via s3transfer
|
||||||
build==1.2.1
|
build==1.2.1
|
||||||
@@ -75,9 +86,9 @@ chardet==5.2.0
|
|||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
# via requests
|
# via requests
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
chroma-hnswlib==0.7.3
|
chroma-hnswlib==0.7.6
|
||||||
# via chromadb
|
# via chromadb
|
||||||
chromadb==0.5.0
|
chromadb==0.5.5
|
||||||
# via langchain-chroma
|
# via langchain-chroma
|
||||||
# via open-webui
|
# via open-webui
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
@@ -101,7 +112,6 @@ cryptography==42.0.7
|
|||||||
ctranslate2==4.2.1
|
ctranslate2==4.2.1
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
dataclasses-json==0.6.6
|
dataclasses-json==0.6.6
|
||||||
# via langchain
|
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via unstructured
|
# via unstructured
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
@@ -112,11 +122,17 @@ defusedxml==0.7.1
|
|||||||
deprecated==1.2.14
|
deprecated==1.2.14
|
||||||
# via opentelemetry-api
|
# via opentelemetry-api
|
||||||
# via opentelemetry-exporter-otlp-proto-grpc
|
# via opentelemetry-exporter-otlp-proto-grpc
|
||||||
|
distro==1.9.0
|
||||||
|
# via anthropic
|
||||||
|
# via openai
|
||||||
dnspython==2.6.1
|
dnspython==2.6.1
|
||||||
# via email-validator
|
# via email-validator
|
||||||
|
# via pymongo
|
||||||
|
docker==7.1.0
|
||||||
|
# via open-webui
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
# via open-webui
|
# via open-webui
|
||||||
duckduckgo-search==6.1.5
|
duckduckgo-search==6.2.6
|
||||||
# via open-webui
|
# via open-webui
|
||||||
easygui==0.98.3
|
easygui==0.98.3
|
||||||
# via oletools
|
# via oletools
|
||||||
@@ -165,7 +181,7 @@ frozenlist==1.4.1
|
|||||||
fsspec==2024.3.1
|
fsspec==2024.3.1
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
# via torch
|
# via torch
|
||||||
google-ai-generativelanguage==0.6.4
|
google-ai-generativelanguage==0.6.6
|
||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
google-api-core==2.19.0
|
google-api-core==2.19.0
|
||||||
# via google-ai-generativelanguage
|
# via google-ai-generativelanguage
|
||||||
@@ -182,7 +198,7 @@ google-auth==2.29.0
|
|||||||
# via kubernetes
|
# via kubernetes
|
||||||
google-auth-httplib2==0.2.0
|
google-auth-httplib2==0.2.0
|
||||||
# via google-api-python-client
|
# via google-api-python-client
|
||||||
google-generativeai==0.5.4
|
google-generativeai==0.7.2
|
||||||
# via open-webui
|
# via open-webui
|
||||||
googleapis-common-protos==1.63.0
|
googleapis-common-protos==1.63.0
|
||||||
# via google-api-core
|
# via google-api-core
|
||||||
@@ -207,8 +223,11 @@ httplib2==0.22.0
|
|||||||
httptools==0.6.1
|
httptools==0.6.1
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
|
# via anthropic
|
||||||
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via langfuse
|
# via langfuse
|
||||||
|
# via openai
|
||||||
huggingface-hub==0.23.0
|
huggingface-hub==0.23.0
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
@@ -228,12 +247,16 @@ importlib-metadata==7.0.0
|
|||||||
# via opentelemetry-api
|
# via opentelemetry-api
|
||||||
importlib-resources==6.4.0
|
importlib-resources==6.4.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
|
iniconfig==2.0.0
|
||||||
|
# via pytest
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
# via flask
|
# via flask
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via flask
|
# via flask
|
||||||
# via torch
|
# via torch
|
||||||
|
jiter==0.5.0
|
||||||
|
# via anthropic
|
||||||
jmespath==1.0.1
|
jmespath==1.0.1
|
||||||
# via boto3
|
# via boto3
|
||||||
# via botocore
|
# via botocore
|
||||||
@@ -248,14 +271,14 @@ jsonpointer==2.4
|
|||||||
# via jsonpatch
|
# via jsonpatch
|
||||||
kubernetes==29.0.0
|
kubernetes==29.0.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
langchain==0.2.0
|
langchain==0.2.12
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langchain-chroma==0.1.1
|
langchain-chroma==0.1.2
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langchain-community==0.2.0
|
langchain-community==0.2.10
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langchain-core==0.2.1
|
langchain-core==0.2.28
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-chroma
|
# via langchain-chroma
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
@@ -264,22 +287,26 @@ langchain-text-splitters==0.2.0
|
|||||||
# via langchain
|
# via langchain
|
||||||
langdetect==1.0.9
|
langdetect==1.0.9
|
||||||
# via unstructured
|
# via unstructured
|
||||||
langfuse==2.33.0
|
langfuse==2.43.3
|
||||||
# via open-webui
|
# via open-webui
|
||||||
langsmith==0.1.57
|
langsmith==0.1.96
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
# via langchain-core
|
# via langchain-core
|
||||||
lark==1.1.8
|
lark==1.1.8
|
||||||
# via rtfde
|
# via rtfde
|
||||||
lxml==5.2.2
|
lxml==5.2.2
|
||||||
|
# via python-pptx
|
||||||
# via unstructured
|
# via unstructured
|
||||||
|
mako==1.3.5
|
||||||
|
# via alembic
|
||||||
markdown==3.6
|
markdown==3.6
|
||||||
# via open-webui
|
# via open-webui
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
# via rich
|
# via rich
|
||||||
markupsafe==2.1.5
|
markupsafe==2.1.5
|
||||||
# via jinja2
|
# via jinja2
|
||||||
|
# via mako
|
||||||
# via werkzeug
|
# via werkzeug
|
||||||
marshmallow==3.21.2
|
marshmallow==3.21.2
|
||||||
# via dataclasses-json
|
# via dataclasses-json
|
||||||
@@ -338,11 +365,13 @@ onnxruntime==1.17.3
|
|||||||
# via chromadb
|
# via chromadb
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
|
openai==1.38.0
|
||||||
|
# via open-webui
|
||||||
opencv-python==4.9.0.80
|
opencv-python==4.9.0.80
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
opencv-python-headless==4.9.0.80
|
opencv-python-headless==4.10.0.84
|
||||||
# via open-webui
|
# via open-webui
|
||||||
openpyxl==3.1.2
|
openpyxl==3.1.5
|
||||||
# via open-webui
|
# via open-webui
|
||||||
opentelemetry-api==1.24.0
|
opentelemetry-api==1.24.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
@@ -379,7 +408,6 @@ ordered-set==4.1.0
|
|||||||
# via deepdiff
|
# via deepdiff
|
||||||
orjson==3.10.3
|
orjson==3.10.3
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via duckduckgo-search
|
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via langsmith
|
# via langsmith
|
||||||
overrides==7.7.0
|
overrides==7.7.0
|
||||||
@@ -392,30 +420,35 @@ packaging==23.2
|
|||||||
# via langfuse
|
# via langfuse
|
||||||
# via marshmallow
|
# via marshmallow
|
||||||
# via onnxruntime
|
# via onnxruntime
|
||||||
|
# via pytest
|
||||||
# via transformers
|
# via transformers
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
pandas==2.2.2
|
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
|
||||||
# via oletools
|
# via oletools
|
||||||
peewee==3.17.5
|
peewee==3.17.6
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via peewee-migrate
|
# via peewee-migrate
|
||||||
peewee-migrate==1.12.2
|
peewee-migrate==1.12.2
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pillow==10.3.0
|
pillow==10.3.0
|
||||||
# via fpdf2
|
# via fpdf2
|
||||||
|
# via python-pptx
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
platformdirs==4.2.1
|
platformdirs==4.2.1
|
||||||
# via black
|
# via black
|
||||||
|
pluggy==1.5.0
|
||||||
|
# via pytest
|
||||||
posthog==3.5.0
|
posthog==3.5.0
|
||||||
# via chromadb
|
# via chromadb
|
||||||
|
primp==0.5.5
|
||||||
|
# via duckduckgo-search
|
||||||
proto-plus==1.23.0
|
proto-plus==1.23.0
|
||||||
# via google-ai-generativelanguage
|
# via google-ai-generativelanguage
|
||||||
# via google-api-core
|
# via google-api-core
|
||||||
@@ -428,6 +461,9 @@ protobuf==4.25.3
|
|||||||
# via onnxruntime
|
# via onnxruntime
|
||||||
# via opentelemetry-proto
|
# via opentelemetry-proto
|
||||||
# via proto-plus
|
# via proto-plus
|
||||||
|
psutil==6.0.0
|
||||||
|
# via open-webui
|
||||||
|
# via unstructured
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pyasn1==0.6.0
|
pyasn1==0.6.0
|
||||||
@@ -440,7 +476,8 @@ pyclipper==1.3.0.post5
|
|||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.7.1
|
pydantic==2.8.2
|
||||||
|
# via anthropic
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
@@ -449,31 +486,36 @@ pydantic==2.7.1
|
|||||||
# via langfuse
|
# via langfuse
|
||||||
# via langsmith
|
# via langsmith
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pydantic-core==2.18.2
|
# via openai
|
||||||
|
pydantic-core==2.20.1
|
||||||
# via pydantic
|
# via pydantic
|
||||||
pydub==0.25.1
|
pydub==0.25.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pygments==2.18.0
|
pygments==2.18.0
|
||||||
# via rich
|
# via rich
|
||||||
pyjwt==2.8.0
|
pyjwt==2.9.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via pyjwt
|
pymongo==4.8.0
|
||||||
pymysql==1.1.0
|
# via open-webui
|
||||||
|
pymysql==1.1.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pypandoc==1.13
|
pypandoc==1.13
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
# via httplib2
|
# via httplib2
|
||||||
# via oletools
|
# via oletools
|
||||||
pypdf==4.2.0
|
pypdf==4.3.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
pypika==0.48.9
|
pypika==0.48.9
|
||||||
# via chromadb
|
# via chromadb
|
||||||
pyproject-hooks==1.1.0
|
pyproject-hooks==1.1.0
|
||||||
# via build
|
# via build
|
||||||
pyreqwest-impersonate==0.4.7
|
pytest==8.2.2
|
||||||
# via duckduckgo-search
|
# via open-webui
|
||||||
|
# via pytest-docker
|
||||||
|
pytest-docker==3.1.1
|
||||||
|
# via open-webui
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via botocore
|
# via botocore
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
@@ -493,7 +535,9 @@ python-magic==0.4.27
|
|||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via open-webui
|
# via open-webui
|
||||||
python-socketio==5.11.2
|
python-pptx==1.0.0
|
||||||
|
# via open-webui
|
||||||
|
python-socketio==5.11.3
|
||||||
# via open-webui
|
# via open-webui
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
@@ -517,15 +561,18 @@ rank-bm25==0.2.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
rapidfuzz==3.9.0
|
rapidfuzz==3.9.0
|
||||||
# via unstructured
|
# via unstructured
|
||||||
rapidocr-onnxruntime==1.3.22
|
rapidocr-onnxruntime==1.3.24
|
||||||
# via open-webui
|
# via open-webui
|
||||||
red-black-tree-mod==1.20
|
red-black-tree-mod==1.20
|
||||||
# via extract-msg
|
# via extract-msg
|
||||||
|
redis==5.0.8
|
||||||
|
# via open-webui
|
||||||
regex==2024.5.10
|
regex==2024.5.10
|
||||||
# via nltk
|
# via nltk
|
||||||
|
# via tiktoken
|
||||||
# via transformers
|
# via transformers
|
||||||
requests==2.32.2
|
requests==2.32.3
|
||||||
# via chromadb
|
# via docker
|
||||||
# via google-api-core
|
# via google-api-core
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
@@ -535,6 +582,7 @@ requests==2.32.2
|
|||||||
# via open-webui
|
# via open-webui
|
||||||
# via posthog
|
# via posthog
|
||||||
# via requests-oauthlib
|
# via requests-oauthlib
|
||||||
|
# via tiktoken
|
||||||
# via transformers
|
# via transformers
|
||||||
# via unstructured
|
# via unstructured
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
@@ -557,9 +605,12 @@ scikit-learn==1.4.2
|
|||||||
scipy==1.13.0
|
scipy==1.13.0
|
||||||
# via scikit-learn
|
# via scikit-learn
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
sentence-transformers==2.7.0
|
sentence-transformers==3.0.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
shapely==2.0.4
|
setuptools==69.5.1
|
||||||
|
# via ctranslate2
|
||||||
|
# via opentelemetry-instrumentation
|
||||||
|
shapely==2.0.5
|
||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
# via typer
|
# via typer
|
||||||
@@ -575,13 +626,17 @@ six==1.16.0
|
|||||||
# via rapidocr-onnxruntime
|
# via rapidocr-onnxruntime
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
|
# via anthropic
|
||||||
# via anyio
|
# via anyio
|
||||||
# via httpx
|
# via httpx
|
||||||
|
# via openai
|
||||||
soupsieve==2.5
|
soupsieve==2.5
|
||||||
# via beautifulsoup4
|
# via beautifulsoup4
|
||||||
sqlalchemy==2.0.30
|
sqlalchemy==2.0.31
|
||||||
|
# via alembic
|
||||||
# via langchain
|
# via langchain
|
||||||
# via langchain-community
|
# via langchain-community
|
||||||
|
# via open-webui
|
||||||
starlette==0.37.2
|
starlette==0.37.2
|
||||||
# via fastapi
|
# via fastapi
|
||||||
sympy==1.12
|
sympy==1.12
|
||||||
@@ -596,7 +651,10 @@ tenacity==8.3.0
|
|||||||
# via langchain-core
|
# via langchain-core
|
||||||
threadpoolctl==3.5.0
|
threadpoolctl==3.5.0
|
||||||
# via scikit-learn
|
# via scikit-learn
|
||||||
|
tiktoken==0.7.0
|
||||||
|
# via open-webui
|
||||||
tokenizers==0.15.2
|
tokenizers==0.15.2
|
||||||
|
# via anthropic
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via faster-whisper
|
# via faster-whisper
|
||||||
# via transformers
|
# via transformers
|
||||||
@@ -607,21 +665,28 @@ tqdm==4.66.4
|
|||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
# via nltk
|
# via nltk
|
||||||
|
# via openai
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
# via transformers
|
# via transformers
|
||||||
|
# via unstructured
|
||||||
transformers==4.39.3
|
transformers==4.39.3
|
||||||
# via sentence-transformers
|
# via sentence-transformers
|
||||||
typer==0.12.3
|
typer==0.12.3
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi-cli
|
# via fastapi-cli
|
||||||
typing-extensions==4.11.0
|
typing-extensions==4.11.0
|
||||||
|
# via alembic
|
||||||
|
# via anthropic
|
||||||
# via chromadb
|
# via chromadb
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via google-generativeai
|
# via google-generativeai
|
||||||
# via huggingface-hub
|
# via huggingface-hub
|
||||||
|
# via langchain-core
|
||||||
|
# via openai
|
||||||
# via opentelemetry-sdk
|
# via opentelemetry-sdk
|
||||||
# via pydantic
|
# via pydantic
|
||||||
# via pydantic-core
|
# via pydantic-core
|
||||||
|
# via python-pptx
|
||||||
# via sqlalchemy
|
# via sqlalchemy
|
||||||
# via torch
|
# via torch
|
||||||
# via typer
|
# via typer
|
||||||
@@ -638,7 +703,7 @@ tzlocal==5.2
|
|||||||
# via extract-msg
|
# via extract-msg
|
||||||
ujson==5.10.0
|
ujson==5.10.0
|
||||||
# via fastapi
|
# via fastapi
|
||||||
unstructured==0.14.0
|
unstructured==0.15.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
unstructured-client==0.22.0
|
unstructured-client==0.22.0
|
||||||
# via unstructured
|
# via unstructured
|
||||||
@@ -646,6 +711,7 @@ uritemplate==4.1.1
|
|||||||
# via google-api-python-client
|
# via google-api-python-client
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
# via botocore
|
# via botocore
|
||||||
|
# via docker
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
# via requests
|
# via requests
|
||||||
# via unstructured-client
|
# via unstructured-client
|
||||||
@@ -653,10 +719,9 @@ 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.33.0
|
||||||
# via open-webui
|
# via open-webui
|
||||||
watchfiles==0.21.0
|
watchfiles==0.21.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
@@ -675,12 +740,11 @@ wsproto==1.2.0
|
|||||||
# via simple-websocket
|
# via simple-websocket
|
||||||
xlrd==2.0.1
|
xlrd==2.0.1
|
||||||
# via open-webui
|
# via open-webui
|
||||||
|
xlsxwriter==3.2.0
|
||||||
|
# via python-pptx
|
||||||
yarl==1.9.4
|
yarl==1.9.4
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
youtube-transcript-api==0.6.2
|
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
|
|
||||||
|
|||||||
+29
-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,20 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password {
|
||||||
|
-webkit-text-security: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codespan {
|
||||||
|
color: #eb5757;
|
||||||
|
border-width: 0px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 600;
|
||||||
|
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
||||||
|
}
|
||||||
|
|||||||
+13
-8
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,3 +131,59 @@ export const synthesizeOpenAISpeech = async (
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getModels = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${AUDIO_API_BASE_URL}/models`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getVoices = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${AUDIO_API_BASE_URL}/voices`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|||||||
@@ -32,10 +32,15 @@ export const createNewChat = async (token: string, chat: object) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getChatList = async (token: string = '') => {
|
export const getChatList = async (token: string = '', page: number | null = null) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, {
|
if (page !== null) {
|
||||||
|
searchParams.append('page', `${page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type ChatCompletedForm = {
|
|||||||
model: string;
|
model: string;
|
||||||
messages: string[];
|
messages: string[];
|
||||||
chat_id: string;
|
chat_id: string;
|
||||||
|
session_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
||||||
@@ -104,6 +105,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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||||
import { titleGenerationTemplate } from '$lib/utils';
|
|
||||||
|
|
||||||
export const getOllamaConfig = async (token: string = '') => {
|
export const getOllamaConfig = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
@@ -203,55 +202,6 @@ export const getOllamaModels = async (token: string = '') => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: migrate to backend
|
|
||||||
export const generateTitle = async (
|
|
||||||
token: string = '',
|
|
||||||
template: string,
|
|
||||||
model: string,
|
|
||||||
prompt: string
|
|
||||||
) => {
|
|
||||||
let error = null;
|
|
||||||
|
|
||||||
template = titleGenerationTemplate(template, prompt);
|
|
||||||
|
|
||||||
console.log(template);
|
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: model,
|
|
||||||
prompt: template,
|
|
||||||
stream: false,
|
|
||||||
options: {
|
|
||||||
// Restrict the number of tokens generated to 50
|
|
||||||
num_predict: 50
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) throw await res.json();
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
if ('detail' in err) {
|
|
||||||
error = err.detail;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res?.response.replace(/["']/g, '') ?? 'New Chat';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
|
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
||||||
import { titleGenerationTemplate } from '$lib/utils';
|
|
||||||
import { type Model, models, settings } from '$lib/stores';
|
|
||||||
|
|
||||||
export const getOpenAIConfig = async (token: string = '') => {
|
export const getOpenAIConfig = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
@@ -260,7 +258,7 @@ export const getOpenAIModelsDirect = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = Array.isArray(res) ? res : res?.data ?? null;
|
const models = Array.isArray(res) ? res : (res?.data ?? null);
|
||||||
|
|
||||||
return models
|
return models
|
||||||
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
|
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
|
||||||
@@ -330,126 +328,3 @@ export const synthesizeOpenAISpeech = async (
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateTitle = async (
|
|
||||||
token: string = '',
|
|
||||||
template: string,
|
|
||||||
model: string,
|
|
||||||
prompt: string,
|
|
||||||
chat_id?: string,
|
|
||||||
url: string = OPENAI_API_BASE_URL
|
|
||||||
) => {
|
|
||||||
let error = null;
|
|
||||||
|
|
||||||
template = titleGenerationTemplate(template, prompt);
|
|
||||||
|
|
||||||
console.log(template);
|
|
||||||
|
|
||||||
const res = await fetch(`${url}/chat/completions`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: model,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: template
|
|
||||||
}
|
|
||||||
],
|
|
||||||
stream: false,
|
|
||||||
// Restricting the max tokens to 50 to avoid long titles
|
|
||||||
max_tokens: 50,
|
|
||||||
...(chat_id && { chat_id: chat_id }),
|
|
||||||
title: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) throw await res.json();
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
if ('detail' in err) {
|
|
||||||
error = err.detail;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateSearchQuery = async (
|
|
||||||
token: string = '',
|
|
||||||
model: string,
|
|
||||||
previousMessages: string[],
|
|
||||||
prompt: string,
|
|
||||||
url: string = OPENAI_API_BASE_URL
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
let error = null;
|
|
||||||
|
|
||||||
// TODO: Allow users to specify the prompt
|
|
||||||
// Get the current date in the format "January 20, 2024"
|
|
||||||
const currentDate = new Intl.DateTimeFormat('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: '2-digit'
|
|
||||||
}).format(new Date());
|
|
||||||
|
|
||||||
const res = await fetch(`${url}/chat/completions`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: model,
|
|
||||||
// Few shot prompting
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: `You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is ${currentDate}.`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: prompt
|
|
||||||
}
|
|
||||||
// {
|
|
||||||
// role: 'user',
|
|
||||||
// content:
|
|
||||||
// (previousMessages.length > 0
|
|
||||||
// ? `Previous Questions:\n${previousMessages.join('\n')}\n\n`
|
|
||||||
// : '') + `Current Question: ${prompt}`
|
|
||||||
// }
|
|
||||||
],
|
|
||||||
stream: false,
|
|
||||||
// Restricting the max tokens to 30 to avoid long search queries
|
|
||||||
max_tokens: 30
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) throw await res.json();
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
if ('detail' in err) {
|
|
||||||
error = err.detail;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? undefined;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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,15 +72,15 @@
|
|||||||
{#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'
|
||||||
: section === 'changed'
|
: section === 'changed'
|
||||||
? 'text-white bg-yellow-600'
|
? 'text-white bg-yellow-600'
|
||||||
: section === 'removed'
|
: section === 'removed'
|
||||||
? 'text-white bg-red-600'
|
? 'text-white bg-red-600'
|
||||||
: ''} w-fit px-3 rounded-full my-2.5"
|
: ''} w-fit px-3 rounded-full my-2.5"
|
||||||
>
|
>
|
||||||
{section}
|
{section}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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';
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
|
||||||
import { user, settings, config } from '$lib/stores';
|
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { getBackendConfig } from '$lib/apis';
|
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import { getBackendConfig } from '$lib/apis';
|
||||||
|
import {
|
||||||
|
getAudioConfig,
|
||||||
|
updateAudioConfig,
|
||||||
|
getModels as _getModels,
|
||||||
|
getVoices as _getVoices
|
||||||
|
} from '$lib/apis/audio';
|
||||||
|
import { user, settings, config } from '$lib/stores';
|
||||||
|
|
||||||
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
@@ -16,6 +22,7 @@
|
|||||||
|
|
||||||
let TTS_OPENAI_API_BASE_URL = '';
|
let TTS_OPENAI_API_BASE_URL = '';
|
||||||
let TTS_OPENAI_API_KEY = '';
|
let TTS_OPENAI_API_KEY = '';
|
||||||
|
let TTS_API_KEY = '';
|
||||||
let TTS_ENGINE = '';
|
let TTS_ENGINE = '';
|
||||||
let TTS_MODEL = '';
|
let TTS_MODEL = '';
|
||||||
let TTS_VOICE = '';
|
let TTS_VOICE = '';
|
||||||
@@ -29,30 +36,41 @@
|
|||||||
let models = [];
|
let models = [];
|
||||||
let nonLocalVoices = false;
|
let nonLocalVoices = false;
|
||||||
|
|
||||||
const getOpenAIVoices = () => {
|
const getModels = async () => {
|
||||||
voices = [
|
if (TTS_ENGINE === '') {
|
||||||
{ name: 'alloy' },
|
models = [];
|
||||||
{ name: 'echo' },
|
} else {
|
||||||
{ name: 'fable' },
|
const res = await _getModels(localStorage.token).catch((e) => {
|
||||||
{ name: 'onyx' },
|
toast.error(e);
|
||||||
{ name: 'nova' },
|
});
|
||||||
{ name: 'shimmer' }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOpenAIModels = () => {
|
if (res) {
|
||||||
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
|
console.log(res);
|
||||||
};
|
models = res.models;
|
||||||
|
|
||||||
const getWebAPIVoices = () => {
|
|
||||||
const getVoicesLoop = setInterval(async () => {
|
|
||||||
voices = await speechSynthesis.getVoices();
|
|
||||||
|
|
||||||
// do your loop
|
|
||||||
if (voices.length > 0) {
|
|
||||||
clearInterval(getVoicesLoop);
|
|
||||||
}
|
}
|
||||||
}, 100);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVoices = async () => {
|
||||||
|
if (TTS_ENGINE === '') {
|
||||||
|
const getVoicesLoop = setInterval(async () => {
|
||||||
|
voices = await speechSynthesis.getVoices();
|
||||||
|
|
||||||
|
// do your loop
|
||||||
|
if (voices.length > 0) {
|
||||||
|
clearInterval(getVoicesLoop);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||||
|
toast.error(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
console.log(res);
|
||||||
|
voices = res.voices;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfigHandler = async () => {
|
const updateConfigHandler = async () => {
|
||||||
@@ -60,6 +78,7 @@
|
|||||||
tts: {
|
tts: {
|
||||||
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
|
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
|
||||||
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
|
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
|
||||||
|
API_KEY: TTS_API_KEY,
|
||||||
ENGINE: TTS_ENGINE,
|
ENGINE: TTS_ENGINE,
|
||||||
MODEL: TTS_MODEL,
|
MODEL: TTS_MODEL,
|
||||||
VOICE: TTS_VOICE
|
VOICE: TTS_VOICE
|
||||||
@@ -86,6 +105,7 @@
|
|||||||
console.log(res);
|
console.log(res);
|
||||||
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
|
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
|
||||||
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
|
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
|
||||||
|
TTS_API_KEY = res.tts.API_KEY;
|
||||||
|
|
||||||
TTS_ENGINE = res.tts.ENGINE;
|
TTS_ENGINE = res.tts.ENGINE;
|
||||||
TTS_MODEL = res.tts.MODEL;
|
TTS_MODEL = res.tts.MODEL;
|
||||||
@@ -98,12 +118,8 @@
|
|||||||
STT_MODEL = res.stt.MODEL;
|
STT_MODEL = res.stt.MODEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TTS_ENGINE === 'openai') {
|
await getVoices();
|
||||||
getOpenAIVoices();
|
await getModels();
|
||||||
getOpenAIModels();
|
|
||||||
} else {
|
|
||||||
getWebAPIVoices();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -138,7 +154,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-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 +172,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"
|
||||||
/>
|
/>
|
||||||
@@ -182,19 +198,23 @@
|
|||||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={TTS_ENGINE}
|
bind:value={TTS_ENGINE}
|
||||||
placeholder="Select a mode"
|
placeholder="Select a mode"
|
||||||
on:change={(e) => {
|
on:change={async (e) => {
|
||||||
|
await updateConfigHandler();
|
||||||
|
await getVoices();
|
||||||
|
await getModels();
|
||||||
|
|
||||||
if (e.target.value === 'openai') {
|
if (e.target.value === 'openai') {
|
||||||
getOpenAIVoices();
|
|
||||||
TTS_VOICE = 'alloy';
|
TTS_VOICE = 'alloy';
|
||||||
TTS_MODEL = 'tts-1';
|
TTS_MODEL = 'tts-1';
|
||||||
} else {
|
} else {
|
||||||
getWebAPIVoices();
|
|
||||||
TTS_VOICE = '';
|
TTS_VOICE = '';
|
||||||
|
TTS_MODEL = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Web API')}</option>
|
<option value="">{$i18n.t('Web API')}</option>
|
||||||
<option value="openai">{$i18n.t('OpenAI')}</option>
|
<option value="openai">{$i18n.t('OpenAI')}</option>
|
||||||
|
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +223,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 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={TTS_OPENAI_API_BASE_URL}
|
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -212,6 +232,17 @@
|
|||||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
|
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if TTS_ENGINE === 'elevenlabs'}
|
||||||
|
<div>
|
||||||
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
|
bind:value={TTS_API_KEY}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-2" />
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
@@ -222,7 +253,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,14 +276,14 @@
|
|||||||
<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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="voice-list">
|
<datalist id="voice-list">
|
||||||
{#each voices as voice}
|
{#each voices as voice}
|
||||||
<option value={voice.name} />
|
<option value={voice.id}>{voice.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,15 +294,56 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="model-list"
|
list="tts-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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="model-list">
|
<datalist id="tts-model-list">
|
||||||
{#each models as model}
|
{#each models as model}
|
||||||
<option value={model.name} />
|
<option value={model.id} />
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if TTS_ENGINE === 'elevenlabs'}
|
||||||
|
<div class=" flex gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
list="voice-list"
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={TTS_VOICE}
|
||||||
|
placeholder="Select a voice"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<datalist id="voice-list">
|
||||||
|
{#each voices as voice}
|
||||||
|
<option value={voice.id}>{voice.name}</option>
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
list="tts-model-list"
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={TTS_MODEL}
|
||||||
|
placeholder="Select a model"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<datalist id="tts-model-list">
|
||||||
|
{#each models as model}
|
||||||
|
<option value={model.id} />
|
||||||
{/each}
|
{/each}
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user