Compare commits
587
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a220bf394f | ||
|
|
c8780dcb7c | ||
|
|
4769070af0 | ||
|
|
0853258f92 | ||
|
|
7f0efa20fa | ||
|
|
308795393a | ||
|
|
4ce6a36b12 | ||
|
|
14e5afa54d | ||
|
|
7ee9296bea | ||
|
|
fc1eaf3287 | ||
|
|
fd7854de7e | ||
|
|
0d4441e2cf | ||
|
|
8dabdb64ea | ||
|
|
cb62c52255 | ||
|
|
668b122632 | ||
|
|
c50c815e81 | ||
|
|
429850531a | ||
|
|
14d53ff11e | ||
|
|
79348cfc12 | ||
|
|
e4c7987cf0 | ||
|
|
d9e25e40f6 | ||
|
|
465d3c53cb | ||
|
|
17cc4e20e7 | ||
|
|
dabd74ac44 | ||
|
|
5fed29ed57 | ||
|
|
703c4b5f83 | ||
|
|
43f76aec9a | ||
|
|
6b3957af9a | ||
|
|
c0b57f2f54 | ||
|
|
3bcc648f07 | ||
|
|
4120cb5eef | ||
|
|
26544610ff | ||
|
|
f785af3cec | ||
|
|
aa5e8a5eff | ||
|
|
aea50cd647 | ||
|
|
fcbfb49ef7 | ||
|
|
28fd666fa6 | ||
|
|
655e596106 | ||
|
|
fbde89eeb9 | ||
|
|
6a9efa8ffd | ||
|
|
a2194300cc | ||
|
|
e4e9271fbe | ||
|
|
62cebcf64d | ||
|
|
4eb9a24997 | ||
|
|
b2956fd544 | ||
|
|
b206e691ca | ||
|
|
436cb0f544 | ||
|
|
e1bbb85e3e | ||
|
|
99b67d56e3 | ||
|
|
3f354c2b06 | ||
|
|
8ae9d0ad8e | ||
|
|
9993b4f13d | ||
|
|
b630259d21 | ||
|
|
ec276ef415 | ||
|
|
810cfb6ea5 | ||
|
|
1989fe923a | ||
|
|
7af76cfa75 | ||
|
|
0f583cfe75 | ||
|
|
5008b98b05 | ||
|
|
df250423d4 | ||
|
|
48ea979f5b | ||
|
|
37f357b4e8 | ||
|
|
c537b130fe | ||
|
|
8eaaef42de | ||
|
|
b8934b743c | ||
|
|
1db2f68b3d | ||
|
|
976d061f9b | ||
|
|
96655436f4 | ||
|
|
648806bc9c | ||
|
|
88ce098f55 | ||
|
|
aea65f1ce0 | ||
|
|
c38d795db5 | ||
|
|
e21af924d8 | ||
|
|
64cc650e78 | ||
|
|
64ebfe2235 | ||
|
|
1ac8c9e434 | ||
|
|
ca4d7ff1f2 | ||
|
|
2917d1d51c | ||
|
|
c79b9821fa | ||
|
|
6422ef2b57 | ||
|
|
509ea9afc4 | ||
|
|
1828cc6a10 | ||
|
|
8d5ff67d05 | ||
|
|
ec63a26511 | ||
|
|
c2b3b89445 | ||
|
|
438dc96334 | ||
|
|
5967bd8e08 | ||
|
|
7e7548cd18 | ||
|
|
1ba3aa718e | ||
|
|
c7b1660650 | ||
|
|
9eff9783fc | ||
|
|
8b3fc51374 | ||
|
|
f5e1f1590f | ||
|
|
09d0cca5b9 | ||
|
|
d8736be194 | ||
|
|
7f15f506f8 | ||
|
|
6aa3fa5193 | ||
|
|
c849be5e52 | ||
|
|
efc21d5e48 | ||
|
|
b3b3088a17 | ||
|
|
72fad39913 | ||
|
|
e12563a4b4 | ||
|
|
e8afed13cc | ||
|
|
7a72bb3571 | ||
|
|
08f30717ad | ||
|
|
2bdfbb9e61 | ||
|
|
06e757a9e9 | ||
|
|
7b84d4daca | ||
|
|
5e0d705a7f | ||
|
|
30ca836e03 | ||
|
|
3f5c035329 | ||
|
|
f4e8788d29 | ||
|
|
b391dc56a0 | ||
|
|
606fac6e1c | ||
|
|
ed36ee5133 | ||
|
|
c35242731f | ||
|
|
d2d07d0345 | ||
|
|
8b908d6150 | ||
|
|
1017a76b4c | ||
|
|
9c5d76c299 | ||
|
|
83987b8eec | ||
|
|
cbd71699de | ||
|
|
5d58a25e52 | ||
|
|
764d9b5fbe | ||
|
|
5ffea6d30e | ||
|
|
2796210e18 | ||
|
|
1b2c71ac75 | ||
|
|
cc4bf22d07 | ||
|
|
7295d9306f | ||
|
|
2a66b49bc7 | ||
|
|
c944a25585 | ||
|
|
2067836851 | ||
|
|
6f95a071b5 | ||
|
|
a543fd3656 | ||
|
|
d94c4415af | ||
|
|
abbed23c64 | ||
|
|
5de4e5b3c0 | ||
|
|
1e17a55e79 | ||
|
|
465dab2eed | ||
|
|
532e0a77cc | ||
|
|
47835e6b25 | ||
|
|
11c537dc1e | ||
|
|
e4420227b5 | ||
|
|
f9e44fe86e | ||
|
|
b256e211dc | ||
|
|
2a34c7a714 | ||
|
|
04132b51f0 | ||
|
|
908f6ada4e | ||
|
|
e7da213f93 | ||
|
|
93dcc67e63 | ||
|
|
a9c92f114f | ||
|
|
3e59fba46a | ||
|
|
e7f789f185 | ||
|
|
f8d381e6c5 | ||
|
|
c29425c0fc | ||
|
|
612f9ee669 | ||
|
|
81a38df814 | ||
|
|
e9eb130731 | ||
|
|
4edaecc04d | ||
|
|
fc8d0a0187 | ||
|
|
96960499ef | ||
|
|
c2687c16b3 | ||
|
|
883944d289 | ||
|
|
a7156a0702 | ||
|
|
dee9d27af8 | ||
|
|
e06f0ee315 | ||
|
|
1d990de7e7 | ||
|
|
78e9d41c83 | ||
|
|
7522ae14b2 | ||
|
|
e09eee8fd1 | ||
|
|
b692ddd80c | ||
|
|
5ebb389d26 | ||
|
|
fff6f7cf2f | ||
|
|
516fd12b8d | ||
|
|
925ca49f21 | ||
|
|
a1577fc48c | ||
|
|
acda0341ed | ||
|
|
98378cfe03 | ||
|
|
3f89a3c742 | ||
|
|
e036075e22 | ||
|
|
fec55e505d | ||
|
|
488bad3424 | ||
|
|
b70136f46e | ||
|
|
aea90b08d9 | ||
|
|
42aecd649c | ||
|
|
5a84cde198 | ||
|
|
4e18ab0ad4 | ||
|
|
a4ee85232f | ||
|
|
a6bd93036b | ||
|
|
3b6f011c6c | ||
|
|
faef9378e2 | ||
|
|
f1c302826c | ||
|
|
e733ccf47b | ||
|
|
757c7227f8 | ||
|
|
2a742486c5 | ||
|
|
d362bdbb4e | ||
|
|
60d6be5b13 | ||
|
|
cc1b55aaf7 | ||
|
|
d1fa17032e | ||
|
|
916d7e1c96 | ||
|
|
86bfb66990 | ||
|
|
c094117ca2 | ||
|
|
c7ef31fbeb | ||
|
|
c6b1311aa3 | ||
|
|
515c3d0c62 | ||
|
|
fe94ddaaeb | ||
|
|
3e8c0fdcef | ||
|
|
055462fe6e | ||
|
|
06e229e8b2 | ||
|
|
28da39f909 | ||
|
|
03fba953ec | ||
|
|
c6edaf70b6 | ||
|
|
fa498ad7a2 | ||
|
|
a279be91a8 | ||
|
|
96c5703486 | ||
|
|
12300f3833 | ||
|
|
b439b6a800 | ||
|
|
11a47127cd | ||
|
|
7a7d8fe19e | ||
|
|
2635d1b3df | ||
|
|
a952487789 | ||
|
|
c9e6db5ef2 | ||
|
|
622db45bd5 | ||
|
|
f41abcd881 | ||
|
|
59b9453592 | ||
|
|
ada481e836 | ||
|
|
4011cfbdbc | ||
|
|
b31e66107c | ||
|
|
cbecb64fef | ||
|
|
6c47ee52eb | ||
|
|
347cd11771 | ||
|
|
4bef5c4667 | ||
|
|
74ec7c8635 | ||
|
|
3b9be7d03f | ||
|
|
e91b96f0bf | ||
|
|
cb49d83e9c | ||
|
|
42d6589ce2 | ||
|
|
f00d0329d4 | ||
|
|
af9d2d00a3 | ||
|
|
ae8cb996c7 | ||
|
|
adb89a8926 | ||
|
|
3a4920c595 | ||
|
|
9b146706fe | ||
|
|
01074e631e | ||
|
|
5af14795af | ||
|
|
276c3b6e02 | ||
|
|
7ff5c85796 | ||
|
|
64b7cd58b2 | ||
|
|
a3683465d3 | ||
|
|
b8330df96e | ||
|
|
7422052759 | ||
|
|
ab0e131e87 | ||
|
|
1375dc3864 | ||
|
|
6530f2bcdb | ||
|
|
7039217c99 | ||
|
|
924d787e41 | ||
|
|
d2fbc37a1b | ||
|
|
baba36d47d | ||
|
|
fab913bce5 | ||
|
|
3de65e6ce4 | ||
|
|
4b2c5ac4ae | ||
|
|
73197e4354 | ||
|
|
d127b97e36 | ||
|
|
d754811d5f | ||
|
|
5dfd83e9ab | ||
|
|
8a5c248fbb | ||
|
|
6f2e4d72d3 | ||
|
|
fde800e337 | ||
|
|
b76a14d71d | ||
|
|
1983e842ca | ||
|
|
f9e4c43a48 | ||
|
|
307dd7a704 | ||
|
|
90d6046345 | ||
|
|
88a9ada626 | ||
|
|
6c5ee0084f | ||
|
|
262fbd7512 | ||
|
|
783a645de0 | ||
|
|
05c7386013 | ||
|
|
4d0bc32dd3 | ||
|
|
b88a8933ed | ||
|
|
fd8e4a80cd | ||
|
|
e61ead98d9 | ||
|
|
3176291f40 | ||
|
|
bcc328eeb5 | ||
|
|
13ddabf832 | ||
|
|
6ac9b4e7fa | ||
|
|
50a6764eaa | ||
|
|
e6fec870db | ||
|
|
743b534212 | ||
|
|
d6336c5d49 | ||
|
|
25fee85f0f | ||
|
|
7073baa737 | ||
|
|
0cf549e14a | ||
|
|
88d9f82941 | ||
|
|
d96a7707a6 | ||
|
|
43f7fcae36 | ||
|
|
78129cf6c3 | ||
|
|
1a69435fff | ||
|
|
48c8495d3e | ||
|
|
33b238343b | ||
|
|
4b8a2c7e6c | ||
|
|
e6dc350cf2 | ||
|
|
5b05ebee28 | ||
|
|
60bf39efd5 | ||
|
|
06d8301a90 | ||
|
|
09cbdda519 | ||
|
|
dc309ced59 | ||
|
|
3ebbe0e4ae | ||
|
|
cc10ca8198 | ||
|
|
cf88aef710 | ||
|
|
0277b9eb30 | ||
|
|
8fbbfbc243 | ||
|
|
f2255e0d27 | ||
|
|
b68289f24a | ||
|
|
5bceac05eb | ||
|
|
febcc4e054 | ||
|
|
36c990a0cf | ||
|
|
658d5e0dcf | ||
|
|
18ae46f7a3 | ||
|
|
cfef48c408 | ||
|
|
1973a2e96f | ||
|
|
b105f21763 | ||
|
|
e5a07e97a4 | ||
|
|
670fa67ef9 | ||
|
|
046df1260a | ||
|
|
9e95e1302e | ||
|
|
59cbba3b37 | ||
|
|
20b31de791 | ||
|
|
654884817a | ||
|
|
5e036e5489 | ||
|
|
e6ede05fe9 | ||
|
|
7edd05100a | ||
|
|
46024b6ad8 | ||
|
|
f1ea6338c3 | ||
|
|
123f128ccc | ||
|
|
6474059eb1 | ||
|
|
479a5613a8 | ||
|
|
d51900974f | ||
|
|
d16b9c5e8b | ||
|
|
f08cc41546 | ||
|
|
2443df2e42 | ||
|
|
1fe71ee5d3 | ||
|
|
2e65ab9d1e | ||
|
|
be221ec89d | ||
|
|
6add64a44d | ||
|
|
8170a2a5f3 | ||
|
|
d144840384 | ||
|
|
8d2eac55d7 | ||
|
|
a7283bccb3 | ||
|
|
24847366e1 | ||
|
|
095e9c3a31 | ||
|
|
70051950ab | ||
|
|
f7881bb635 | ||
|
|
b6b3e747f3 | ||
|
|
18f740402f | ||
|
|
15a5317bd3 | ||
|
|
1369a2cdfc | ||
|
|
f1e79a7ad6 | ||
|
|
ac0c909d11 | ||
|
|
945503523b | ||
|
|
fe7e2dc62f | ||
|
|
3bb21a9a88 | ||
|
|
dc09384b1d | ||
|
|
3213741104 | ||
|
|
890e5aa294 | ||
|
|
0bf3416431 | ||
|
|
685e291e65 | ||
|
|
ddd5e3c46b | ||
|
|
2204ccaed5 | ||
|
|
a4756e953d | ||
|
|
c15dd6d994 | ||
|
|
72b30ff313 | ||
|
|
aa58dfb598 | ||
|
|
7dd7b3dfa8 | ||
|
|
d033124552 | ||
|
|
2fd8f98f2d | ||
|
|
140726a1ea | ||
|
|
90e373199c | ||
|
|
7d6d173b01 | ||
|
|
2dc6b08044 | ||
|
|
896e5e20c0 | ||
|
|
143bc121da | ||
|
|
c0a5ec4e67 | ||
|
|
4213e6b150 | ||
|
|
b462cb041d | ||
|
|
a77f508b05 | ||
|
|
9d81f0da10 | ||
|
|
d2b58f8636 | ||
|
|
c758131fb9 | ||
|
|
482ec5e712 | ||
|
|
36a4c98732 | ||
|
|
dec1cb3798 | ||
|
|
4fd80c8d59 | ||
|
|
3ada29bbe0 | ||
|
|
8f817644e9 | ||
|
|
fcf87ec68c | ||
|
|
1ac9a6ba25 | ||
|
|
3d9feaba0b | ||
|
|
0271ca6e5f | ||
|
|
c944ffe558 | ||
|
|
28b3306867 | ||
|
|
aab31292d8 | ||
|
|
545bdecff2 | ||
|
|
7d92097526 | ||
|
|
edcd3fc2e3 | ||
|
|
be413650ba | ||
|
|
87d305da1d | ||
|
|
505fe93d2c | ||
|
|
e08568b3b5 | ||
|
|
2d2cc7cab1 | ||
|
|
2838957004 | ||
|
|
f6852205d2 | ||
|
|
9802329b91 | ||
|
|
a65845e5fa | ||
|
|
da92158f26 | ||
|
|
e65905b4b3 | ||
|
|
17ccc0b995 | ||
|
|
3d99416631 | ||
|
|
53e9a62b6e | ||
|
|
19ad4b6a72 | ||
|
|
701f591c5e | ||
|
|
ac0031b75d | ||
|
|
c7e4cb0c29 | ||
|
|
d040afebfb | ||
|
|
1648d8ac6b | ||
|
|
afee5e28d2 | ||
|
|
4a7195be12 | ||
|
|
923507baeb | ||
|
|
6aacd46b03 | ||
|
|
d93902eaff | ||
|
|
4d5f845b8c | ||
|
|
c9cdc34abc | ||
|
|
737f315b53 | ||
|
|
c91e48127a | ||
|
|
0eb582492f | ||
|
|
74ec0c78b9 | ||
|
|
abee50b90e | ||
|
|
e891790ce0 | ||
|
|
ce847f2b6f | ||
|
|
26f95a3bca | ||
|
|
0950bf609d | ||
|
|
5b939067e1 | ||
|
|
465e7011e5 | ||
|
|
fe79fed793 | ||
|
|
c0b8e901ba | ||
|
|
007b28e2ed | ||
|
|
480deebf02 | ||
|
|
f1045bc099 | ||
|
|
8a7da3192f | ||
|
|
bc312f249e | ||
|
|
a392199048 | ||
|
|
337c4f8f87 | ||
|
|
c517805213 | ||
|
|
049208c095 | ||
|
|
c07f3d9373 | ||
|
|
f39b15f76d | ||
|
|
cb02cdd545 | ||
|
|
3190ea955a | ||
|
|
9dca9e139f | ||
|
|
3adb119dd3 | ||
|
|
13778d96a0 | ||
|
|
121a2e7b87 | ||
|
|
29602d261c | ||
|
|
bf5f916cd4 | ||
|
|
5eabe14982 | ||
|
|
6c59bdf724 | ||
|
|
02e8867588 | ||
|
|
3bca4b3485 | ||
|
|
8f19a9163a | ||
|
|
1a4ad7edb2 | ||
|
|
178c301c5f | ||
|
|
1e2ebf43f9 | ||
|
|
c5597305e0 | ||
|
|
0e06266770 | ||
|
|
4ee61733ec | ||
|
|
dc959e0c0f | ||
|
|
7b8770d203 | ||
|
|
4b53ad8b9d | ||
|
|
1b9db252bc | ||
|
|
3caeb28f4a | ||
|
|
360668492f | ||
|
|
4707a4226a | ||
|
|
ca0aca6fd7 | ||
|
|
9b6796df1c | ||
|
|
1af4d746d3 | ||
|
|
a82290fe8f | ||
|
|
fc377d4b8f | ||
|
|
0ec4940c9a | ||
|
|
acac966cdc | ||
|
|
79984e1c84 | ||
|
|
b66c7c9a52 | ||
|
|
e052b2e27f | ||
|
|
9bfe856f59 | ||
|
|
321263af61 | ||
|
|
37b07b1709 | ||
|
|
72a75fe3bb | ||
|
|
5869016c42 | ||
|
|
e653935288 | ||
|
|
08e974ed48 | ||
|
|
cf0d5afcef | ||
|
|
3a42945fcc | ||
|
|
b8a2f991c1 | ||
|
|
5c359b710a | ||
|
|
4ffe1870d1 | ||
|
|
2f892071d2 | ||
|
|
effcefca34 | ||
|
|
617ba37988 | ||
|
|
addd754eb3 | ||
|
|
f2138ac15e | ||
|
|
f9bd5ad923 | ||
|
|
dd1d24e341 | ||
|
|
7731f1bdc0 | ||
|
|
5270f78ff1 | ||
|
|
34f4e3b145 | ||
|
|
360fa45b3e | ||
|
|
9187eb61ac | ||
|
|
5c311d3eef | ||
|
|
1884f03350 | ||
|
|
333c50f663 | ||
|
|
71e9755d6c | ||
|
|
6cfd1abb59 | ||
|
|
5e7ea24340 | ||
|
|
71d07d8770 | ||
|
|
6dd0f85f67 | ||
|
|
85fbeb644a | ||
|
|
b8a4586b51 | ||
|
|
d51692a62d | ||
|
|
6f6198f719 | ||
|
|
cd86ffd6f8 | ||
|
|
20a06fdb94 | ||
|
|
4bb1165c9d | ||
|
|
3a7a5075de | ||
|
|
f5fa4d8213 | ||
|
|
51d36b5c9e | ||
|
|
2674190bb4 | ||
|
|
ff07021c22 | ||
|
|
d2380dd951 | ||
|
|
a7fb5513e4 | ||
|
|
76f67f096e | ||
|
|
da9870b5e1 | ||
|
|
79a681b88b | ||
|
|
0ed37b82d3 | ||
|
|
fbb1043f58 | ||
|
|
65b9dc09a2 | ||
|
|
6eed85af89 | ||
|
|
4df8c6662f | ||
|
|
d89a92dbad | ||
|
|
4ebee54ffd | ||
|
|
1b6b811108 | ||
|
|
ab0b2282ab | ||
|
|
f02de24470 | ||
|
|
665515da40 | ||
|
|
68fb92c8d5 | ||
|
|
7a20d54847 | ||
|
|
53e12ff376 | ||
|
|
055143f67a | ||
|
|
b4a8f7f180 | ||
|
|
e5d4f37469 | ||
|
|
556fc7a03a | ||
|
|
cc37dfcb01 | ||
|
|
3de75a3315 | ||
|
|
28c2b6ee95 | ||
|
|
1cde846bcf | ||
|
|
ce0dee9afb | ||
|
|
11ae998de0 | ||
|
|
901923457c | ||
|
|
1b28bacb46 | ||
|
|
918989b98a | ||
|
|
372930ce71 | ||
|
|
652dccd2c4 | ||
|
|
90b6a7f012 | ||
|
|
7efe5a4783 | ||
|
|
0300a4d881 | ||
|
|
85c8d026a2 | ||
|
|
c09d61d8bc | ||
|
|
f482bf0aa1 | ||
|
|
2eb328104a | ||
|
|
91a240c072 | ||
|
|
dd70011fd9 | ||
|
|
954be8443c | ||
|
|
cae5553f1a | ||
|
|
fc7fabe544 | ||
|
|
d74499c09d | ||
|
|
29858d0db0 | ||
|
|
77732c7bb1 | ||
|
|
dde79fc9bf | ||
|
|
4ee43b8afc |
@@ -4,6 +4,7 @@ Read this document when a change introduces Effects, navigation side effects, me
|
||||
|
||||
## Effects
|
||||
|
||||
- Keep render pure: do not read or write `ref.current` during render except for predictable null-guarded lazy initialization. Update interaction-owned refs in event handlers, synchronize external-system refs after commit, and use state or derivation for rendered values.
|
||||
- Use Effects only to synchronize with a named external system such as a browser API, subscription, timer, analytics integration, non-React widget, or imperative DOM API.
|
||||
- Do not use Effects to transform render state, handle user actions, copy query data, reset state from props, or fetch data owned by framework APIs or TanStack Query.
|
||||
- Initialize query-backed forms with keyed remounts or surface-entry hydration instead of copying data through Effects.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "npx -y [email protected]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
**/*.pyc
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
knowledge-fs/
|
||||
.git
|
||||
.github
|
||||
*.md
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
title: "General Discussion"
|
||||
title: 'General Discussion'
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "To make sure we get to you in time, please check the following :)"
|
||||
description: 'To make sure we get to you in time, please check the following :)'
|
||||
options:
|
||||
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
|
||||
required: true
|
||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||
required: true
|
||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
- label: 'Please do not modify this template :) and fill in all the required fields.'
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
title: "Help"
|
||||
title: 'Help'
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "To make sure we get to you in time, please check the following :)"
|
||||
description: 'To make sure we get to you in time, please check the following :)'
|
||||
options:
|
||||
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
|
||||
required: true
|
||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||
required: true
|
||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
- label: 'Please do not modify this template :) and fill in all the required fields.'
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -3,15 +3,15 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "To make sure we get to you in time, please check the following :)"
|
||||
description: 'To make sure we get to you in time, please check the following :)'
|
||||
options:
|
||||
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
|
||||
required: true
|
||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||
required: true
|
||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
- label: 'Please do not modify this template :) and fill in all the required fields.'
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "🕷️ Bug report"
|
||||
name: '🕷️ Bug report'
|
||||
description: Report errors or unexpected behavior
|
||||
labels:
|
||||
- bug
|
||||
@@ -6,7 +6,7 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "To make sure we get to you in time, please check the following :)"
|
||||
description: 'To make sure we get to you in time, please check the following :)'
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
|
||||
required: true
|
||||
@@ -18,7 +18,7 @@ body:
|
||||
required: true
|
||||
- label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
- label: 'Please do not modify this template :) and fill in all the required fields.'
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "\U0001F510 Security Vulnerabilities"
|
||||
url: "https://github.com/langgenius/dify/security/advisories/new"
|
||||
url: 'https://github.com/langgenius/dify/security/advisories/new'
|
||||
about: Report security vulnerabilities through GitHub Security Advisories to ensure responsible disclosure. 💡 Please do not report security vulnerabilities in public issues.
|
||||
- name: "\U0001F4A1 Model Providers & Plugins"
|
||||
url: "https://github.com/langgenius/dify-official-plugins/issues/new/choose"
|
||||
url: 'https://github.com/langgenius/dify-official-plugins/issues/new/choose'
|
||||
about: Report issues with official plugins or model providers, you will need to provide the plugin version and other relevant details.
|
||||
- name: "\U0001F4AC Documentation Issues"
|
||||
url: "https://github.com/langgenius/dify-docs/issues/new"
|
||||
url: 'https://github.com/langgenius/dify-docs/issues/new'
|
||||
about: Report issues with the documentation, such as typos, outdated information, or missing content. Please provide the specific section and details of the issue.
|
||||
- name: "\U0001F4E7 Discussions"
|
||||
url: https://github.com/langgenius/dify/discussions/categories/general
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "⭐ Feature or enhancement request"
|
||||
name: '⭐ Feature or enhancement request'
|
||||
description: Propose something new.
|
||||
labels:
|
||||
- enhancement
|
||||
@@ -6,7 +6,7 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "To make sure we get to you in time, please check the following :)"
|
||||
description: 'To make sure we get to you in time, please check the following :)'
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
|
||||
required: true
|
||||
@@ -14,7 +14,7 @@ body:
|
||||
required: true
|
||||
- label: I confirm that I am using English to submit this report, otherwise it will be closed.
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
- label: 'Please do not modify this template :) and fill in all the required fields.'
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
name: "✨ Refactor or Chore"
|
||||
name: '✨ Refactor or Chore'
|
||||
description: Refactor existing code or perform maintenance chores to improve readability and reliability.
|
||||
title: "[Refactor/Chore] "
|
||||
title: '[Refactor/Chore] '
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "To make sure we get to you in time, please check the following :)"
|
||||
description: 'To make sure we get to you in time, please check the following :)'
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
|
||||
required: true
|
||||
@@ -17,26 +17,26 @@ body:
|
||||
required: true
|
||||
- label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
- label: 'Please do not modify this template :) and fill in all the required fields.'
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
placeholder: "Describe the refactor or chore you are proposing."
|
||||
placeholder: 'Describe the refactor or chore you are proposing.'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: Motivation
|
||||
placeholder: "Explain why this refactor or chore is necessary."
|
||||
placeholder: 'Explain why this refactor or chore is necessary.'
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
placeholder: "Add any other context or screenshots about the request here."
|
||||
placeholder: 'Add any other context or screenshots about the request here.'
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -5,11 +5,11 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
run_install: false
|
||||
- name: Setup Vite+
|
||||
uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1.10.0
|
||||
uses: voidzero-dev/setup-vp@143f5f385f39b1b753ffed1a01ad443811855c8b # v1.16.1
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: true
|
||||
|
||||
+173
-164
@@ -1,223 +1,232 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/api"
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/knowledge-fs'
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
knowledge-fs-dependencies:
|
||||
patterns:
|
||||
- '*'
|
||||
- package-ecosystem: 'uv'
|
||||
directory: '/api'
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
flask:
|
||||
patterns:
|
||||
- "flask"
|
||||
- "flask-*"
|
||||
- "werkzeug"
|
||||
- "gunicorn"
|
||||
- 'flask'
|
||||
- 'flask-*'
|
||||
- 'werkzeug'
|
||||
- 'gunicorn'
|
||||
google:
|
||||
patterns:
|
||||
- "google-*"
|
||||
- "googleapis-*"
|
||||
- 'google-*'
|
||||
- 'googleapis-*'
|
||||
opentelemetry:
|
||||
patterns:
|
||||
- "opentelemetry-*"
|
||||
- 'opentelemetry-*'
|
||||
pydantic:
|
||||
patterns:
|
||||
- "pydantic"
|
||||
- "pydantic-*"
|
||||
- 'pydantic'
|
||||
- 'pydantic-*'
|
||||
llm:
|
||||
patterns:
|
||||
- "langfuse"
|
||||
- "langsmith"
|
||||
- "litellm"
|
||||
- "mlflow*"
|
||||
- "opik"
|
||||
- "weave*"
|
||||
- "arize*"
|
||||
- "tiktoken"
|
||||
- "transformers"
|
||||
- 'langfuse'
|
||||
- 'langsmith'
|
||||
- 'litellm'
|
||||
- 'mlflow*'
|
||||
- 'opik'
|
||||
- 'weave*'
|
||||
- 'arize*'
|
||||
- 'tiktoken'
|
||||
- 'transformers'
|
||||
database:
|
||||
patterns:
|
||||
- "sqlalchemy"
|
||||
- "psycopg2*"
|
||||
- "psycogreen"
|
||||
- "redis*"
|
||||
- "alembic*"
|
||||
- 'sqlalchemy'
|
||||
- 'psycopg2*'
|
||||
- 'psycogreen'
|
||||
- 'redis*'
|
||||
- 'alembic*'
|
||||
storage:
|
||||
patterns:
|
||||
- "boto3*"
|
||||
- "botocore*"
|
||||
- "azure-*"
|
||||
- "bce-*"
|
||||
- "cos-python-*"
|
||||
- "esdk-obs-*"
|
||||
- "google-cloud-storage"
|
||||
- "opendal"
|
||||
- "oss2"
|
||||
- "supabase*"
|
||||
- "tos*"
|
||||
- 'boto3*'
|
||||
- 'botocore*'
|
||||
- 'azure-*'
|
||||
- 'bce-*'
|
||||
- 'cos-python-*'
|
||||
- 'esdk-obs-*'
|
||||
- 'google-cloud-storage'
|
||||
- 'opendal'
|
||||
- 'oss2'
|
||||
- 'supabase*'
|
||||
- 'tos*'
|
||||
vdb:
|
||||
patterns:
|
||||
- "alibabacloud*"
|
||||
- "chromadb"
|
||||
- "clickhouse-*"
|
||||
- "clickzetta-*"
|
||||
- "couchbase"
|
||||
- "elasticsearch"
|
||||
- "opensearch-py"
|
||||
- "oracledb"
|
||||
- "pgvect*"
|
||||
- "pymilvus"
|
||||
- "pymochow"
|
||||
- "pyobvector"
|
||||
- "qdrant-client"
|
||||
- "intersystems-*"
|
||||
- "tablestore"
|
||||
- "tcvectordb"
|
||||
- "tidb-vector"
|
||||
- "upstash-*"
|
||||
- "volcengine-*"
|
||||
- "weaviate-*"
|
||||
- "xinference-*"
|
||||
- "mo-vector"
|
||||
- "mysql-connector-*"
|
||||
- 'alibabacloud*'
|
||||
- 'chromadb'
|
||||
- 'clickhouse-*'
|
||||
- 'clickzetta-*'
|
||||
- 'couchbase'
|
||||
- 'elasticsearch'
|
||||
- 'opensearch-py'
|
||||
- 'oracledb'
|
||||
- 'pgvect*'
|
||||
- 'pymilvus'
|
||||
- 'pymochow'
|
||||
- 'pyobvector'
|
||||
- 'qdrant-client'
|
||||
- 'intersystems-*'
|
||||
- 'tablestore'
|
||||
- 'tcvectordb'
|
||||
- 'tidb-vector'
|
||||
- 'upstash-*'
|
||||
- 'volcengine-*'
|
||||
- 'weaviate-*'
|
||||
- 'xinference-*'
|
||||
- 'mo-vector'
|
||||
- 'mysql-connector-*'
|
||||
dev:
|
||||
patterns:
|
||||
- "coverage"
|
||||
- "dotenv-linter"
|
||||
- "faker"
|
||||
- "lxml-stubs"
|
||||
- "basedpyright"
|
||||
- "ruff"
|
||||
- "pytest*"
|
||||
- "types-*"
|
||||
- "boto3-stubs"
|
||||
- "hypothesis"
|
||||
- "pandas-stubs"
|
||||
- "scipy-stubs"
|
||||
- "import-linter"
|
||||
- "celery-types"
|
||||
- "mypy*"
|
||||
- "pyrefly"
|
||||
- 'coverage'
|
||||
- 'dotenv-linter'
|
||||
- 'faker'
|
||||
- 'lxml-stubs'
|
||||
- 'basedpyright'
|
||||
- 'ruff'
|
||||
- 'pytest*'
|
||||
- 'types-*'
|
||||
- 'boto3-stubs'
|
||||
- 'hypothesis'
|
||||
- 'pandas-stubs'
|
||||
- 'scipy-stubs'
|
||||
- 'import-linter'
|
||||
- 'celery-types'
|
||||
- 'mypy*'
|
||||
- 'pyrefly'
|
||||
python-packages:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
- '*'
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
open-pull-requests-limit: 5
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
github-actions-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/api"
|
||||
target-branch: "lts/1.13.x"
|
||||
- '*'
|
||||
- package-ecosystem: 'uv'
|
||||
directory: '/api'
|
||||
target-branch: 'lts/1.13.x'
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
flask:
|
||||
patterns:
|
||||
- "flask"
|
||||
- "flask-*"
|
||||
- "werkzeug"
|
||||
- "gunicorn"
|
||||
- 'flask'
|
||||
- 'flask-*'
|
||||
- 'werkzeug'
|
||||
- 'gunicorn'
|
||||
google:
|
||||
patterns:
|
||||
- "google-*"
|
||||
- "googleapis-*"
|
||||
- 'google-*'
|
||||
- 'googleapis-*'
|
||||
opentelemetry:
|
||||
patterns:
|
||||
- "opentelemetry-*"
|
||||
- 'opentelemetry-*'
|
||||
pydantic:
|
||||
patterns:
|
||||
- "pydantic"
|
||||
- "pydantic-*"
|
||||
- 'pydantic'
|
||||
- 'pydantic-*'
|
||||
llm:
|
||||
patterns:
|
||||
- "langfuse"
|
||||
- "langsmith"
|
||||
- "litellm"
|
||||
- "mlflow*"
|
||||
- "opik"
|
||||
- "weave*"
|
||||
- "arize*"
|
||||
- "tiktoken"
|
||||
- "transformers"
|
||||
- 'langfuse'
|
||||
- 'langsmith'
|
||||
- 'litellm'
|
||||
- 'mlflow*'
|
||||
- 'opik'
|
||||
- 'weave*'
|
||||
- 'arize*'
|
||||
- 'tiktoken'
|
||||
- 'transformers'
|
||||
database:
|
||||
patterns:
|
||||
- "sqlalchemy"
|
||||
- "psycopg2*"
|
||||
- "psycogreen"
|
||||
- "redis*"
|
||||
- "alembic*"
|
||||
- 'sqlalchemy'
|
||||
- 'psycopg2*'
|
||||
- 'psycogreen'
|
||||
- 'redis*'
|
||||
- 'alembic*'
|
||||
storage:
|
||||
patterns:
|
||||
- "boto3*"
|
||||
- "botocore*"
|
||||
- "azure-*"
|
||||
- "bce-*"
|
||||
- "cos-python-*"
|
||||
- "esdk-obs-*"
|
||||
- "google-cloud-storage"
|
||||
- "opendal"
|
||||
- "oss2"
|
||||
- "supabase*"
|
||||
- "tos*"
|
||||
- 'boto3*'
|
||||
- 'botocore*'
|
||||
- 'azure-*'
|
||||
- 'bce-*'
|
||||
- 'cos-python-*'
|
||||
- 'esdk-obs-*'
|
||||
- 'google-cloud-storage'
|
||||
- 'opendal'
|
||||
- 'oss2'
|
||||
- 'supabase*'
|
||||
- 'tos*'
|
||||
vdb:
|
||||
patterns:
|
||||
- "alibabacloud*"
|
||||
- "chromadb"
|
||||
- "clickhouse-*"
|
||||
- "clickzetta-*"
|
||||
- "couchbase"
|
||||
- "elasticsearch"
|
||||
- "opensearch-py"
|
||||
- "oracledb"
|
||||
- "pgvect*"
|
||||
- "pymilvus"
|
||||
- "pymochow"
|
||||
- "pyobvector"
|
||||
- "qdrant-client"
|
||||
- "intersystems-*"
|
||||
- "tablestore"
|
||||
- "tcvectordb"
|
||||
- "tidb-vector"
|
||||
- "upstash-*"
|
||||
- "volcengine-*"
|
||||
- "weaviate-*"
|
||||
- "xinference-*"
|
||||
- "mo-vector"
|
||||
- "mysql-connector-*"
|
||||
- 'alibabacloud*'
|
||||
- 'chromadb'
|
||||
- 'clickhouse-*'
|
||||
- 'clickzetta-*'
|
||||
- 'couchbase'
|
||||
- 'elasticsearch'
|
||||
- 'opensearch-py'
|
||||
- 'oracledb'
|
||||
- 'pgvect*'
|
||||
- 'pymilvus'
|
||||
- 'pymochow'
|
||||
- 'pyobvector'
|
||||
- 'qdrant-client'
|
||||
- 'intersystems-*'
|
||||
- 'tablestore'
|
||||
- 'tcvectordb'
|
||||
- 'tidb-vector'
|
||||
- 'upstash-*'
|
||||
- 'volcengine-*'
|
||||
- 'weaviate-*'
|
||||
- 'xinference-*'
|
||||
- 'mo-vector'
|
||||
- 'mysql-connector-*'
|
||||
dev:
|
||||
patterns:
|
||||
- "coverage"
|
||||
- "dotenv-linter"
|
||||
- "faker"
|
||||
- "lxml-stubs"
|
||||
- "basedpyright"
|
||||
- "ruff"
|
||||
- "pytest*"
|
||||
- "types-*"
|
||||
- "boto3-stubs"
|
||||
- "hypothesis"
|
||||
- "pandas-stubs"
|
||||
- "scipy-stubs"
|
||||
- "import-linter"
|
||||
- "celery-types"
|
||||
- "mypy*"
|
||||
- "pyrefly"
|
||||
- 'coverage'
|
||||
- 'dotenv-linter'
|
||||
- 'faker'
|
||||
- 'lxml-stubs'
|
||||
- 'basedpyright'
|
||||
- 'ruff'
|
||||
- 'pytest*'
|
||||
- 'types-*'
|
||||
- 'boto3-stubs'
|
||||
- 'hypothesis'
|
||||
- 'pandas-stubs'
|
||||
- 'scipy-stubs'
|
||||
- 'import-linter'
|
||||
- 'celery-types'
|
||||
- 'mypy*'
|
||||
- 'pyrefly'
|
||||
python-packages:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "lts/1.13.x"
|
||||
- '*'
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
target-branch: 'lts/1.13.x'
|
||||
open-pull-requests-limit: 5
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
github-actions-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
- '*'
|
||||
|
||||
@@ -1 +1 @@
|
||||
failure-threshold: "error"
|
||||
failure-threshold: 'error'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"Verbose": false,
|
||||
"Debug": false,
|
||||
"IgnoreDefaults": false,
|
||||
"SpacesAfterTabs": false,
|
||||
"NoColor": false,
|
||||
"Exclude": [
|
||||
"^web/public/vs/",
|
||||
"^web/public/pdf.worker.min.mjs$",
|
||||
"web/app/components/base/icons/src/vender/"
|
||||
],
|
||||
"AllowedContentTypes": [],
|
||||
"PassedFiles": [],
|
||||
"Disable": {
|
||||
"EndOfLine": false,
|
||||
"Indentation": false,
|
||||
"IndentSize": true,
|
||||
"InsertFinalNewline": false,
|
||||
"TrimTrailingWhitespace": false,
|
||||
"MaxLineLength": false
|
||||
}
|
||||
"Verbose": false,
|
||||
"Debug": false,
|
||||
"IgnoreDefaults": false,
|
||||
"SpacesAfterTabs": false,
|
||||
"NoColor": false,
|
||||
"Exclude": [
|
||||
"^web/public/vs/",
|
||||
"^web/public/pdf.worker.min.mjs$",
|
||||
"web/app/components/base/icons/src/vender/"
|
||||
],
|
||||
"AllowedContentTypes": [],
|
||||
"PassedFiles": [],
|
||||
"Disable": {
|
||||
"EndOfLine": false,
|
||||
"Indentation": false,
|
||||
"IndentSize": true,
|
||||
"InsertFinalNewline": false,
|
||||
"TrimTrailingWhitespace": false,
|
||||
"MaxLineLength": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
## Screenshots
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| ... | ... |
|
||||
| ------ | ----- |
|
||||
| ... | ... |
|
||||
|
||||
## Checklist
|
||||
|
||||
|
||||
@@ -8,31 +8,31 @@ const headSha = process.env.HEAD_SHA || ''
|
||||
const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean)
|
||||
const outputPath = process.env.I18N_CHANGES_OUTPUT_PATH || '/tmp/i18n-changes.json'
|
||||
|
||||
const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
|
||||
const englishPath = (fileStem) => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
|
||||
|
||||
const readCurrentJson = (fileStem) => {
|
||||
const filePath = englishPath(fileStem)
|
||||
if (!fs.existsSync(filePath))
|
||||
return null
|
||||
if (!fs.existsSync(filePath)) return null
|
||||
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
const readBaseJson = (fileStem) => {
|
||||
if (!baseSha)
|
||||
return null
|
||||
if (!baseSha) return null
|
||||
|
||||
try {
|
||||
const relativePath = `web/i18n/en-US/${fileStem}.json`
|
||||
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' })
|
||||
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
return JSON.parse(content)
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue)
|
||||
const compareJson = (beforeValue, afterValue) =>
|
||||
JSON.stringify(beforeValue) === JSON.stringify(afterValue)
|
||||
|
||||
const changes = {}
|
||||
|
||||
@@ -59,8 +59,7 @@ for (const fileStem of files) {
|
||||
}
|
||||
|
||||
for (const key of Object.keys(beforeJson)) {
|
||||
if (!(key in afterJson))
|
||||
deleted.push(key)
|
||||
if (!(key in afterJson)) deleted.push(key)
|
||||
}
|
||||
|
||||
changes[fileStem] = {
|
||||
@@ -78,5 +77,5 @@ fs.writeFileSync(
|
||||
headSha,
|
||||
files,
|
||||
changes,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
name: Manual WCAG Accessibility Audit
|
||||
run-name: WCAG ${{ inputs.level }} · ${{ inputs.page }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
page:
|
||||
description: Page to scan
|
||||
required: true
|
||||
default: home
|
||||
type: choice
|
||||
options:
|
||||
- home
|
||||
- sign-in
|
||||
- studio
|
||||
- agents
|
||||
- knowledge
|
||||
- integrations
|
||||
- all
|
||||
level:
|
||||
description: WCAG level to scan
|
||||
required: true
|
||||
default: aa
|
||||
type: choice
|
||||
options:
|
||||
- aa
|
||||
- a
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: accessibility-e2e-${{ github.ref }}-${{ inputs.page }}-${{ inputs.level }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
accessibility:
|
||||
name: WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} · ${{ inputs.page }}
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
timeout-minutes: 120
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup web dependencies
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: |
|
||||
api/uv.lock
|
||||
|
||||
- name: Install API dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Install Chromium for accessibility E2E
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci:chromium
|
||||
|
||||
- name: Run automated WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} checks
|
||||
working-directory: ./e2e
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: [email protected]
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_FORCE_WEB_BUILD: '1'
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
WCAG_PAGE: ${{ inputs.page }}
|
||||
run: |
|
||||
if [[ "$WCAG_PAGE" == "all" ]]; then
|
||||
vp run e2e:accessibility:${{ inputs.level }}
|
||||
else
|
||||
pnpm exec tsx ./scripts/run-cucumber.ts --full -- --tags "@axe and @wcag-${{ inputs.level }} and @wcag-page-$WCAG_PAGE"
|
||||
fi
|
||||
|
||||
- name: Upload accessibility Cucumber report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cucumber-report-accessibility-${{ inputs.level }}-${{ inputs.page }}
|
||||
path: e2e/cucumber-report
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload accessibility E2E logs
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-logs-accessibility-${{ inputs.level }}-${{ inputs.page }}
|
||||
path: e2e/.logs/*.log
|
||||
include-hidden-files: true
|
||||
retention-days: 7
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.12"
|
||||
- '3.12'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.12"
|
||||
- '3.12'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: api/uv.lock
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name: autofix.ci
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -53,8 +53,7 @@ jobs:
|
||||
oxlint-suppressions.json
|
||||
eslint-suppressions.json
|
||||
.vscode/**
|
||||
.github/workflows/autofix.yml
|
||||
.github/workflows/style.yml
|
||||
.github/**
|
||||
- name: Check api inputs
|
||||
if: github.event_name != 'merge_group'
|
||||
id: api-changes
|
||||
@@ -86,7 +85,7 @@ jobs:
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: '3.11'
|
||||
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
|
||||
@@ -3,13 +3,13 @@ name: Build and Push API & Web
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "deploy/**"
|
||||
- "build/**"
|
||||
- "release/e-*"
|
||||
- "hotfix/**"
|
||||
- 'main'
|
||||
- 'deploy/**'
|
||||
- 'build/**'
|
||||
- 'release/e-*'
|
||||
- 'hotfix/**'
|
||||
tags:
|
||||
- "*"
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: build-push-${{ github.head_ref || github.run_id }}
|
||||
@@ -33,60 +33,60 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: "build-api-amd64"
|
||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||
artifact_context: "api"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "api/Dockerfile"
|
||||
- service_name: 'build-api-amd64'
|
||||
image_name_env: 'DIFY_API_IMAGE_NAME'
|
||||
artifact_context: 'api'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'api/Dockerfile'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-api-arm64"
|
||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||
artifact_context: "api"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "api/Dockerfile"
|
||||
- service_name: 'build-api-arm64'
|
||||
image_name_env: 'DIFY_API_IMAGE_NAME'
|
||||
artifact_context: 'api'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'api/Dockerfile'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-web-amd64"
|
||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||
artifact_context: "web"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "web/Dockerfile"
|
||||
- service_name: 'build-web-amd64'
|
||||
image_name_env: 'DIFY_WEB_IMAGE_NAME'
|
||||
artifact_context: 'web'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'web/Dockerfile'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-web-arm64"
|
||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||
artifact_context: "web"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "web/Dockerfile"
|
||||
- service_name: 'build-web-arm64'
|
||||
image_name_env: 'DIFY_WEB_IMAGE_NAME'
|
||||
artifact_context: 'web'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'web/Dockerfile'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-agent-amd64"
|
||||
image_name_env: "DIFY_AGENT_IMAGE_NAME"
|
||||
artifact_context: "agent"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "dify-agent/Dockerfile"
|
||||
- service_name: 'build-agent-amd64'
|
||||
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
|
||||
artifact_context: 'agent'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'dify-agent/Dockerfile'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-agent-arm64"
|
||||
image_name_env: "DIFY_AGENT_IMAGE_NAME"
|
||||
artifact_context: "agent"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "dify-agent/Dockerfile"
|
||||
- service_name: 'build-agent-arm64'
|
||||
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
|
||||
artifact_context: 'agent'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'dify-agent/Dockerfile'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-agent-local-sandbox-amd64"
|
||||
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
|
||||
artifact_context: "local-sandbox"
|
||||
build_context: "{{defaultContext}}:dify-agent-runtime"
|
||||
file: "docker/Dockerfile"
|
||||
- service_name: 'build-agent-local-sandbox-amd64'
|
||||
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
|
||||
artifact_context: 'local-sandbox'
|
||||
build_context: '{{defaultContext}}:dify-agent-runtime'
|
||||
file: 'docker/Dockerfile'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
- service_name: "build-agent-local-sandbox-arm64"
|
||||
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
|
||||
artifact_context: "local-sandbox"
|
||||
build_context: "{{defaultContext}}:dify-agent-runtime"
|
||||
file: "docker/Dockerfile"
|
||||
- service_name: 'build-agent-local-sandbox-arm64'
|
||||
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
|
||||
artifact_context: 'local-sandbox'
|
||||
build_context: '{{defaultContext}}:dify-agent-runtime'
|
||||
file: 'docker/Dockerfile'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
|
||||
@@ -146,18 +146,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: "validate-api-amd64"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "api/Dockerfile"
|
||||
- service_name: "validate-web-amd64"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "web/Dockerfile"
|
||||
- service_name: "validate-agent-amd64"
|
||||
build_context: "{{defaultContext}}"
|
||||
file: "dify-agent/Dockerfile"
|
||||
- service_name: "validate-agent-local-sandbox-amd64"
|
||||
build_context: "{{defaultContext}}:dify-agent-runtime"
|
||||
file: "docker/Dockerfile"
|
||||
- service_name: 'validate-api-amd64'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'api/Dockerfile'
|
||||
- service_name: 'validate-web-amd64'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'web/Dockerfile'
|
||||
- service_name: 'validate-agent-amd64'
|
||||
build_context: '{{defaultContext}}'
|
||||
file: 'dify-agent/Dockerfile'
|
||||
- service_name: 'validate-agent-local-sandbox-amd64'
|
||||
build_context: '{{defaultContext}}:dify-agent-runtime'
|
||||
file: 'docker/Dockerfile'
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
@@ -178,18 +178,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: "merge-api-images"
|
||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||
context: "api"
|
||||
- service_name: "merge-web-images"
|
||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||
context: "web"
|
||||
- service_name: "merge-agent-images"
|
||||
image_name_env: "DIFY_AGENT_IMAGE_NAME"
|
||||
context: "agent"
|
||||
- service_name: "merge-agent-local-sandbox-images"
|
||||
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
|
||||
context: "local-sandbox"
|
||||
- service_name: 'merge-api-images'
|
||||
image_name_env: 'DIFY_API_IMAGE_NAME'
|
||||
context: 'api'
|
||||
- service_name: 'merge-web-images'
|
||||
image_name_env: 'DIFY_WEB_IMAGE_NAME'
|
||||
context: 'web'
|
||||
- service_name: 'merge-agent-images'
|
||||
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
|
||||
context: 'agent'
|
||||
- service_name: 'merge-agent-local-sandbox-images'
|
||||
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
|
||||
context: 'local-sandbox'
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
|
||||
+114
-115
@@ -4,19 +4,19 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cli_ref:
|
||||
description: "Git ref (default: current branch)"
|
||||
description: 'Git ref (default: current branch)'
|
||||
type: string
|
||||
required: false
|
||||
|
||||
edition:
|
||||
description: "Dify edition"
|
||||
description: 'Dify edition'
|
||||
type: choice
|
||||
required: false
|
||||
default: ee
|
||||
options: [ee, ce]
|
||||
|
||||
test_scope:
|
||||
description: "smoke = [P0] only / full = all cases"
|
||||
description: 'smoke = [P0] only / full = all cases'
|
||||
type: choice
|
||||
required: false
|
||||
default: full
|
||||
@@ -24,23 +24,23 @@ on:
|
||||
|
||||
# ── Suite on/off ────────────────────────────────────────────────────────
|
||||
suite_framework_output_error:
|
||||
description: "framework + output + error-handling suites"
|
||||
description: 'framework + output + error-handling suites'
|
||||
type: boolean
|
||||
default: true
|
||||
suite_discovery:
|
||||
description: "discovery suite (get app / describe app)"
|
||||
description: 'discovery suite (get app / describe app)'
|
||||
type: boolean
|
||||
default: true
|
||||
suite_run:
|
||||
description: "run suite (basic / streaming / conversation / file / hitl)"
|
||||
description: 'run suite (basic / streaming / conversation / file / hitl)'
|
||||
type: boolean
|
||||
default: true
|
||||
suite_auth:
|
||||
description: "auth suite (login / status / whoami / use / devices / logout)"
|
||||
description: 'auth suite (login / status / whoami / use / devices / logout)'
|
||||
type: boolean
|
||||
default: true
|
||||
suite_agent:
|
||||
description: "agent suite"
|
||||
description: 'agent suite'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
@@ -51,32 +51,31 @@ permissions:
|
||||
# Each job reads DIFY_E2E_TOKEN + app IDs from the provision job outputs,
|
||||
# so global-setup skips minting and finds existing apps in < 10 s.
|
||||
env:
|
||||
DIFY_E2E_NO_KEYRING: "1" # Linux CI has no keychain; skip probe
|
||||
VITEST_RETRY: "2" # Retry flaky staging responses
|
||||
DIFY_E2E_NO_KEYRING: '1' # Linux CI has no keychain; skip probe
|
||||
VITEST_RETRY: '2' # Retry flaky staging responses
|
||||
|
||||
jobs:
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 0. PROVISION — mint token + import DSL fixtures (runs once, outputs IDs)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 0. PROVISION — mint token + import DSL fixtures (runs once, outputs IDs)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
provision:
|
||||
name: "Provision: mint token + DSL apps"
|
||||
name: 'Provision: mint token + DSL apps'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
token: ${{ steps.out.outputs.DIFY_E2E_TOKEN }}
|
||||
workspace_id: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_ID }}
|
||||
workspace_name: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_NAME }}
|
||||
ws2_id: ${{ steps.out.outputs.DIFY_E2E_WS2_ID }}
|
||||
chat_app_id: ${{ steps.out.outputs.DIFY_E2E_CHAT_APP_ID }}
|
||||
workflow_app_id: ${{ steps.out.outputs.DIFY_E2E_WORKFLOW_APP_ID }}
|
||||
file_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_APP_ID }}
|
||||
file_chat_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_CHAT_APP_ID }}
|
||||
hitl_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_APP_ID }}
|
||||
hitl_external_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_EXTERNAL_APP_ID }}
|
||||
hitl_single_action_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID }}
|
||||
hitl_multi_node_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_MULTI_NODE_APP_ID }}
|
||||
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
|
||||
token: ${{ steps.out.outputs.DIFY_E2E_TOKEN }}
|
||||
workspace_id: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_ID }}
|
||||
workspace_name: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_NAME }}
|
||||
ws2_id: ${{ steps.out.outputs.DIFY_E2E_WS2_ID }}
|
||||
chat_app_id: ${{ steps.out.outputs.DIFY_E2E_CHAT_APP_ID }}
|
||||
workflow_app_id: ${{ steps.out.outputs.DIFY_E2E_WORKFLOW_APP_ID }}
|
||||
file_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_APP_ID }}
|
||||
file_chat_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_CHAT_APP_ID }}
|
||||
hitl_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_APP_ID }}
|
||||
hitl_external_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_EXTERNAL_APP_ID }}
|
||||
hitl_single_action_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID }}
|
||||
hitl_multi_node_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_MULTI_NODE_APP_ID }}
|
||||
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
@@ -88,7 +87,7 @@ jobs:
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
package_json_field: packageManager
|
||||
run_install: false
|
||||
@@ -101,18 +100,18 @@ jobs:
|
||||
id: out
|
||||
working-directory: cli
|
||||
env:
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_TOKEN: ${{ secrets.DIFY_E2E_TOKEN }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ secrets.DIFY_E2E_TOKEN }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
run: bun scripts/e2e-provision.ts
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-B. framework + output + error-handling (parallel with run/discovery)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-B. framework + output + error-handling (parallel with run/discovery)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
suite-framework-output-error:
|
||||
name: "Suite: framework + output + error-handling"
|
||||
name: 'Suite: framework + output + error-handling'
|
||||
if: ${{ inputs.suite_framework_output_error != 'false' }}
|
||||
needs: provision
|
||||
runs-on: ubuntu-latest
|
||||
@@ -131,23 +130,23 @@ jobs:
|
||||
- uses: ./.github/actions/setup-web
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with: { bun-version: latest }
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with: { package_json_field: packageManager, run_install: false }
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm tree:gen
|
||||
|
||||
- name: Run framework + output + error-handling
|
||||
env:
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
|
||||
DIFY_E2E_INCLUDE: "test/e2e/suites/framework/**/*.e2e.ts,test/e2e/suites/output/**/*.e2e.ts,test/e2e/suites/error-handling/**/*.e2e.ts"
|
||||
DIFY_E2E_INCLUDE: 'test/e2e/suites/framework/**/*.e2e.ts,test/e2e/suites/output/**/*.e2e.ts,test/e2e/suites/error-handling/**/*.e2e.ts'
|
||||
run: |
|
||||
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
|
||||
pnpm test:e2e -- -t "\[P0\]"
|
||||
@@ -155,11 +154,11 @@ jobs:
|
||||
pnpm test:e2e
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-C. Discovery (parallel)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-C. Discovery (parallel)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
suite-discovery:
|
||||
name: "Suite: discovery"
|
||||
name: 'Suite: discovery'
|
||||
if: ${{ inputs.suite_discovery != 'false' }}
|
||||
needs: provision
|
||||
runs-on: ubuntu-latest
|
||||
@@ -178,24 +177,24 @@ jobs:
|
||||
- uses: ./.github/actions/setup-web
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with: { bun-version: latest }
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with: { package_json_field: packageManager, run_install: false }
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm tree:gen
|
||||
|
||||
- name: Run discovery suite
|
||||
env:
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
|
||||
DIFY_E2E_INCLUDE: "test/e2e/suites/discovery/**/*.e2e.ts"
|
||||
DIFY_E2E_INCLUDE: 'test/e2e/suites/discovery/**/*.e2e.ts'
|
||||
run: |
|
||||
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
|
||||
pnpm test:e2e -- -t "\[P0\]"
|
||||
@@ -203,11 +202,11 @@ jobs:
|
||||
pnpm test:e2e
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-D. Run suite — 5 files in matrix (parallel)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-D. Run suite — 5 files in matrix (parallel)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
suite-run:
|
||||
name: "Suite: run / ${{ matrix.name }}"
|
||||
name: 'Suite: run / ${{ matrix.name }}'
|
||||
if: ${{ inputs.suite_run != 'false' }}
|
||||
needs: provision
|
||||
runs-on: ubuntu-latest
|
||||
@@ -241,30 +240,30 @@ jobs:
|
||||
- uses: ./.github/actions/setup-web
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with: { bun-version: latest }
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with: { package_json_field: packageManager, run_install: false }
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm tree:gen
|
||||
|
||||
- name: "Run run/${{ matrix.name }}"
|
||||
- name: 'Run run/${{ matrix.name }}'
|
||||
env:
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_SSO_TOKEN: ${{ secrets.DIFY_E2E_SSO_TOKEN }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
|
||||
DIFY_E2E_FILE_APP_ID: ${{ needs.provision.outputs.file_app_id }}
|
||||
DIFY_E2E_FILE_CHAT_APP_ID: ${{ needs.provision.outputs.file_chat_app_id }}
|
||||
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
|
||||
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_SSO_TOKEN: ${{ secrets.DIFY_E2E_SSO_TOKEN }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
|
||||
DIFY_E2E_FILE_APP_ID: ${{ needs.provision.outputs.file_app_id }}
|
||||
DIFY_E2E_FILE_CHAT_APP_ID: ${{ needs.provision.outputs.file_chat_app_id }}
|
||||
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
|
||||
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
|
||||
DIFY_E2E_HITL_SINGLE_ACTION_APP_ID: ${{ needs.provision.outputs.hitl_single_action_app_id }}
|
||||
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
|
||||
DIFY_E2E_INCLUDE: "test/e2e/suites/run/${{ matrix.file }}"
|
||||
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
|
||||
DIFY_E2E_INCLUDE: 'test/e2e/suites/run/${{ matrix.file }}'
|
||||
run: |
|
||||
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
|
||||
pnpm test:e2e -- -t "\[P0\]"
|
||||
@@ -280,11 +279,11 @@ jobs:
|
||||
path: cli/test-results/
|
||||
retention-days: 3
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-E. auth/login + status + whoami (parallel, read-only, safe)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 1-E. auth/login + status + whoami (parallel, read-only, safe)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
suite-auth-safe:
|
||||
name: "Suite: auth (login / status / whoami)"
|
||||
name: 'Suite: auth (login / status / whoami)'
|
||||
if: ${{ inputs.suite_auth != 'false' }}
|
||||
needs: provision
|
||||
runs-on: ubuntu-latest
|
||||
@@ -303,22 +302,22 @@ jobs:
|
||||
- uses: ./.github/actions/setup-web
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with: { bun-version: latest }
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with: { package_json_field: packageManager, run_install: false }
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm tree:gen
|
||||
|
||||
- name: Run auth/login + status + whoami
|
||||
env:
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
|
||||
DIFY_E2E_INCLUDE: "test/e2e/suites/auth/login.e2e.ts,test/e2e/suites/auth/status.e2e.ts,test/e2e/suites/auth/whoami.e2e.ts"
|
||||
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
|
||||
DIFY_E2E_INCLUDE: 'test/e2e/suites/auth/login.e2e.ts,test/e2e/suites/auth/status.e2e.ts,test/e2e/suites/auth/whoami.e2e.ts'
|
||||
run: |
|
||||
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
|
||||
pnpm test:e2e -- -t "\[P0\]"
|
||||
@@ -326,13 +325,13 @@ jobs:
|
||||
pnpm test:e2e
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 2. DESTRUCTIVE — auth/use + devices + logout + agent (serial, runs LAST)
|
||||
# Must wait for ALL parallel suites to finish to avoid token revocation
|
||||
# invalidating other in-flight requests.
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 2. DESTRUCTIVE — auth/use + devices + logout + agent (serial, runs LAST)
|
||||
# Must wait for ALL parallel suites to finish to avoid token revocation
|
||||
# invalidating other in-flight requests.
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
suite-last:
|
||||
name: "Suite: auth-use + devices + logout + agent (last, serial)"
|
||||
name: 'Suite: auth-use + devices + logout + agent (last, serial)'
|
||||
# Runs when auth is selected; also runs after all parallel jobs finish
|
||||
if: ${{ inputs.suite_auth != 'false' || inputs.suite_agent != 'false' }}
|
||||
needs:
|
||||
@@ -359,27 +358,27 @@ jobs:
|
||||
- uses: ./.github/actions/setup-web
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with: { bun-version: latest }
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with: { package_json_field: packageManager, run_install: false }
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm tree:gen
|
||||
|
||||
- name: Run use / devices / logout / agent (serial)
|
||||
env:
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
|
||||
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
|
||||
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
|
||||
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
|
||||
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
|
||||
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
|
||||
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
|
||||
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
|
||||
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
|
||||
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
|
||||
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
|
||||
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
|
||||
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
|
||||
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
|
||||
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
|
||||
DIFY_E2E_HITL_SINGLE_ACTION_APP_ID: ${{ needs.provision.outputs.hitl_single_action_app_id }}
|
||||
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
|
||||
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
|
||||
run: |
|
||||
# Collect files in safe order: use → devices → logout (revokes last) → agent
|
||||
FILES=()
|
||||
|
||||
@@ -2,10 +2,11 @@ name: CLI Edge Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, deploy/konwledge]
|
||||
paths:
|
||||
- 'cli/**'
|
||||
- 'packages/contracts/generated/api/openapi/**'
|
||||
- '.github/workflows/cli-edge.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: cli/.bun-version
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
run: node scripts/release-naming.mjs github-env >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: cli/.bun-version
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dify_version:
|
||||
description: "Dify image tag to test against (e.g. 1.7.0)"
|
||||
description: 'Dify image tag to test against (e.g. 1.7.0)'
|
||||
type: string
|
||||
required: true
|
||||
cli_ref:
|
||||
description: "Git ref to build the cli from (default: current branch)"
|
||||
description: 'Git ref to build the cli from (default: current branch)'
|
||||
type: string
|
||||
required: false
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: api/uv.lock
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
cp envs/middleware.env.example middleware.env
|
||||
|
||||
- name: Set up Middlewares
|
||||
uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0
|
||||
uses: hoverkraft-tech/compose-action@ee6af68587292d72db67743171809c19787df4c9 # v3.1.0
|
||||
with:
|
||||
compose-file: |
|
||||
docker/docker-compose.middleware.yaml
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: api/uv.lock
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
|
||||
|
||||
- name: Set up Middlewares
|
||||
uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0
|
||||
uses: hoverkraft-tech/compose-action@ee6af68587292d72db67743171809c19787df4c9 # v3.1.0
|
||||
with:
|
||||
compose-file: |
|
||||
docker/docker-compose.middleware.yaml
|
||||
|
||||
@@ -5,9 +5,9 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
workflows: ['Build and Push API & Web']
|
||||
branches:
|
||||
- "deploy/agent"
|
||||
- 'deploy/agent'
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
@@ -2,23 +2,70 @@ name: Deploy Dev
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
workflows: ['KnowledgeFS CI']
|
||||
branches:
|
||||
- "deploy/dev"
|
||||
- 'deploy/konwledge'
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: depot-ubuntu-24.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'deploy/dev'
|
||||
github.event.workflow_run.head_branch == 'deploy/konwledge'
|
||||
steps:
|
||||
- name: Wait for API and Web image build
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
timeout-minutes: 35
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const workflowId = "build-push.yml";
|
||||
const headBranch = context.payload.workflow_run.head_branch;
|
||||
const headSha = context.payload.workflow_run.head_sha;
|
||||
const deadline = Date.now() + 30 * 60 * 1000;
|
||||
const pollIntervalMs = 15 * 1000;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: workflowId,
|
||||
branch: headBranch,
|
||||
event: "push",
|
||||
head_sha: headSha,
|
||||
per_page: 10,
|
||||
});
|
||||
const run = data.workflow_runs[0];
|
||||
|
||||
if (!run) {
|
||||
core.info(`Waiting for ${workflowId} to start for ${headSha}.`);
|
||||
} else if (run.status !== "completed") {
|
||||
core.info(`Waiting for ${run.html_url}; current status is ${run.status}.`);
|
||||
} else if (run.conclusion !== "success") {
|
||||
throw new Error(
|
||||
`${workflowId} did not succeed for ${headSha}: ${run.conclusion} (${run.html_url})`,
|
||||
);
|
||||
} else {
|
||||
core.info(`Both image workflows succeeded for ${headSha}: ${run.html_url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for ${workflowId} to succeed for ${headSha}.`);
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
host: ${{ secrets.SSH_NEW_RAG_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
|
||||
@@ -5,9 +5,9 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
workflows: ['Build and Push API & Web']
|
||||
branches:
|
||||
- "deploy/enterprise"
|
||||
- 'deploy/enterprise'
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
workflows: ['Build and Push API & Web']
|
||||
branches:
|
||||
- "deploy/konwledge"
|
||||
- 'deploy/konwledge'
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -68,4 +68,4 @@ jobs:
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
|
||||
${{ vars.SSH_KNOWLEDGE_SCRIPT || secrets.SSH_KNOWLEDGE_SCRIPT }}
|
||||
|
||||
@@ -5,9 +5,9 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
workflows: ['Build and Push API & Web']
|
||||
branches:
|
||||
- "deploy/saas"
|
||||
- 'deploy/saas'
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Build docker image
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- 'main'
|
||||
paths:
|
||||
- api/Dockerfile
|
||||
- api/Dockerfile.dockerignore
|
||||
@@ -33,36 +33,36 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: "api-amd64"
|
||||
- service_name: 'api-amd64'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
context: "{{defaultContext}}"
|
||||
file: "api/Dockerfile"
|
||||
- service_name: "api-arm64"
|
||||
context: '{{defaultContext}}'
|
||||
file: 'api/Dockerfile'
|
||||
- service_name: 'api-arm64'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
context: "{{defaultContext}}"
|
||||
file: "api/Dockerfile"
|
||||
- service_name: "web-amd64"
|
||||
context: '{{defaultContext}}'
|
||||
file: 'api/Dockerfile'
|
||||
- service_name: 'web-amd64'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
context: "{{defaultContext}}"
|
||||
file: "web/Dockerfile"
|
||||
- service_name: "web-arm64"
|
||||
context: '{{defaultContext}}'
|
||||
file: 'web/Dockerfile'
|
||||
- service_name: 'web-arm64'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
context: "{{defaultContext}}"
|
||||
file: "web/Dockerfile"
|
||||
- service_name: "local-sandbox-amd64"
|
||||
context: '{{defaultContext}}'
|
||||
file: 'web/Dockerfile'
|
||||
- service_name: 'local-sandbox-amd64'
|
||||
platform: linux/amd64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
context: "{{defaultContext}}:dify-agent-runtime"
|
||||
file: "docker/Dockerfile"
|
||||
- service_name: "local-sandbox-arm64"
|
||||
context: '{{defaultContext}}:dify-agent-runtime'
|
||||
file: 'docker/Dockerfile'
|
||||
- service_name: 'local-sandbox-arm64'
|
||||
platform: linux/arm64
|
||||
runs_on: depot-ubuntu-24.04-4
|
||||
context: "{{defaultContext}}:dify-agent-runtime"
|
||||
file: "docker/Dockerfile"
|
||||
context: '{{defaultContext}}:dify-agent-runtime'
|
||||
file: 'docker/Dockerfile'
|
||||
steps:
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||
@@ -85,15 +85,15 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: "api-amd64"
|
||||
context: "{{defaultContext}}"
|
||||
file: "api/Dockerfile"
|
||||
- service_name: "web-amd64"
|
||||
context: "{{defaultContext}}"
|
||||
file: "web/Dockerfile"
|
||||
- service_name: "local-sandbox-amd64"
|
||||
context: "{{defaultContext}}:dify-agent-runtime"
|
||||
file: "docker/Dockerfile"
|
||||
- service_name: 'api-amd64'
|
||||
context: '{{defaultContext}}'
|
||||
file: 'api/Dockerfile'
|
||||
- service_name: 'web-amd64'
|
||||
context: '{{defaultContext}}'
|
||||
file: 'web/Dockerfile'
|
||||
- service_name: 'local-sandbox-amd64'
|
||||
context: '{{defaultContext}}:dify-agent-runtime'
|
||||
file: 'docker/Dockerfile'
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
name: KnowledgeFS CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
merge_group:
|
||||
branches: ['main']
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: ['main', 'deploy/konwledge']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: knowledge-fs-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CI: true
|
||||
DIFY_KNOWLEDGE_FS_API_IMAGE_NAME: >-
|
||||
${{ vars.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME || 'langgenius/dify-knowledge-fs-api' }}
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
name: Check KnowledgeFS changes
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
outputs:
|
||||
knowledge-fs: ${{ steps.changes.outputs.knowledge-fs }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect KnowledgeFS changes
|
||||
id: changes
|
||||
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
with:
|
||||
filters: |
|
||||
knowledge-fs:
|
||||
- 'knowledge-fs/**'
|
||||
- 'knowledge-fs/packages/api/src/dify-capability-v2.ts'
|
||||
- 'knowledge-fs/packages/api/src/knowledge-space-routes.ts'
|
||||
- 'knowledge-fs/packages/api/src/upload-session-routes.ts'
|
||||
- 'knowledge-fs/scripts/export-capability-v2-operations.mjs'
|
||||
- 'knowledge-fs/scripts/export-openapi.mjs'
|
||||
- 'api/dev/generate_knowledge_fs_contract.py'
|
||||
- 'api/dev/knowledge_fs_product_contract.py'
|
||||
- 'api/knowledge-fs-contract.lock.json'
|
||||
- 'api/knowledge-fs-product-operation-gaps.json'
|
||||
- 'api/knowledge-fs-product-operations.json'
|
||||
- 'api/**/knowledge_fs/**'
|
||||
- 'api/**/*knowledge_fs*'
|
||||
- 'api/**/*knowledge-fs*'
|
||||
- 'api/.env.example'
|
||||
- 'api/app_factory.py'
|
||||
- 'api/commands/__init__.py'
|
||||
- 'api/controllers/console/__init__.py'
|
||||
- 'api/controllers/console/workspace/rbac.py'
|
||||
- 'api/controllers/service_api/__init__.py'
|
||||
- 'api/core/agent/base_agent_runner.py'
|
||||
- 'api/core/app/apps/agent_app/runtime_request_builder.py'
|
||||
- 'api/core/rbac/entities.py'
|
||||
- 'api/core/tools/__base/tool_runtime.py'
|
||||
- 'api/core/tools/builtin_tool/_position.yaml'
|
||||
- 'api/core/workflow/node_runtime.py'
|
||||
- 'api/core/workflow/nodes/agent_v2/runtime_request_builder.py'
|
||||
- 'api/extensions/ext_celery.py'
|
||||
- 'api/extensions/ext_commands.py'
|
||||
- 'api/models/__init__.py'
|
||||
- 'api/services/account_service.py'
|
||||
- 'api/services/agent_tool_inner_service.py'
|
||||
- 'api/services/enterprise/rbac_service.py'
|
||||
- 'api/services/entities/agent_tool_inner.py'
|
||||
- 'api/services/knowledge_fs/**'
|
||||
- 'api/services/knowledge_fs_capability.py'
|
||||
- 'api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py'
|
||||
- 'api/tests/unit_tests/controllers/console/workspace/test_rbac.py'
|
||||
- 'api/tests/unit_tests/core/agent/test_base_agent_runner.py'
|
||||
- 'api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py'
|
||||
- 'api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py'
|
||||
- 'api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py'
|
||||
- 'api/tests/unit_tests/core/workflow/test_node_runtime.py'
|
||||
- 'api/tests/unit_tests/services/enterprise/test_rbac_service.py'
|
||||
- 'api/tests/unit_tests/services/test_account_service.py'
|
||||
- 'api/tests/unit_tests/services/test_agent_tool_inner_service.py'
|
||||
- 'api/tests/unit_tests/services/test_knowledge_fs_capability.py'
|
||||
- 'api/tests/unit_tests/services/test_knowledge_fs_product_operations.py'
|
||||
- 'api/pyproject.toml'
|
||||
- 'api/uv.lock'
|
||||
- 'dify-agent/src/dify_agent/layers/dify_core_tools/client.py'
|
||||
- 'dify-agent/tests/local/dify_agent/layers/dify_core_tools/test_client.py'
|
||||
- 'packages/contracts/generated/api/console/**'
|
||||
- 'packages/contracts/generated/api/service/**'
|
||||
- 'docker/.env.example'
|
||||
- 'docker/README.md'
|
||||
- 'docker/dify-env-sync.py'
|
||||
- 'docker/dify-env-sync.sh'
|
||||
- 'docker/docker-compose-template.yaml'
|
||||
- 'docker/docker-compose.yaml'
|
||||
- 'docker/envs/core-services/api.env.example'
|
||||
- 'docker/envs/core-services/knowledge-fs.env.example'
|
||||
- 'docker/generate_docker_compose'
|
||||
- 'docs/design/knowledge-fs*'
|
||||
- '.github/dependabot.yml'
|
||||
- '.github/workflows/knowledge-fs-ci.yml'
|
||||
|
||||
build:
|
||||
name: Build KnowledgeFS API production image
|
||||
needs:
|
||||
- check-changes
|
||||
- quality
|
||||
if: >-
|
||||
(needs.check-changes.outputs.knowledge-fs == 'true' || github.event_name == 'workflow_dispatch') &&
|
||||
needs.quality.result == 'success'
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/deploy/konwledge'))
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract KnowledgeFS image metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
with:
|
||||
images: ${{ env.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=ref,event=branch
|
||||
type=sha,format=long
|
||||
|
||||
- name: Build KnowledgeFS API image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: ./knowledge-fs
|
||||
file: ./knowledge-fs/apps/api/Dockerfile
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
push: >-
|
||||
${{ github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/deploy/konwledge')) }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
quality:
|
||||
name: Run KnowledgeFS quality and contract gates
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.knowledge-fs == 'true' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: ./knowledge-fs
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: knowledge-fs/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: knowledge-fs/pnpm-lock.yaml
|
||||
|
||||
- name: Install KnowledgeFS dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Scan KnowledgeFS secrets
|
||||
run: pnpm security:secrets
|
||||
|
||||
- name: Audit KnowledgeFS production dependencies
|
||||
run: pnpm security:dependencies
|
||||
|
||||
- name: Run KnowledgeFS checks
|
||||
run: pnpm check
|
||||
|
||||
- name: Build KnowledgeFS
|
||||
run: pnpm build
|
||||
|
||||
- name: Lint KnowledgeFS
|
||||
run: pnpm lint:backend
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: |
|
||||
api/uv.lock
|
||||
dify-agent/uv.lock
|
||||
|
||||
- name: Verify Dify dependency lock
|
||||
working-directory: .
|
||||
run: uv lock --project api --check
|
||||
|
||||
- name: Install Dify contract dependencies
|
||||
working-directory: .
|
||||
run: uv sync --project api --locked --dev
|
||||
|
||||
- name: Collect Dify KnowledgeFS gate targets
|
||||
working-directory: .
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
target_dir="${RUNNER_TEMP:?}/knowledge-fs-ci-targets"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
production_targets=()
|
||||
add_production_target() {
|
||||
local path="$1"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
echo "required Dify KnowledgeFS production target is missing: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
production_targets+=("$path")
|
||||
}
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
if [[ "$path" == *knowledge_fs* ]]; then
|
||||
add_production_target "$path"
|
||||
fi
|
||||
done < <(
|
||||
find api \
|
||||
\( -path 'api/.venv' -o -path 'api/tests' -o -path 'api/storage' \) -prune \
|
||||
-o -type f -name '*.py' -print0
|
||||
)
|
||||
|
||||
production_touchpoints=(
|
||||
api/app_factory.py
|
||||
api/commands/__init__.py
|
||||
api/controllers/console/__init__.py
|
||||
api/controllers/console/workspace/rbac.py
|
||||
api/controllers/service_api/__init__.py
|
||||
api/core/agent/base_agent_runner.py
|
||||
api/core/app/apps/agent_app/runtime_request_builder.py
|
||||
api/core/rbac/entities.py
|
||||
api/core/tools/__base/tool_runtime.py
|
||||
api/core/workflow/node_runtime.py
|
||||
api/core/workflow/nodes/agent_v2/runtime_request_builder.py
|
||||
api/extensions/ext_celery.py
|
||||
api/extensions/ext_commands.py
|
||||
api/models/__init__.py
|
||||
api/services/account_service.py
|
||||
api/services/agent_tool_inner_service.py
|
||||
api/services/enterprise/rbac_service.py
|
||||
api/services/entities/agent_tool_inner.py
|
||||
)
|
||||
for path in "${production_touchpoints[@]}"; do
|
||||
add_production_target "$path"
|
||||
done
|
||||
printf '%s\0' "${production_touchpoints[@]}" > "$target_dir/glue-files"
|
||||
|
||||
if ((${#production_targets[@]} == 0)); then
|
||||
echo "Dify KnowledgeFS production target set is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\0' "${production_targets[@]}" > "$target_dir/production-files"
|
||||
|
||||
test_targets=()
|
||||
add_test_target() {
|
||||
local path="$1"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
echo "required Dify KnowledgeFS unit test is missing: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
test_targets+=("$path")
|
||||
}
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
if [[ "$path" == *knowledge_fs* ]]; then
|
||||
add_test_target "$path"
|
||||
fi
|
||||
done < <(find api/tests/unit_tests -type f -name '*.py' -print0)
|
||||
|
||||
test_touchpoints=(
|
||||
api/tests/unit_tests/controllers/console/workspace/test_rbac.py
|
||||
api/tests/unit_tests/core/agent/test_base_agent_runner.py
|
||||
api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py
|
||||
api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py
|
||||
api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py
|
||||
api/tests/unit_tests/core/workflow/test_node_runtime.py
|
||||
api/tests/unit_tests/services/enterprise/test_rbac_service.py
|
||||
api/tests/unit_tests/services/test_account_service.py
|
||||
api/tests/unit_tests/services/test_agent_tool_inner_service.py
|
||||
)
|
||||
for path in "${test_touchpoints[@]}"; do
|
||||
add_test_target "$path"
|
||||
done
|
||||
|
||||
required_test_scopes=(
|
||||
/commands/
|
||||
/configs/
|
||||
/controllers/
|
||||
/core/agent/
|
||||
/core/app/apps/agent_app/
|
||||
/core/tools/builtin_tool/providers/knowledge_fs/
|
||||
/core/workflow/
|
||||
/dev/
|
||||
/extensions/
|
||||
/migrations/
|
||||
/models/
|
||||
/repositories/
|
||||
/services/
|
||||
/tasks/
|
||||
)
|
||||
for required_scope in "${required_test_scopes[@]}"; do
|
||||
scope_found=false
|
||||
for path in "${test_targets[@]}"; do
|
||||
if [[ "$path" == *"$required_scope"* ]]; then
|
||||
scope_found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$scope_found" != true ]]; then
|
||||
echo "required Dify KnowledgeFS test scope is empty: $required_scope" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ((${#test_targets[@]} == 0)); then
|
||||
echo "Dify KnowledgeFS unit test target set is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\0' "${test_targets[@]}" > "$target_dir/unit-test-files"
|
||||
|
||||
- name: Lint Dify KnowledgeFS integration
|
||||
working-directory: .
|
||||
run: |
|
||||
set -euo pipefail
|
||||
targets=()
|
||||
while IFS= read -r -d '' path; do
|
||||
targets+=("$path")
|
||||
done < "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/production-files"
|
||||
if ((${#targets[@]} == 0)); then
|
||||
echo "Dify KnowledgeFS production target manifest is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
uv run --project api --dev ruff format --check "${targets[@]}"
|
||||
uv run --project api --dev ruff check "${targets[@]}"
|
||||
|
||||
- name: Type-check Dify KnowledgeFS integration
|
||||
working-directory: .
|
||||
run: |
|
||||
set -euo pipefail
|
||||
targets=()
|
||||
while IFS= read -r -d '' path; do
|
||||
targets+=("$path")
|
||||
done < "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/production-files"
|
||||
if ((${#targets[@]} == 0)); then
|
||||
echo "Dify KnowledgeFS production target manifest is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
PYREFLY_OUTPUT_FORMAT=github ./dev/pyrefly-check-local "${targets[@]}"
|
||||
|
||||
mypy_targets=()
|
||||
for path in "${targets[@]}"; do
|
||||
if [[ "$path" != api/migrations/* ]]; then
|
||||
mypy_targets+=("${path#api/}")
|
||||
fi
|
||||
done
|
||||
if ((${#mypy_targets[@]} == 0)); then
|
||||
echo "Dify KnowledgeFS Mypy target set is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
uv run --directory api --dev mypy \
|
||||
--explicit-package-bases \
|
||||
--exclude-gitignore \
|
||||
--exclude '(^|/)conftest\.py$' \
|
||||
--exclude 'tests/' \
|
||||
--exclude 'migrations/' \
|
||||
--check-untyped-defs \
|
||||
--disable-error-code=import-untyped \
|
||||
"${mypy_targets[@]}"
|
||||
|
||||
- name: Test Dify KnowledgeFS unit surface
|
||||
working-directory: .
|
||||
env:
|
||||
COVERAGE_FILE: ${{ runner.temp }}/dify-knowledge-fs.coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
targets=()
|
||||
while IFS= read -r -d '' path; do
|
||||
targets+=("$path")
|
||||
done < "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/unit-test-files"
|
||||
if ((${#targets[@]} == 0)); then
|
||||
echo "Dify KnowledgeFS unit test target manifest is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
uv run --project api --dev coverage run --branch --source=api -m pytest "${targets[@]}" --no-cov -q
|
||||
|
||||
- name: Enforce Dify KnowledgeFS focused coverage
|
||||
working-directory: .
|
||||
env:
|
||||
COVERAGE_FILE: ${{ runner.temp }}/dify-knowledge-fs.coverage
|
||||
KNOWLEDGE_FS_COVERAGE_BASE: >-
|
||||
${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
report="${RUNNER_TEMP:?}/dify-knowledge-fs-coverage.json"
|
||||
uv run --project api --dev coverage json --show-contexts -o "$report"
|
||||
uv run --project api --dev python api/dev/check_knowledge_fs_coverage.py \
|
||||
--coverage-json "$report" \
|
||||
--glue-manifest "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/glue-files" \
|
||||
--base "$KNOWLEDGE_FS_COVERAGE_BASE" \
|
||||
--minimum 90 \
|
||||
--glue-minimum 90
|
||||
|
||||
- name: Verify Dify KnowledgeFS contract
|
||||
working-directory: .
|
||||
run: uv run --project api python api/dev/generate_knowledge_fs_contract.py --check
|
||||
|
||||
- name: Verify Dify Agent dependency lock
|
||||
working-directory: .
|
||||
run: uv lock --project dify-agent --check
|
||||
|
||||
- name: Install Dify Agent gate dependencies
|
||||
working-directory: .
|
||||
run: uv sync --project dify-agent --locked --dev
|
||||
|
||||
- name: Lint Dify Agent KnowledgeFS integration
|
||||
working-directory: ./dify-agent
|
||||
run: |
|
||||
uv run --project . --dev ruff format --check \
|
||||
src/dify_agent/layers/dify_core_tools/client.py \
|
||||
tests/local/dify_agent/layers/dify_core_tools/test_client.py
|
||||
uv run --project . --dev ruff check \
|
||||
src/dify_agent/layers/dify_core_tools/client.py \
|
||||
tests/local/dify_agent/layers/dify_core_tools/test_client.py
|
||||
|
||||
- name: Type-check Dify Agent KnowledgeFS integration
|
||||
working-directory: ./dify-agent
|
||||
run: >-
|
||||
uv run --project . --dev basedpyright --level error
|
||||
src/dify_agent/layers/dify_core_tools/client.py
|
||||
tests/local/dify_agent/layers/dify_core_tools/test_client.py
|
||||
|
||||
- name: Test Dify Agent KnowledgeFS integration
|
||||
working-directory: ./dify-agent
|
||||
run: >-
|
||||
uv run --project . --dev python -m pytest
|
||||
tests/local/dify_agent/layers/dify_core_tools/test_client.py
|
||||
-q
|
||||
|
||||
skip:
|
||||
name: Skip KnowledgeFS quality and contract gates
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.knowledge-fs != 'true' && github.event_name != 'workflow_dispatch'
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
steps:
|
||||
- name: Report skipped KnowledgeFS checks
|
||||
run: echo "No KnowledgeFS-related changes detected; skipping KnowledgeFS checks."
|
||||
|
||||
final:
|
||||
name: KnowledgeFS CI
|
||||
if: ${{ always() }}
|
||||
needs:
|
||||
- check-changes
|
||||
- build
|
||||
- quality
|
||||
- skip
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
steps:
|
||||
- name: Finalize KnowledgeFS CI status
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BUILD_RESULT: ${{ needs.build.result }}
|
||||
KNOWLEDGE_FS_CHANGED: ${{ needs.check-changes.outputs.knowledge-fs }}
|
||||
QUALITY_RESULT: ${{ needs.quality.result }}
|
||||
SKIP_RESULT: ${{ needs.skip.result }}
|
||||
run: |
|
||||
if [[ "$EVENT_NAME" == 'workflow_dispatch' || "$KNOWLEDGE_FS_CHANGED" == 'true' ]]; then
|
||||
if [[ "$BUILD_RESULT" == 'success' && "$QUALITY_RESULT" == 'success' ]]; then
|
||||
echo "KnowledgeFS build and checks ran successfully."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "KnowledgeFS build or checks failed: build=$BUILD_RESULT quality=$QUALITY_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||
echo "KnowledgeFS checks were skipped because no related files changed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "KnowledgeFS change detection or skip reporting failed with result: $SKIP_RESULT" >&2
|
||||
exit 1
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Pull Request Labeler"
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ name: Main CI Pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
types: [checks_requested]
|
||||
|
||||
permissions:
|
||||
@@ -15,7 +15,7 @@ permissions:
|
||||
statuses: write
|
||||
|
||||
concurrency:
|
||||
group: main-ci-${{ github.head_ref || github.run_id }}
|
||||
group: main-ci-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
sandbox-runtime-changed: ${{ steps.changes.outputs.sandbox-runtime }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
@@ -89,6 +89,7 @@ jobs:
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.nvmrc'
|
||||
- '.github/workflows/main-ci.yml'
|
||||
- '.github/workflows/web-tests.yml'
|
||||
- '.github/actions/setup-web/**'
|
||||
e2e:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Post-Merge Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
external-e2e-changed: ${{ steps.changes.outputs.external_e2e }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
cache-dependency-path: dify-agent-runtime/go.sum
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v6.5.0
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
|
||||
with:
|
||||
working-directory: dify-agent-runtime
|
||||
version: latest
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- reopened
|
||||
- synchronize
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -11,7 +11,6 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: depot-ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -23,8 +22,8 @@ jobs:
|
||||
days-before-issue-stale: 15
|
||||
days-before-issue-close: 3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: "Closed due to inactivity. If you have any questions, you can reopen it."
|
||||
stale-pr-message: "Closed due to inactivity. If you have any questions, you can reopen it."
|
||||
stale-issue-message: 'Closed due to inactivity. If you have any questions, you can reopen it.'
|
||||
stale-pr-message: 'Closed due to inactivity. If you have any questions, you can reopen it.'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
any-of-labels: '🌚 invalid,🙋♂️ question,wont-fix,no-issue-activity,no-pr-activity,💪 enhancement,🤔 cant-reproduce,🙏 help wanted'
|
||||
|
||||
@@ -7,10 +7,6 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: style-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
statuses: write
|
||||
@@ -48,7 +44,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: false
|
||||
python-version: "3.12"
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: api/uv.lock
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -109,6 +105,8 @@ jobs:
|
||||
package.json
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
knip.config.ts
|
||||
scripts/check-web-production-unused-after-knip-fix.mjs
|
||||
.nvmrc
|
||||
.github/workflows/style.yml
|
||||
.github/actions/setup-web/**
|
||||
@@ -125,14 +123,17 @@ jobs:
|
||||
|
||||
- name: Web dead code check
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: .
|
||||
run: vp run knip
|
||||
|
||||
- name: Web dead code check production
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: .
|
||||
run: vp run knip:production
|
||||
|
||||
- name: Web production unused declarations check
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: .
|
||||
run: vp run knip:production-unused-check
|
||||
|
||||
ts-common-style:
|
||||
@@ -168,9 +169,7 @@ jobs:
|
||||
oxlint-suppressions.json
|
||||
eslint-suppressions.json
|
||||
.vscode/**
|
||||
.github/workflows/autofix.yml
|
||||
.github/workflows/style.yml
|
||||
.github/actions/setup-web/**
|
||||
.github/**
|
||||
|
||||
- name: Setup web environment
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code for Translation Sync
|
||||
if: steps.context.outputs.CHANGED_FILES != ''
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
|
||||
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1.0.187
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.12"
|
||||
- '3.12'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -48,16 +48,16 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
# - name: Set up Vector Store (TiDB)
|
||||
# uses: hoverkraft-tech/[email protected]
|
||||
# with:
|
||||
# compose-file: docker/tidb/docker-compose.yaml
|
||||
# services: |
|
||||
# tidb
|
||||
# tiflash
|
||||
# - name: Set up Vector Store (TiDB)
|
||||
# uses: hoverkraft-tech/[email protected]
|
||||
# with:
|
||||
# compose-file: docker/tidb/docker-compose.yaml
|
||||
# services: |
|
||||
# tidb
|
||||
# tiflash
|
||||
|
||||
# - name: Check VDB Ready (TiDB)
|
||||
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||
# - name: Check VDB Ready (TiDB)
|
||||
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||
|
||||
- name: Test Vector Stores
|
||||
run: |
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.12"
|
||||
- '3.12'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -45,16 +45,16 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
# - name: Set up Vector Store (TiDB)
|
||||
# uses: hoverkraft-tech/[email protected]
|
||||
# with:
|
||||
# compose-file: docker/tidb/docker-compose.yaml
|
||||
# services: |
|
||||
# tidb
|
||||
# tiflash
|
||||
# - name: Set up Vector Store (TiDB)
|
||||
# uses: hoverkraft-tech/[email protected]
|
||||
# with:
|
||||
# compose-file: docker/tidb/docker-compose.yaml
|
||||
# services: |
|
||||
# tidb
|
||||
# tiflash
|
||||
|
||||
# - name: Check VDB Ready (TiDB)
|
||||
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||
# - name: Check VDB Ready (TiDB)
|
||||
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||
|
||||
- name: Test Vector Stores
|
||||
run: |
|
||||
|
||||
@@ -11,10 +11,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: web-e2e-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Web Full-Stack E2E
|
||||
@@ -30,6 +26,11 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Switch Ubuntu mirror
|
||||
run: |
|
||||
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo apt-get update
|
||||
|
||||
- name: Setup web dependencies
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: |
|
||||
api/uv.lock
|
||||
dify-agent/uv.lock
|
||||
@@ -50,9 +51,17 @@ jobs:
|
||||
working-directory: ./e2e
|
||||
run: vp run test:unit
|
||||
|
||||
- name: Install Playwright browser
|
||||
- name: Install Playwright browsers for core E2E
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install
|
||||
run: vp run e2e:install:ci
|
||||
|
||||
- name: Install Chromium for external runtime E2E
|
||||
if: ${{ inputs.run-external-runtime }}
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci:chromium
|
||||
|
||||
- name: Run isolated source-api and built-web Cucumber E2E tests
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
@@ -61,7 +70,7 @@ jobs:
|
||||
E2E_ADMIN_EMAIL: [email protected]
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_FORCE_WEB_BUILD: "1"
|
||||
E2E_FORCE_WEB_BUILD: '1'
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
run: vp run e2e:full
|
||||
|
||||
@@ -121,7 +130,7 @@ jobs:
|
||||
E2E_AGENT_DECISION_MODEL_NAME: ${{ vars.E2E_AGENT_DECISION_MODEL_NAME || 'gpt-5.5' }}
|
||||
E2E_AGENT_DECISION_MODEL_PROVIDER: ${{ vars.E2E_AGENT_DECISION_MODEL_PROVIDER || 'openai' }}
|
||||
E2E_AGENT_DECISION_MODEL_TYPE: ${{ vars.E2E_AGENT_DECISION_MODEL_TYPE || 'llm' }}
|
||||
E2E_FORCE_WEB_BUILD: "1"
|
||||
E2E_FORCE_WEB_BUILD: '1'
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
E2E_MARKETPLACE_API_URL: ${{ vars.E2E_MARKETPLACE_API_URL }}
|
||||
E2E_MARKETPLACE_PLUGIN_IDS: ${{ vars.E2E_MARKETPLACE_PLUGIN_IDS }}
|
||||
|
||||
@@ -9,10 +9,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: web-tests-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
@@ -91,6 +87,7 @@ jobs:
|
||||
dify-ui-test:
|
||||
name: dify-ui Tests
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
defaults:
|
||||
@@ -104,11 +101,17 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Switch Ubuntu mirror
|
||||
run: |
|
||||
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo apt-get update
|
||||
|
||||
- name: Setup web environment
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Install Chromium for Browser Mode
|
||||
run: vp exec playwright install --with-deps chromium
|
||||
timeout-minutes: 15
|
||||
run: vp exec playwright install --with-deps --only-shell chromium
|
||||
|
||||
- name: Run dify-ui tests
|
||||
run: vp test run --project unit --coverage --silent=passed-only
|
||||
@@ -125,6 +128,7 @@ jobs:
|
||||
dify-ui-storybook-test:
|
||||
name: dify-ui Storybook Tests
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -136,11 +140,17 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Switch Ubuntu mirror
|
||||
run: |
|
||||
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo apt-get update
|
||||
|
||||
- name: Setup web environment
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Install Chromium for Browser Mode
|
||||
run: vp exec playwright install --with-deps chromium
|
||||
timeout-minutes: 15
|
||||
run: vp exec playwright install --with-deps --only-shell chromium
|
||||
|
||||
- name: Run dify-ui Storybook tests
|
||||
run: vp run test:storybook
|
||||
|
||||
@@ -30,6 +30,11 @@ share/python-wheels/
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# KnowledgeFS is an independently rooted TypeScript workspace. Its admin `lib`
|
||||
# directory contains source files rather than Python build output.
|
||||
!/knowledge-fs/apps/admin/lib/
|
||||
!/knowledge-fs/apps/admin/lib/**
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
@@ -140,6 +145,9 @@ dmypy.json
|
||||
pyrightconfig.json
|
||||
!api/pyrightconfig.json
|
||||
|
||||
# import-linter
|
||||
.import_linter_cache/
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
.idea/'
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# KnowledgeFS metadata catalog
|
||||
|
||||
## What changed
|
||||
|
||||
- Added a durable, per-knowledge-space metadata field catalog with `string`, `number`, and `time` field types.
|
||||
- Added document-to-field bindings so field usage counts no longer require scanning every document.
|
||||
- Added metadata field list, create, rename, and delete APIs in KnowledgeFS and the Dify console proxy.
|
||||
- Made document metadata writes validate field definitions and update the document, bindings, and active retrieval asset in one database transaction.
|
||||
- Made field rename and delete cascade to bound document values and active retrieval metadata.
|
||||
- Updated the KnowledgeFS document UI to read and mutate the field catalog directly instead of deriving it from paginated document data.
|
||||
- Included nested `userMetadata` in retrieval metadata filtering.
|
||||
|
||||
## Data migration
|
||||
|
||||
Migration `0040_knowledge_space_metadata` creates the catalog and binding tables and backfills consistently typed custom metadata already stored on logical documents. Reserved system keys and inconsistently typed values are left unbound rather than inventing an unsafe catalog type.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Existing document metadata remains readable.
|
||||
- A custom field must exist in the space catalog before a new value can be assigned.
|
||||
- Catalog mutations use row-version compare-and-swap semantics.
|
||||
- No default value is written to every document when a field is created.
|
||||
|
||||
## Verification
|
||||
|
||||
- Database schema and migration suites.
|
||||
- Metadata repository, handler, logical-document lifecycle, and retrieval candidate suites.
|
||||
- Dify facade, product operation, and controller delegation suites.
|
||||
- KnowledgeFS TypeScript type checking and Biome checks.
|
||||
- Frontend metadata page tests, Vite+ checks, and TypeScript type checking.
|
||||
@@ -6,3 +6,4 @@ Dify is an open-source platform for building LLM applications, agentic workflows
|
||||
|
||||
- Run backend commands through `uv run --project api <command>`.
|
||||
- Backend integration tests are CI-only and are not expected to run locally.
|
||||
- Keep `docker/.env.example` limited to variables required for a default Docker Compose deployment to start. Put optional and provider-specific settings in the matching `docker/envs/*.env.example` file; `docker/.env` overrides those service-specific env files.
|
||||
|
||||
@@ -209,12 +209,11 @@ At the same time, please consider supporting Dify by sharing it on social media
|
||||
|
||||
## Star History
|
||||
|
||||
<!-- GitHub token name: star-history -->
|
||||
<a href="https://www.star-history.com/?repos=langgenius%2Fdify&type=date&legend=top-left">
|
||||
<a href="https://star-history.dera.page/#langgenius/dify&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&theme=dark&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
+36
-3
@@ -39,6 +39,9 @@ ENABLE_COLLABORATION_MODE=true
|
||||
# Learn app feature toggle
|
||||
ENABLE_LEARN_APP=true
|
||||
|
||||
# Show the license expiry countdown badge in the console (enterprise only)
|
||||
ENABLE_LICENSE_EXPIRY_NOTICE=true
|
||||
|
||||
# Access token expiration time in minutes
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
|
||||
@@ -117,6 +120,19 @@ SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
|
||||
# storage type: opendal, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
|
||||
STORAGE_TYPE=opendal
|
||||
|
||||
# Key provider configuration, used to encrypt/decrypt tenant credentials (LLM/tool provider secrets)
|
||||
# key provider type: local, azure-keyvault
|
||||
KEY_PROVIDER_TYPE=local
|
||||
|
||||
# Azure Key Vault configuration, required when KEY_PROVIDER_TYPE=azure-keyvault
|
||||
# authentication uses DefaultAzureCredential (managed identity, environment variables, or Azure CLI login)
|
||||
AZURE_KEYVAULT_VAULT_URL=https://<your-vault-name>.vault.azure.net
|
||||
AZURE_KEYVAULT_KEY_SIZE=2048
|
||||
# optional: auto-rotate each tenant's key every N days. Leave empty to manage rotation manually.
|
||||
# rotation is safe because old key versions are never given an expiry and stay usable forever --
|
||||
# do NOT set a rotation policy on this key in the Azure portal that expires old versions.
|
||||
AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS=
|
||||
|
||||
# Apache OpenDAL storage configuration, refer to https://github.com/apache/opendal
|
||||
OPENDAL_SCHEME=fs
|
||||
OPENDAL_FS_ROOT=storage
|
||||
@@ -326,6 +342,7 @@ TIDB_VECTOR_PORT=4000
|
||||
TIDB_VECTOR_USER=xxx.root
|
||||
TIDB_VECTOR_PASSWORD=xxxxxx
|
||||
TIDB_VECTOR_DATABASE=dify
|
||||
TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH=false
|
||||
|
||||
# Tidb on qdrant configuration
|
||||
TIDB_ON_QDRANT_URL=http://127.0.0.1
|
||||
@@ -333,6 +350,7 @@ TIDB_ON_QDRANT_API_KEY=dify
|
||||
TIDB_ON_QDRANT_CLIENT_TIMEOUT=20
|
||||
TIDB_ON_QDRANT_GRPC_ENABLED=false
|
||||
TIDB_ON_QDRANT_GRPC_PORT=6334
|
||||
TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB=sandbox:60,professional:6400,team:25600
|
||||
TIDB_PUBLIC_KEY=dify
|
||||
TIDB_PRIVATE_KEY=dify
|
||||
TIDB_API_URL=http://127.0.0.1
|
||||
@@ -432,6 +450,7 @@ OPENGAUSS_MAX_CONNECTION=5
|
||||
|
||||
# Upload configuration
|
||||
UPLOAD_FILE_SIZE_LIMIT=15
|
||||
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN=15
|
||||
UPLOAD_FILE_BATCH_LIMIT=5
|
||||
UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
|
||||
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
||||
@@ -687,11 +706,25 @@ AGENT_BACKEND_RUN_TIMEOUT_SECONDS=1200
|
||||
|
||||
# KnowledgeFS (Dataset 2.0)
|
||||
KNOWLEDGE_FS_ENABLED=false
|
||||
# Production deployments require HTTPS; plain HTTP is limited to non-production or loopback.
|
||||
KNOWLEDGE_FS_BASE_URL=
|
||||
# Shared with KnowledgeFS; use at least 32 random characters.
|
||||
KNOWLEDGE_FS_JWT_SECRET=
|
||||
KNOWLEDGE_FS_SSE_READ_TIMEOUT_SECONDS=300
|
||||
KNOWLEDGE_FS_DIRECT_ORIGIN=
|
||||
KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED=false
|
||||
KNOWLEDGE_FS_INTEGRATED_PROVISION_READY=false
|
||||
KNOWLEDGE_FS_LEGACY_ACL_FREEZE_READY=false
|
||||
KNOWLEDGE_FS_LIFECYCLE_POLL_INTERVAL_SECONDS=15
|
||||
KNOWLEDGE_FS_LIFECYCLE_LEASE_SECONDS=60
|
||||
KNOWLEDGE_FS_LIFECYCLE_BATCH_SIZE=25
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_ENABLED=false
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID=
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM=
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_PREVIOUS_PUBLIC_JWKS=
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_ISSUER=dify-control-plane
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_AUDIENCE=knowledge-fs
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_MAX_TTL_SECONDS=60
|
||||
KNOWLEDGE_FS_TIMEOUT_SECONDS=10
|
||||
KNOWLEDGE_FS_JWKS_CACHE_MAX_AGE_SECONDS=300
|
||||
KNOWLEDGE_FS_PRODUCT_MAX_RESPONSE_BYTES=4194304
|
||||
|
||||
# Marketplace configuration
|
||||
MARKETPLACE_ENABLED=true
|
||||
|
||||
@@ -8,7 +8,134 @@ root_packages =
|
||||
extensions
|
||||
factories
|
||||
libs
|
||||
machinery
|
||||
models
|
||||
repositories
|
||||
tasks
|
||||
services
|
||||
include_external_packages = True
|
||||
|
||||
[importlinter:contract:no-direct-rsa-imports]
|
||||
# Note: `libs` itself is deliberately excluded from source_modules -- import-linter's
|
||||
# `forbidden` contract cannot check a package against its own descendant (libs.rsa lives
|
||||
# inside libs). Sibling modules under libs/ importing libs.rsa are not covered by this
|
||||
# contract; the realistic risk this guards against is application/service code reaching
|
||||
# past the key provider abstraction, which is fully covered below.
|
||||
name = Only the key provider abstraction may import libs.rsa directly
|
||||
type = forbidden
|
||||
source_modules =
|
||||
core
|
||||
constants
|
||||
context
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
factories
|
||||
models
|
||||
tasks
|
||||
services
|
||||
forbidden_modules =
|
||||
libs.rsa
|
||||
allow_indirect_imports = True
|
||||
|
||||
[importlinter:contract:machinery-framework-boundary]
|
||||
name = API machinery is framework neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
machinery
|
||||
forbidden_modules =
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:workspace-query-service-boundary]
|
||||
name = Workspace query application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.workspace_query_service
|
||||
forbidden_modules =
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:explore-banner-query-service-boundary]
|
||||
name = Explore banner query application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.explore_banner_query_service
|
||||
forbidden_modules =
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:feature-query-service-boundary]
|
||||
name = Feature query application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.feature_query_service
|
||||
forbidden_modules =
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:workspace-member-query-service-boundary]
|
||||
name = Workspace member query application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.workspace_member_query_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
services.enterprise
|
||||
services.workspace_member_role_resolver
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:setup-service-boundary]
|
||||
name = Setup application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.setup_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:schema-definition-service-boundary]
|
||||
name = Schema definition application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.schema_definition_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
+3
-1
@@ -50,7 +50,9 @@ The scripts resolve paths relative to their location, so you can run them from a
|
||||
./dev/start-worker
|
||||
```
|
||||
|
||||
1. Optional: start Celery Beat (scheduled tasks).
|
||||
1. Start Celery Beat when scheduled tasks are needed. This is required when
|
||||
`KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED=true` so provisioning and deletion
|
||||
outbox commands are dispatched to the lifecycle worker.
|
||||
|
||||
```bash
|
||||
./dev/start-beat
|
||||
|
||||
+7
-1
@@ -16,7 +16,7 @@ from core.logging.context import init_request_context
|
||||
from dify_app import DifyApp
|
||||
from extensions.ext_socketio import sio
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import LicenseStatus
|
||||
from services.entities.feature_entities import LicenseStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -172,6 +172,7 @@ def initialize_extensions(app: DifyApp):
|
||||
from context.flask_app_context import init_flask_context
|
||||
from extensions import (
|
||||
ext_app_metrics,
|
||||
ext_application_services,
|
||||
ext_blueprints,
|
||||
ext_celery,
|
||||
ext_code_based_extension,
|
||||
@@ -183,6 +184,8 @@ def initialize_extensions(app: DifyApp):
|
||||
ext_forward_refs,
|
||||
ext_hosting_provider,
|
||||
ext_import_modules,
|
||||
ext_key_provider,
|
||||
ext_knowledge_fs_observability,
|
||||
ext_logging,
|
||||
ext_login,
|
||||
ext_logstore,
|
||||
@@ -218,6 +221,7 @@ def initialize_extensions(app: DifyApp):
|
||||
ext_migrate,
|
||||
ext_redis,
|
||||
ext_storage,
|
||||
ext_key_provider, # Initialize after storage, since RSAKeyProvider reads private keys from it
|
||||
ext_set_secretkey,
|
||||
ext_logstore, # Initialize logstore after storage, before celery
|
||||
ext_celery,
|
||||
@@ -233,6 +237,8 @@ def initialize_extensions(app: DifyApp):
|
||||
ext_enterprise_telemetry,
|
||||
ext_request_logging,
|
||||
ext_session_factory,
|
||||
ext_knowledge_fs_observability,
|
||||
ext_application_services,
|
||||
ext_oauth_bearer,
|
||||
]
|
||||
for ext in extensions:
|
||||
|
||||
@@ -10,6 +10,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dify_agent.protocol import RunFailureType
|
||||
|
||||
|
||||
class AgentBackendError(Exception):
|
||||
"""Base error for API-side Agent backend integration failures."""
|
||||
@@ -54,6 +56,7 @@ class AgentBackendRunFailedError(AgentBackendError):
|
||||
|
||||
run_id: str
|
||||
detail: Any
|
||||
error_type: RunFailureType | None
|
||||
reason: str | None
|
||||
source_event_id: str | None
|
||||
|
||||
@@ -63,11 +66,13 @@ class AgentBackendRunFailedError(AgentBackendError):
|
||||
detail: Any,
|
||||
*,
|
||||
message: str | None = None,
|
||||
error_type: RunFailureType | None = None,
|
||||
reason: str | None = None,
|
||||
source_event_id: str | None = None,
|
||||
) -> None:
|
||||
self.run_id = run_id
|
||||
self.detail = detail
|
||||
self.error_type = error_type
|
||||
self.reason = reason
|
||||
self.source_event_id = source_event_id
|
||||
display_message = message or f"Agent backend run failed: {run_id}"
|
||||
|
||||
@@ -22,6 +22,7 @@ from dify_agent.protocol import (
|
||||
RunCancelledEvent,
|
||||
RunEvent,
|
||||
RunFailedEvent,
|
||||
RunFailureType,
|
||||
RunStartedEvent,
|
||||
RunSucceededEvent,
|
||||
)
|
||||
@@ -96,6 +97,7 @@ class AgentBackendRunFailedInternalEvent(AgentBackendInternalEventBase):
|
||||
|
||||
type: Literal[AgentBackendInternalEventType.RUN_FAILED] = AgentBackendInternalEventType.RUN_FAILED
|
||||
error: str
|
||||
error_type: RunFailureType | None = None
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
@@ -180,6 +182,7 @@ class AgentBackendRunEventAdapter:
|
||||
run_id=event.run_id,
|
||||
source_event_id=event.id,
|
||||
error=event.data.error,
|
||||
error_type=event.data.error_type,
|
||||
reason=event.data.reason,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -159,8 +159,12 @@ class AgentBackendModelConfig(BaseModel):
|
||||
# ``DifyPluginLLMLayerConfig.model_settings`` is pydantic_ai's ``ModelSettings``
|
||||
# TypedDict (closed: unknown keys are rejected, explicit ``None`` values fail the
|
||||
# per-field type checks). Agent Soul model settings carry a wider, nullable shape
|
||||
# (``stop`` / ``response_format`` plus null-padded fields), so the layer config
|
||||
# only receives the keys the runtime contract accepts.
|
||||
# (``stop`` / ``response_format`` plus null-padded fields, plus arbitrary
|
||||
# plugin-declared parameters such as Qwen's ``enable_thinking``), so the layer
|
||||
# config only receives the keys the runtime contract accepts directly; anything
|
||||
# else is forwarded through ``extra_body``, the TypedDict's own escape hatch for
|
||||
# provider-specific parameters (see
|
||||
# ``dify_agent.adapters.llm.model._map_model_settings_to_parameters``).
|
||||
_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
|
||||
"temperature",
|
||||
"top_p",
|
||||
@@ -168,6 +172,7 @@ _AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
|
||||
"frequency_penalty",
|
||||
"max_tokens",
|
||||
)
|
||||
_AGENT_MODEL_SETTINGS_KNOWN_KEYS = frozenset({*_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS, "stop", "response_format"})
|
||||
|
||||
|
||||
def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None:
|
||||
@@ -177,6 +182,15 @@ def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonVa
|
||||
stop = settings.get("stop")
|
||||
if isinstance(stop, list) and stop:
|
||||
sanitized["stop_sequences"] = stop
|
||||
|
||||
extra_body: dict[str, JsonValue] = {
|
||||
key: value
|
||||
for key, value in settings.items()
|
||||
if key not in _AGENT_MODEL_SETTINGS_KNOWN_KEYS and value is not None
|
||||
}
|
||||
if extra_body:
|
||||
sanitized["extra_body"] = extra_body
|
||||
|
||||
return sanitized or None
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from .data_migration import (
|
||||
import_migration_data,
|
||||
migration_data_wizard,
|
||||
)
|
||||
from .knowledge_fs import knowledge_fs_control_space
|
||||
from .plugin import (
|
||||
backfill_plugin_auto_upgrade,
|
||||
extract_plugins,
|
||||
@@ -75,6 +76,7 @@ __all__ = [
|
||||
"import_migration_data",
|
||||
"install_plugins",
|
||||
"install_rag_pipeline_plugins",
|
||||
"knowledge_fs_control_space",
|
||||
"legacy_model_types",
|
||||
"migrate_annotation_vector_database",
|
||||
"migrate_data_for_plugin",
|
||||
|
||||
@@ -33,7 +33,7 @@ def reset_password(email, new_password, password_confirm):
|
||||
|
||||
try:
|
||||
valid_password(new_password)
|
||||
except:
|
||||
except ValueError:
|
||||
click.echo(click.style(f"Invalid password. Must match {password_pattern}", fg="red"))
|
||||
return
|
||||
|
||||
@@ -75,7 +75,7 @@ def reset_email(email, new_email, email_confirm):
|
||||
|
||||
try:
|
||||
email_validate(normalized_new_email)
|
||||
except:
|
||||
except ValueError:
|
||||
click.echo(click.style(f"Invalid email: {new_email}", fg="red"))
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
"""Operator commands for the independent KnowledgeFS control-plane."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from services.knowledge_fs.cleanup import (
|
||||
CleanupApprovalInput,
|
||||
CleanupCompletionEvidenceInput,
|
||||
CleanupReadinessEvidenceInput,
|
||||
CleanupStartInput,
|
||||
KnowledgeFSCleanupError,
|
||||
KnowledgeFSCleanupService,
|
||||
)
|
||||
from services.knowledge_fs.control_space_commands import KnowledgeFSControlSpaceCommandService
|
||||
from services.knowledge_fs.control_space_lifecycle import KnowledgeFSControlSpaceLifecycleError
|
||||
from services.knowledge_fs.control_space_management import (
|
||||
KnowledgeFSControlSpaceManagementService,
|
||||
KnowledgeFSControlSpaceRegistration,
|
||||
)
|
||||
from services.knowledge_fs.cutover import (
|
||||
CutoverSmokeResultsInput,
|
||||
FinalDeltaInput,
|
||||
KnowledgeFSCutoverError,
|
||||
KnowledgeFSWorkspaceCutoverService,
|
||||
LegacyDependencyInput,
|
||||
QuarantineResolutionInput,
|
||||
ShadowAuthorizationObservationInput,
|
||||
ShadowCompletionInput,
|
||||
WorkspaceInventoryInput,
|
||||
)
|
||||
from services.knowledge_fs.greenfield_initializer import KnowledgeFSWorkspaceGreenfieldInitializer
|
||||
from services.knowledge_fs.orphan_reconciler import KnowledgeFSOrphanReconciler
|
||||
from services.knowledge_fs.remote_registry import get_knowledge_fs_lifecycle_remote
|
||||
|
||||
|
||||
@click.group("knowledge-fs-control-space")
|
||||
def knowledge_fs_control_space() -> None:
|
||||
"""Inspect and repair Dify-owned KnowledgeFS control-space state."""
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("dry-run")
|
||||
@click.option("--tenant-id", default=None)
|
||||
def dry_run(tenant_id: str | None) -> None:
|
||||
report = _management_service().dry_run(tenant_id=tenant_id)
|
||||
click.echo(json.dumps(report._asdict(), sort_keys=True))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("inventory")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Create ledgers; omitted means read-only inventory.")
|
||||
def inventory(input_path: Path, apply: bool) -> None:
|
||||
"""Validate strict Workspace inventory JSONL and optionally create ledgers."""
|
||||
|
||||
service = _cutover_service()
|
||||
for payload in _read_jsonl(input_path, WorkspaceInventoryInput):
|
||||
report = _operator_call(partial(service.inventory, payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("register")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--owner-account-id", required=True)
|
||||
@click.option("--provisioning-key", required=True)
|
||||
@click.option("--knowledge-space-id", required=True)
|
||||
@click.option("--knowledge-space-revision", type=click.IntRange(min=0), required=True)
|
||||
def register(
|
||||
tenant_id: str,
|
||||
owner_account_id: str,
|
||||
provisioning_key: str,
|
||||
knowledge_space_id: str,
|
||||
knowledge_space_revision: int,
|
||||
) -> None:
|
||||
control_space, replayed = _management_service().register(
|
||||
KnowledgeFSControlSpaceRegistration(
|
||||
tenant_id,
|
||||
owner_account_id,
|
||||
provisioning_key,
|
||||
knowledge_space_id,
|
||||
knowledge_space_revision,
|
||||
)
|
||||
)
|
||||
click.echo(json.dumps({"control_space_id": control_space.id, "replayed": replayed}, sort_keys=True))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("backfill")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist registrations; omitted means dry-run.")
|
||||
def backfill(input_path: Path, apply: bool) -> None:
|
||||
"""Backfill strict Workspace inventory JSONL; dry-run unless --apply is explicit."""
|
||||
|
||||
service = _cutover_service()
|
||||
for payload in _read_jsonl(input_path, WorkspaceInventoryInput):
|
||||
report = _operator_call(partial(service.backfill, payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("quarantine-resolve")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist resolutions; omitted means dry-run.")
|
||||
def quarantine_resolve(input_path: Path, apply: bool) -> None:
|
||||
"""Resolve strict tenant-scoped quarantine JSONL with immutable operator evidence."""
|
||||
|
||||
service = _cutover_service()
|
||||
for payload in _read_jsonl(input_path, QuarantineResolutionInput):
|
||||
report = _operator_call(partial(service.resolve_quarantine, payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("shadow-start")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--at", "started_at", default=None, help="Optional explicit timezone-aware shadow start.")
|
||||
def shadow_start(tenant_id: str, expected_cas_version: int, started_at: str | None) -> None:
|
||||
service = _cutover_service()
|
||||
_operator_call(
|
||||
lambda: service.begin_shadow(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
started_at=_parse_timestamp(started_at) if started_at is not None else None,
|
||||
)
|
||||
)
|
||||
_echo_json(service.status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("shadow-report")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist observations; omitted means dry-run.")
|
||||
def shadow_report(input_path: Path, apply: bool) -> None:
|
||||
observations = _read_jsonl(input_path, ShadowAuthorizationObservationInput)
|
||||
report = _operator_call(lambda: _cutover_service().record_shadow_report(observations, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("shadow-complete")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist completion; omitted means dry-run.")
|
||||
def shadow_complete(input_path: Path, apply: bool) -> None:
|
||||
payload = _read_one_jsonl(input_path, ShadowCompletionInput)
|
||||
report = _operator_call(lambda: _cutover_service().complete_shadow(payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("issue-approve")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--issue-key", required=True)
|
||||
@click.option("--account-id", required=True)
|
||||
@click.option("--at", "approved_at", required=True)
|
||||
def issue_approve(tenant_id: str, issue_key: str, account_id: str, approved_at: str) -> None:
|
||||
_operator_call(
|
||||
lambda: _cutover_service().approve_issue_fail_closed(
|
||||
tenant_id=tenant_id,
|
||||
issue_key=issue_key,
|
||||
account_id=account_id,
|
||||
approved_at=_parse_timestamp(approved_at),
|
||||
)
|
||||
)
|
||||
_echo_json(_cutover_service().status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("issue-resolve")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--issue-key", required=True)
|
||||
@click.option("--account-id", required=True)
|
||||
@click.option("--at", "resolved_at", required=True)
|
||||
def issue_resolve(tenant_id: str, issue_key: str, account_id: str, resolved_at: str) -> None:
|
||||
_operator_call(
|
||||
lambda: _cutover_service().resolve_issue(
|
||||
tenant_id=tenant_id,
|
||||
issue_key=issue_key,
|
||||
account_id=account_id,
|
||||
resolved_at=_parse_timestamp(resolved_at),
|
||||
)
|
||||
)
|
||||
_echo_json(_cutover_service().status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("shadow-approve")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--diff-key", required=True)
|
||||
@click.option("--account-id", required=True)
|
||||
@click.option("--at", "approved_at", required=True)
|
||||
def shadow_approve(tenant_id: str, diff_key: str, account_id: str, approved_at: str) -> None:
|
||||
_operator_call(
|
||||
lambda: _cutover_service().approve_shadow_diff(
|
||||
tenant_id=tenant_id,
|
||||
diff_key=diff_key,
|
||||
account_id=account_id,
|
||||
approved_at=_parse_timestamp(approved_at),
|
||||
)
|
||||
)
|
||||
_echo_json(_cutover_service().status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("shadow-resolve")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--diff-key", required=True)
|
||||
@click.option("--account-id", required=True)
|
||||
@click.option("--at", "resolved_at", required=True)
|
||||
def shadow_resolve(tenant_id: str, diff_key: str, account_id: str, resolved_at: str) -> None:
|
||||
_operator_call(
|
||||
lambda: _cutover_service().resolve_shadow_diff(
|
||||
tenant_id=tenant_id,
|
||||
diff_key=diff_key,
|
||||
account_id=account_id,
|
||||
resolved_at=_parse_timestamp(resolved_at),
|
||||
)
|
||||
)
|
||||
_echo_json(_cutover_service().status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("legacy-dashboard")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--checked-at", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), default=None)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist gate evidence; omitted means read-only report.")
|
||||
def legacy_dashboard(
|
||||
tenant_id: str,
|
||||
input_path: Path,
|
||||
checked_at: str,
|
||||
expected_cas_version: int | None,
|
||||
apply: bool,
|
||||
) -> None:
|
||||
dependencies = _read_jsonl(input_path, LegacyDependencyInput, allow_empty=True)
|
||||
report = _operator_call(
|
||||
lambda: _cutover_service().legacy_dependency_dashboard(
|
||||
tenant_id=tenant_id,
|
||||
dependencies=dependencies,
|
||||
expected_cas_version=expected_cas_version,
|
||||
checked_at=_parse_timestamp(checked_at),
|
||||
apply=apply,
|
||||
)
|
||||
)
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("legacy-check")
|
||||
@click.option("--tenant-id", required=True)
|
||||
def legacy_check(tenant_id: str) -> None:
|
||||
status_report = _operator_call(lambda: _cutover_service().status(tenant_id=tenant_id))
|
||||
passed = (
|
||||
bool(status_report["legacy_dependency_ready"])
|
||||
and status_report["open_issues"] == 0
|
||||
and status_report["unresolved_cutover_quarantine"] == 0
|
||||
)
|
||||
_echo_json({"tenant_id": tenant_id, "passed": passed, "status": status_report})
|
||||
if not passed:
|
||||
raise click.exceptions.Exit(1)
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("freeze")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--at", "freeze_at", required=True)
|
||||
def freeze(tenant_id: str, expected_cas_version: int, freeze_at: str) -> None:
|
||||
service = _cutover_service()
|
||||
_operator_call(
|
||||
lambda: service.freeze(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
freeze_at=_parse_timestamp(freeze_at),
|
||||
)
|
||||
)
|
||||
_echo_json(service.status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("final-delta")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
def final_delta(input_path: Path) -> None:
|
||||
payload = _read_one_jsonl(input_path, FinalDeltaInput)
|
||||
service = _cutover_service()
|
||||
_operator_call(lambda: service.apply_final_delta(payload))
|
||||
_echo_json(service.status(tenant_id=str(payload.tenant_id)))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("cutover")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--at", "cutover_at", required=True)
|
||||
@click.option("--rollback-cutoff-at", required=True)
|
||||
def cutover(tenant_id: str, expected_cas_version: int, cutover_at: str, rollback_cutoff_at: str) -> None:
|
||||
service = _cutover_service()
|
||||
_operator_call(
|
||||
lambda: service.cutover(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
cutover_at=_parse_timestamp(cutover_at),
|
||||
rollback_cutoff_at=_parse_timestamp(rollback_cutoff_at),
|
||||
)
|
||||
)
|
||||
_echo_json(service.status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("smoke")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
def smoke(tenant_id: str, expected_cas_version: int, input_path: Path) -> None:
|
||||
results = _read_one_jsonl(input_path, CutoverSmokeResultsInput)
|
||||
service = _cutover_service()
|
||||
_operator_call(
|
||||
lambda: service.record_smoke_results(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
results=results,
|
||||
)
|
||||
)
|
||||
_echo_json(service.status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("observe")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--started-at", default=None)
|
||||
@click.option("--window-ends-at", default=None)
|
||||
@click.option("--maximum-task-expires-at", default=None)
|
||||
@click.option("--observed-at", default=None)
|
||||
def observe(
|
||||
tenant_id: str,
|
||||
expected_cas_version: int,
|
||||
started_at: str | None,
|
||||
window_ends_at: str | None,
|
||||
maximum_task_expires_at: str | None,
|
||||
observed_at: str | None,
|
||||
) -> None:
|
||||
service = _cutover_service()
|
||||
if observed_at is not None:
|
||||
if any(value is not None for value in (started_at, window_ends_at, maximum_task_expires_at)):
|
||||
raise click.UsageError("--observed-at cannot be combined with observation start options")
|
||||
_operator_call(
|
||||
lambda: service.complete_observation(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
observed_at=_parse_timestamp(observed_at),
|
||||
)
|
||||
)
|
||||
else:
|
||||
if started_at is None or window_ends_at is None or maximum_task_expires_at is None:
|
||||
raise click.UsageError(
|
||||
"observation start requires --started-at, --window-ends-at, and --maximum-task-expires-at"
|
||||
)
|
||||
_operator_call(
|
||||
lambda: service.begin_observation(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
started_at=_parse_timestamp(started_at),
|
||||
window_ends_at=_parse_timestamp(window_ends_at),
|
||||
maximum_task_expires_at=_parse_timestamp(maximum_task_expires_at),
|
||||
)
|
||||
)
|
||||
_echo_json(service.status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("rollback")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--at", "rolled_back_at", required=True)
|
||||
def rollback(tenant_id: str, expected_cas_version: int, rolled_back_at: str) -> None:
|
||||
service = _cutover_service()
|
||||
_operator_call(
|
||||
lambda: service.rollback(
|
||||
tenant_id=tenant_id,
|
||||
expected_cas_version=expected_cas_version,
|
||||
rolled_back_at=_parse_timestamp(rolled_back_at),
|
||||
)
|
||||
)
|
||||
_echo_json(service.status(tenant_id=tenant_id))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("status")
|
||||
@click.option("--tenant-id", required=True)
|
||||
def status(tenant_id: str) -> None:
|
||||
_echo_json(_operator_call(lambda: _cutover_service().status(tenant_id=tenant_id)))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("greenfield-initialize")
|
||||
@click.option("--tenant-id", required=True)
|
||||
def greenfield_initialize(tenant_id: str) -> None:
|
||||
"""Idempotently initialize one Workspace that has no KnowledgeFS state."""
|
||||
|
||||
_operator_call(lambda: _greenfield_initializer().ensure_initialized(tenant_id=tenant_id))
|
||||
_echo_json(_operator_call(lambda: _cutover_service().status(tenant_id=tenant_id)))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("cleanup-request")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist readiness evidence; omitted means dry-run.")
|
||||
def cleanup_request(input_path: Path, apply: bool) -> None:
|
||||
payload = _read_one_jsonl(input_path, CleanupReadinessEvidenceInput)
|
||||
report = _cleanup_call(lambda: _cleanup_service().request_cleanup(payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("cleanup-approve")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist four-eyes approval; omitted means dry-run.")
|
||||
def cleanup_approve(input_path: Path, apply: bool) -> None:
|
||||
payload = _read_one_jsonl(input_path, CleanupApprovalInput)
|
||||
report = _cleanup_call(lambda: _cleanup_service().approve_cleanup(payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("cleanup-start")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist the irreversible fence; never runs deletion.")
|
||||
@click.option(
|
||||
"--acknowledge-irreversible",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Required with --apply; confirms rollback will be permanently closed.",
|
||||
)
|
||||
def cleanup_start(input_path: Path, apply: bool, acknowledge_irreversible: bool) -> None:
|
||||
payload = _read_one_jsonl(input_path, CleanupStartInput)
|
||||
if apply and not acknowledge_irreversible:
|
||||
raise click.UsageError("--apply requires --acknowledge-irreversible")
|
||||
report = _cleanup_call(lambda: _cleanup_service().start_cleanup(payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("cleanup-complete")
|
||||
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist externally verified cleanup completion.")
|
||||
@click.option(
|
||||
"--acknowledge-executed",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Required with --apply; confirms the reviewed destructive bundle already executed.",
|
||||
)
|
||||
def cleanup_complete(input_path: Path, apply: bool, acknowledge_executed: bool) -> None:
|
||||
payload = _read_one_jsonl(input_path, CleanupCompletionEvidenceInput)
|
||||
if apply and not acknowledge_executed:
|
||||
raise click.UsageError("--apply requires --acknowledge-executed")
|
||||
report = _cleanup_call(lambda: _cleanup_service().complete_cleanup(payload, apply=apply))
|
||||
_echo_json(report._asdict())
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("cleanup-status")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--request-id", required=True)
|
||||
def cleanup_status(tenant_id: str, request_id: str) -> None:
|
||||
_echo_json(_cleanup_call(lambda: _cleanup_service().status(tenant_id=tenant_id, request_id=request_id)))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("repair")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--control-space-id", required=True)
|
||||
@click.option("--expected-resource-version", type=click.IntRange(min=0), required=True)
|
||||
@click.option("--knowledge-space-id", required=True)
|
||||
@click.option("--knowledge-space-revision", type=click.IntRange(min=0), required=True)
|
||||
def repair(
|
||||
tenant_id: str,
|
||||
control_space_id: str,
|
||||
expected_resource_version: int,
|
||||
knowledge_space_id: str,
|
||||
knowledge_space_revision: int,
|
||||
) -> None:
|
||||
control_space = _management_service().repair_registration(
|
||||
tenant_id=tenant_id,
|
||||
control_space_id=control_space_id,
|
||||
expected_resource_version=expected_resource_version,
|
||||
knowledge_space_id=knowledge_space_id,
|
||||
knowledge_space_revision=knowledge_space_revision,
|
||||
)
|
||||
click.echo(json.dumps({"control_space_id": control_space.id, "state": control_space.state.value}, sort_keys=True))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("orphan-report")
|
||||
@click.option("--limit", type=click.IntRange(min=1, max=10_000), default=500, show_default=True)
|
||||
def orphan_report(limit: int) -> None:
|
||||
report = KnowledgeFSOrphanReconciler(
|
||||
session_factory.get_session_maker(),
|
||||
get_knowledge_fs_lifecycle_remote(),
|
||||
).reconcile(limit=limit, apply_repairs=False)
|
||||
click.echo(json.dumps(report._asdict(), sort_keys=True))
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("workspace-delete-request")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Persist durable deletion intents; omitted means dry-run.")
|
||||
def workspace_delete_request(tenant_id: str, apply: bool) -> None:
|
||||
"""Route every KnowledgeFS Space through the canonical lifecycle deletion path."""
|
||||
|
||||
if not apply:
|
||||
report = _management_service().dry_run(tenant_id=tenant_id)
|
||||
_echo_json(
|
||||
{
|
||||
"apply": False,
|
||||
"by_state": report.by_state,
|
||||
"tenant_id": tenant_id,
|
||||
"total": report.total,
|
||||
}
|
||||
)
|
||||
return
|
||||
results = _lifecycle_call(lambda: _lifecycle_service().request_workspace_cleanup(tenant_id=tenant_id))
|
||||
_echo_json(
|
||||
{
|
||||
"apply": True,
|
||||
"control_space_ids": [result.control_space.id for result in results],
|
||||
"operation_ids": [result.outbox.operation_id for result in results if result.outbox is not None],
|
||||
"tenant_id": tenant_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@knowledge_fs_control_space.command("workspace-delete-finalize")
|
||||
@click.option("--tenant-id", required=True)
|
||||
@click.option("--apply", is_flag=True, default=False, help="Purge terminal local control-plane rows.")
|
||||
@click.option(
|
||||
"--acknowledge-control-plane-purge",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Required with --apply after every remote Space has reached deleted.",
|
||||
)
|
||||
def workspace_delete_finalize(tenant_id: str, apply: bool, acknowledge_control_plane_purge: bool) -> None:
|
||||
"""Release the Workspace FK only after all remote deletions are terminal."""
|
||||
|
||||
service = _lifecycle_service()
|
||||
if not apply:
|
||||
_lifecycle_call(lambda: service.assert_workspace_deletion_allowed(tenant_id=tenant_id))
|
||||
_echo_json({"apply": False, "ready": True, "tenant_id": tenant_id})
|
||||
return
|
||||
if not acknowledge_control_plane_purge:
|
||||
raise click.UsageError("--apply requires --acknowledge-control-plane-purge")
|
||||
deleted = _lifecycle_call(lambda: service.finalize_workspace_deletion(tenant_id=tenant_id))
|
||||
_echo_json({"apply": True, "purged_control_spaces": deleted, "tenant_id": tenant_id})
|
||||
|
||||
|
||||
def _read_jsonl[InputT: BaseModel](
|
||||
input_path: Path, input_type: type[InputT], *, allow_empty: bool = False
|
||||
) -> tuple[InputT, ...]:
|
||||
records: list[InputT] = []
|
||||
for line_number, line in enumerate(input_path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
records.append(input_type.model_validate_json(line))
|
||||
except ValidationError as exc:
|
||||
raise click.ClickException(f"invalid strict JSONL at line {line_number}: {exc}") from exc
|
||||
if not records and not allow_empty:
|
||||
raise click.ClickException("strict JSONL input must contain at least one record")
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def _read_one_jsonl[InputT: BaseModel](input_path: Path, input_type: type[InputT]) -> InputT:
|
||||
records = _read_jsonl(input_path, input_type)
|
||||
if len(records) != 1:
|
||||
raise click.ClickException("this command requires exactly one JSONL record")
|
||||
return records[0]
|
||||
|
||||
|
||||
def _parse_timestamp(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(f"invalid ISO-8601 timestamp: {value}") from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise click.ClickException("operator timestamps must include an explicit timezone")
|
||||
return parsed
|
||||
|
||||
|
||||
def _operator_call[ResultT](operation: Callable[[], ResultT]) -> ResultT:
|
||||
try:
|
||||
return operation()
|
||||
except KnowledgeFSCutoverError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
|
||||
|
||||
def _cleanup_call[ResultT](operation: Callable[[], ResultT]) -> ResultT:
|
||||
try:
|
||||
return operation()
|
||||
except KnowledgeFSCleanupError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
|
||||
|
||||
def _lifecycle_call[ResultT](operation: Callable[[], ResultT]) -> ResultT:
|
||||
try:
|
||||
return operation()
|
||||
except KnowledgeFSControlSpaceLifecycleError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
|
||||
|
||||
def _echo_json(payload: object) -> None:
|
||||
click.echo(json.dumps(payload, default=str, sort_keys=True))
|
||||
|
||||
|
||||
def _management_service() -> KnowledgeFSControlSpaceManagementService:
|
||||
return KnowledgeFSControlSpaceManagementService(session_factory.get_session_maker())
|
||||
|
||||
|
||||
def _cutover_service() -> KnowledgeFSWorkspaceCutoverService:
|
||||
return KnowledgeFSWorkspaceCutoverService(
|
||||
session_factory.get_session_maker(),
|
||||
remote_factory=get_knowledge_fs_lifecycle_remote,
|
||||
)
|
||||
|
||||
|
||||
def _greenfield_initializer() -> KnowledgeFSWorkspaceGreenfieldInitializer:
|
||||
return KnowledgeFSWorkspaceGreenfieldInitializer(
|
||||
session_factory.get_session_maker(),
|
||||
cutover=_cutover_service(),
|
||||
)
|
||||
|
||||
|
||||
def _cleanup_service() -> KnowledgeFSCleanupService:
|
||||
return KnowledgeFSCleanupService(session_factory.get_session_maker())
|
||||
|
||||
|
||||
def _lifecycle_service() -> KnowledgeFSControlSpaceCommandService:
|
||||
return KnowledgeFSControlSpaceCommandService(session_factory.get_session_maker())
|
||||
|
||||
|
||||
__all__ = ["knowledge_fs_control_space"]
|
||||
@@ -6,12 +6,11 @@ from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from events.app_event import app_was_created
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.db_migration_lock import DbMigrationAutoRenewLock
|
||||
from libs.rsa import generate_key_pair
|
||||
from libs.key_providers import generate_key_pair
|
||||
from models import Tenant
|
||||
from models.model import App, AppMode, Conversation
|
||||
from models.provider import Provider, ProviderModel
|
||||
@@ -43,7 +42,7 @@ def reset_encrypt_key_pair():
|
||||
After the reset, all LLM credentials will become invalid, requiring re-entry.
|
||||
Only support SELF_HOSTED mode.
|
||||
"""
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
if dify_config.EDITION != "SELF_HOSTED":
|
||||
click.echo(click.style("This command is only for SELF_HOSTED installations.", fg="red"))
|
||||
return
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
|
||||
@@ -25,6 +25,12 @@ class EnterpriseFeatureConfig(BaseSettings):
|
||||
default=False,
|
||||
)
|
||||
|
||||
ENABLE_LICENSE_EXPIRY_NOTICE: bool = Field(
|
||||
description="Show the license expiry countdown badge in the console when the license is expiring. "
|
||||
"Disable to hide the badge; license status and all enforcement remain unaffected.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
ENTERPRISE_REQUEST_TIMEOUT: int = Field(
|
||||
ge=1, description="Maximum timeout in seconds for enterprise requests", default=5
|
||||
)
|
||||
|
||||
@@ -1,30 +1,63 @@
|
||||
"""Configuration for the optional KnowledgeFS Console bridge."""
|
||||
"""Configuration for the optional KnowledgeFS control-plane integration."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from pydantic import Field, PositiveFloat, SecretStr, field_validator, model_validator
|
||||
from pydantic import Field, PositiveFloat, PositiveInt, SecretStr, field_validator, model_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class KnowledgeFSConfig(BaseSettings):
|
||||
"""Server-only settings for the KnowledgeFS production connection."""
|
||||
"""Server-only KnowledgeFS connection and rollout settings."""
|
||||
|
||||
KNOWLEDGE_FS_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Enable the private KnowledgeFS Console bridge.",
|
||||
description="Enable the KnowledgeFS control-plane product routes.",
|
||||
)
|
||||
KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Enable delivery of durable KnowledgeFS lifecycle commands after every rollout gate is ready.",
|
||||
)
|
||||
KNOWLEDGE_FS_INTEGRATED_PROVISION_READY: bool = Field(
|
||||
default=False,
|
||||
description="Confirm that the Capability-v2 integrated provision route is deployed and verified.",
|
||||
)
|
||||
KNOWLEDGE_FS_LEGACY_ACL_FREEZE_READY: bool = Field(
|
||||
default=False,
|
||||
description="Confirm that legacy KFS ACL mutation is frozen for integrated mode.",
|
||||
)
|
||||
KNOWLEDGE_FS_LIFECYCLE_POLL_INTERVAL_SECONDS: PositiveInt = Field(default=15, le=300)
|
||||
KNOWLEDGE_FS_LIFECYCLE_LEASE_SECONDS: PositiveInt = Field(default=60, le=600)
|
||||
KNOWLEDGE_FS_LIFECYCLE_BATCH_SIZE: PositiveInt = Field(default=25, le=1_000)
|
||||
KNOWLEDGE_FS_BASE_URL: str | None = Field(default=None, description="KnowledgeFS gateway base URL.")
|
||||
KNOWLEDGE_FS_JWT_SECRET: SecretStr | None = Field(
|
||||
default=None,
|
||||
min_length=32,
|
||||
description="Shared secret used to sign short-lived KnowledgeFS service JWTs.",
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Prepare resource-scoped Capability v2 issuance; disabled until rollout approval.",
|
||||
)
|
||||
KNOWLEDGE_FS_SSE_READ_TIMEOUT_SECONDS: PositiveFloat = Field(default=300.0, le=3600.0, allow_inf_nan=False)
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID: str | None = Field(
|
||||
default=None,
|
||||
description="Identifier for the current asymmetric Capability v2 signing key.",
|
||||
)
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM: SecretStr | None = Field(
|
||||
default=None,
|
||||
description="Server-only PEM for the current Capability v2 RSA signing key.",
|
||||
)
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_PREVIOUS_PUBLIC_JWKS: str | None = Field(
|
||||
default=None,
|
||||
description="Optional public-only JWKS JSON retained during key rotation overlap.",
|
||||
)
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_ISSUER: str = Field(default="dify-control-plane", min_length=1)
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_AUDIENCE: str = Field(default="knowledge-fs", min_length=1)
|
||||
KNOWLEDGE_FS_CAPABILITY_V2_MAX_TTL_SECONDS: PositiveInt = Field(default=60, le=60)
|
||||
KNOWLEDGE_FS_JWKS_CACHE_MAX_AGE_SECONDS: PositiveInt = Field(default=300, le=86_400)
|
||||
KNOWLEDGE_FS_PRODUCT_MAX_RESPONSE_BYTES: PositiveInt = Field(default=4 * 1024 * 1024, le=16 * 1024 * 1024)
|
||||
KNOWLEDGE_FS_TIMEOUT_SECONDS: PositiveFloat = Field(default=10.0, le=60.0, allow_inf_nan=False)
|
||||
|
||||
@field_validator(
|
||||
"KNOWLEDGE_FS_BASE_URL",
|
||||
"KNOWLEDGE_FS_JWT_SECRET",
|
||||
"KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID",
|
||||
"KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM",
|
||||
"KNOWLEDGE_FS_CAPABILITY_V2_PREVIOUS_PUBLIC_JWKS",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
@@ -40,25 +73,48 @@ class KnowledgeFSConfig(BaseSettings):
|
||||
@field_validator("KNOWLEDGE_FS_BASE_URL")
|
||||
@classmethod
|
||||
def validate_base_url(cls, value: str | None) -> str | None:
|
||||
return cls._validate_origin(value, name="KNOWLEDGE_FS_BASE_URL")
|
||||
|
||||
@classmethod
|
||||
def _validate_origin(cls, value: str | None, *, name: str) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
parsed = urlsplit(value)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise ValueError("KNOWLEDGE_FS_BASE_URL must be an absolute HTTP(S) URL")
|
||||
raise ValueError(f"{name} must be an absolute HTTP(S) URL")
|
||||
try:
|
||||
_ = parsed.port
|
||||
except ValueError as exc:
|
||||
raise ValueError("KNOWLEDGE_FS_BASE_URL must include a valid port") from exc
|
||||
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
||||
raise ValueError("KNOWLEDGE_FS_BASE_URL must not include credentials, query, or fragment")
|
||||
raise ValueError(f"{name} must include a valid port") from exc
|
||||
if parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
|
||||
raise ValueError(f"{name} must be an origin without credentials, path, query, or fragment")
|
||||
return value.rstrip("/")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_enabled_connection(self) -> "KnowledgeFSConfig":
|
||||
if not self.KNOWLEDGE_FS_ENABLED:
|
||||
return self
|
||||
if bool(self.KNOWLEDGE_FS_BASE_URL) != bool(self.KNOWLEDGE_FS_JWT_SECRET):
|
||||
raise ValueError("KNOWLEDGE_FS_BASE_URL and KNOWLEDGE_FS_JWT_SECRET must be configured together")
|
||||
if not self.KNOWLEDGE_FS_BASE_URL:
|
||||
raise ValueError("KnowledgeFS connection settings are required when the integration is enabled")
|
||||
if str(getattr(self, "DEPLOY_ENV", "")).strip().upper() == "PRODUCTION":
|
||||
if self.KNOWLEDGE_FS_BASE_URL and not self._is_secure_or_loopback_origin(self.KNOWLEDGE_FS_BASE_URL):
|
||||
raise ValueError("KNOWLEDGE_FS_BASE_URL must use HTTPS in production unless it targets loopback")
|
||||
if self.KNOWLEDGE_FS_ENABLED:
|
||||
if not self.KNOWLEDGE_FS_BASE_URL:
|
||||
raise ValueError("KnowledgeFS base URL is required when the integration is enabled")
|
||||
if not self.KNOWLEDGE_FS_CAPABILITY_V2_ENABLED:
|
||||
raise ValueError("KnowledgeFS product routes require Capability v2 when enabled")
|
||||
if self.KNOWLEDGE_FS_CAPABILITY_V2_ENABLED and not (
|
||||
self.KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID and self.KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM
|
||||
):
|
||||
raise ValueError("Capability v2 signing kid and private key are required when issuance is enabled")
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _is_secure_or_loopback_origin(value: str) -> bool:
|
||||
parsed = urlsplit(value)
|
||||
if parsed.scheme == "https":
|
||||
return True
|
||||
hostname = (parsed.hostname or "").rstrip(".").lower()
|
||||
if hostname == "localhost":
|
||||
return True
|
||||
try:
|
||||
return ip_address(hostname).is_loopback
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -450,6 +450,11 @@ class FileUploadConfig(BaseSettings):
|
||||
default=15,
|
||||
)
|
||||
|
||||
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN: NonNegativeInt = Field(
|
||||
description="Maximum allowed file size for knowledge uploads on paid cloud plans in megabytes",
|
||||
default=15,
|
||||
)
|
||||
|
||||
UPLOAD_FILE_BATCH_LIMIT: NonNegativeInt = Field(
|
||||
description="Maximum number of files allowed in a single upload batch",
|
||||
default=5,
|
||||
|
||||
@@ -451,6 +451,12 @@ class HostedFetchAppTemplateConfig(BaseSettings):
|
||||
default="https://tmpl.dify.ai",
|
||||
)
|
||||
|
||||
HOSTED_FETCH_APP_TEMPLATES_CACHE_TTL: int = Field(
|
||||
description="TTL in seconds for caching remote app template HTTP responses. 0 disables caching.",
|
||||
default=600,
|
||||
ge=0,
|
||||
)
|
||||
|
||||
|
||||
class HostedFetchPipelineTemplateConfig(BaseSettings):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ from pydantic_settings import BaseSettings
|
||||
|
||||
from .cache.redis_config import RedisConfig
|
||||
from .cache.redis_pubsub_config import RedisPubSubConfig
|
||||
from .key_provider.azure_keyvault_config import AzureKeyVaultConfig
|
||||
from .storage.aliyun_oss_storage_config import AliyunOSSStorageConfig
|
||||
from .storage.amazon_s3_storage_config import S3StorageConfig
|
||||
from .storage.azure_blob_storage_config import AzureBlobStorageConfig
|
||||
@@ -83,6 +84,22 @@ class StorageConfig(BaseSettings):
|
||||
)
|
||||
|
||||
|
||||
_VALID_KEY_PROVIDER_TYPE = Literal[
|
||||
"local",
|
||||
"azure-keyvault",
|
||||
]
|
||||
|
||||
|
||||
class KeyProviderConfig(BaseSettings):
|
||||
KEY_PROVIDER_TYPE: _VALID_KEY_PROVIDER_TYPE = Field(
|
||||
description="Key provider used to encrypt/decrypt tenant credentials (LLM/tool provider secrets)."
|
||||
" Options: 'local' (per-tenant RSA key pair, private key kept in the STORAGE_TYPE backend),"
|
||||
" 'azure-keyvault' (per-tenant RSA key kept in Azure Key Vault, private key never leaves the vault)."
|
||||
" Default is 'local'.",
|
||||
default=cast(_VALID_KEY_PROVIDER_TYPE, "local"),
|
||||
)
|
||||
|
||||
|
||||
class VectorStoreConfig(BaseSettings):
|
||||
VECTOR_STORE: str | None = Field(
|
||||
description="Type of vector store to use for efficient similarity search."
|
||||
@@ -359,6 +376,9 @@ class MiddlewareConfig(
|
||||
KeywordStoreConfig,
|
||||
RedisConfig,
|
||||
RedisPubSubConfig,
|
||||
# configs of the tenant credential encryption key provider
|
||||
KeyProviderConfig,
|
||||
AzureKeyVaultConfig,
|
||||
# configs of storage and storage providers
|
||||
StorageConfig,
|
||||
AliyunOSSStorageConfig,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class AzureKeyVaultConfig(BaseSettings):
|
||||
"""
|
||||
Configuration settings for Azure Key Vault, used as a tenant credential encryption key provider
|
||||
"""
|
||||
|
||||
AZURE_KEYVAULT_VAULT_URL: str | None = Field(
|
||||
description="URL of the Azure Key Vault instance (e.g., 'https://<your-vault-name>.vault.azure.net')."
|
||||
" Required when KEY_PROVIDER_TYPE is set to 'azure-keyvault'. Authentication uses"
|
||||
" DefaultAzureCredential (managed identity, environment variables, or Azure CLI login).",
|
||||
default=None,
|
||||
)
|
||||
|
||||
AZURE_KEYVAULT_KEY_SIZE: int = Field(
|
||||
description="RSA key size (in bits) used when Dify provisions a new per-tenant key in Azure Key Vault.",
|
||||
default=2048,
|
||||
)
|
||||
|
||||
AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS: int | None = Field(
|
||||
description="If set, Dify configures each newly-created per-tenant Key Vault key to auto-rotate every"
|
||||
" N days (using a 'time after create' trigger, with no expiry set on generated versions)."
|
||||
" Old key versions are kept forever and remain usable for decrypting credentials encrypted"
|
||||
" before the rotation, so rotation requires no manual re-encryption. Leave unset (default) to"
|
||||
" not configure a rotation policy and manage rotation manually in Azure. Must be at least 7 days.",
|
||||
default=None,
|
||||
ge=7,
|
||||
)
|
||||
|
||||
@field_validator("AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS", mode="before")
|
||||
@classmethod
|
||||
def _empty_string_to_none_for_rotation_interval(cls, v):
|
||||
"""Allow empty string in env/.env (e.g. an unfilled template value) to mean 'unset'."""
|
||||
if isinstance(v, str) and v.strip() == "":
|
||||
return None
|
||||
return v
|
||||
@@ -32,6 +32,11 @@ class TidbOnQdrantConfig(BaseSettings):
|
||||
default=6334,
|
||||
)
|
||||
|
||||
TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB: str = Field(
|
||||
description="Cloud pre-write thresholds for projected TiDB vector storage usage, in plan:MB pairs.",
|
||||
default="sandbox:60,professional:6400,team:25600",
|
||||
)
|
||||
|
||||
TIDB_PUBLIC_KEY: str | None = Field(
|
||||
description="Tidb account public key",
|
||||
default=None,
|
||||
|
||||
@@ -31,3 +31,8 @@ class TiDBVectorConfig(BaseSettings):
|
||||
description="Name of the TiDB Vector database to connect to",
|
||||
default=None,
|
||||
)
|
||||
|
||||
TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH: bool = Field(
|
||||
description="Enable TiDB Vector full-text and hybrid search features",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@@ -77,3 +77,5 @@ COOKIE_NAME_PASSPORT = "passport"
|
||||
HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
|
||||
HEADER_NAME_APP_CODE = "X-App-Code"
|
||||
HEADER_NAME_PASSPORT = "X-App-Passport"
|
||||
HEADER_NAME_IDEMPOTENCY_KEY = "Idempotency-Key"
|
||||
HEADER_NAME_REQUEST_ID = "X-Request-ID"
|
||||
|
||||
@@ -88,8 +88,9 @@ default_app_templates: Mapping[AppMode, Mapping] = {
|
||||
AppMode.AGENT: {
|
||||
"app": {
|
||||
"mode": AppMode.AGENT,
|
||||
"enable_site": True,
|
||||
"enable_api": True,
|
||||
# Public access is enabled atomically by the first successful publish.
|
||||
"enable_site": False,
|
||||
"enable_api": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.agent.publish_visibility import agent_has_workflow_callable_active_snapshot
|
||||
from core.app.apps.agent_app.app_feature_projection import merge_agent_app_features
|
||||
from core.app.apps.agent_app.app_variable_projection import agent_app_variables_to_user_input_form
|
||||
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
|
||||
@@ -34,9 +35,7 @@ def get_published_agent_app_feature_dict_and_user_input_form(
|
||||
)
|
||||
if agent is None:
|
||||
raise AgentAppGeneratorError("Agent App has no bound Agent")
|
||||
# active_config_is_published means the draft has no unpublished edits; the public app
|
||||
# can still read parameters from the active snapshot while a newer draft is pending.
|
||||
if not agent.active_config_snapshot_id:
|
||||
if not agent_has_workflow_callable_active_snapshot(session=session, agent=agent):
|
||||
raise AgentAppNotPublishedError("Agent has not been published")
|
||||
|
||||
snapshot = session.scalar(
|
||||
|
||||
@@ -234,15 +234,9 @@ def _query_param_from_property(
|
||||
items = param_schema.get("items")
|
||||
if isinstance(items, Mapping):
|
||||
item_schema = _resolve_schema_ref(items, definitions)
|
||||
item_type = item_schema.get("type")
|
||||
if isinstance(item_type, str):
|
||||
param_doc["items"] = {"type": item_type}
|
||||
item_enum = item_schema.get("enum")
|
||||
if isinstance(item_enum, list):
|
||||
param_doc.setdefault("items", {})["enum"] = item_enum
|
||||
item_format = item_schema.get("format")
|
||||
if isinstance(item_format, str):
|
||||
param_doc.setdefault("items", {})["format"] = item_format
|
||||
item_doc = _query_param_item_schema(item_schema)
|
||||
if item_doc:
|
||||
param_doc["items"] = item_doc
|
||||
|
||||
enum = param_schema.get("enum")
|
||||
if isinstance(enum, list):
|
||||
@@ -303,6 +297,31 @@ def _query_param_from_property(
|
||||
return param_doc
|
||||
|
||||
|
||||
def _query_param_item_schema(item_schema: Mapping[str, Any]) -> dict[str, object]:
|
||||
item_doc: dict[str, object] = {}
|
||||
|
||||
for key in ("type", "format", "pattern"):
|
||||
value = item_schema.get(key)
|
||||
if isinstance(value, str):
|
||||
item_doc[key] = value
|
||||
|
||||
enum = item_schema.get("enum")
|
||||
if isinstance(enum, list):
|
||||
item_doc["enum"] = enum
|
||||
|
||||
for key in ("minLength", "maxLength"):
|
||||
value = item_schema.get(key)
|
||||
if isinstance(value, int):
|
||||
item_doc[key] = value
|
||||
|
||||
for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"):
|
||||
value = item_schema.get(key)
|
||||
if isinstance(value, int | float):
|
||||
item_doc[key] = value
|
||||
|
||||
return item_doc
|
||||
|
||||
|
||||
def _resolve_schema_ref(property_schema: Mapping[str, Any], definitions: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
ref = property_schema.get("$ref")
|
||||
if not isinstance(ref, str):
|
||||
|
||||
@@ -152,7 +152,7 @@ def _extract_resource_id(
|
||||
if resource_type == RBACResourceScope.APP:
|
||||
app_id = matched_args.get("app_id")
|
||||
if app_id:
|
||||
return str(app_id)
|
||||
return str(app_id) # pyrefly: ignore[unnecessary-type-conversion]
|
||||
|
||||
agent_id = matched_args.get("agent_id")
|
||||
if agent_id:
|
||||
@@ -161,7 +161,7 @@ def _extract_resource_id(
|
||||
|
||||
resource_id = matched_args.get("resource_id")
|
||||
if resource_id:
|
||||
return str(resource_id)
|
||||
return str(resource_id) # pyrefly: ignore[unnecessary-type-conversion]
|
||||
raise ValueError("Missing app_id in request path")
|
||||
|
||||
if resource_type == RBACResourceScope.DATASET:
|
||||
@@ -174,6 +174,6 @@ def _extract_resource_id(
|
||||
dataset = db.session.scalar(select(Dataset).where(Dataset.pipeline_id == str(pipeline_id)))
|
||||
if not dataset:
|
||||
raise NotFound("Dataset not found for pipeline")
|
||||
return str(dataset.id)
|
||||
return str(dataset.id) # pyrefly: ignore[unnecessary-type-conversion]
|
||||
raise ValueError("Missing dataset_id or pipeline_id in request path")
|
||||
raise ValueError(f"Unknown resource_type: {resource_type}")
|
||||
|
||||
@@ -38,13 +38,11 @@ from . import (
|
||||
feature,
|
||||
human_input_form,
|
||||
init_validate,
|
||||
knowledge_fs_proxy,
|
||||
notification,
|
||||
onboarding,
|
||||
ping,
|
||||
setup,
|
||||
spec,
|
||||
version,
|
||||
system,
|
||||
workflow_run_archive,
|
||||
)
|
||||
from .agent import composer as agent_composer
|
||||
@@ -127,6 +125,7 @@ from .explore import (
|
||||
saved_message,
|
||||
trial,
|
||||
)
|
||||
from .knowledge_fs import resources as knowledge_fs_resources
|
||||
from .snippets import snippet_workflow, snippet_workflow_draft_variable
|
||||
from .socketio import workflow as socketio_workflow
|
||||
|
||||
@@ -197,7 +196,7 @@ __all__ = [
|
||||
"human_input_form",
|
||||
"init_validate",
|
||||
"installed_app",
|
||||
"knowledge_fs_proxy",
|
||||
"knowledge_fs_resources",
|
||||
"load_balancing_config",
|
||||
"login",
|
||||
"mcp_server",
|
||||
@@ -213,7 +212,6 @@ __all__ = [
|
||||
"onboarding",
|
||||
"ops_trace",
|
||||
"parameter",
|
||||
"ping",
|
||||
"plugin",
|
||||
"rag_pipeline",
|
||||
"rag_pipeline_datasets",
|
||||
@@ -231,11 +229,11 @@ __all__ = [
|
||||
"socketio_workflow",
|
||||
"spec",
|
||||
"statistic",
|
||||
"system",
|
||||
"tags",
|
||||
"tool_providers",
|
||||
"trial",
|
||||
"trigger_providers",
|
||||
"version",
|
||||
"website",
|
||||
"workflow",
|
||||
"workflow_app_log",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound
|
||||
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -64,8 +64,16 @@ class WorkflowAgentComposerApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
def get(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
|
||||
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(WorkflowAgentComposerQuery)
|
||||
def get(
|
||||
self,
|
||||
req_data: WorkflowAgentComposerQuery,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
app_model: App,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.load_workflow_composer(
|
||||
@@ -74,7 +82,7 @@ class WorkflowAgentComposerApi(Resource):
|
||||
app_id=app_model.id,
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
snapshot_id=query.snapshot_id,
|
||||
snapshot_id=req_data.snapshot_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -91,8 +99,16 @@ class WorkflowAgentComposerApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
def put(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ComposerSavePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: ComposerSavePayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
app_model: App,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.save_workflow_composer(
|
||||
@@ -101,7 +117,7 @@ class WorkflowAgentComposerApi(Resource):
|
||||
app_id=app_model.id,
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -123,8 +139,16 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
|
||||
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(WorkflowComposerCopyFromRosterPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowComposerCopyFromRosterPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
app_model: App,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.copy_workflow_composer_from_roster(
|
||||
@@ -133,9 +157,9 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
|
||||
app_id=app_model.id,
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
source_agent_id=payload.source_agent_id,
|
||||
source_snapshot_id=payload.source_snapshot_id,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
source_agent_id=req_data.source_agent_id,
|
||||
source_snapshot_id=req_data.source_snapshot_id,
|
||||
idempotency_key=req_data.idempotency_key,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -152,16 +176,16 @@ class WorkflowAgentComposerValidateApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
ComposerConfigValidator.validate_publish_payload(payload)
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
|
||||
ComposerConfigValidator.validate_publish_payload(req_data)
|
||||
AgentComposerService.validate_knowledge_datasets(
|
||||
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
|
||||
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
|
||||
)
|
||||
findings = AgentComposerService.collect_validation_findings(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
|
||||
session=session, tenant_id=tenant_id, app_id=app_model.id, node_id=node_id
|
||||
),
|
||||
@@ -204,9 +228,9 @@ class WorkflowAgentComposerImpactApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
|
||||
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
|
||||
if not current_snapshot_id:
|
||||
return dump_response(
|
||||
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
|
||||
@@ -235,8 +259,16 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: ComposerSavePayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
app_model: App,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.save_workflow_composer(
|
||||
@@ -245,14 +277,14 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
|
||||
app_id=app_model.id,
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _require_snippet_app_id(*, session: Session, tenant_id: str, snippet_id: UUID) -> str:
|
||||
snippet = SnippetService(session=session).get_snippet_by_id(
|
||||
snippet_id=str(snippet_id),
|
||||
snippet_id=str(snippet_id), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
if snippet is None:
|
||||
@@ -270,8 +302,16 @@ class SnippetAgentComposerApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def get(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
|
||||
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(WorkflowAgentComposerQuery)
|
||||
def get(
|
||||
self,
|
||||
req_data: WorkflowAgentComposerQuery,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
snippet_id: UUID,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.load_workflow_composer(
|
||||
@@ -280,7 +320,7 @@ class SnippetAgentComposerApi(Resource):
|
||||
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
snapshot_id=query.snapshot_id,
|
||||
snapshot_id=req_data.snapshot_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -296,8 +336,16 @@ class SnippetAgentComposerApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def put(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ComposerSavePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: ComposerSavePayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
snippet_id: UUID,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.save_workflow_composer(
|
||||
@@ -306,7 +354,7 @@ class SnippetAgentComposerApi(Resource):
|
||||
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -327,8 +375,16 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
|
||||
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(WorkflowComposerCopyFromRosterPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowComposerCopyFromRosterPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
snippet_id: UUID,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.copy_workflow_composer_from_roster(
|
||||
@@ -337,9 +393,9 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
|
||||
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
source_agent_id=payload.source_agent_id,
|
||||
source_snapshot_id=payload.source_snapshot_id,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
source_agent_id=req_data.source_agent_id,
|
||||
source_snapshot_id=req_data.source_snapshot_id,
|
||||
idempotency_key=req_data.idempotency_key,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -355,17 +411,17 @@ class SnippetAgentComposerValidateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
|
||||
app_id = _require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
ComposerConfigValidator.validate_publish_payload(payload)
|
||||
ComposerConfigValidator.validate_publish_payload(req_data)
|
||||
AgentComposerService.validate_knowledge_datasets(
|
||||
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
|
||||
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
|
||||
)
|
||||
findings = AgentComposerService.collect_validation_findings(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
@@ -409,10 +465,10 @@ class SnippetAgentComposerImpactApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
|
||||
_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
|
||||
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
|
||||
if not current_snapshot_id:
|
||||
return dump_response(
|
||||
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
|
||||
@@ -444,8 +500,16 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: ComposerSavePayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
snippet_id: UUID,
|
||||
node_id: str,
|
||||
):
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.save_workflow_composer(
|
||||
@@ -454,7 +518,7 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
|
||||
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -484,8 +548,8 @@ class AgentComposerApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def put(self, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ComposerSavePayload)
|
||||
def put(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
|
||||
return dump_response(
|
||||
AgentAppComposerResponse,
|
||||
AgentComposerService.save_agent_composer(
|
||||
@@ -493,7 +557,7 @@ class AgentComposerApi(Resource):
|
||||
tenant_id=tenant_id,
|
||||
agent_id=str(agent_id),
|
||||
account_id=account_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -509,17 +573,17 @@ class AgentComposerValidateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, agent_id: UUID):
|
||||
@model_validate(ComposerSavePayload)
|
||||
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, agent_id: UUID):
|
||||
AgentComposerService.load_agent_composer(session=session, tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
ComposerConfigValidator.validate_publish_payload(payload)
|
||||
ComposerConfigValidator.validate_publish_payload(req_data)
|
||||
AgentComposerService.validate_knowledge_datasets(
|
||||
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
|
||||
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
|
||||
)
|
||||
findings = AgentComposerService.collect_validation_findings(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
agent_id=str(agent_id),
|
||||
)
|
||||
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})
|
||||
|
||||
@@ -38,11 +38,13 @@ from controllers.console.wraps import (
|
||||
edit_permission_required,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from core.agent.publish_visibility import agent_has_workflow_callable_active_snapshot
|
||||
from fields.agent_fields import (
|
||||
AgentConfigDraftSummaryResponse,
|
||||
AgentConfigSnapshotDetailResponse,
|
||||
@@ -139,6 +141,7 @@ class AgentApiStatusPayload(BaseModel):
|
||||
|
||||
|
||||
class AgentApiAccessResponse(BaseModel):
|
||||
access_ready: bool
|
||||
enabled: bool
|
||||
service_api_base_url: str
|
||||
streaming_only: bool = True
|
||||
@@ -257,6 +260,7 @@ class AgentAppDetailWithSite(GenericAppDetailWithSite):
|
||||
debug_conversation_has_messages: bool = False
|
||||
debug_conversation_message_count: int = 0
|
||||
role: str | None = None
|
||||
access_ready: bool = False
|
||||
|
||||
|
||||
class AgentDebugConversationRefreshResponse(BaseModel):
|
||||
@@ -400,6 +404,7 @@ def _serialize_agent_app_detail(
|
||||
payload["debug_conversation_has_messages"] = message_count > 0
|
||||
payload["debug_conversation_message_count"] = message_count
|
||||
payload["role"] = agent.role or ""
|
||||
payload["access_ready"] = agent_has_workflow_callable_active_snapshot(session=session, agent=agent)
|
||||
return payload
|
||||
|
||||
|
||||
@@ -491,10 +496,20 @@ def _agent_api_key_count(session: Session, app_id: str) -> int:
|
||||
)
|
||||
|
||||
|
||||
def _agent_app_access_ready(session: Session, app_model: App) -> bool:
|
||||
agent = _agent_roster_service(session).get_app_backing_agent(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=str(app_model.id),
|
||||
)
|
||||
return bool(agent and agent_has_workflow_callable_active_snapshot(session=session, agent=agent))
|
||||
|
||||
|
||||
def _serialize_agent_api_access(session: Session, app_model: App) -> dict:
|
||||
base_url = app_model.api_base_url
|
||||
access_ready = _agent_app_access_ready(session, app_model)
|
||||
response = AgentApiAccessResponse(
|
||||
enabled=bool(app_model.enable_api),
|
||||
access_ready=access_ready,
|
||||
enabled=bool(app_model.enable_api and access_ready),
|
||||
service_api_base_url=base_url,
|
||||
chat_endpoint=f"{base_url}/chat-messages",
|
||||
stop_endpoint=f"{base_url}/chat-messages/{{task_id}}/stop",
|
||||
@@ -593,16 +608,16 @@ class AgentAppListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account):
|
||||
args = AgentAppCreatePayload.model_validate(console_ns.payload)
|
||||
@model_validate(AgentAppCreatePayload)
|
||||
def post(self, req_data: AgentAppCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
|
||||
params = CreateAppParams(
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
mode="agent",
|
||||
agent_role=args.role or "",
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
agent_role=req_data.role or "",
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
)
|
||||
|
||||
app = AppService().create_app(current_tenant_id, params, current_user, session=session)
|
||||
@@ -635,18 +650,25 @@ class AgentAppApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
@model_validate(AgentAppUpdatePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: AgentAppUpdatePayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
|
||||
args_dict: AppService.ArgsDict = {
|
||||
"name": args.name,
|
||||
"description": args.description or "",
|
||||
"icon_type": args.icon_type,
|
||||
"icon": args.icon or "",
|
||||
"icon_background": args.icon_background or "",
|
||||
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": args.max_active_requests or 0,
|
||||
"role": args.role,
|
||||
"name": req_data.name,
|
||||
"description": req_data.description or "",
|
||||
"icon_type": req_data.icon_type,
|
||||
"icon": req_data.icon or "",
|
||||
"icon_background": req_data.icon_background or "",
|
||||
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": req_data.max_active_requests or 0,
|
||||
"role": req_data.role,
|
||||
}
|
||||
updated = AppService().update_app(app_model, args_dict, session=session)
|
||||
return _serialize_agent_app_detail(session, updated, current_user=current_user)
|
||||
@@ -707,14 +729,21 @@ class AgentPublishApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
args = AgentPublishPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentPublishPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentPublishPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
return AgentComposerService.publish_agent_app_draft(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=str(agent_id),
|
||||
account_id=current_user.id,
|
||||
version_note=args.version_note,
|
||||
version_note=req_data.version_note,
|
||||
)
|
||||
|
||||
|
||||
@@ -730,14 +759,21 @@ class AgentBuildDraftCheckoutApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
args = AgentBuildDraftCheckoutPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentBuildDraftCheckoutPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentBuildDraftCheckoutPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
return AgentComposerService.checkout_agent_app_build_draft(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=str(agent_id),
|
||||
account_id=current_user.id,
|
||||
force=args.force,
|
||||
force=req_data.force,
|
||||
)
|
||||
|
||||
|
||||
@@ -769,14 +805,21 @@ class AgentBuildDraftApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ComposerSavePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: ComposerSavePayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
return AgentComposerService.save_agent_app_build_draft(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=str(agent_id),
|
||||
account_id=current_user.id,
|
||||
payload=payload,
|
||||
payload=req_data,
|
||||
)
|
||||
|
||||
@console_ns.response(200, "Agent build draft discarded", console_ns.models[AgentSimpleResultResponse.__name__])
|
||||
@@ -830,18 +873,25 @@ class AgentAppCopyApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
args = AgentAppCopyPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentAppCopyPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentAppCopyPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
copied_app = _agent_roster_service(session).duplicate_agent_app(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=str(agent_id),
|
||||
account=current_user,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
role=args.role,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
role=req_data.role,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
)
|
||||
return _serialize_agent_app_detail(session, copied_app, current_user=current_user), 201
|
||||
|
||||
@@ -873,10 +923,10 @@ class AgentApiStatusApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, agent_id: UUID):
|
||||
@model_validate(AgentApiStatusPayload)
|
||||
def post(self, req_data: AgentApiStatusPayload, session: Session, tenant_id: str, agent_id: UUID):
|
||||
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
args = AgentApiStatusPayload.model_validate(console_ns.payload)
|
||||
app_model = AppService().update_app_api_status(app_model, args.enable_api, session=session)
|
||||
app_model = AppService().update_app_api_status(app_model, req_data.enable_api, session=session)
|
||||
return _serialize_agent_api_access(session, app_model)
|
||||
|
||||
|
||||
@@ -944,16 +994,16 @@ class AgentInviteOptionsApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, tenant_id: str):
|
||||
query = AgentInviteOptionsQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(AgentInviteOptionsQuery)
|
||||
def get(self, req_data: AgentInviteOptionsQuery, session: Session, tenant_id: str):
|
||||
return dump_response(
|
||||
AgentInviteOptionsResponse,
|
||||
_agent_roster_service(session).list_invite_options(
|
||||
tenant_id=tenant_id,
|
||||
page=query.page,
|
||||
limit=query.limit,
|
||||
keyword=query.keyword,
|
||||
app_id=query.app_id,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
keyword=req_data.keyword,
|
||||
app_id=req_data.app_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1019,7 +1069,7 @@ class AgentLogMessagesApi(Resource):
|
||||
payload = _agent_observability_service(session).list_log_messages(
|
||||
app=app_model,
|
||||
agent_id=str(agent_id),
|
||||
conversation_id=str(conversation_id),
|
||||
conversation_id=str(conversation_id), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
params=AgentLogQueryParams(
|
||||
page=query.page,
|
||||
limit=query.limit,
|
||||
@@ -1068,16 +1118,23 @@ class AgentStatisticsSummaryApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
@model_validate(AgentStatisticsQuery)
|
||||
def get(
|
||||
self,
|
||||
req_data: AgentStatisticsQuery,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
app_model = _resolve_agent_runtime_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
query = AgentStatisticsQuery.model_validate(request.args.to_dict(flat=True))
|
||||
timezone = current_user.timezone or "UTC"
|
||||
start, end = _parse_observability_time_range(query.start, query.end, current_user)
|
||||
start, end = _parse_observability_time_range(req_data.start, req_data.end, current_user)
|
||||
try:
|
||||
payload = _agent_observability_service(session).get_statistics_summary(
|
||||
app=app_model,
|
||||
agent_id=str(agent_id),
|
||||
params=AgentStatisticsQueryParams(source=query.source, start=start, end=end, timezone=timezone),
|
||||
params=AgentStatisticsQueryParams(source=req_data.source, start=start, end=end, timezone=timezone),
|
||||
)
|
||||
except ValueError as exc:
|
||||
abort(400, description=str(exc))
|
||||
|
||||
@@ -21,6 +21,7 @@ from models.dataset import Dataset
|
||||
from models.enums import ApiTokenType
|
||||
from models.model import ApiToken, App
|
||||
from services.api_token_service import ApiTokenCache
|
||||
from services.app_service import AppService
|
||||
|
||||
from . import console_ns
|
||||
from .wraps import (
|
||||
@@ -103,7 +104,9 @@ class BaseApiKeyListResource(Resource):
|
||||
|
||||
def _create_api_key(self, resource_id: str, current_tenant_id: str, *, session: Session) -> ApiToken:
|
||||
assert self.resource_id_field is not None, "resource_id_field must be set"
|
||||
_get_resource(resource_id, current_tenant_id, self.resource_model, session=session)
|
||||
resource = _get_resource(resource_id, current_tenant_id, self.resource_model, session=session)
|
||||
if isinstance(resource, App):
|
||||
AppService.ensure_agent_app_access_ready(resource, session=session)
|
||||
current_key_count: int = (
|
||||
session.scalar(
|
||||
select(func.count(ApiToken.id)).where(
|
||||
@@ -201,7 +204,7 @@ class AppApiKeyListResource(BaseApiKeyListResource):
|
||||
"""Get all API keys for an app"""
|
||||
return dump_response(
|
||||
ApiKeyList,
|
||||
self._get_api_key_list(str(resource_id), current_tenant_id, session=session),
|
||||
self._get_api_key_list(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
)
|
||||
|
||||
@console_ns.doc("create_app_api_key")
|
||||
@@ -218,7 +221,7 @@ class AppApiKeyListResource(BaseApiKeyListResource):
|
||||
"""Create a new API key for an app"""
|
||||
return dump_response(
|
||||
ApiKeyItem,
|
||||
self._create_api_key(str(resource_id), current_tenant_id, session=session),
|
||||
self._create_api_key(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
), 201
|
||||
|
||||
resource_type = ApiTokenType.APP
|
||||
@@ -248,7 +251,7 @@ class AppApiKeyResource(BaseApiKeyResource):
|
||||
) -> tuple[str, int]:
|
||||
"""Delete an API key for an app"""
|
||||
self._delete_api_key(
|
||||
str(resource_id),
|
||||
str(resource_id), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
str(api_key_id),
|
||||
current_tenant_id,
|
||||
current_user,
|
||||
@@ -273,7 +276,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
|
||||
"""Get all API keys for a dataset"""
|
||||
return dump_response(
|
||||
ApiKeyList,
|
||||
self._get_api_key_list(str(resource_id), current_tenant_id, session=session),
|
||||
self._get_api_key_list(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
)
|
||||
|
||||
@console_ns.doc("create_dataset_api_key")
|
||||
@@ -289,7 +292,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
|
||||
"""Create a new API key for a dataset"""
|
||||
return dump_response(
|
||||
ApiKeyItem,
|
||||
self._create_api_key(str(resource_id), current_tenant_id, session=session),
|
||||
self._create_api_key(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
), 201
|
||||
|
||||
resource_type = ApiTokenType.DATASET
|
||||
@@ -318,7 +321,7 @@ class DatasetApiKeyResource(BaseApiKeyResource):
|
||||
) -> tuple[str, int]:
|
||||
"""Delete an API key for a dataset"""
|
||||
self._delete_api_key(
|
||||
str(resource_id),
|
||||
str(resource_id), # pyrefly: ignore[unnecessary-type-conversion]
|
||||
str(api_key_id),
|
||||
current_tenant_id,
|
||||
current_user,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -10,7 +9,7 @@ from controllers.common.schema import (
|
||||
register_response_schema_models,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
|
||||
@@ -49,12 +48,12 @@ class AdvancedPromptTemplateList(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(AdvancedPromptTemplateQuery)
|
||||
def get(self, req_data: AdvancedPromptTemplateQuery):
|
||||
prompt_args: AdvancedPromptTemplateArgs = {
|
||||
"app_mode": args.app_mode,
|
||||
"model_mode": args.model_mode,
|
||||
"model_name": args.model_name,
|
||||
"has_context": args.has_context,
|
||||
"app_mode": req_data.app_mode,
|
||||
"model_mode": req_data.model_mode,
|
||||
"model_name": req_data.model_name,
|
||||
"has_context": req_data.has_context,
|
||||
}
|
||||
return AdvancedPromptTemplateService.get_prompt(prompt_args)
|
||||
|
||||
@@ -21,6 +21,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -211,12 +212,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model:
|
||||
|
||||
|
||||
def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True):
|
||||
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
|
||||
query = query_params_from_request(AgentDriveMutationQuery)
|
||||
node_id = query.node_id if allow_node_id else None
|
||||
agent_id = _resolve_agent_id(session, app_model, node_id)
|
||||
if not agent_id:
|
||||
return _agent_not_bound()
|
||||
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
upload_file = session.scalar(
|
||||
select(UploadFile).where(
|
||||
@@ -341,11 +342,11 @@ class AgentLogApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=[AppMode.AGENT_CHAT])
|
||||
def get(self, session: Session, app_model: App):
|
||||
@model_validate(AgentLogQuery)
|
||||
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
|
||||
"""Get agent logs"""
|
||||
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session)
|
||||
return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
|
||||
|
||||
@@ -25,6 +25,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -87,14 +88,21 @@ class AgentAppFeatureConfigResource(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
@model_validate(AgentAppFeaturesPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentAppFeaturesPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
new_app_model_config = AgentAppFeatureConfigService.update_features(
|
||||
app_model=app_model,
|
||||
account=current_user,
|
||||
config=args.model_dump(exclude_none=True),
|
||||
config=req_data.model_dump(exclude_none=True),
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Console routes for Agent App and workflow Agent sandbox file access.
|
||||
|
||||
The API accepts product-facing Conversation, Build Draft, or Workflow Node
|
||||
Execution locators and proxies list/read/upload to the agent backend's
|
||||
``/sandbox`` contract.
|
||||
Execution locators and proxies list/read/download to the agent backend's
|
||||
``/execution-bindings/files`` contract.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,10 +11,8 @@ from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from dify_agent.client import DifyAgentClientError, DifyAgentHTTPError, DifyAgentTimeoutError
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
@@ -22,12 +20,12 @@ from controllers.common.schema import (
|
||||
register_response_schema_models,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.common.session import with_session
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
|
||||
from controllers.console.app.error import AppNotFoundError
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -43,11 +41,17 @@ from services.agent_app_sandbox_service import (
|
||||
WorkflowAgentSandboxService,
|
||||
)
|
||||
|
||||
_BINDING_PATH_DESCRIPTION = (
|
||||
"Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; "
|
||||
"`~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside "
|
||||
"Workspace are governed by backend isolation, not a Workspace-root restriction"
|
||||
)
|
||||
|
||||
|
||||
class AgentSandboxListQuery(BaseModel):
|
||||
caller_type: Literal["conversation", "build_draft"]
|
||||
caller_id: str = Field(min_length=1, description="Agent App caller ID")
|
||||
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
|
||||
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class AgentSandboxInfoQuery(BaseModel):
|
||||
@@ -58,28 +62,28 @@ class AgentSandboxInfoQuery(BaseModel):
|
||||
class AgentSandboxFileQuery(BaseModel):
|
||||
caller_type: Literal["conversation", "build_draft"]
|
||||
caller_id: str = Field(min_length=1, description="Agent App caller ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class AgentSandboxUploadPayload(BaseModel):
|
||||
class AgentSandboxDownloadPayload(BaseModel):
|
||||
caller_type: Literal["conversation", "build_draft"]
|
||||
caller_id: str = Field(min_length=1, description="Agent App caller ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class WorkflowAgentSandboxListQuery(BaseModel):
|
||||
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
|
||||
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
|
||||
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class WorkflowAgentSandboxFileQuery(BaseModel):
|
||||
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class WorkflowAgentSandboxUploadPayload(BaseModel):
|
||||
class WorkflowAgentSandboxDownloadPayload(BaseModel):
|
||||
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class SandboxFileEntryResponse(ResponseModel):
|
||||
@@ -107,21 +111,21 @@ class SandboxReadResponse(ResponseModel):
|
||||
text: str | None = None
|
||||
|
||||
|
||||
class SandboxUploadResponse(ResponseModel):
|
||||
class SandboxDownloadResponse(ResponseModel):
|
||||
url: str
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
AgentSandboxUploadPayload,
|
||||
WorkflowAgentSandboxUploadPayload,
|
||||
AgentSandboxDownloadPayload,
|
||||
WorkflowAgentSandboxDownloadPayload,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SandboxInfoResponse,
|
||||
SandboxListResponse,
|
||||
SandboxReadResponse,
|
||||
SandboxUploadResponse,
|
||||
SandboxDownloadResponse,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,14 +156,14 @@ class AgentAppSandboxInfoResource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
query = query_params_from_request(AgentSandboxInfoQuery)
|
||||
try:
|
||||
result = AgentAppSandboxService().get_info(
|
||||
result = service.get_info(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=query.caller_type,
|
||||
caller_id=query.caller_id,
|
||||
@@ -181,14 +185,14 @@ class AgentAppSandboxListResource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
query = query_params_from_request(AgentSandboxListQuery)
|
||||
try:
|
||||
result = AgentAppSandboxService().list_files(
|
||||
result = service.list_files(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=query.caller_type,
|
||||
caller_id=query.caller_id,
|
||||
@@ -211,14 +215,14 @@ class AgentAppSandboxReadResource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
query = query_params_from_request(AgentSandboxFileQuery)
|
||||
try:
|
||||
result = AgentAppSandboxService().read_file(
|
||||
result = service.read_file(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=query.caller_type,
|
||||
caller_id=query.caller_id,
|
||||
@@ -230,30 +234,36 @@ class AgentAppSandboxReadResource(Resource):
|
||||
return result.model_dump()
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/upload")
|
||||
class AgentAppSandboxUploadResource(Resource):
|
||||
@console_ns.doc("upload_agent_app_sandbox_file")
|
||||
@console_ns.doc(description="Upload one Agent App sandbox file and return a signed download URL")
|
||||
@console_ns.expect(console_ns.models[AgentSandboxUploadPayload.__name__])
|
||||
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
|
||||
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/download")
|
||||
class AgentAppSandboxDownloadResource(Resource):
|
||||
@console_ns.doc("download_agent_app_sandbox_file")
|
||||
@console_ns.doc(description="Create a ToolFile from one Agent App Binding file and return its download URL")
|
||||
@console_ns.expect(console_ns.models[AgentSandboxDownloadPayload.__name__])
|
||||
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
|
||||
@model_validate(AgentSandboxDownloadPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentSandboxDownloadPayload,
|
||||
current_user: Account,
|
||||
tenant_id: str,
|
||||
agent_id: UUID,
|
||||
):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
try:
|
||||
result = AgentAppSandboxService().upload_file(
|
||||
result = service.download_file(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=payload.caller_type,
|
||||
caller_id=payload.caller_id,
|
||||
caller_type=req_data.caller_type,
|
||||
caller_id=req_data.caller_id,
|
||||
account_id=current_user.id,
|
||||
path=payload.path,
|
||||
path=req_data.path,
|
||||
)
|
||||
except Exception as exc:
|
||||
return _handle(exc)
|
||||
@@ -333,29 +343,41 @@ class WorkflowAgentSandboxReadResource(Resource):
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/upload"
|
||||
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/download"
|
||||
)
|
||||
class WorkflowAgentSandboxUploadResource(Resource):
|
||||
@console_ns.doc("upload_workflow_agent_sandbox_file")
|
||||
@console_ns.doc(description="Upload one workflow Agent sandbox file and return a signed download URL")
|
||||
@console_ns.expect(console_ns.models[WorkflowAgentSandboxUploadPayload.__name__])
|
||||
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
|
||||
class WorkflowAgentSandboxDownloadResource(Resource):
|
||||
@console_ns.doc("download_workflow_agent_sandbox_file")
|
||||
@console_ns.doc(description="Create a ToolFile from one workflow Agent Binding file and return its download URL")
|
||||
@console_ns.expect(console_ns.models[WorkflowAgentSandboxDownloadPayload.__name__])
|
||||
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str):
|
||||
payload = WorkflowAgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
|
||||
@model_validate(WorkflowAgentSandboxDownloadPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowAgentSandboxDownloadPayload,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
app_id: UUID,
|
||||
workflow_run_id: UUID,
|
||||
node_id: str,
|
||||
):
|
||||
service = WorkflowAgentSandboxService()
|
||||
resolved_app_id = service.resolve_app_id(tenant_id=tenant_id, app_id=str(app_id))
|
||||
if resolved_app_id is None:
|
||||
raise AppNotFoundError()
|
||||
try:
|
||||
result = WorkflowAgentSandboxService().upload_file(
|
||||
result = service.download_file(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=resolved_app_id,
|
||||
workflow_run_id=str(workflow_run_id),
|
||||
node_id=node_id,
|
||||
node_execution_id=payload.node_execution_id,
|
||||
path=payload.path,
|
||||
session=db.session(),
|
||||
node_execution_id=req_data.node_execution_id,
|
||||
account_id=current_user.id,
|
||||
path=req_data.path,
|
||||
)
|
||||
except Exception as exc:
|
||||
return _handle(exc)
|
||||
|
||||
@@ -30,6 +30,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -810,14 +811,21 @@ class AgentConfigFilesByAgentApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentConfigFileUploadPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentConfigFileUploadPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
return _with_agent_route_target(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
current_user=current_user,
|
||||
action=lambda target: _file_upload_response(target, payload),
|
||||
action=lambda target: _file_upload_response(target, req_data),
|
||||
)
|
||||
|
||||
|
||||
@@ -849,13 +857,13 @@ class AgentConfigFilesApi(Resource):
|
||||
@with_current_user
|
||||
@with_session
|
||||
@get_app_model(mode=_WORKFLOW_APP_MODES)
|
||||
def post(self, session: Session, current_user: Account, app_model: App):
|
||||
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentConfigFileUploadPayload)
|
||||
def post(self, req_data: AgentConfigFileUploadPayload, session: Session, current_user: Account, app_model: App):
|
||||
return _with_app_route_target(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
current_user=current_user,
|
||||
action=lambda target: _file_upload_response(target, payload),
|
||||
action=lambda target: _file_upload_response(target, req_data),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from controllers.console.wraps import (
|
||||
annotation_import_rate_limit,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -180,14 +181,14 @@ class AnnotationReplyActionApi(Resource):
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID, action: Literal["enable", "disable"]):
|
||||
args = AnnotationReplyPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AnnotationReplyPayload)
|
||||
def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]):
|
||||
match action:
|
||||
case "enable":
|
||||
enable_args: EnableAnnotationArgs = {
|
||||
"score_threshold": args.score_threshold,
|
||||
"embedding_provider_name": args.embedding_provider_name,
|
||||
"embedding_model_name": args.embedding_model_name,
|
||||
"score_threshold": req_data.score_threshold,
|
||||
"embedding_provider_name": req_data.embedding_provider_name,
|
||||
"embedding_model_name": req_data.embedding_model_name,
|
||||
}
|
||||
result = AppAnnotationService.enable_app_annotation(enable_args, str(app_id))
|
||||
case "disable":
|
||||
@@ -231,12 +232,17 @@ class AppAnnotationSettingUpdateApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, app_id: UUID, annotation_setting_id: UUID):
|
||||
@model_validate(AnnotationSettingUpdatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AnnotationSettingUpdatePayload,
|
||||
session: Session,
|
||||
app_id: UUID,
|
||||
annotation_setting_id: UUID,
|
||||
):
|
||||
annotation_setting_id_str = str(annotation_setting_id)
|
||||
|
||||
args = AnnotationSettingUpdatePayload.model_validate(console_ns.payload)
|
||||
|
||||
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": args.score_threshold}
|
||||
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": req_data.score_threshold}
|
||||
result = AppAnnotationService.update_app_annotation_setting(
|
||||
str(app_id), annotation_setting_id_str, setting_args, session
|
||||
)
|
||||
@@ -290,11 +296,11 @@ class AnnotationApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, app_id: UUID):
|
||||
args = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
page = args.page
|
||||
limit = args.limit
|
||||
keyword = args.keyword
|
||||
@model_validate(AnnotationListQuery)
|
||||
def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID):
|
||||
page = req_data.page
|
||||
limit = req_data.limit
|
||||
keyword = req_data.keyword
|
||||
|
||||
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
|
||||
str(app_id), page, limit, keyword, session
|
||||
@@ -317,17 +323,17 @@ class AnnotationApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, app_id: UUID):
|
||||
args = CreateAnnotationPayload.model_validate(console_ns.payload)
|
||||
@model_validate(CreateAnnotationPayload)
|
||||
def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID):
|
||||
upsert_args: UpsertAnnotationArgs = {}
|
||||
if args.answer is not None:
|
||||
upsert_args["answer"] = args.answer
|
||||
if args.content is not None:
|
||||
upsert_args["content"] = args.content
|
||||
if args.message_id is not None:
|
||||
upsert_args["message_id"] = args.message_id
|
||||
if args.question is not None:
|
||||
upsert_args["question"] = args.question
|
||||
if req_data.answer is not None:
|
||||
upsert_args["answer"] = req_data.answer
|
||||
if req_data.content is not None:
|
||||
upsert_args["content"] = req_data.content
|
||||
if req_data.message_id is not None:
|
||||
upsert_args["message_id"] = req_data.message_id
|
||||
if req_data.question is not None:
|
||||
upsert_args["question"] = req_data.question
|
||||
annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id), session)
|
||||
return dump_response(Annotation, annotation), 201
|
||||
|
||||
@@ -407,13 +413,13 @@ class AnnotationUpdateDeleteApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, app_id: UUID, annotation_id: UUID):
|
||||
args = UpdateAnnotationPayload.model_validate(console_ns.payload)
|
||||
@model_validate(UpdateAnnotationPayload)
|
||||
def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID):
|
||||
update_args: UpdateAnnotationArgs = {}
|
||||
if args.answer is not None:
|
||||
update_args["answer"] = args.answer
|
||||
if args.question is not None:
|
||||
update_args["question"] = args.question
|
||||
if req_data.answer is not None:
|
||||
update_args["answer"] = req_data.answer
|
||||
if req_data.question is not None:
|
||||
update_args["question"] = req_data.question
|
||||
app_ref = _get_app_ref(session, str(app_id))
|
||||
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
|
||||
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, session)
|
||||
|
||||
@@ -4,7 +4,6 @@ from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import AliasChoices, BaseModel, Field, ValidationInfo, computed_field, field_validator, model_validator
|
||||
from sqlalchemy import select
|
||||
@@ -33,6 +32,7 @@ from controllers.console.wraps import (
|
||||
edit_permission_required,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -697,16 +697,16 @@ class AppListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account):
|
||||
@model_validate(CreateAppPayload)
|
||||
def post(self, req_data: CreateAppPayload, session: Session, current_tenant_id: str, current_user: Account):
|
||||
"""Create app"""
|
||||
args = CreateAppPayload.model_validate(console_ns.payload)
|
||||
params = CreateAppParams(
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
mode=args.mode,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
mode=req_data.mode,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
)
|
||||
|
||||
app_service = AppService()
|
||||
@@ -908,20 +908,20 @@ class AppApi(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def put(self, session: Session, app_model: App):
|
||||
@model_validate(UpdateAppPayload)
|
||||
def put(self, req_data: UpdateAppPayload, session: Session, app_model: App):
|
||||
"""Update app"""
|
||||
args = UpdateAppPayload.model_validate(console_ns.payload)
|
||||
|
||||
app_service = AppService()
|
||||
|
||||
args_dict: AppService.ArgsDict = {
|
||||
"name": args.name,
|
||||
"description": args.description or "",
|
||||
"icon_type": args.icon_type,
|
||||
"icon": args.icon or "",
|
||||
"icon_background": args.icon_background or "",
|
||||
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": args.max_active_requests or 0,
|
||||
"name": req_data.name,
|
||||
"description": req_data.description or "",
|
||||
"icon_type": req_data.icon_type,
|
||||
"icon": req_data.icon or "",
|
||||
"icon_background": req_data.icon_background or "",
|
||||
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": req_data.max_active_requests or 0,
|
||||
}
|
||||
app_model = app_service.update_app(app_model, args_dict, session=session)
|
||||
return AppDetailWithSite.model_validate(
|
||||
@@ -969,10 +969,10 @@ class AppCopyApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model(mode=None)
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
@model_validate(CopyAppPayload)
|
||||
def post(self, req_data: CopyAppPayload, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
"""Copy app"""
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
args = CopyAppPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
import_service = AppDslService(session)
|
||||
@@ -982,11 +982,11 @@ class AppCopyApi(Resource):
|
||||
account=current_user,
|
||||
import_mode=ImportMode.YAML_CONTENT,
|
||||
yaml_content=yaml_content,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
)
|
||||
except NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
@@ -1045,16 +1045,16 @@ class AppExportApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
|
||||
@agent_manage_required_for_agent_app
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
@model_validate(AppExportQuery)
|
||||
def get(self, req_data: AppExportQuery, app_model: App):
|
||||
"""Export app"""
|
||||
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
response = AppExportResponse(
|
||||
data=AppDslService.export_dsl(
|
||||
app_model=app_model,
|
||||
session=db.session(),
|
||||
include_secret=args.include_secret,
|
||||
workflow_id=args.workflow_id,
|
||||
include_secret=req_data.include_secret,
|
||||
workflow_id=req_data.workflow_id,
|
||||
)
|
||||
)
|
||||
return response.model_dump(mode="json")
|
||||
@@ -1102,11 +1102,11 @@ class AppNameApi(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppNamePayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppNamePayload)
|
||||
def post(self, req_data: AppNamePayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_name(app_model, args.name, session=session)
|
||||
app_model = app_service.update_app_name(app_model, req_data.name, session=session)
|
||||
return AppDetail.model_validate(
|
||||
app_model,
|
||||
from_attributes=True,
|
||||
@@ -1130,15 +1130,15 @@ class AppIconApi(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppIconPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AppIconPayload)
|
||||
def post(self, req_data: AppIconPayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_icon(
|
||||
app_model,
|
||||
args.icon or "",
|
||||
args.icon_background or "",
|
||||
args.icon_type,
|
||||
req_data.icon or "",
|
||||
req_data.icon_background or "",
|
||||
req_data.icon_type,
|
||||
session=session,
|
||||
)
|
||||
return AppDetail.model_validate(
|
||||
@@ -1164,11 +1164,11 @@ class AppSiteStatus(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppSiteStatusPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppSiteStatusPayload)
|
||||
def post(self, req_data: AppSiteStatusPayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=session)
|
||||
app_model = app_service.update_app_site_status(app_model, req_data.enable_site, session=session)
|
||||
return AppDetail.model_validate(
|
||||
app_model,
|
||||
from_attributes=True,
|
||||
@@ -1192,11 +1192,11 @@ class AppApiStatus(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppApiStatusPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppApiStatusPayload)
|
||||
def post(self, req_data: AppApiStatusPayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=session)
|
||||
app_model = app_service.update_app_api_status(app_model, req_data.enable_api, session=session)
|
||||
return AppDetail.model_validate(
|
||||
app_model,
|
||||
from_attributes=True,
|
||||
@@ -1242,14 +1242,14 @@ class AppTraceApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
@model_validate(AppTracePayload)
|
||||
def post(self, req_data: AppTracePayload, app_model: App):
|
||||
# add app trace
|
||||
args = AppTracePayload.model_validate(console_ns.payload)
|
||||
|
||||
OpsTraceManager.update_app_tracing_config(
|
||||
app_id=app_model.id,
|
||||
enabled=args.enabled,
|
||||
tracing_provider=args.tracing_provider,
|
||||
enabled=req_data.enabled,
|
||||
tracing_provider=req_data.tracing_provider,
|
||||
)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -83,8 +84,8 @@ class AppImportApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account | None = None):
|
||||
args = AppImportPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppImportPayload)
|
||||
def post(self, req_data: AppImportPayload, current_user: Account | None = None):
|
||||
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
|
||||
|
||||
# AppDslService performs internal commits for some creation paths, so use a plain
|
||||
@@ -96,15 +97,15 @@ class AppImportApi(Resource):
|
||||
try:
|
||||
result = import_service.import_app(
|
||||
account=account,
|
||||
import_mode=args.mode,
|
||||
yaml_content=args.yaml_content,
|
||||
yaml_url=args.yaml_url,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
app_id=args.app_id,
|
||||
import_mode=req_data.mode,
|
||||
yaml_content=req_data.yaml_content,
|
||||
yaml_url=req_data.yaml_url,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
app_id=req_data.app_id,
|
||||
)
|
||||
except NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
@@ -113,7 +114,7 @@ class AppImportApi(Resource):
|
||||
else:
|
||||
session.commit()
|
||||
|
||||
is_created_app = args.app_id is None and result.status in {
|
||||
is_created_app = req_data.app_id is None and result.status in {
|
||||
ImportStatus.COMPLETED,
|
||||
ImportStatus.COMPLETED_WITH_WARNINGS,
|
||||
}
|
||||
@@ -162,10 +163,7 @@ class AppImportConfirmApi(Resource):
|
||||
import_service = AppDslService(session)
|
||||
# Confirm import
|
||||
account = current_user
|
||||
try:
|
||||
result = import_service.confirm_import(import_id=import_id, account=account)
|
||||
except NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
result = import_service.confirm_import(import_id=import_id, account=account)
|
||||
if result.status == ImportStatus.FAILED:
|
||||
session.rollback()
|
||||
else:
|
||||
|
||||
@@ -31,6 +31,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -276,17 +277,15 @@ class ChatMessageTextApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
@model_validate(TextToSpeechPayload)
|
||||
def post(self, req_data: TextToSpeechPayload, app_model: App):
|
||||
try:
|
||||
payload_data = dict(console_ns.payload or {})
|
||||
payload_data.setdefault("text", "")
|
||||
payload = TextToSpeechPayload.model_validate(payload_data)
|
||||
message_ref = None
|
||||
if payload.message_id:
|
||||
if req_data.message_id:
|
||||
app_ref = AppRefService.create_app_ref(app_model)
|
||||
message_ref = AppRefService.create_message_ref(
|
||||
app_ref,
|
||||
payload.message_id,
|
||||
req_data.message_id,
|
||||
account_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -294,8 +293,8 @@ class ChatMessageTextApi(Resource):
|
||||
return AudioService.transcript_tts(
|
||||
app_model=app_model,
|
||||
session=db.session(),
|
||||
text=payload.text,
|
||||
voice=payload.voice,
|
||||
text=req_data.text,
|
||||
voice=req_data.voice,
|
||||
message_ref=message_ref,
|
||||
is_draft=True,
|
||||
)
|
||||
@@ -341,13 +340,12 @@ class TextModesApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
@model_validate(TextToSpeechVoiceQuery)
|
||||
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):
|
||||
try:
|
||||
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
response = AudioService.transcript_tts_voices(
|
||||
tenant_id=app_model.tenant_id,
|
||||
language=args.language,
|
||||
language=req_data.language,
|
||||
)
|
||||
|
||||
return dump_response(TextToSpeechVoiceListResponse, response)
|
||||
|
||||
@@ -29,6 +29,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -160,11 +161,11 @@ class CompletionMessageApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@with_session
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def post(self, session: Session, current_user: Account, app_model: App):
|
||||
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
|
||||
args = args_model.model_dump(exclude_none=True, by_alias=True)
|
||||
@model_validate(CompletionMessagePayload)
|
||||
def post(self, req_data: CompletionMessagePayload, session: Session, current_user: Account, app_model: App):
|
||||
args = req_data.model_dump(exclude_none=True, by_alias=True)
|
||||
|
||||
streaming = args_model.response_mode != "blocking"
|
||||
streaming = req_data.response_mode != "blocking"
|
||||
args["auto_generate_name"] = False
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import abort, request
|
||||
from flask import abort
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import func, or_
|
||||
@@ -18,6 +18,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -108,17 +109,17 @@ class CompletionConversationApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, session: Session, current_user: Account, app_model: App):
|
||||
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(CompletionConversationQuery)
|
||||
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
|
||||
|
||||
query = sa.select(Conversation).where(
|
||||
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
|
||||
)
|
||||
|
||||
if args.keyword:
|
||||
if req_data.keyword:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
escaped_keyword = escape_like_pattern(args.keyword)
|
||||
escaped_keyword = escape_like_pattern(req_data.keyword)
|
||||
query = query.join(Message, Message.conversation_id == Conversation.id).where(
|
||||
or_(
|
||||
Message.query.ilike(f"%{escaped_keyword}%", escape="\\"),
|
||||
@@ -130,7 +131,7 @@ class CompletionConversationApi(Resource):
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -142,7 +143,7 @@ class CompletionConversationApi(Resource):
|
||||
query = query.where(Conversation.created_at < end_datetime_utc)
|
||||
|
||||
# FIXME, the type ignore in this file
|
||||
if args.annotation_status == "annotated":
|
||||
if req_data.annotation_status == "annotated":
|
||||
query = (
|
||||
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
|
||||
.join( # type: ignore
|
||||
@@ -150,7 +151,7 @@ class CompletionConversationApi(Resource):
|
||||
)
|
||||
.group_by(Conversation.id)
|
||||
)
|
||||
elif args.annotation_status == "not_annotated":
|
||||
elif req_data.annotation_status == "not_annotated":
|
||||
query = (
|
||||
query.outerjoin(MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id)
|
||||
.group_by(Conversation.id)
|
||||
@@ -159,7 +160,7 @@ class CompletionConversationApi(Resource):
|
||||
|
||||
query = query.order_by(Conversation.created_at.desc())
|
||||
|
||||
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
|
||||
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
|
||||
|
||||
return dump_response(
|
||||
ConversationPaginationResponse,
|
||||
@@ -238,8 +239,8 @@ class ChatConversationApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, session: Session, current_user: Account, app_model: App):
|
||||
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ChatConversationQuery)
|
||||
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
|
||||
|
||||
subquery = (
|
||||
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
|
||||
@@ -249,10 +250,10 @@ class ChatConversationApi(Resource):
|
||||
|
||||
query = sa.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
|
||||
|
||||
if args.keyword:
|
||||
if req_data.keyword:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
escaped_keyword = escape_like_pattern(args.keyword)
|
||||
escaped_keyword = escape_like_pattern(req_data.keyword)
|
||||
keyword_filter = f"%{escaped_keyword}%"
|
||||
query = (
|
||||
query.join(
|
||||
@@ -276,12 +277,12 @@ class ChatConversationApi(Resource):
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
if start_datetime_utc:
|
||||
match args.sort_by:
|
||||
match req_data.sort_by:
|
||||
case "updated_at" | "-updated_at":
|
||||
query = query.where(Conversation.updated_at >= start_datetime_utc)
|
||||
case "created_at" | "-created_at" | _:
|
||||
@@ -289,13 +290,13 @@ class ChatConversationApi(Resource):
|
||||
|
||||
if end_datetime_utc:
|
||||
end_datetime_utc = end_datetime_utc.replace(second=59)
|
||||
match args.sort_by:
|
||||
match req_data.sort_by:
|
||||
case "updated_at" | "-updated_at":
|
||||
query = query.where(Conversation.updated_at <= end_datetime_utc)
|
||||
case "created_at" | "-created_at" | _:
|
||||
query = query.where(Conversation.created_at <= end_datetime_utc)
|
||||
|
||||
match args.annotation_status:
|
||||
match req_data.annotation_status:
|
||||
case "annotated":
|
||||
query = (
|
||||
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
|
||||
@@ -316,7 +317,7 @@ class ChatConversationApi(Resource):
|
||||
if app_model.mode == AppMode.ADVANCED_CHAT:
|
||||
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER)
|
||||
|
||||
match args.sort_by:
|
||||
match req_data.sort_by:
|
||||
case "created_at":
|
||||
query = query.order_by(Conversation.created_at.asc())
|
||||
case "-created_at":
|
||||
@@ -328,7 +329,7 @@ class ChatConversationApi(Resource):
|
||||
case _:
|
||||
query = query.order_by(Conversation.created_at.desc())
|
||||
|
||||
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
|
||||
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
|
||||
|
||||
return dump_response(
|
||||
ConversationWithSummaryPaginationResponse,
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
@@ -16,6 +15,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
def get(self, app_model: App):
|
||||
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ConversationVariablesQuery)
|
||||
def get(self, req_data: ConversationVariablesQuery, app_model: App):
|
||||
|
||||
stmt = (
|
||||
select(ConversationVariable)
|
||||
.where(ConversationVariable.app_id == app_model.id)
|
||||
.order_by(ConversationVariable.created_at)
|
||||
)
|
||||
stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id)
|
||||
stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id)
|
||||
|
||||
# NOTE: This is a temporary solution to avoid performance issues.
|
||||
page = 1
|
||||
|
||||
@@ -17,7 +17,12 @@ from controllers.console.app.error import (
|
||||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
)
|
||||
from core.app.app_config.entities import ModelConfig
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.helper.code_executor.code_node_provider import CodeNodeProvider
|
||||
@@ -238,11 +243,11 @@ class RuleGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = RuleGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(RuleGeneratePayload)
|
||||
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
|
||||
|
||||
try:
|
||||
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=args)
|
||||
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
except QuotaExceededError:
|
||||
@@ -267,13 +272,13 @@ class RuleCodeGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = RuleCodeGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(RuleCodeGeneratePayload)
|
||||
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
|
||||
|
||||
try:
|
||||
code_result = LLMGenerator.generate_code(
|
||||
tenant_id=current_tenant_id,
|
||||
args=args,
|
||||
args=req_data,
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
@@ -299,13 +304,13 @@ class RuleStructuredOutputGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = RuleStructuredOutputPayload.model_validate(console_ns.payload)
|
||||
@model_validate(RuleStructuredOutputPayload)
|
||||
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
|
||||
|
||||
try:
|
||||
structured_output = LLMGenerator.generate_structured_output(
|
||||
tenant_id=current_tenant_id,
|
||||
args=args,
|
||||
args=req_data,
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
@@ -332,36 +337,36 @@ class InstructionGenerateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, current_tenant_id: str):
|
||||
args = InstructionGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(InstructionGeneratePayload)
|
||||
def post(self, req_data: InstructionGeneratePayload, session: Session, current_tenant_id: str):
|
||||
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
|
||||
code_provider: type[CodeNodeProvider] | None = next(
|
||||
(p for p in providers if p.is_accept_language(args.language)), None
|
||||
(p for p in providers if p.is_accept_language(req_data.language)), None
|
||||
)
|
||||
code_template = code_provider.get_default_code() if code_provider else ""
|
||||
try:
|
||||
# Generate from nothing for a workflow node
|
||||
if (args.current in (code_template, "")) and args.node_id != "":
|
||||
if (req_data.current in (code_template, "")) and req_data.node_id != "":
|
||||
app = session.scalar(
|
||||
select(App).where(App.id == args.flow_id, App.tenant_id == current_tenant_id).limit(1)
|
||||
select(App).where(App.id == req_data.flow_id, App.tenant_id == current_tenant_id).limit(1)
|
||||
)
|
||||
if not app:
|
||||
return {"error": f"app {args.flow_id} not found"}, 400
|
||||
return {"error": f"app {req_data.flow_id} not found"}, 400
|
||||
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
|
||||
if not workflow:
|
||||
return {"error": f"workflow {args.flow_id} not found"}, 400
|
||||
return {"error": f"workflow {req_data.flow_id} not found"}, 400
|
||||
nodes: Sequence = workflow.graph_dict["nodes"]
|
||||
node = [node for node in nodes if node["id"] == args.node_id]
|
||||
node = [node for node in nodes if node["id"] == req_data.node_id]
|
||||
if len(node) == 0:
|
||||
return {"error": f"node {args.node_id} not found"}, 400
|
||||
return {"error": f"node {req_data.node_id} not found"}, 400
|
||||
node_type = node[0]["data"]["type"]
|
||||
match node_type:
|
||||
case "llm":
|
||||
return LLMGenerator.generate_rule_config(
|
||||
current_tenant_id,
|
||||
args=RuleGeneratePayload(
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
no_variable=True,
|
||||
),
|
||||
)
|
||||
@@ -369,8 +374,8 @@ class InstructionGenerateApi(Resource):
|
||||
return LLMGenerator.generate_rule_config(
|
||||
current_tenant_id,
|
||||
args=RuleGeneratePayload(
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
no_variable=True,
|
||||
),
|
||||
)
|
||||
@@ -378,31 +383,31 @@ class InstructionGenerateApi(Resource):
|
||||
return LLMGenerator.generate_code(
|
||||
tenant_id=current_tenant_id,
|
||||
args=RuleCodeGeneratePayload(
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
code_language=args.language,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
code_language=req_data.language,
|
||||
),
|
||||
)
|
||||
case _:
|
||||
return {"error": f"invalid node type: {node_type}"}
|
||||
if args.node_id == "" and args.current != "": # For legacy app without a workflow
|
||||
if req_data.node_id == "" and req_data.current != "": # For legacy app without a workflow
|
||||
return LLMGenerator.instruction_modify_legacy(
|
||||
tenant_id=current_tenant_id,
|
||||
flow_id=args.flow_id,
|
||||
current=args.current,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
flow_id=req_data.flow_id,
|
||||
current=req_data.current,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
)
|
||||
if args.node_id != "" and args.current != "": # For workflow node
|
||||
if req_data.node_id != "" and req_data.current != "": # For workflow node
|
||||
return LLMGenerator.instruction_modify_workflow(
|
||||
tenant_id=current_tenant_id,
|
||||
flow_id=args.flow_id,
|
||||
node_id=args.node_id,
|
||||
current=args.current,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
flow_id=req_data.flow_id,
|
||||
node_id=req_data.node_id,
|
||||
current=req_data.current,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
workflow_service=WorkflowService(),
|
||||
)
|
||||
return {"error": "incompatible parameters"}, 400
|
||||
@@ -426,9 +431,9 @@ class InstructionGenerationTemplateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
args = InstructionTemplatePayload.model_validate(console_ns.payload)
|
||||
match args.type:
|
||||
@model_validate(InstructionTemplatePayload)
|
||||
def post(self, req_data: InstructionTemplatePayload):
|
||||
match req_data.type:
|
||||
case "prompt":
|
||||
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
|
||||
|
||||
@@ -438,7 +443,7 @@ class InstructionGenerationTemplateApi(Resource):
|
||||
|
||||
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
|
||||
case _:
|
||||
raise ValueError(f"Invalid type: {args.type}")
|
||||
raise ValueError(f"Invalid type: {req_data.type}")
|
||||
|
||||
|
||||
def _workflow_instruction_guard(args: WorkflowGeneratePayload) -> tuple[dict, int] | None:
|
||||
@@ -492,24 +497,24 @@ class WorkflowGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(WorkflowGeneratePayload)
|
||||
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
|
||||
|
||||
# Reject empty / over-length instructions at the boundary (shared with
|
||||
# the streaming endpoint) before spending a planner+builder roundtrip.
|
||||
guard = _workflow_instruction_guard(args)
|
||||
guard = _workflow_instruction_guard(req_data)
|
||||
if guard is not None:
|
||||
return guard
|
||||
|
||||
try:
|
||||
result = WorkflowGeneratorService.generate_workflow_graph(
|
||||
tenant_id=current_tenant_id,
|
||||
mode=args.mode,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
current_graph=args.current_graph.model_dump(by_alias=True, exclude_none=True)
|
||||
if args.current_graph
|
||||
mode=req_data.mode,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
current_graph=req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
|
||||
if req_data.current_graph
|
||||
else None,
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
@@ -547,13 +552,13 @@ class WorkflowInstructionSuggestionsApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = WorkflowInstructionSuggestionsPayload.model_validate(console_ns.payload)
|
||||
@model_validate(WorkflowInstructionSuggestionsPayload)
|
||||
def post(self, req_data: WorkflowInstructionSuggestionsPayload, current_tenant_id: str):
|
||||
suggestions = LLMGenerator.generate_workflow_instruction_suggestions(
|
||||
tenant_id=current_tenant_id,
|
||||
mode=args.mode,
|
||||
language=args.language,
|
||||
count=args.count,
|
||||
mode=req_data.mode,
|
||||
language=req_data.language,
|
||||
count=req_data.count,
|
||||
)
|
||||
return dump_response(WorkflowInstructionSuggestionsResponse, {"suggestions": suggestions})
|
||||
|
||||
@@ -583,12 +588,12 @@ class WorkflowGenerateStreamApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(WorkflowGeneratePayload)
|
||||
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
|
||||
|
||||
# Same boundary guards as the blocking endpoint — return a normal 400
|
||||
# JSON for these BEFORE opening the stream.
|
||||
guard = _workflow_instruction_guard(args)
|
||||
guard = _workflow_instruction_guard(req_data)
|
||||
if guard is not None:
|
||||
return guard
|
||||
|
||||
@@ -596,12 +601,14 @@ class WorkflowGenerateStreamApi(Resource):
|
||||
try:
|
||||
for event_name, payload in WorkflowGeneratorService.generate_workflow_graph_stream(
|
||||
tenant_id=current_tenant_id,
|
||||
mode=args.mode,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
mode=req_data.mode,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
current_graph=(
|
||||
args.current_graph.model_dump(by_alias=True, exclude_none=True) if args.current_graph else None
|
||||
req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
|
||||
if req_data.current_graph
|
||||
else None
|
||||
),
|
||||
):
|
||||
body = {"event": event_name, **payload}
|
||||
|
||||
@@ -15,6 +15,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -109,17 +110,16 @@ class AppMCPServerController(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_tenant_id
|
||||
@get_app_model
|
||||
def post(self, current_tenant_id: str, app_model: App):
|
||||
payload = MCPServerCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
description = payload.description
|
||||
@model_validate(MCPServerCreatePayload)
|
||||
def post(self, req_data: MCPServerCreatePayload, current_tenant_id: str, app_model: App):
|
||||
description = req_data.description
|
||||
if not description:
|
||||
description = app_model.description or ""
|
||||
|
||||
server = AppMCPServer(
|
||||
name=app_model.name,
|
||||
description=description,
|
||||
parameters=json.dumps(payload.parameters, ensure_ascii=False),
|
||||
parameters=json.dumps(req_data.parameters, ensure_ascii=False),
|
||||
status=AppMCPServerStatus.ACTIVE,
|
||||
app_id=app_model.id,
|
||||
tenant_id=current_tenant_id,
|
||||
@@ -144,10 +144,10 @@ class AppMCPServerController(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model
|
||||
def put(self, app_model: App):
|
||||
payload = MCPServerUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(MCPServerUpdatePayload)
|
||||
def put(self, req_data: MCPServerUpdatePayload, app_model: App):
|
||||
app_ref = AppRefService.create_app_ref(app_model)
|
||||
server_ref = AppRefService.create_mcp_server_ref(app_ref, payload.id)
|
||||
server_ref = AppRefService.create_mcp_server_ref(app_ref, req_data.id)
|
||||
server = db.session.scalar(
|
||||
select(AppMCPServer)
|
||||
.where(
|
||||
@@ -160,7 +160,7 @@ class AppMCPServerController(Resource):
|
||||
if not server:
|
||||
raise NotFound()
|
||||
|
||||
description = payload.description
|
||||
description = req_data.description
|
||||
if description is None or not description:
|
||||
server.description = app_model.description or ""
|
||||
else:
|
||||
@@ -168,10 +168,10 @@ class AppMCPServerController(Resource):
|
||||
|
||||
server.name = app_model.name
|
||||
|
||||
server.parameters = json.dumps(payload.parameters, ensure_ascii=False)
|
||||
if payload.status:
|
||||
server.parameters = json.dumps(req_data.parameters, ensure_ascii=False)
|
||||
if req_data.status:
|
||||
try:
|
||||
server.status = AppMCPServerStatus(payload.status)
|
||||
server.status = AppMCPServerStatus(req_data.status)
|
||||
except ValueError:
|
||||
raise ValueError("Invalid status")
|
||||
db.session.commit()
|
||||
|
||||
@@ -28,6 +28,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -320,8 +321,8 @@ class MessageFeedbackExportApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
args = FeedbackExportQuery.model_validate(request.args.to_dict())
|
||||
@model_validate(FeedbackExportQuery)
|
||||
def get(self, req_data: FeedbackExportQuery, app_model: App):
|
||||
|
||||
# Import the service function
|
||||
from services.feedback_service import FeedbackService
|
||||
@@ -330,12 +331,12 @@ class MessageFeedbackExportApi(Resource):
|
||||
export_data = FeedbackService.export_feedbacks(
|
||||
app_model.id,
|
||||
session=db.session(),
|
||||
from_source=args.from_source,
|
||||
rating=args.rating,
|
||||
has_comment=args.has_comment,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
format_type=args.format,
|
||||
from_source=req_data.from_source,
|
||||
rating=req_data.rating,
|
||||
has_comment=req_data.has_comment,
|
||||
start_date=req_data.start_date,
|
||||
end_date=req_data.end_date,
|
||||
format_type=req_data.format,
|
||||
)
|
||||
return export_data
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from werkzeug.exceptions import BadRequest
|
||||
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -74,12 +74,11 @@ class TraceAppConfigApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
|
||||
|
||||
@model_validate(TraceProviderQuery)
|
||||
def get(self, req_data: TraceProviderQuery, app_model: App):
|
||||
try:
|
||||
trace_config = OpsService.get_tracing_app_config(
|
||||
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
|
||||
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
|
||||
)
|
||||
if not trace_config:
|
||||
return {"has_not_configured": True}
|
||||
@@ -104,15 +103,14 @@ class TraceAppConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
@model_validate(TraceConfigPayload)
|
||||
def post(self, req_data: TraceConfigPayload, app_model: App):
|
||||
"""Create a new trace app configuration"""
|
||||
args = TraceConfigPayload.model_validate(console_ns.payload)
|
||||
|
||||
try:
|
||||
result = OpsService.create_tracing_app_config(
|
||||
app_id=app_model.id,
|
||||
tracing_provider=args.tracing_provider,
|
||||
tracing_config=args.tracing_config,
|
||||
tracing_provider=req_data.tracing_provider,
|
||||
tracing_config=req_data.tracing_config,
|
||||
session=db.session(),
|
||||
)
|
||||
if not result:
|
||||
@@ -140,15 +138,14 @@ class TraceAppConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def patch(self, app_model: App):
|
||||
@model_validate(TraceConfigPayload)
|
||||
def patch(self, req_data: TraceConfigPayload, app_model: App):
|
||||
"""Update an existing trace app configuration"""
|
||||
args = TraceConfigPayload.model_validate(console_ns.payload)
|
||||
|
||||
try:
|
||||
result = OpsService.update_tracing_app_config(
|
||||
app_id=app_model.id,
|
||||
tracing_provider=args.tracing_provider,
|
||||
tracing_config=args.tracing_config,
|
||||
tracing_provider=req_data.tracing_provider,
|
||||
tracing_config=req_data.tracing_config,
|
||||
session=db.session(),
|
||||
)
|
||||
if not result:
|
||||
@@ -170,13 +167,12 @@ class TraceAppConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def delete(self, app_model: App):
|
||||
@model_validate(TraceProviderQuery)
|
||||
def delete(self, req_data: TraceProviderQuery, app_model: App):
|
||||
"""Delete an existing trace app configuration"""
|
||||
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
try:
|
||||
result = OpsService.delete_tracing_app_config(
|
||||
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
|
||||
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
|
||||
)
|
||||
if not result:
|
||||
raise TracingConfigNotExist()
|
||||
|
||||
@@ -10,13 +10,14 @@ from constants.languages import supported_language
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.common.session import with_session
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -93,13 +94,12 @@ class AppSite(Resource):
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@agent_manage_required_for_agent_app
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_session
|
||||
@get_app_model
|
||||
def post(self, session: Session, current_user: Account, app_model: App):
|
||||
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AppSiteUpdatePayload)
|
||||
def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App):
|
||||
site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
|
||||
if not site:
|
||||
raise NotFound
|
||||
@@ -123,7 +123,7 @@ class AppSite(Resource):
|
||||
"show_workflow_steps",
|
||||
"use_icon_as_answer_icon",
|
||||
]:
|
||||
value = getattr(args, attr_name)
|
||||
value = getattr(req_data, attr_name)
|
||||
if value is not None:
|
||||
setattr(site, attr_name, value)
|
||||
|
||||
@@ -146,7 +146,6 @@ class AppSiteAccessTokenReset(Resource):
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@agent_manage_required_for_agent_app
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_session
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import abort, request
|
||||
from flask import abort
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -157,8 +158,8 @@ class DailyMessageStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -177,7 +178,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -217,8 +218,8 @@ class DailyConversationStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -237,7 +238,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -276,8 +277,8 @@ class DailyTerminalsStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -296,7 +297,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -336,8 +337,8 @@ class DailyTokenCostStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -357,7 +358,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -399,8 +400,8 @@ class AverageSessionInteractionStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("c.created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -427,7 +428,7 @@ FROM
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -478,8 +479,8 @@ class UserSatisfactionRateStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("m.created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -502,7 +503,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -547,8 +548,8 @@ class AverageResponseTimeStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -567,7 +568,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -607,8 +608,8 @@ class TokensPerSecondStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -630,7 +631,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
|
||||
@@ -2,11 +2,20 @@ import json
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, NotRequired, TypedDict
|
||||
from typing import Any, NotRequired, Self, TypedDict
|
||||
|
||||
from flask import abort, request
|
||||
from flask_restx import Resource, fields
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, RootModel, ValidationError, field_validator
|
||||
from pydantic import (
|
||||
AliasChoices,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
RootModel,
|
||||
ValidationError,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
|
||||
|
||||
@@ -54,6 +63,10 @@ from core.trigger.debug.event_selectors import (
|
||||
create_event_poller,
|
||||
select_trigger_debug_events,
|
||||
)
|
||||
from core.workflow.llm_environment_variable import (
|
||||
LLM_ENVIRONMENT_VARIABLE_VALUE_TYPE,
|
||||
LLMEnvironmentVariable,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from factories import file_factory, variable_factory
|
||||
@@ -103,14 +116,35 @@ class EnvironmentVariableResponseDict(TypedDict):
|
||||
description: NotRequired[str | None]
|
||||
|
||||
|
||||
class SyncEnvironmentVariablePatchPayload(BaseModel):
|
||||
environment_variables: list[dict[str, Any]] = Field(default_factory=list)
|
||||
deleted_environment_variable_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_patch(self) -> Self:
|
||||
"""Require stable, disjoint IDs so the service can merge the patch deterministically."""
|
||||
upsert_ids = [variable.get("id") for variable in self.environment_variables]
|
||||
if any(not isinstance(variable_id, str) or not variable_id for variable_id in upsert_ids):
|
||||
raise ValueError("patched environment variables require an id")
|
||||
if len(set(upsert_ids)) != len(upsert_ids):
|
||||
raise ValueError("patched environment variable ids must be unique")
|
||||
if any(not variable_id for variable_id in self.deleted_environment_variable_ids):
|
||||
raise ValueError("deleted environment variable ids must not be empty")
|
||||
if len(set(self.deleted_environment_variable_ids)) != len(self.deleted_environment_variable_ids):
|
||||
raise ValueError("deleted environment variable ids must be unique")
|
||||
if set(upsert_ids).intersection(self.deleted_environment_variable_ids):
|
||||
raise ValueError("an environment variable cannot be upserted and deleted in the same patch")
|
||||
return self
|
||||
|
||||
|
||||
class SyncDraftWorkflowPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
graph: dict[str, Any]
|
||||
features: dict[str, Any]
|
||||
hash: str | None = None
|
||||
is_collaborative: bool = Field(default=False, alias="_is_collaborative")
|
||||
environment_variables: list[dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
)
|
||||
environment_variable_patch: SyncEnvironmentVariablePatchPayload | None = None
|
||||
conversation_variables: list[dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
)
|
||||
@@ -483,6 +517,15 @@ def _parse_file(workflow: Workflow, files: list[dict] | None = None) -> Sequence
|
||||
|
||||
def _serialize_environment_variable(value: Any) -> EnvironmentVariableResponseDict | Any:
|
||||
match value:
|
||||
case LLMEnvironmentVariable():
|
||||
return {
|
||||
"id": value.id,
|
||||
"name": value.name,
|
||||
"value": value.value,
|
||||
"value_type": LLM_ENVIRONMENT_VARIABLE_VALUE_TYPE,
|
||||
"description": value.description,
|
||||
}
|
||||
|
||||
case SecretVariable():
|
||||
return {
|
||||
"id": value.id,
|
||||
@@ -507,6 +550,8 @@ def _serialize_environment_variable(value: Any) -> EnvironmentVariableResponseDi
|
||||
raise TypeError(
|
||||
f"unexpected type for value_type field, value={value_type_str}, type={type(value_type_str)}"
|
||||
)
|
||||
if value_type_str == LLM_ENVIRONMENT_VARIABLE_VALUE_TYPE:
|
||||
return value
|
||||
value_type = SegmentType(value_type_str).exposed_type()
|
||||
if value_type not in ENVIRONMENT_VARIABLE_SUPPORTED_TYPES:
|
||||
raise ValueError(f"Unsupported environment variable value type: {value_type}")
|
||||
@@ -590,30 +635,38 @@ class DraftWorkflowApi(Resource):
|
||||
return {"message": "Invalid JSON data"}, 400
|
||||
else:
|
||||
abort(415)
|
||||
args = args_model.model_dump()
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
try:
|
||||
environment_variables_list = Workflow.normalize_environment_variable_mappings(
|
||||
args.get("environment_variables") or [],
|
||||
)
|
||||
environment_variables = [
|
||||
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
|
||||
]
|
||||
conversation_variables_list = args.get("conversation_variables") or []
|
||||
environment_variable_patch = args_model.environment_variable_patch
|
||||
environment_variable_upserts: list[VariableBase] | None = None
|
||||
deleted_environment_variable_ids: list[str] = []
|
||||
if environment_variable_patch is not None:
|
||||
environment_variable_upsert_mappings = Workflow.normalize_environment_variable_mappings(
|
||||
environment_variable_patch.environment_variables,
|
||||
)
|
||||
environment_variable_upserts = [
|
||||
variable_factory.build_environment_variable_from_mapping(obj)
|
||||
for obj in environment_variable_upsert_mappings
|
||||
]
|
||||
deleted_environment_variable_ids = environment_variable_patch.deleted_environment_variable_ids
|
||||
conversation_variables = [
|
||||
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
|
||||
variable_factory.build_conversation_variable_from_mapping(obj)
|
||||
for obj in args_model.conversation_variables
|
||||
]
|
||||
workflow = workflow_service.sync_draft_workflow(
|
||||
app_model=app_model,
|
||||
graph=args["graph"],
|
||||
features=args["features"],
|
||||
unique_hash=args.get("hash"),
|
||||
graph=args_model.graph,
|
||||
features=args_model.features,
|
||||
unique_hash=args_model.hash,
|
||||
account=current_user,
|
||||
environment_variables=environment_variables,
|
||||
environment_variables=[],
|
||||
conversation_variables=conversation_variables,
|
||||
session=db.session(),
|
||||
graph_only=args["is_collaborative"],
|
||||
environment_variable_upserts=environment_variable_upserts,
|
||||
deleted_environment_variable_ids=deleted_environment_variable_ids,
|
||||
preserve_environment_variables=True,
|
||||
graph_only=args_model.is_collaborative,
|
||||
)
|
||||
except WorkflowHashNotEqualError:
|
||||
raise DraftWorkflowNotSync()
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from dateutil.parser import isoparse
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowAppLogQuery)
|
||||
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
|
||||
"""
|
||||
Get workflow app logs
|
||||
"""
|
||||
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
# get paginate workflow app logs
|
||||
workflow_app_service = WorkflowAppService()
|
||||
@@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource):
|
||||
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
keyword=args.keyword,
|
||||
status=args.status,
|
||||
created_at_before=args.created_at__before,
|
||||
created_at_after=args.created_at__after,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
detail=args.detail,
|
||||
created_by_end_user_session_id=args.created_by_end_user_session_id,
|
||||
created_by_account=args.created_by_account,
|
||||
keyword=req_data.keyword,
|
||||
status=req_data.status,
|
||||
created_at_before=req_data.created_at__before,
|
||||
created_at_after=req_data.created_at__after,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
detail=req_data.detail,
|
||||
created_by_end_user_session_id=req_data.created_by_end_user_session_id,
|
||||
created_by_account=req_data.created_by_account,
|
||||
)
|
||||
|
||||
return WorkflowAppLogPaginationResponse.model_validate(
|
||||
@@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowAppLogQuery)
|
||||
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
|
||||
"""
|
||||
Get workflow archived logs
|
||||
"""
|
||||
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
workflow_app_service = WorkflowAppService()
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
)
|
||||
|
||||
return WorkflowArchivedLogPaginationResponse.model_validate(
|
||||
|
||||
@@ -10,6 +10,7 @@ from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -239,18 +240,24 @@ class WorkflowCommentListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
@model_validate(WorkflowCommentCreatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowCommentCreatePayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
):
|
||||
"""Create a new workflow comment."""
|
||||
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
result = WorkflowCommentService.create_comment(
|
||||
tenant_id=current_tenant_id,
|
||||
app_id=app_model.id,
|
||||
created_by=current_user.id,
|
||||
content=payload.content,
|
||||
position_x=payload.position_x,
|
||||
position_y=payload.position_y,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
content=req_data.content,
|
||||
position_x=req_data.position_x,
|
||||
position_y=req_data.position_y,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentCreate, result), 201
|
||||
@@ -289,19 +296,26 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
@model_validate(WorkflowCommentUpdatePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: WorkflowCommentUpdatePayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
comment_id: str,
|
||||
):
|
||||
"""Update a workflow comment."""
|
||||
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
result = WorkflowCommentService.update_comment(
|
||||
tenant_id=current_tenant_id,
|
||||
app_id=app_model.id,
|
||||
comment_id=comment_id,
|
||||
user_id=current_user.id,
|
||||
content=payload.content,
|
||||
position_x=payload.position_x,
|
||||
position_y=payload.position_y,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
content=req_data.content,
|
||||
position_x=req_data.position_x,
|
||||
position_y=req_data.position_y,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentUpdate, result)
|
||||
@@ -372,20 +386,26 @@ class WorkflowCommentReplyApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
@model_validate(WorkflowCommentReplyPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowCommentReplyPayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
comment_id: str,
|
||||
):
|
||||
"""Add a reply to a workflow comment."""
|
||||
# Validate comment access first
|
||||
WorkflowCommentService.validate_comment_access(
|
||||
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
|
||||
)
|
||||
|
||||
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
result = WorkflowCommentService.create_reply(
|
||||
comment_id=comment_id,
|
||||
content=payload.content,
|
||||
content=req_data.content,
|
||||
created_by=current_user.id,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentReplyCreate, result), 201
|
||||
@@ -407,23 +427,30 @@ class WorkflowCommentReplyDetailApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
|
||||
@model_validate(WorkflowCommentReplyPayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: WorkflowCommentReplyPayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
comment_id: str,
|
||||
reply_id: str,
|
||||
):
|
||||
"""Update a comment reply."""
|
||||
# Validate comment access first
|
||||
WorkflowCommentService.validate_comment_access(
|
||||
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
|
||||
)
|
||||
|
||||
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
reply = WorkflowCommentService.update_reply(
|
||||
tenant_id=current_tenant_id,
|
||||
app_id=app_model.id,
|
||||
comment_id=comment_id,
|
||||
reply_id=reply_id,
|
||||
user_id=current_user.id,
|
||||
content=payload.content,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
content=req_data.content,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentReplyUpdate, reply)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate, TypedDict, override
|
||||
from typing import Any, Concatenate, Self, TypedDict, override
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Response, request
|
||||
from flask import Response
|
||||
from flask_restx import Resource, fields, marshal, marshal_with
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
@@ -22,11 +22,13 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.workflow.llm_environment_variable import environment_variable_value_type
|
||||
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||
from extensions.ext_database import db
|
||||
from factories import variable_factory
|
||||
@@ -109,6 +111,35 @@ class EnvironmentVariableUpdatePayload(BaseModel):
|
||||
...,
|
||||
description="Environment variables for the draft workflow",
|
||||
)
|
||||
patch: bool = Field(
|
||||
default=False,
|
||||
description="Treat environment_variables as per-ID upserts instead of replacing the full collection",
|
||||
)
|
||||
deleted_environment_variable_ids: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Environment variable IDs to delete when patch is true",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_patch(self) -> Self:
|
||||
"""Validate the per-variable patch contract without changing legacy replacement requests."""
|
||||
if not self.patch:
|
||||
if self.deleted_environment_variable_ids:
|
||||
raise ValueError("deleted_environment_variable_ids requires patch=true")
|
||||
return self
|
||||
|
||||
upsert_ids = [variable.id for variable in self.environment_variables]
|
||||
if any(not variable_id for variable_id in upsert_ids):
|
||||
raise ValueError("patched environment variables require an id")
|
||||
if len(set(upsert_ids)) != len(upsert_ids):
|
||||
raise ValueError("patched environment variable ids must be unique")
|
||||
if any(not variable_id for variable_id in self.deleted_environment_variable_ids):
|
||||
raise ValueError("deleted environment variable ids must not be empty")
|
||||
if len(set(self.deleted_environment_variable_ids)) != len(self.deleted_environment_variable_ids):
|
||||
raise ValueError("deleted environment variable ids must be unique")
|
||||
if set(upsert_ids).intersection(self.deleted_environment_variable_ids):
|
||||
raise ValueError("an environment variable cannot be upserted and deleted in the same patch")
|
||||
return self
|
||||
|
||||
|
||||
class EnvironmentVariableItemResponse(ResponseModel):
|
||||
@@ -329,11 +360,11 @@ class WorkflowVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_without_value_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
@model_validate(WorkflowDraftVariableListQuery)
|
||||
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
"""
|
||||
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
# fetch draft workflow by app_model
|
||||
workflow_service = WorkflowService()
|
||||
@@ -348,8 +379,8 @@ class WorkflowVariableCollectionApi(Resource):
|
||||
)
|
||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
||||
app_id=app_model.id,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -449,7 +480,14 @@ class VariableApi(Resource):
|
||||
@console_ns.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_model)
|
||||
def patch(self, current_user: Account, app_model: App, variable_id: UUID):
|
||||
@model_validate(WorkflowDraftVariableUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: WorkflowDraftVariableUpdatePayload,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
variable_id: UUID,
|
||||
):
|
||||
# Request payload for file types:
|
||||
#
|
||||
# Local File:
|
||||
@@ -474,7 +512,6 @@ class VariableApi(Resource):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
variable_id_str = str(variable_id)
|
||||
variable = ensure_variable_access(
|
||||
@@ -484,8 +521,8 @@ class VariableApi(Resource):
|
||||
current_user_id=current_user.id,
|
||||
)
|
||||
|
||||
new_name = args_model.name
|
||||
raw_value = args_model.value
|
||||
new_name = req_data.name
|
||||
raw_value = req_data.value
|
||||
if new_name is None and raw_value is None:
|
||||
return variable
|
||||
|
||||
@@ -630,13 +667,13 @@ class ConversationVariableCollectionApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ConversationVariableUpdatePayload)
|
||||
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
|
||||
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
conversation_variables_list = [
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables
|
||||
]
|
||||
conversation_variables = [
|
||||
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
|
||||
@@ -698,7 +735,7 @@ class EnvironmentVariableCollectionApi(Resource):
|
||||
"name": v.name,
|
||||
"description": v.description,
|
||||
"selector": v.selector,
|
||||
"value_type": str(v.value_type.exposed_type()),
|
||||
"value_type": environment_variable_value_type(v),
|
||||
"value": v.value,
|
||||
# Do not track edited for env vars.
|
||||
"edited": False,
|
||||
@@ -725,23 +762,32 @@ class EnvironmentVariableCollectionApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(EnvironmentVariableUpdatePayload)
|
||||
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
|
||||
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
environment_variables_list = [
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables
|
||||
]
|
||||
environment_variables = [
|
||||
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
|
||||
]
|
||||
|
||||
workflow_service.update_draft_workflow_environment_variables(
|
||||
app_model=app_model,
|
||||
account=current_user,
|
||||
environment_variables=environment_variables,
|
||||
session=db.session(),
|
||||
)
|
||||
if req_data.patch:
|
||||
workflow_service.patch_draft_workflow_environment_variables(
|
||||
app_model=app_model,
|
||||
account=current_user,
|
||||
environment_variables=environment_variables,
|
||||
deleted_environment_variable_ids=req_data.deleted_environment_variable_ids,
|
||||
session=db.session(),
|
||||
)
|
||||
else:
|
||||
workflow_service.update_draft_workflow_environment_variables(
|
||||
app_model=app_model,
|
||||
account=current_user,
|
||||
environment_variables=environment_variables,
|
||||
session=db.session(),
|
||||
)
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunListQuery)
|
||||
def get(self, req_data: WorkflowRunListQuery, app_model: App):
|
||||
"""
|
||||
Get advanced chat app workflow run list
|
||||
"""
|
||||
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args: WorkflowRunListArgs = {"limit": args_model.limit}
|
||||
if args_model.last_id is not None:
|
||||
args["last_id"] = args_model.last_id
|
||||
if args_model.status is not None:
|
||||
args["status"] = args_model.status
|
||||
args: WorkflowRunListArgs = {"limit": req_data.limit}
|
||||
if req_data.last_id is not None:
|
||||
args["last_id"] = req_data.last_id
|
||||
if req_data.status is not None:
|
||||
args["status"] = req_data.status
|
||||
|
||||
# Default to DEBUGGING if not specified
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
@@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunCountQuery)
|
||||
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
|
||||
"""
|
||||
Get advanced chat workflow runs count statistics
|
||||
"""
|
||||
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args = args_model.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
# Default to DEBUGGING if not specified
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
@@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunListQuery)
|
||||
def get(self, req_data: WorkflowRunListQuery, app_model: App):
|
||||
"""
|
||||
Get workflow run list
|
||||
"""
|
||||
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args: WorkflowRunListArgs = {"limit": args_model.limit}
|
||||
if args_model.last_id is not None:
|
||||
args["last_id"] = args_model.last_id
|
||||
if args_model.status is not None:
|
||||
args["status"] = args_model.status
|
||||
args: WorkflowRunListArgs = {"limit": req_data.limit}
|
||||
if req_data.last_id is not None:
|
||||
args["last_id"] = req_data.last_id
|
||||
if req_data.status is not None:
|
||||
args["status"] = req_data.status
|
||||
|
||||
# Default to DEBUGGING for workflow if not specified (backward compatibility)
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
@@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunCountQuery)
|
||||
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
|
||||
"""
|
||||
Get workflow runs count statistics
|
||||
"""
|
||||
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args = args_model.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
# Default to DEBUGGING for workflow if not specified (backward compatibility)
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user