Compare commits
620 Commits
main
...
feat/datas
| Author | SHA1 | Date |
|---|---|---|
|
|
6aba39a2dd | 1 year ago |
|
|
49bb15fae1 | 1 year ago |
|
|
e165f4a102 | 1 year ago |
|
|
83cc484c24 | 1 year ago |
|
|
1ff9c07a92 | 1 year ago |
|
|
b25b284d7f | 1 year ago |
|
|
2414dbb5f8 | 1 year ago |
|
|
916a8c76e7 | 1 year ago |
|
|
9783832223 | 1 year ago |
|
|
b77081a19e | 1 year ago |
|
|
896906ae77 | 1 year ago |
|
|
2365a3a5fc | 1 year ago |
|
|
dd792210f6 | 1 year ago |
|
|
6ba4a4c165 | 1 year ago |
|
|
0a6dbf6ee2 | 1 year ago |
|
|
ca0979dd43 | 1 year ago |
|
|
0762e5ae50 | 1 year ago |
|
|
48f53f3b9b | 1 year ago |
|
|
af64f29e87 | 1 year ago |
|
|
b9f59e3a75 | 1 year ago |
|
|
b12a8eeb90 | 1 year ago |
|
|
e551cf65c9 | 1 year ago |
|
|
3899211c41 | 1 year ago |
|
|
335e1e3602 | 1 year ago |
|
|
725fc72c6f | 1 year ago |
|
|
b618f3bd9e | 1 year ago |
|
|
95ba55af4d | 1 year ago |
|
|
f4e1ea9011 | 1 year ago |
|
|
3d0e288e85 | 1 year ago |
|
|
9620d6bcd8 | 1 year ago |
|
|
f7fbded8b9 | 1 year ago |
|
|
0c5706b3f6 | 1 year ago |
|
|
82d0a70cb4 | 1 year ago |
|
|
55516c4e57 | 1 year ago |
|
|
cc2cd85ff5 | 1 year ago |
|
|
6ec742539a | 1 year ago |
|
|
09e0a54070 | 1 year ago |
|
|
5d25199f42 | 1 year ago |
|
|
387826674c | 1 year ago |
|
|
02ae479636 | 1 year ago |
|
|
a103324f25 | 1 year ago |
|
|
643efc5d85 | 1 year ago |
|
|
43e5798e13 | 1 year ago |
|
|
8aca70cd50 | 1 year ago |
|
|
2cf980026e | 1 year ago |
|
|
224111081b | 1 year ago |
|
|
4dc6cad588 | 1 year ago |
|
|
f85e6a0dea | 1 year ago |
|
|
4b3a54633f | 1 year ago |
|
|
6f67a34349 | 1 year ago |
|
|
e51d308312 | 1 year ago |
|
|
379c92bd82 | 1 year ago |
|
|
fa9f0ebfb1 | 1 year ago |
|
|
ac917bb56d | 1 year ago |
|
|
f7a4e5d1a6 | 1 year ago |
|
|
515d34bbfb | 1 year ago |
|
|
66de2e1f0a | 1 year ago |
|
|
7f7ea92a45 | 1 year ago |
|
|
a014345688 | 1 year ago |
|
|
cf66d111ba | 1 year ago |
|
|
2d01b1a808 | 1 year ago |
|
|
739ebf2117 | 1 year ago |
|
|
176b844cd5 | 1 year ago |
|
|
8fc6684ab1 | 1 year ago |
|
|
7c41f71248 | 1 year ago |
|
|
2c2bfb4f54 | 1 year ago |
|
|
3164f90327 | 1 year ago |
|
|
90ac52482c | 1 year ago |
|
|
879ac940dd | 1 year ago |
|
|
796797d12b | 1 year ago |
|
|
7ac0f0c08c | 1 year ago |
|
|
5cc6a2bf33 | 1 year ago |
|
|
2db0b19044 | 1 year ago |
|
|
1d2ee9020c | 1 year ago |
|
|
f2538bf381 | 1 year ago |
|
|
f37e28a368 | 1 year ago |
|
|
c5976f5a09 | 1 year ago |
|
|
64a9181ee4 | 1 year ago |
|
|
33cd32382f | 1 year ago |
|
|
9456c59290 | 1 year ago |
|
|
ce0bd421ae | 1 year ago |
|
|
f9d04c6975 | 1 year ago |
|
|
ecb07a5d0d | 1 year ago |
|
|
a165ba2059 | 1 year ago |
|
|
12fd2903d8 | 1 year ago |
|
|
0a2c569b3b | 1 year ago |
|
|
9ab0d5fe60 | 1 year ago |
|
|
1d71fd5b56 | 1 year ago |
|
|
b277acc298 | 1 year ago |
|
|
8d47d8ce4f | 1 year ago |
|
|
41fef8a21f | 1 year ago |
|
|
b853a42e37 | 1 year ago |
|
|
1633626d23 | 1 year ago |
|
|
6c7a40c571 | 1 year ago |
|
|
abb2ed66e7 | 1 year ago |
|
|
5ae78f79b0 | 1 year ago |
|
|
e3b3a6d040 | 1 year ago |
|
|
6622ce6ad8 | 1 year ago |
|
|
5ccb8d9736 | 1 year ago |
|
|
55906c8375 | 1 year ago |
|
|
0908f310fc | 1 year ago |
|
|
58842898e1 | 1 year ago |
|
|
1c17c8fa36 | 1 year ago |
|
|
26aff400e4 | 1 year ago |
|
|
4b11d29ede | 1 year ago |
|
|
b2b95412b9 | 1 year ago |
|
|
5c228bca4f | 1 year ago |
|
|
7bd2509ad5 | 1 year ago |
|
|
2a5d70d9e1 | 1 year ago |
|
|
b0107f4128 | 1 year ago |
|
|
dc3c5362e4 | 1 year ago |
|
|
1d106c3660 | 1 year ago |
|
|
fcb2fa04e7 | 1 year ago |
|
|
55bff10f0d | 1 year ago |
|
|
45c9b77e82 | 1 year ago |
|
|
767860e76b | 1 year ago |
|
|
80f656f79a | 1 year ago |
|
|
c891eb28fc | 1 year ago |
|
|
b9fa3f54e9 | 1 year ago |
|
|
4d2f904d72 | 1 year ago |
|
|
26b7911177 | 1 year ago |
|
|
dd91edf70b | 1 year ago |
|
|
d994e6b6c7 | 1 year ago |
|
|
aba48bde0b | 1 year ago |
|
|
3e5d9884cb | 1 year ago |
|
|
faadad62ff | 1 year ago |
|
|
406d70e4a3 | 1 year ago |
|
|
f17f256b2b | 1 year ago |
|
|
b367f48de6 | 1 year ago |
|
|
dee7b6eb22 | 1 year ago |
|
|
6f17200dec | 1 year ago |
|
|
d3dbfbe8b3 | 1 year ago |
|
|
b1f250862f | 1 year ago |
|
|
141d6b1abf | 1 year ago |
|
|
a7eb534761 | 1 year ago |
|
|
808f792f55 | 1 year ago |
|
|
346d066128 | 1 year ago |
|
|
5c41922b8a | 1 year ago |
|
|
9c3e3b00d0 | 1 year ago |
|
|
da3a3ce165 | 1 year ago |
|
|
b525bc2b81 | 1 year ago |
|
|
14dc3e8642 | 1 year ago |
|
|
e52c905aa5 | 1 year ago |
|
|
7b9a3c1084 | 1 year ago |
|
|
ce8ddae11e | 1 year ago |
|
|
4e8184bc56 | 1 year ago |
|
|
9eb8597957 | 1 year ago |
|
|
cde584046d | 1 year ago |
|
|
b7f9d7e94a | 1 year ago |
|
|
88817bf974 | 1 year ago |
|
|
92e6c52c0e | 1 year ago |
|
|
309dfe8829 | 1 year ago |
|
|
1d8b390584 | 1 year ago |
|
|
7dea7f77ac | 1 year ago |
|
|
4d9b15e519 | 1 year ago |
|
|
45a708f17e | 1 year ago |
|
|
5f08a9314c | 1 year ago |
|
|
5802b2b437 | 1 year ago |
|
|
f995436eec | 1 year ago |
|
|
25f0c61e65 | 1 year ago |
|
|
66fa68fa18 | 1 year ago |
|
|
3e5781c6f1 | 1 year ago |
|
|
a6f7560d2f | 1 year ago |
|
|
45c76c1d68 | 1 year ago |
|
|
14d5af468c | 1 year ago |
|
|
874e1bc41d | 1 year ago |
|
|
d2ae695b3b | 1 year ago |
|
|
6ecdac6344 | 1 year ago |
|
|
3c2ce07f38 | 1 year ago |
|
|
5c58b11b22 | 1 year ago |
|
|
be92122f17 | 1 year ago |
|
|
2972a06f16 | 1 year ago |
|
|
caa275fdbd | 1 year ago |
|
|
5dbda7f4c5 | 1 year ago |
|
|
0564651f6f | 1 year ago |
|
|
eff8108f1c | 1 year ago |
|
|
127a77d807 | 1 year ago |
|
|
265842223c | 1 year ago |
|
|
95a24156de | 1 year ago |
|
|
80ca5b3356 | 1 year ago |
|
|
e934503fa0 | 1 year ago |
|
|
442bcd18c0 | 1 year ago |
|
|
aeb1d1946c | 1 year ago |
|
|
12f2913e08 | 1 year ago |
|
|
0aeeee49f7 | 1 year ago |
|
|
eb7479b1ea | 1 year ago |
|
|
80b219707e | 1 year ago |
|
|
65ac022245 | 1 year ago |
|
|
6e6090d5a9 | 1 year ago |
|
|
58b5daeef3 | 1 year ago |
|
|
33fd1fa79d | 1 year ago |
|
|
978118f770 | 1 year ago |
|
|
a2610b22cc | 1 year ago |
|
|
f4789d750d | 1 year ago |
|
|
176f9ea2f4 | 1 year ago |
|
|
5e71f7c825 | 1 year ago |
|
|
7624edd32d | 1 year ago |
|
|
7b79354849 | 1 year ago |
|
|
a7ff2ab470 | 1 year ago |
|
|
d3eedaf0ec | 1 year ago |
|
|
bcb0496bf4 | 1 year ago |
|
|
4d967544f3 | 1 year ago |
|
|
c18ee4be50 | 1 year ago |
|
|
65873aa411 | 1 year ago |
|
|
b95256d624 | 1 year ago |
|
|
c0d3452494 | 1 year ago |
|
|
c91456de1b | 1 year ago |
|
|
e1ce156433 | 1 year ago |
|
|
9e19ed4e67 | 1 year ago |
|
|
ba383b1b0d | 1 year ago |
|
|
ad3d9cf782 | 1 year ago |
|
|
69053332e4 | 1 year ago |
|
|
5b4d04b348 | 1 year ago |
|
|
47664f8fd3 | 1 year ago |
|
|
8d8f21addd | 1 year ago |
|
|
9b9640b3db | 1 year ago |
|
|
83ba61203b | 1 year ago |
|
|
fcbd5febeb | 1 year ago |
|
|
b8813e199f | 1 year ago |
|
|
2322496552 | 1 year ago |
|
|
21a3509bef | 1 year ago |
|
|
3e2f12b065 | 1 year ago |
|
|
55e20d189a | 1 year ago |
|
|
1aa13bd20d | 1 year ago |
|
|
cc2dd052df | 1 year ago |
|
|
4ffdf68a20 | 1 year ago |
|
|
547bd3cc1b | 1 year ago |
|
|
f3e9761c75 | 1 year ago |
|
|
83ca59e0f1 | 1 year ago |
|
|
d725aa8791 | 1 year ago |
|
|
cc8ee0ff69 | 1 year ago |
|
|
4a249c40b1 | 1 year ago |
|
|
04e4a1e3aa | 1 year ago |
|
|
d2d5fc62ae | 1 year ago |
|
|
52460f6929 | 1 year ago |
|
|
06dfc32e0f | 1 year ago |
|
|
0ca38d8215 | 1 year ago |
|
|
3da6becad3 | 1 year ago |
|
|
f9d0a7bdc8 | 1 year ago |
|
|
e961722597 | 1 year ago |
|
|
2ddd2616ec | 1 year ago |
|
|
a82a9fb9d4 | 1 year ago |
|
|
3fce6f2581 | 1 year ago |
|
|
3db864561e | 1 year ago |
|
|
d2750f1a02 | 1 year ago |
|
|
30a50c5cc8 | 1 year ago |
|
|
0ff746ebf6 | 1 year ago |
|
|
5193fa2118 | 1 year ago |
|
|
9a0dc82e6a | 1 year ago |
|
|
8e4165defe | 1 year ago |
|
|
d917bc8ed0 | 1 year ago |
|
|
ef7bd262c5 | 1 year ago |
|
|
d3e29ffa74 | 1 year ago |
|
|
70432952fd | 1 year ago |
|
|
cf2ef93ad5 | 1 year ago |
|
|
cbf0864edc | 1 year ago |
|
|
bce2bdd0de | 1 year ago |
|
|
82e7c8a2f9 | 1 year ago |
|
|
2acdb0a4ea | 1 year ago |
|
|
350ea6be6e | 1 year ago |
|
|
4664174ef3 | 1 year ago |
|
|
f0413f359a | 1 year ago |
|
|
53b32c8b22 | 1 year ago |
|
|
b8ef1d9585 | 1 year ago |
|
|
90ca98ff3a | 1 year ago |
|
|
d4a1d045f8 | 1 year ago |
|
|
91fefa0e37 | 1 year ago |
|
|
067ec17539 | 1 year ago |
|
|
c084b57933 | 1 year ago |
|
|
876be7e6e9 | 1 year ago |
|
|
468bfdfed9 | 1 year ago |
|
|
82d817f612 | 1 year ago |
|
|
9e84a5321d | 1 year ago |
|
|
d77e27ac05 | 1 year ago |
|
|
8a86a2c817 | 1 year ago |
|
|
fdc4c36b77 | 1 year ago |
|
|
52c118f5b8 | 1 year ago |
|
|
5d7c7023c3 | 1 year ago |
|
|
3e0a10b7ed | 1 year ago |
|
|
84f5272f72 | 1 year ago |
|
|
6286f368f1 | 1 year ago |
|
|
cb2ca0b533 | 1 year ago |
|
|
5fe5da7c1d | 1 year ago |
|
|
c83370f701 | 1 year ago |
|
|
7506867fb9 | 1 year ago |
|
|
842136959b | 1 year ago |
|
|
4c2cc98ebc | 1 year ago |
|
|
44b9f49ab1 | 1 year ago |
|
|
f7f7952951 | 1 year ago |
|
|
a7fa5044e3 | 1 year ago |
|
|
eb84134706 | 1 year ago |
|
|
fbca9010f3 | 1 year ago |
|
|
0bf0c7dbe8 | 1 year ago |
|
|
e071bd63e6 | 1 year ago |
|
|
8a147a00e8 | 1 year ago |
|
|
c9a4c66b07 | 1 year ago |
|
|
edec654b68 | 1 year ago |
|
|
a82ab1d152 | 1 year ago |
|
|
9934eac15c | 1 year ago |
|
|
c155afac29 | 1 year ago |
|
|
7080c9f279 | 1 year ago |
|
|
e41699cbc8 | 1 year ago |
|
|
133193e7d0 | 1 year ago |
|
|
9d6371e0a3 | 1 year ago |
|
|
dfe091789c | 1 year ago |
|
|
4c9bf78363 | 1 year ago |
|
|
b95ecaf8a3 | 1 year ago |
|
|
7a0e8108ae | 1 year ago |
|
|
3afd5e73c9 | 1 year ago |
|
|
c09c8c6e5b | 1 year ago |
|
|
cab491795a | 1 year ago |
|
|
e290ddc3e5 | 1 year ago |
|
|
db154e33b7 | 1 year ago |
|
|
32f9004b5f | 1 year ago |
|
|
225402280e | 1 year ago |
|
|
abcca11479 | 1 year ago |
|
|
9cdd2cbb27 | 1 year ago |
|
|
309fffd1e4 | 1 year ago |
|
|
0a9f50e85f | 1 year ago |
|
|
ed1d71f4d0 | 1 year ago |
|
|
7039ec33b9 | 1 year ago |
|
|
025dc7c781 | 1 year ago |
|
|
4130c50643 | 1 year ago |
|
|
7b7f8ef51d | 1 year ago |
|
|
bad451d5ec | 1 year ago |
|
|
87c15062e6 | 1 year ago |
|
|
573cd15e77 | 1 year ago |
|
|
ab1730bbaa | 1 year ago |
|
|
163bae3aaf | 1 year ago |
|
|
270edd43ab | 1 year ago |
|
|
b8f3b23b1a | 1 year ago |
|
|
b9c6496fea | 1 year ago |
|
|
0486aa3445 | 1 year ago |
|
|
5fb771218c | 1 year ago |
|
|
3fb02a7933 | 1 year ago |
|
|
898495b5c4 | 1 year ago |
|
|
08624878cf | 1 year ago |
|
|
6fe473f0fa | 1 year ago |
|
|
11cf23e5fc | 1 year ago |
|
|
631768ea1d | 1 year ago |
|
|
e1d658b482 | 1 year ago |
|
|
1274aaed5d | 1 year ago |
|
|
9be036e0ca | 1 year ago |
|
|
7284569c5f | 1 year ago |
|
|
976b465e76 | 1 year ago |
|
|
804e55824d | 1 year ago |
|
|
69529fb16d | 1 year ago |
|
|
cb5cfb2dae | 1 year ago |
|
|
a826879cf7 | 1 year ago |
|
|
e7c48c0b69 | 1 year ago |
|
|
558a280fc8 | 1 year ago |
|
|
2158c03231 | 1 year ago |
|
|
a61f1f8eb0 | 1 year ago |
|
|
9f724c19db | 1 year ago |
|
|
4ae936b263 | 1 year ago |
|
|
80875a109a | 1 year ago |
|
|
121e54f3e3 | 1 year ago |
|
|
1c2c4b62f8 | 1 year ago |
|
|
9176790adf | 1 year ago |
|
|
6ff6525d1d | 1 year ago |
|
|
71ce505631 | 1 year ago |
|
|
11dfe3713f | 1 year ago |
|
|
a025db137d | 1 year ago |
|
|
797d044714 | 1 year ago |
|
|
c4169f8aa0 | 1 year ago |
|
|
3005419573 | 1 year ago |
|
|
7f59ffe7af | 1 year ago |
|
|
cc7ad5ac97 | 1 year ago |
|
|
769b5e185a | 1 year ago |
|
|
9e763c9e87 | 1 year ago |
|
|
b9214ca76b | 1 year ago |
|
|
29d2f2339b | 1 year ago |
|
|
5ac1e3584d | 1 year ago |
|
|
dd0cf6fadc | 1 year ago |
|
|
b320ebe2ba | 1 year ago |
|
|
377093b776 | 1 year ago |
|
|
70119a054a | 1 year ago |
|
|
69d1e3ec7d | 1 year ago |
|
|
365157c37d | 1 year ago |
|
|
4bc0a1bd37 | 1 year ago |
|
|
d6640f2adf | 1 year ago |
|
|
987f845e79 | 1 year ago |
|
|
84daf49047 | 1 year ago |
|
|
31e183ef0d | 1 year ago |
|
|
754a1d1197 | 1 year ago |
|
|
049a6de4b3 | 1 year ago |
|
|
6bd28cadc4 | 1 year ago |
|
|
3b9a0b1d25 | 1 year ago |
|
|
db963a638c | 1 year ago |
|
|
dcb4c9e84a | 1 year ago |
|
|
5fc2bc58a9 | 1 year ago |
|
|
d333645e09 | 1 year ago |
|
|
2812c774c6 | 1 year ago |
|
|
e2f3f0ae4c | 1 year ago |
|
|
83ca7f8deb | 1 year ago |
|
|
e6c6fa8ed8 | 1 year ago |
|
|
678d6ffe2b | 1 year ago |
|
|
cef77a3717 | 1 year ago |
|
|
28726b6cf3 | 1 year ago |
|
|
ef0e41de07 | 1 year ago |
|
|
dc2b63b832 | 1 year ago |
|
|
0478fc9649 | 1 year ago |
|
|
1b07e612d2 | 1 year ago |
|
|
38cce3f62a | 1 year ago |
|
|
35be8721b9 | 1 year ago |
|
|
665ffbdc10 | 1 year ago |
|
|
b5f88c77a3 | 1 year ago |
|
|
324c0d7b4c | 1 year ago |
|
|
13e3f17493 | 1 year ago |
|
|
841bd35ebb | 1 year ago |
|
|
ccefd41606 | 1 year ago |
|
|
ec1c4efca9 | 1 year ago |
|
|
0f10852b6b | 1 year ago |
|
|
6d547447d3 | 1 year ago |
|
|
6123f1ab21 | 1 year ago |
|
|
e7370766bd | 1 year ago |
|
|
db4958be05 | 1 year ago |
|
|
a15bf8e8fe | 1 year ago |
|
|
70d2c78176 | 1 year ago |
|
|
42fcda3dc8 | 1 year ago |
|
|
ac049d938e | 1 year ago |
|
|
3af61f4b5d | 1 year ago |
|
|
e19adbbbc5 | 1 year ago |
|
|
64d997fdb0 | 1 year ago |
|
|
a49942b949 | 1 year ago |
|
|
4460d96e58 | 1 year ago |
|
|
a7d5f2f53b | 1 year ago |
|
|
c9bf99a1e2 | 1 year ago |
|
|
4300ebc8aa | 1 year ago |
|
|
720ce79901 | 1 year ago |
|
|
693107a6c8 | 1 year ago |
|
|
583db24ee7 | 1 year ago |
|
|
7d92574e02 | 1 year ago |
|
|
5aaa06c8b0 | 1 year ago |
|
|
52b773770b | 1 year ago |
|
|
23adc7d8a8 | 1 year ago |
|
|
e3708bfa85 | 1 year ago |
|
|
7d65e9980c | 1 year ago |
|
|
b93d26ee9f | 1 year ago |
|
|
b82b26bba5 | 1 year ago |
|
|
21c24977d8 | 1 year ago |
|
|
fe435c23c3 | 1 year ago |
|
|
ead1209f98 | 1 year ago |
|
|
3994bb1771 | 1 year ago |
|
|
327690e4a7 | 1 year ago |
|
|
c2a7e0e986 | 1 year ago |
|
|
faf6b9ea03 | 1 year ago |
|
|
3bfc602561 | 1 year ago |
|
|
5fa2aca2c8 | 1 year ago |
|
|
69a60101fe | 1 year ago |
|
|
b18519b824 | 1 year ago |
|
|
0d01025254 | 1 year ago |
|
|
eef1542cb3 | 1 year ago |
|
|
9aef4b6d6b | 1 year ago |
|
|
7dba83754f | 1 year ago |
|
|
e2585bc778 | 1 year ago |
|
|
cc6e2558ef | 1 year ago |
|
|
20343facad | 1 year ago |
|
|
eff123a11c | 1 year ago |
|
|
9bafd3a226 | 1 year ago |
|
|
82be119fec | 1 year ago |
|
|
a64df507f6 | 1 year ago |
|
|
cf73faf174 | 1 year ago |
|
|
ba52bf27c1 | 1 year ago |
|
|
55f4177b01 | 1 year ago |
|
|
14a9052d60 | 1 year ago |
|
|
314a2f9be8 | 1 year ago |
|
|
8eee344fbb | 1 year ago |
|
|
0e0a266142 | 1 year ago |
|
|
7bce35913d | 1 year ago |
|
|
7898dbd5bf | 1 year ago |
|
|
bd1073ff1a | 1 year ago |
|
|
1bbd572593 | 1 year ago |
|
|
5199297f61 | 1 year ago |
|
|
c5a2f43ceb | 1 year ago |
|
|
8d4ced227e | 1 year ago |
|
|
f481075f8f | 1 year ago |
|
|
836cf6453e | 1 year ago |
|
|
8bea88c8cc | 1 year ago |
|
|
4b7274f9a5 | 1 year ago |
|
|
7de5585da6 | 1 year ago |
|
|
87dc80f6fa | 1 year ago |
|
|
a008c04331 | 1 year ago |
|
|
56b66b8a57 | 1 year ago |
|
|
35a7add4e9 | 1 year ago |
|
|
f1fe143962 | 1 year ago |
|
|
9e72afee3c | 1 year ago |
|
|
613b94a6e6 | 1 year ago |
|
|
7b0d38f7d3 | 1 year ago |
|
|
4ff971c8a3 | 1 year ago |
|
|
019ef74bf2 | 1 year ago |
|
|
2670557258 | 1 year ago |
|
|
93ac6d37e9 | 1 year ago |
|
|
e710a8402c | 1 year ago |
|
|
360f8a3375 | 1 year ago |
|
|
818eb46a8b | 1 year ago |
|
|
f5c297708b | 1 year ago |
|
|
bf8324f7f7 | 1 year ago |
|
|
b730d153ea | 1 year ago |
|
|
11977596c9 | 1 year ago |
|
|
612dca8b7d | 1 year ago |
|
|
53018289d4 | 1 year ago |
|
|
958ff44707 | 1 year ago |
|
|
d910770b3c | 1 year ago |
|
|
5a8f10520f | 1 year ago |
|
|
df928772c0 | 1 year ago |
|
|
b713218cab | 1 year ago |
|
|
9ea2123e7f | 1 year ago |
|
|
de0cb06f8c | 1 year ago |
|
|
cfb6d59513 | 1 year ago |
|
|
4c30d1c1eb | 1 year ago |
|
|
5bb02c79cc | 1 year ago |
|
|
0a891e5392 | 1 year ago |
|
|
f6978ce6b1 | 1 year ago |
|
|
4d68aadc1c | 1 year ago |
|
|
cef6463847 | 1 year ago |
|
|
39b8331f81 | 1 year ago |
|
|
212d4c5899 | 1 year ago |
|
|
97ec855df4 | 1 year ago |
|
|
d83b9b70e3 | 1 year ago |
|
|
b51c18c2cf | 1 year ago |
|
|
7e31da7882 | 1 year ago |
|
|
d9ed61287d | 1 year ago |
|
|
6024dbe98d | 1 year ago |
|
|
13ce6317f1 | 1 year ago |
|
|
0099f2296d | 1 year ago |
|
|
2d93bc6725 | 1 year ago |
|
|
cb52f9ecc5 | 1 year ago |
|
|
1fbeb3a21a | 1 year ago |
|
|
38f1a42ce8 | 1 year ago |
|
|
3d11af2dd6 | 1 year ago |
|
|
d1fd5db7f8 | 1 year ago |
|
|
c240cf3bb1 | 1 year ago |
|
|
bbbcd68258 | 1 year ago |
|
|
7ce9710229 | 1 year ago |
|
|
3f7f21ce70 | 1 year ago |
|
|
fa8ab4ea04 | 1 year ago |
|
|
3f1363503b | 1 year ago |
|
|
3f52f491d7 | 1 year ago |
|
|
e86a3fc672 | 1 year ago |
|
|
6f77f67427 | 1 year ago |
|
|
4025cd0b46 | 1 year ago |
|
|
3bbb22750c | 1 year ago |
|
|
d196872059 | 1 year ago |
|
|
a478d95950 | 1 year ago |
|
|
12c060b795 | 1 year ago |
|
|
c480c3d881 | 1 year ago |
|
|
a998022c12 | 1 year ago |
|
|
a25cc4e8af | 1 year ago |
|
|
b4bccf5fef | 1 year ago |
|
|
14ad34af71 | 1 year ago |
|
|
7ed398267f | 1 year ago |
|
|
fc9556e057 | 1 year ago |
|
|
acf6872a50 | 1 year ago |
|
|
e689f21a60 | 1 year ago |
|
|
a7f9259e27 | 1 year ago |
|
|
a46b4e3616 | 1 year ago |
|
|
e7e12c1d2e | 1 year ago |
|
|
66176c4d71 | 1 year ago |
|
|
2613a380b6 | 1 year ago |
|
|
9392ce259f | 1 year ago |
|
|
d1287f08b4 | 1 year ago |
|
|
7ee8472a5f | 1 year ago |
|
|
cdb615deeb | 1 year ago |
|
|
abbba1d004 | 1 year ago |
|
|
3c386c63a6 | 1 year ago |
|
|
49d1846e63 | 1 year ago |
|
|
53f2882077 | 1 year ago |
|
|
8f07e088f5 | 1 year ago |
|
|
f71b0eccb2 | 1 year ago |
|
|
5b89d36ea1 | 1 year ago |
|
|
7c3af74b0d | 1 year ago |
|
|
d1d83f8a2a | 1 year ago |
|
|
839fe12087 | 1 year ago |
|
|
fd8ee9f53e | 1 year ago |
|
|
c2d02f8f4d | 1 year ago |
|
|
8367ae85de | 1 year ago |
|
|
d1f0e6e5c2 | 1 year ago |
|
|
7deb44f864 | 1 year ago |
|
|
d12e9b81e3 | 1 year ago |
|
|
b1fbaaed95 | 1 year ago |
|
|
3f8b0b937c | 1 year ago |
|
|
734c62998f | 1 year ago |
|
|
4792ca1813 | 1 year ago |
|
|
d4007ae073 | 1 year ago |
|
|
389f15f8e3 | 1 year ago |
|
|
9437145218 | 1 year ago |
|
|
076924bbd6 | 1 year ago |
|
|
97cf6b2d65 | 1 year ago |
|
|
f317ef2fe2 | 1 year ago |
|
|
f7de55364f | 1 year ago |
|
|
de30e9278c | 1 year ago |
|
|
b9ab1555fb | 1 year ago |
|
|
44b9ce0951 | 1 year ago |
|
|
d768094376 | 1 year ago |
|
|
93f83086c1 | 1 year ago |
|
|
8d9c252811 | 1 year ago |
|
|
c7f4b41920 | 1 year ago |
|
|
efb27eb443 | 1 year ago |
|
|
5b8c43052e | 1 year ago |
|
|
e04ae927b6 | 1 year ago |
|
|
ac68d62d1c | 1 year ago |
|
|
caa17b8fe9 | 1 year ago |
|
|
cd1562ee24 | 1 year ago |
|
|
47af1a9c42 | 1 year ago |
|
|
0cd6a427af | 1 year ago |
|
|
51165408ed | 1 year ago |
|
|
a2dc38f90a | 1 year ago |
|
|
a36436b585 | 1 year ago |
|
|
2d87823fc6 | 1 year ago |
|
|
d238da9826 | 1 year ago |
|
|
6eef5990c9 | 1 year ago |
|
|
5c4bf2a9e4 | 1 year ago |
|
|
0345eb4659 | 1 year ago |
|
|
71f78e0d33 | 1 year ago |
|
|
942648e9e9 | 1 year ago |
|
|
d841581679 | 1 year ago |
|
|
9f8e05d9f0 | 1 year ago |
|
|
3340775052 | 1 year ago |
|
|
9987774471 | 1 year ago |
@ -1,11 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "\U0001F4A1 Model Providers & Plugins"
|
||||
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"
|
||||
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
|
||||
about: General discussions and seek help from the community
|
||||
about: General discussions and request help from the community
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
name: autofix.ci
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Use uv to ensure we have the same ruff version in CI and locally.
|
||||
- uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f
|
||||
- run: |
|
||||
cd api
|
||||
uv sync --dev
|
||||
# Fix lint errors
|
||||
uv run ruff check --fix-only .
|
||||
# Format code
|
||||
uv run ruff format .
|
||||
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class PyProjectConfig(BaseModel):
|
||||
version: str = Field(description="Dify version", default="")
|
||||
|
||||
|
||||
class PyProjectTomlConfig(BaseSettings):
|
||||
"""
|
||||
configs in api/pyproject.toml
|
||||
"""
|
||||
|
||||
project: PyProjectConfig = Field(
|
||||
description="configs in the project section of pyproject.toml",
|
||||
default=PyProjectConfig(),
|
||||
)
|
||||
@ -1,119 +0,0 @@
|
||||
import json
|
||||
from enum import StrEnum
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from fields.app_fields import app_server_fields
|
||||
from libs.login import login_required
|
||||
from models.model import AppMCPServer
|
||||
|
||||
|
||||
class AppMCPServerStatus(StrEnum):
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
|
||||
|
||||
class AppMCPServerController(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
@marshal_with(app_server_fields)
|
||||
def get(self, app_model):
|
||||
server = db.session.query(AppMCPServer).where(AppMCPServer.app_id == app_model.id).first()
|
||||
return server
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
@marshal_with(app_server_fields)
|
||||
def post(self, app_model):
|
||||
if not current_user.is_editor:
|
||||
raise NotFound()
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("description", type=str, required=False, location="json")
|
||||
parser.add_argument("parameters", type=dict, required=True, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
description = args.get("description")
|
||||
if not description:
|
||||
description = app_model.description or ""
|
||||
|
||||
server = AppMCPServer(
|
||||
name=app_model.name,
|
||||
description=description,
|
||||
parameters=json.dumps(args["parameters"], ensure_ascii=False),
|
||||
status=AppMCPServerStatus.ACTIVE,
|
||||
app_id=app_model.id,
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
server_code=AppMCPServer.generate_server_code(16),
|
||||
)
|
||||
db.session.add(server)
|
||||
db.session.commit()
|
||||
return server
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
@marshal_with(app_server_fields)
|
||||
def put(self, app_model):
|
||||
if not current_user.is_editor:
|
||||
raise NotFound()
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("id", type=str, required=True, location="json")
|
||||
parser.add_argument("description", type=str, required=False, location="json")
|
||||
parser.add_argument("parameters", type=dict, required=True, location="json")
|
||||
parser.add_argument("status", type=str, required=False, location="json")
|
||||
args = parser.parse_args()
|
||||
server = db.session.query(AppMCPServer).where(AppMCPServer.id == args["id"]).first()
|
||||
if not server:
|
||||
raise NotFound()
|
||||
|
||||
description = args.get("description")
|
||||
if description is None:
|
||||
pass
|
||||
elif not description:
|
||||
server.description = app_model.description or ""
|
||||
else:
|
||||
server.description = description
|
||||
|
||||
server.parameters = json.dumps(args["parameters"], ensure_ascii=False)
|
||||
if args["status"]:
|
||||
if args["status"] not in [status.value for status in AppMCPServerStatus]:
|
||||
raise ValueError("Invalid status")
|
||||
server.status = args["status"]
|
||||
db.session.commit()
|
||||
return server
|
||||
|
||||
|
||||
class AppMCPServerRefreshController(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(app_server_fields)
|
||||
def get(self, server_id):
|
||||
if not current_user.is_editor:
|
||||
raise NotFound()
|
||||
server = (
|
||||
db.session.query(AppMCPServer)
|
||||
.where(AppMCPServer.id == server_id)
|
||||
.where(AppMCPServer.tenant_id == current_user.current_tenant_id)
|
||||
.first()
|
||||
)
|
||||
if not server:
|
||||
raise NotFound()
|
||||
server.server_code = AppMCPServer.generate_server_code(16)
|
||||
db.session.commit()
|
||||
return server
|
||||
|
||||
|
||||
api.add_resource(AppMCPServerController, "/apps/<uuid:app_id>/server")
|
||||
api.add_resource(AppMCPServerRefreshController, "/apps/<uuid:server_id>/server/refresh")
|
||||
@ -1,426 +0,0 @@
|
||||
import logging
|
||||
from typing import Any, NoReturn
|
||||
|
||||
from flask import Response
|
||||
from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.error import (
|
||||
DraftWorkflowNotExist,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from core.variables.segment_group import SegmentGroup
|
||||
from core.variables.segments import ArrayFileSegment, FileSegment, Segment
|
||||
from core.variables.types import SegmentType
|
||||
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||
from factories.file_factory import build_from_mapping, build_from_mappings
|
||||
from factories.variable_factory import build_segment_with_type
|
||||
from libs.login import current_user, login_required
|
||||
from models import App, AppMode, db
|
||||
from models.workflow import WorkflowDraftVariable
|
||||
from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _convert_values_to_json_serializable_object(value: Segment) -> Any:
|
||||
if isinstance(value, FileSegment):
|
||||
return value.value.model_dump()
|
||||
elif isinstance(value, ArrayFileSegment):
|
||||
return [i.model_dump() for i in value.value]
|
||||
elif isinstance(value, SegmentGroup):
|
||||
return [_convert_values_to_json_serializable_object(i) for i in value.value]
|
||||
else:
|
||||
return value.value
|
||||
|
||||
|
||||
def _serialize_var_value(variable: WorkflowDraftVariable) -> Any:
|
||||
value = variable.get_value()
|
||||
# create a copy of the value to avoid affecting the model cache.
|
||||
value = value.model_copy(deep=True)
|
||||
# Refresh the url signature before returning it to client.
|
||||
if isinstance(value, FileSegment):
|
||||
file = value.value
|
||||
file.remote_url = file.generate_url()
|
||||
elif isinstance(value, ArrayFileSegment):
|
||||
files = value.value
|
||||
for file in files:
|
||||
file.remote_url = file.generate_url()
|
||||
return _convert_values_to_json_serializable_object(value)
|
||||
|
||||
|
||||
def _create_pagination_parser():
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
"page",
|
||||
type=inputs.int_range(1, 100_000),
|
||||
required=False,
|
||||
default=1,
|
||||
location="args",
|
||||
help="the page of data requested",
|
||||
)
|
||||
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
|
||||
return parser
|
||||
|
||||
|
||||
def _serialize_variable_type(workflow_draft_var: WorkflowDraftVariable) -> str:
|
||||
value_type = workflow_draft_var.value_type
|
||||
return value_type.exposed_type().value
|
||||
|
||||
|
||||
_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS = {
|
||||
"id": fields.String,
|
||||
"type": fields.String(attribute=lambda model: model.get_variable_type()),
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"selector": fields.List(fields.String, attribute=lambda model: model.get_selector()),
|
||||
"value_type": fields.String(attribute=_serialize_variable_type),
|
||||
"edited": fields.Boolean(attribute=lambda model: model.edited),
|
||||
"visible": fields.Boolean,
|
||||
}
|
||||
|
||||
_WORKFLOW_DRAFT_VARIABLE_FIELDS = dict(
|
||||
_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS,
|
||||
value=fields.Raw(attribute=_serialize_var_value),
|
||||
)
|
||||
|
||||
_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS = {
|
||||
"id": fields.String,
|
||||
"type": fields.String(attribute=lambda _: "env"),
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"selector": fields.List(fields.String, attribute=lambda model: model.get_selector()),
|
||||
"value_type": fields.String(attribute=_serialize_variable_type),
|
||||
"edited": fields.Boolean(attribute=lambda model: model.edited),
|
||||
"visible": fields.Boolean,
|
||||
}
|
||||
|
||||
_WORKFLOW_DRAFT_ENV_VARIABLE_LIST_FIELDS = {
|
||||
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS)),
|
||||
}
|
||||
|
||||
|
||||
def _get_items(var_list: WorkflowDraftVariableList) -> list[WorkflowDraftVariable]:
|
||||
return var_list.variables
|
||||
|
||||
|
||||
_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS = {
|
||||
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS), attribute=_get_items),
|
||||
"total": fields.Raw(),
|
||||
}
|
||||
|
||||
_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS = {
|
||||
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_FIELDS), attribute=_get_items),
|
||||
}
|
||||
|
||||
|
||||
def _api_prerequisite(f):
|
||||
"""Common prerequisites for all draft workflow variable APIs.
|
||||
|
||||
It ensures the following conditions are satisfied:
|
||||
|
||||
- Dify has been property setup.
|
||||
- The request user has logged in and initialized.
|
||||
- The requested app is a workflow or a chat flow.
|
||||
- The request user has the edit permission for the app.
|
||||
"""
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def wrapper(*args, **kwargs):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class WorkflowVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
"""
|
||||
parser = _create_pagination_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# fetch draft workflow by app_model
|
||||
workflow_service = WorkflowService()
|
||||
workflow_exist = workflow_service.is_workflow_exist(app_model=app_model)
|
||||
if not workflow_exist:
|
||||
raise DraftWorkflowNotExist()
|
||||
|
||||
# fetch draft workflow by app_model
|
||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=session,
|
||||
)
|
||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
||||
app_id=app_model.id,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
)
|
||||
|
||||
return workflow_vars
|
||||
|
||||
@_api_prerequisite
|
||||
def delete(self, app_model: App):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
draft_var_srv.delete_workflow_variables(app_model.id)
|
||||
db.session.commit()
|
||||
return Response("", 204)
|
||||
|
||||
|
||||
def validate_node_id(node_id: str) -> NoReturn | None:
|
||||
if node_id in [
|
||||
CONVERSATION_VARIABLE_NODE_ID,
|
||||
SYSTEM_VARIABLE_NODE_ID,
|
||||
]:
|
||||
# NOTE(QuantumGhost): While we store the system and conversation variables as node variables
|
||||
# with specific `node_id` in database, we still want to make the API separated. By disallowing
|
||||
# accessing system and conversation variables in `WorkflowDraftNodeVariableListApi`,
|
||||
# we mitigate the risk that user of the API depending on the implementation detail of the API.
|
||||
#
|
||||
# ref: [Hyrum's Law](https://www.hyrumslaw.com/)
|
||||
|
||||
raise InvalidArgumentError(
|
||||
f"invalid node_id, please use correspond api for conversation and system variables, node_id={node_id}",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class NodeVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
def get(self, app_model: App, node_id: str):
|
||||
validate_node_id(node_id)
|
||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=session,
|
||||
)
|
||||
node_vars = draft_var_srv.list_node_variables(app_model.id, node_id)
|
||||
|
||||
return node_vars
|
||||
|
||||
@_api_prerequisite
|
||||
def delete(self, app_model: App, node_id: str):
|
||||
validate_node_id(node_id)
|
||||
srv = WorkflowDraftVariableService(db.session())
|
||||
srv.delete_node_variables(app_model.id, node_id)
|
||||
db.session.commit()
|
||||
return Response("", 204)
|
||||
|
||||
|
||||
class VariableApi(Resource):
|
||||
_PATCH_NAME_FIELD = "name"
|
||||
_PATCH_VALUE_FIELD = "value"
|
||||
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
def get(self, app_model: App, variable_id: str):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
||||
if variable is None:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
if variable.app_id != app_model.id:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
return variable
|
||||
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
def patch(self, app_model: App, variable_id: str):
|
||||
# Request payload for file types:
|
||||
#
|
||||
# Local File:
|
||||
#
|
||||
# {
|
||||
# "type": "image",
|
||||
# "transfer_method": "local_file",
|
||||
# "url": "",
|
||||
# "upload_file_id": "daded54f-72c7-4f8e-9d18-9b0abdd9f190"
|
||||
# }
|
||||
#
|
||||
# Remote File:
|
||||
#
|
||||
#
|
||||
# {
|
||||
# "type": "image",
|
||||
# "transfer_method": "remote_url",
|
||||
# "url": "http://127.0.0.1:5001/files/1602650a-4fe4-423c-85a2-af76c083e3c4/file-preview?timestamp=1750041099&nonce=...&sign=...=",
|
||||
# "upload_file_id": "1602650a-4fe4-423c-85a2-af76c083e3c4"
|
||||
# }
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(self._PATCH_NAME_FIELD, type=str, required=False, nullable=True, location="json")
|
||||
# Parse 'value' field as-is to maintain its original data structure
|
||||
parser.add_argument(self._PATCH_VALUE_FIELD, type=lambda x: x, required=False, nullable=True, location="json")
|
||||
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
args = parser.parse_args(strict=True)
|
||||
|
||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
||||
if variable is None:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
if variable.app_id != app_model.id:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
|
||||
new_name = args.get(self._PATCH_NAME_FIELD, None)
|
||||
raw_value = args.get(self._PATCH_VALUE_FIELD, None)
|
||||
if new_name is None and raw_value is None:
|
||||
return variable
|
||||
|
||||
new_value = None
|
||||
if raw_value is not None:
|
||||
if variable.value_type == SegmentType.FILE:
|
||||
if not isinstance(raw_value, dict):
|
||||
raise InvalidArgumentError(description=f"expected dict for file, got {type(raw_value)}")
|
||||
raw_value = build_from_mapping(mapping=raw_value, tenant_id=app_model.tenant_id)
|
||||
elif variable.value_type == SegmentType.ARRAY_FILE:
|
||||
if not isinstance(raw_value, list):
|
||||
raise InvalidArgumentError(description=f"expected list for files, got {type(raw_value)}")
|
||||
if len(raw_value) > 0 and not isinstance(raw_value[0], dict):
|
||||
raise InvalidArgumentError(description=f"expected dict for files[0], got {type(raw_value)}")
|
||||
raw_value = build_from_mappings(mappings=raw_value, tenant_id=app_model.tenant_id)
|
||||
new_value = build_segment_with_type(variable.value_type, raw_value)
|
||||
draft_var_srv.update_variable(variable, name=new_name, value=new_value)
|
||||
db.session.commit()
|
||||
return variable
|
||||
|
||||
@_api_prerequisite
|
||||
def delete(self, app_model: App, variable_id: str):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
||||
if variable is None:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
if variable.app_id != app_model.id:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
draft_var_srv.delete_variable(variable)
|
||||
db.session.commit()
|
||||
return Response("", 204)
|
||||
|
||||
|
||||
class VariableResetApi(Resource):
|
||||
@_api_prerequisite
|
||||
def put(self, app_model: App, variable_id: str):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
|
||||
workflow_srv = WorkflowService()
|
||||
draft_workflow = workflow_srv.get_draft_workflow(app_model)
|
||||
if draft_workflow is None:
|
||||
raise NotFoundError(
|
||||
f"Draft workflow not found, app_id={app_model.id}",
|
||||
)
|
||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
||||
if variable is None:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
if variable.app_id != app_model.id:
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
|
||||
resetted = draft_var_srv.reset_variable(draft_workflow, variable)
|
||||
db.session.commit()
|
||||
if resetted is None:
|
||||
return Response("", 204)
|
||||
else:
|
||||
return marshal(resetted, _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
|
||||
|
||||
def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList:
|
||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=session,
|
||||
)
|
||||
if node_id == CONVERSATION_VARIABLE_NODE_ID:
|
||||
draft_vars = draft_var_srv.list_conversation_variables(app_model.id)
|
||||
elif node_id == SYSTEM_VARIABLE_NODE_ID:
|
||||
draft_vars = draft_var_srv.list_system_variables(app_model.id)
|
||||
else:
|
||||
draft_vars = draft_var_srv.list_node_variables(app_id=app_model.id, node_id=node_id)
|
||||
return draft_vars
|
||||
|
||||
|
||||
class ConversationVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
def get(self, app_model: App):
|
||||
# NOTE(QuantumGhost): Prefill conversation variables into the draft variables table
|
||||
# so their IDs can be returned to the caller.
|
||||
workflow_srv = WorkflowService()
|
||||
draft_workflow = workflow_srv.get_draft_workflow(app_model)
|
||||
if draft_workflow is None:
|
||||
raise NotFoundError(description=f"draft workflow not found, id={app_model.id}")
|
||||
draft_var_srv = WorkflowDraftVariableService(db.session())
|
||||
draft_var_srv.prefill_conversation_variable_default_values(draft_workflow)
|
||||
db.session.commit()
|
||||
return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID)
|
||||
|
||||
|
||||
class SystemVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
def get(self, app_model: App):
|
||||
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID)
|
||||
|
||||
|
||||
class EnvironmentVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
"""
|
||||
# fetch draft workflow by app_model
|
||||
workflow_service = WorkflowService()
|
||||
workflow = workflow_service.get_draft_workflow(app_model=app_model)
|
||||
if workflow is None:
|
||||
raise DraftWorkflowNotExist()
|
||||
|
||||
env_vars = workflow.environment_variables
|
||||
env_vars_list = []
|
||||
for v in env_vars:
|
||||
env_vars_list.append(
|
||||
{
|
||||
"id": v.id,
|
||||
"type": "env",
|
||||
"name": v.name,
|
||||
"description": v.description,
|
||||
"selector": v.selector,
|
||||
"value_type": v.value_type.exposed_type().value,
|
||||
"value": v.value,
|
||||
# Do not track edited for env vars.
|
||||
"edited": False,
|
||||
"visible": True,
|
||||
"editable": True,
|
||||
}
|
||||
)
|
||||
|
||||
return {"items": env_vars_list}
|
||||
|
||||
|
||||
api.add_resource(
|
||||
WorkflowVariableCollectionApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/variables",
|
||||
)
|
||||
api.add_resource(NodeVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/variables")
|
||||
api.add_resource(VariableApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>")
|
||||
api.add_resource(VariableResetApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>/reset")
|
||||
|
||||
api.add_resource(ConversationVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/conversation-variables")
|
||||
api.add_resource(SystemVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/system-variables")
|
||||
api.add_resource(EnvironmentVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/environment-variables")
|
||||
@ -0,0 +1,195 @@
|
||||
from flask import redirect, request
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import ( # type: ignore
|
||||
Resource, # type: ignore
|
||||
reqparse,
|
||||
)
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
setup_required,
|
||||
)
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from core.plugin.impl.oauth import OAuthHandler
|
||||
from extensions.ext_database import db
|
||||
from libs.login import login_required
|
||||
from models.oauth import DatasourceOauthParamConfig, DatasourceProvider
|
||||
from services.datasource_provider_service import DatasourceProviderService
|
||||
|
||||
|
||||
class DatasourcePluginOauthApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("provider", type=str, required=True, nullable=False, location="args")
|
||||
parser.add_argument("plugin_id", type=str, required=True, nullable=False, location="args")
|
||||
args = parser.parse_args()
|
||||
provider = args["provider"]
|
||||
plugin_id = args["plugin_id"]
|
||||
# Check user role first
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
# get all plugin oauth configs
|
||||
plugin_oauth_config = (
|
||||
db.session.query(DatasourceOauthParamConfig).filter_by(provider=provider, plugin_id=plugin_id).first()
|
||||
)
|
||||
if not plugin_oauth_config:
|
||||
raise NotFound()
|
||||
oauth_handler = OAuthHandler()
|
||||
redirect_url = (
|
||||
f"{dify_config.CONSOLE_WEB_URL}/oauth/datasource/callback?provider={provider}&plugin_id={plugin_id}"
|
||||
)
|
||||
system_credentials = plugin_oauth_config.system_credentials
|
||||
if system_credentials:
|
||||
system_credentials["redirect_url"] = redirect_url
|
||||
response = oauth_handler.get_authorization_url(
|
||||
current_user.current_tenant.id, current_user.id, plugin_id, provider, system_credentials=system_credentials
|
||||
)
|
||||
return response.model_dump()
|
||||
|
||||
|
||||
class DatasourceOauthCallback(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("provider", type=str, required=True, nullable=False, location="args")
|
||||
parser.add_argument("plugin_id", type=str, required=True, nullable=False, location="args")
|
||||
args = parser.parse_args()
|
||||
provider = args["provider"]
|
||||
plugin_id = args["plugin_id"]
|
||||
oauth_handler = OAuthHandler()
|
||||
plugin_oauth_config = (
|
||||
db.session.query(DatasourceOauthParamConfig).filter_by(provider=provider, plugin_id=plugin_id).first()
|
||||
)
|
||||
if not plugin_oauth_config:
|
||||
raise NotFound()
|
||||
credentials = oauth_handler.get_credentials(
|
||||
current_user.current_tenant.id,
|
||||
current_user.id,
|
||||
plugin_id,
|
||||
provider,
|
||||
system_credentials=plugin_oauth_config.system_credentials,
|
||||
request=request,
|
||||
)
|
||||
datasource_provider = DatasourceProvider(
|
||||
plugin_id=plugin_id, provider=provider, auth_type="oauth", encrypted_credentials=credentials
|
||||
)
|
||||
db.session.add(datasource_provider)
|
||||
db.session.commit()
|
||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}")
|
||||
|
||||
|
||||
class DatasourceAuth(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("provider", type=str, required=True, nullable=False, location="json")
|
||||
parser.add_argument("plugin_id", type=str, required=True, nullable=False, location="json")
|
||||
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
|
||||
try:
|
||||
datasource_provider_service.datasource_provider_credentials_validate(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
provider=args["provider"],
|
||||
plugin_id=args["plugin_id"],
|
||||
credentials=args["credentials"],
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ValueError(str(ex))
|
||||
|
||||
return {"result": "success"}, 201
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("provider", type=str, required=True, nullable=False, location="args")
|
||||
parser.add_argument("plugin_id", type=str, required=True, nullable=False, location="args")
|
||||
args = parser.parse_args()
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasources = datasource_provider_service.get_datasource_credentials(
|
||||
tenant_id=current_user.current_tenant_id, provider=args["provider"], plugin_id=args["plugin_id"]
|
||||
)
|
||||
return {"result": datasources}, 200
|
||||
|
||||
|
||||
class DatasourceAuthUpdateDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def delete(self, auth_id: str):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("provider", type=str, required=True, nullable=False, location="args")
|
||||
parser.add_argument("plugin_id", type=str, required=True, nullable=False, location="args")
|
||||
args = parser.parse_args()
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.remove_datasource_credentials(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
auth_id=auth_id,
|
||||
provider=args["provider"],
|
||||
plugin_id=args["plugin_id"],
|
||||
)
|
||||
return {"result": "success"}, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def patch(self, auth_id: str):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("provider", type=str, required=True, nullable=False, location="args")
|
||||
parser.add_argument("plugin_id", type=str, required=True, nullable=False, location="args")
|
||||
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
||||
args = parser.parse_args()
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.update_datasource_credentials(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
auth_id=auth_id,
|
||||
provider=args["provider"],
|
||||
plugin_id=args["plugin_id"],
|
||||
credentials=args["credentials"],
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ValueError(str(ex))
|
||||
|
||||
return {"result": "success"}, 201
|
||||
|
||||
|
||||
# Import Rag Pipeline
|
||||
api.add_resource(
|
||||
DatasourcePluginOauthApi,
|
||||
"/oauth/plugin/datasource",
|
||||
)
|
||||
api.add_resource(
|
||||
DatasourceOauthCallback,
|
||||
"/oauth/plugin/datasource/callback",
|
||||
)
|
||||
api.add_resource(
|
||||
DatasourceAuth,
|
||||
"/auth/plugin/datasource",
|
||||
)
|
||||
|
||||
api.add_resource(
|
||||
DatasourceAuthUpdateDeleteApi,
|
||||
"/auth/plugin/datasource/<string:auth_id>",
|
||||
)
|
||||
@ -0,0 +1,54 @@
|
||||
from flask_restful import ( # type: ignore
|
||||
Resource, # type: ignore
|
||||
reqparse,
|
||||
)
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import current_user, login_required
|
||||
from models import Account
|
||||
from models.dataset import Pipeline
|
||||
from services.rag_pipeline.rag_pipeline import RagPipelineService
|
||||
|
||||
|
||||
class DataSourceContentPreviewApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
def post(self, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run datasource content preview
|
||||
"""
|
||||
if not isinstance(current_user, Account):
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
|
||||
parser.add_argument("datasource_type", type=str, required=True, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
inputs = args.get("inputs")
|
||||
if inputs is None:
|
||||
raise ValueError("missing inputs")
|
||||
datasource_type = args.get("datasource_type")
|
||||
if datasource_type is None:
|
||||
raise ValueError("missing datasource_type")
|
||||
|
||||
rag_pipeline_service = RagPipelineService()
|
||||
preview_content = rag_pipeline_service.run_datasource_node_preview(
|
||||
pipeline=pipeline,
|
||||
node_id=node_id,
|
||||
user_inputs=inputs,
|
||||
account=current_user,
|
||||
datasource_type=datasource_type,
|
||||
is_published=True,
|
||||
)
|
||||
return preview_content, 200
|
||||
|
||||
api.add_resource(
|
||||
DataSourceContentPreviewApi,
|
||||
"/rag/pipelines/<uuid:pipeline_id>/workflows/published/datasource/nodes/<string:node_id>/preview"
|
||||
)
|
||||
@ -0,0 +1,162 @@
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource, reqparse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
enterprise_license_required,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from libs.login import login_required
|
||||
from models.dataset import PipelineCustomizedTemplate
|
||||
from services.entities.knowledge_entities.rag_pipeline_entities import PipelineTemplateInfoEntity
|
||||
from services.rag_pipeline.rag_pipeline import RagPipelineService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _validate_name(name):
|
||||
if not name or len(name) < 1 or len(name) > 40:
|
||||
raise ValueError("Name must be between 1 to 40 characters.")
|
||||
return name
|
||||
|
||||
|
||||
def _validate_description_length(description):
|
||||
if len(description) > 400:
|
||||
raise ValueError("Description cannot exceed 400 characters.")
|
||||
return description
|
||||
|
||||
|
||||
class PipelineTemplateListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def get(self):
|
||||
type = request.args.get("type", default="built-in", type=str)
|
||||
language = request.args.get("language", default="en-US", type=str)
|
||||
# get pipeline templates
|
||||
pipeline_templates = RagPipelineService.get_pipeline_templates(type, language)
|
||||
return pipeline_templates, 200
|
||||
|
||||
|
||||
class PipelineTemplateDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def get(self, template_id: str):
|
||||
type = request.args.get("type", default="built-in", type=str)
|
||||
rag_pipeline_service = RagPipelineService()
|
||||
pipeline_template = rag_pipeline_service.get_pipeline_template_detail(template_id, type)
|
||||
return pipeline_template, 200
|
||||
|
||||
|
||||
class CustomizedPipelineTemplateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def patch(self, template_id: str):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
"name",
|
||||
nullable=False,
|
||||
required=True,
|
||||
help="Name must be between 1 to 40 characters.",
|
||||
type=_validate_name,
|
||||
)
|
||||
parser.add_argument(
|
||||
"description",
|
||||
type=str,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default="",
|
||||
)
|
||||
parser.add_argument(
|
||||
"icon_info",
|
||||
type=dict,
|
||||
location="json",
|
||||
nullable=True,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
pipeline_template_info = PipelineTemplateInfoEntity(**args)
|
||||
RagPipelineService.update_customized_pipeline_template(template_id, pipeline_template_info)
|
||||
return 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def delete(self, template_id: str):
|
||||
RagPipelineService.delete_customized_pipeline_template(template_id)
|
||||
return 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def post(self, template_id: str):
|
||||
with Session(db.engine) as session:
|
||||
template = (
|
||||
session.query(PipelineCustomizedTemplate).filter(PipelineCustomizedTemplate.id == template_id).first()
|
||||
)
|
||||
if not template:
|
||||
raise ValueError("Customized pipeline template not found.")
|
||||
|
||||
return {"data": template.yaml_content}, 200
|
||||
|
||||
|
||||
class PublishCustomizedPipelineTemplateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def post(self, pipeline_id: str):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
"name",
|
||||
nullable=False,
|
||||
required=True,
|
||||
help="Name must be between 1 to 40 characters.",
|
||||
type=_validate_name,
|
||||
)
|
||||
parser.add_argument(
|
||||
"description",
|
||||
type=str,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default="",
|
||||
)
|
||||
parser.add_argument(
|
||||
"icon_info",
|
||||
type=dict,
|
||||
location="json",
|
||||
nullable=True,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
rag_pipeline_service = RagPipelineService()
|
||||
rag_pipeline_service.publish_customized_pipeline_template(pipeline_id, args)
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
api.add_resource(
|
||||
PipelineTemplateListApi,
|
||||
"/rag/pipeline/templates",
|
||||
)
|
||||
api.add_resource(
|
||||
PipelineTemplateDetailApi,
|
||||
"/rag/pipeline/templates/<string:template_id>",
|
||||
)
|
||||
api.add_resource(
|
||||
CustomizedPipelineTemplateApi,
|
||||
"/rag/pipeline/customized/templates/<string:template_id>",
|
||||
)
|
||||
api.add_resource(
|
||||
PublishCustomizedPipelineTemplateApi,
|
||||
"/rag/pipelines/<string:pipeline_id>/customized/publish",
|
||||
)
|
||||
@ -0,0 +1,171 @@
|
||||
from flask_login import current_user # type: ignore # type: ignore
|
||||
from flask_restful import Resource, marshal, reqparse # type: ignore
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
import services
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.error import DatasetNameDuplicateError
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
setup_required,
|
||||
)
|
||||
from fields.dataset_fields import dataset_detail_fields
|
||||
from libs.login import login_required
|
||||
from models.dataset import DatasetPermissionEnum
|
||||
from services.dataset_service import DatasetPermissionService, DatasetService
|
||||
from services.entities.knowledge_entities.rag_pipeline_entities import RagPipelineDatasetCreateEntity
|
||||
from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineDslService
|
||||
|
||||
|
||||
def _validate_name(name):
|
||||
if not name or len(name) < 1 or len(name) > 40:
|
||||
raise ValueError("Name must be between 1 to 40 characters.")
|
||||
return name
|
||||
|
||||
|
||||
def _validate_description_length(description):
|
||||
if len(description) > 400:
|
||||
raise ValueError("Description cannot exceed 400 characters.")
|
||||
return description
|
||||
|
||||
|
||||
class CreateRagPipelineDatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
"name",
|
||||
nullable=False,
|
||||
required=True,
|
||||
help="type is required. Name must be between 1 to 40 characters.",
|
||||
type=_validate_name,
|
||||
)
|
||||
parser.add_argument(
|
||||
"description",
|
||||
type=str,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default="",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"icon_info",
|
||||
type=dict,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default={},
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"permission",
|
||||
type=str,
|
||||
choices=(DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM, DatasetPermissionEnum.PARTIAL_TEAM),
|
||||
nullable=True,
|
||||
required=False,
|
||||
default=DatasetPermissionEnum.ONLY_ME,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"partial_member_list",
|
||||
type=list,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default=[],
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"yaml_content",
|
||||
type=str,
|
||||
nullable=False,
|
||||
required=True,
|
||||
help="yaml_content is required.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
rag_pipeline_dataset_create_entity = RagPipelineDatasetCreateEntity(**args)
|
||||
try:
|
||||
import_info = RagPipelineDslService.create_rag_pipeline_dataset(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
rag_pipeline_dataset_create_entity=rag_pipeline_dataset_create_entity,
|
||||
)
|
||||
if rag_pipeline_dataset_create_entity.permission == "partial_members":
|
||||
DatasetPermissionService.update_partial_member_list(
|
||||
current_user.current_tenant_id,
|
||||
import_info["dataset_id"],
|
||||
rag_pipeline_dataset_create_entity.partial_member_list,
|
||||
)
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
raise DatasetNameDuplicateError()
|
||||
|
||||
return import_info, 201
|
||||
|
||||
|
||||
class CreateEmptyRagPipelineDatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
"name",
|
||||
nullable=False,
|
||||
required=True,
|
||||
help="type is required. Name must be between 1 to 40 characters.",
|
||||
type=_validate_name,
|
||||
)
|
||||
parser.add_argument(
|
||||
"description",
|
||||
type=str,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default="",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"icon_info",
|
||||
type=dict,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default={},
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"permission",
|
||||
type=str,
|
||||
choices=(DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM, DatasetPermissionEnum.PARTIAL_TEAM),
|
||||
nullable=True,
|
||||
required=False,
|
||||
default=DatasetPermissionEnum.ONLY_ME,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"partial_member_list",
|
||||
type=list,
|
||||
nullable=True,
|
||||
required=False,
|
||||
default=[],
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
dataset = DatasetService.create_empty_rag_pipeline_dataset(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
rag_pipeline_dataset_create_entity=RagPipelineDatasetCreateEntity(**args),
|
||||
)
|
||||
return marshal(dataset, dataset_detail_fields), 201
|
||||
|
||||
|
||||
api.add_resource(CreateRagPipelineDatasetApi, "/rag/pipeline/dataset")
|
||||
api.add_resource(CreateEmptyRagPipelineDatasetApi, "/rag/pipeline/empty-dataset")
|
||||
@ -0,0 +1,146 @@
|
||||
from typing import cast
|
||||
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.rag_pipeline_fields import pipeline_import_check_dependencies_fields, pipeline_import_fields
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from models.dataset import Pipeline
|
||||
from services.app_dsl_service import ImportStatus
|
||||
from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineDslService
|
||||
|
||||
|
||||
class RagPipelineImportApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(pipeline_import_fields)
|
||||
def post(self):
|
||||
# Check user role first
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("mode", type=str, required=True, location="json")
|
||||
parser.add_argument("yaml_content", type=str, location="json")
|
||||
parser.add_argument("yaml_url", type=str, location="json")
|
||||
parser.add_argument("name", type=str, location="json")
|
||||
parser.add_argument("description", type=str, location="json")
|
||||
parser.add_argument("icon_type", type=str, location="json")
|
||||
parser.add_argument("icon", type=str, location="json")
|
||||
parser.add_argument("icon_background", type=str, location="json")
|
||||
parser.add_argument("pipeline_id", type=str, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create service with session
|
||||
with Session(db.engine) as session:
|
||||
import_service = RagPipelineDslService(session)
|
||||
# Import app
|
||||
account = cast(Account, current_user)
|
||||
result = import_service.import_rag_pipeline(
|
||||
account=account,
|
||||
import_mode=args["mode"],
|
||||
yaml_content=args.get("yaml_content"),
|
||||
yaml_url=args.get("yaml_url"),
|
||||
pipeline_id=args.get("pipeline_id"),
|
||||
)
|
||||
session.commit()
|
||||
|
||||
# Return appropriate status code based on result
|
||||
status = result.status
|
||||
if status == ImportStatus.FAILED.value:
|
||||
return result.model_dump(mode="json"), 400
|
||||
elif status == ImportStatus.PENDING.value:
|
||||
return result.model_dump(mode="json"), 202
|
||||
return result.model_dump(mode="json"), 200
|
||||
|
||||
|
||||
class RagPipelineImportConfirmApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(pipeline_import_fields)
|
||||
def post(self, import_id):
|
||||
# Check user role first
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
# Create service with session
|
||||
with Session(db.engine) as session:
|
||||
import_service = RagPipelineDslService(session)
|
||||
# Confirm import
|
||||
account = cast(Account, current_user)
|
||||
result = import_service.confirm_import(import_id=import_id, account=account)
|
||||
session.commit()
|
||||
|
||||
# Return appropriate status code based on result
|
||||
if result.status == ImportStatus.FAILED.value:
|
||||
return result.model_dump(mode="json"), 400
|
||||
return result.model_dump(mode="json"), 200
|
||||
|
||||
|
||||
class RagPipelineImportCheckDependenciesApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@get_rag_pipeline
|
||||
@account_initialization_required
|
||||
@marshal_with(pipeline_import_check_dependencies_fields)
|
||||
def get(self, pipeline: Pipeline):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
with Session(db.engine) as session:
|
||||
import_service = RagPipelineDslService(session)
|
||||
result = import_service.check_dependencies(pipeline=pipeline)
|
||||
|
||||
return result.model_dump(mode="json"), 200
|
||||
|
||||
|
||||
class RagPipelineExportApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@get_rag_pipeline
|
||||
@account_initialization_required
|
||||
def get(self, pipeline: Pipeline):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
# Add include_secret params
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("include_secret", type=bool, default=False, location="args")
|
||||
args = parser.parse_args()
|
||||
|
||||
with Session(db.engine) as session:
|
||||
export_service = RagPipelineDslService(session)
|
||||
result = export_service.export_rag_pipeline_dsl(pipeline=pipeline, include_secret=args["include_secret"])
|
||||
|
||||
return {"data": result}, 200
|
||||
|
||||
|
||||
# Import Rag Pipeline
|
||||
api.add_resource(
|
||||
RagPipelineImportApi,
|
||||
"/rag/pipelines/imports",
|
||||
)
|
||||
api.add_resource(
|
||||
RagPipelineImportConfirmApi,
|
||||
"/rag/pipelines/imports/<string:import_id>/confirm",
|
||||
)
|
||||
api.add_resource(
|
||||
RagPipelineImportCheckDependenciesApi,
|
||||
"/rag/pipelines/imports/<string:pipeline_id>/check-dependencies",
|
||||
)
|
||||
api.add_resource(
|
||||
RagPipelineExportApi,
|
||||
"/rag/pipelines/<string:pipeline_id>/exports",
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Optional
|
||||
|
||||
from controllers.console.datasets.error import PipelineNotFoundError
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_user
|
||||
from models.dataset import Pipeline
|
||||
|
||||
|
||||
def get_rag_pipeline(
|
||||
view: Optional[Callable] = None,
|
||||
):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if not kwargs.get("pipeline_id"):
|
||||
raise ValueError("missing pipeline_id in path parameters")
|
||||
|
||||
pipeline_id = kwargs.get("pipeline_id")
|
||||
pipeline_id = str(pipeline_id)
|
||||
|
||||
del kwargs["pipeline_id"]
|
||||
|
||||
pipeline = (
|
||||
db.session.query(Pipeline)
|
||||
.filter(Pipeline.id == pipeline_id, Pipeline.tenant_id == current_user.current_tenant_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not pipeline:
|
||||
raise PipelineNotFoundError()
|
||||
|
||||
kwargs["pipeline"] = pipeline
|
||||
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
if view is None:
|
||||
return decorator
|
||||
else:
|
||||
return decorator(view)
|
||||
@ -1,8 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from libs.external_api import ExternalApi
|
||||
|
||||
bp = Blueprint("mcp", __name__, url_prefix="/mcp")
|
||||
api = ExternalApi(bp)
|
||||
|
||||
from . import mcp
|
||||
@ -1,104 +0,0 @@
|
||||
from flask_restful import Resource, reqparse
|
||||
from pydantic import ValidationError
|
||||
|
||||
from controllers.console.app.mcp_server import AppMCPServerStatus
|
||||
from controllers.mcp import api
|
||||
from core.app.app_config.entities import VariableEntity
|
||||
from core.mcp import types
|
||||
from core.mcp.server.streamable_http import MCPServerStreamableHTTPRequestHandler
|
||||
from core.mcp.types import ClientNotification, ClientRequest
|
||||
from core.mcp.utils import create_mcp_error_response
|
||||
from extensions.ext_database import db
|
||||
from libs import helper
|
||||
from models.model import App, AppMCPServer, AppMode
|
||||
|
||||
|
||||
class MCPAppApi(Resource):
|
||||
def post(self, server_code):
|
||||
def int_or_str(value):
|
||||
if isinstance(value, (int, str)):
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("jsonrpc", type=str, required=True, location="json")
|
||||
parser.add_argument("method", type=str, required=True, location="json")
|
||||
parser.add_argument("params", type=dict, required=False, location="json")
|
||||
parser.add_argument("id", type=int_or_str, required=False, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
request_id = args.get("id")
|
||||
|
||||
server = db.session.query(AppMCPServer).where(AppMCPServer.server_code == server_code).first()
|
||||
if not server:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "Server Not Found")
|
||||
)
|
||||
|
||||
if server.status != AppMCPServerStatus.ACTIVE:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "Server is not active")
|
||||
)
|
||||
|
||||
app = db.session.query(App).where(App.id == server.app_id).first()
|
||||
if not app:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "App Not Found")
|
||||
)
|
||||
|
||||
if app.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
|
||||
workflow = app.workflow
|
||||
if workflow is None:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "App is unavailable")
|
||||
)
|
||||
|
||||
user_input_form = workflow.user_input_form(to_old_structure=True)
|
||||
else:
|
||||
app_model_config = app.app_model_config
|
||||
if app_model_config is None:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "App is unavailable")
|
||||
)
|
||||
|
||||
features_dict = app_model_config.to_dict()
|
||||
user_input_form = features_dict.get("user_input_form", [])
|
||||
converted_user_input_form: list[VariableEntity] = []
|
||||
try:
|
||||
for item in user_input_form:
|
||||
variable_type = item.get("type", "") or list(item.keys())[0]
|
||||
variable = item[variable_type]
|
||||
converted_user_input_form.append(
|
||||
VariableEntity(
|
||||
type=variable_type,
|
||||
variable=variable.get("variable"),
|
||||
description=variable.get("description") or "",
|
||||
label=variable.get("label"),
|
||||
required=variable.get("required", False),
|
||||
max_length=variable.get("max_length"),
|
||||
options=variable.get("options") or [],
|
||||
)
|
||||
)
|
||||
except ValidationError as e:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_PARAMS, f"Invalid user_input_form: {str(e)}")
|
||||
)
|
||||
|
||||
try:
|
||||
request: ClientRequest | ClientNotification = ClientRequest.model_validate(args)
|
||||
except ValidationError as e:
|
||||
try:
|
||||
notification = ClientNotification.model_validate(args)
|
||||
request = notification
|
||||
except ValidationError as e:
|
||||
return helper.compact_generate_response(
|
||||
create_mcp_error_response(request_id, types.INVALID_PARAMS, f"Invalid MCP request: {str(e)}")
|
||||
)
|
||||
|
||||
mcp_server_handler = MCPServerStreamableHTTPRequestHandler(app, request, converted_user_input_form)
|
||||
response = mcp_server_handler.handle()
|
||||
return helper.compact_generate_response(response)
|
||||
|
||||
|
||||
api.add_resource(MCPAppApi, "/server/<string:server_code>/mcp")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue