mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Compare commits
2810 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa01f86b19 | ||
|
|
9583095734 | ||
|
|
a5b2eb3a28 | ||
|
|
72f2784588 | ||
|
|
5c5b730bb8 | ||
|
|
b9ec6b4315 | ||
|
|
4b83fa3549 | ||
|
|
a69e81076a | ||
|
|
4cd2b73f19 | ||
|
|
4ea0bebd92 | ||
|
|
bbcdae25a1 | ||
|
|
9b5ee2e694 | ||
|
|
e932d86b69 | ||
|
|
96f05311b8 | ||
|
|
3e2d68782c | ||
|
|
db2a4349cb | ||
|
|
2014fe83a3 | ||
|
|
55439aab5e | ||
|
|
8c91864f1c | ||
|
|
9319ec5bb2 | ||
|
|
83e4023c19 | ||
|
|
a14701bdd2 | ||
|
|
379dd011ff | ||
|
|
49b3ccfe2b | ||
|
|
16608370a6 | ||
|
|
53015c9d8e | ||
|
|
6d68b89ea0 | ||
|
|
254582da89 | ||
|
|
af54b7cfef | ||
|
|
f13149db8e | ||
|
|
79912a4067 | ||
|
|
c0b6b85ec0 | ||
|
|
a4895f5166 | ||
|
|
4d7670a12e | ||
|
|
8c21954049 | ||
|
|
132fab1c03 | ||
|
|
e7b8d71010 | ||
|
|
fff8cfdee0 | ||
|
|
3e45a3b4d8 | ||
|
|
7c66e21356 | ||
|
|
c477a49777 | ||
|
|
5a38c09f8d | ||
|
|
fe4657b122 | ||
|
|
c1dcd2e57d | ||
|
|
26d993674e | ||
|
|
9d475001ee | ||
|
|
34eb25b0ba | ||
|
|
716b935177 | ||
|
|
92528af600 | ||
|
|
2606e6b82d | ||
|
|
b965ce7376 | ||
|
|
048f1b53c0 | ||
|
|
43340c4aa8 | ||
|
|
9f073fcbcf | ||
|
|
c0c60a4875 | ||
|
|
94f682e461 | ||
|
|
1086bfe1ba | ||
|
|
d441d5763f | ||
|
|
c0a2daa3a3 | ||
|
|
3de51b6a65 | ||
|
|
a741388447 | ||
|
|
1ea9b87498 | ||
|
|
0cab007c37 | ||
|
|
4a331db5fc | ||
|
|
904b0bf2da | ||
|
|
90425542f8 | ||
|
|
eae0141d50 | ||
|
|
9594c82005 | ||
|
|
657aacceb5 | ||
|
|
a35dbf99a6 | ||
|
|
0d80f5d752 | ||
|
|
b36f4dfd08 | ||
|
|
fddee69f92 | ||
|
|
ec270a3b54 | ||
|
|
c97d1e3363 | ||
|
|
554c1ed1f7 | ||
|
|
a90b286482 | ||
|
|
cc78ea7222 | ||
|
|
7f2cc3b232 | ||
|
|
00b10f17c1 | ||
|
|
cab6305462 | ||
|
|
7218403ad7 | ||
|
|
811dfecf98 | ||
|
|
acbbf30a0e | ||
|
|
4d29f8f679 | ||
|
|
13fcbe3e74 | ||
|
|
850b79f459 | ||
|
|
9e6f970bc4 | ||
|
|
cbcb717aee | ||
|
|
5aea46c214 | ||
|
|
6394720c5a | ||
|
|
6af627ea97 | ||
|
|
85277f2b4f | ||
|
|
7b0876204e | ||
|
|
cf65942504 | ||
|
|
7369b02bf4 | ||
|
|
1438fea76b | ||
|
|
e0912f0cf0 | ||
|
|
838525b452 | ||
|
|
774cbbf47a | ||
|
|
d15bc6d32c | ||
|
|
99e0766f53 | ||
|
|
51225b18b2 | ||
|
|
96ab01b0c1 | ||
|
|
a4eb4ea66d | ||
|
|
54819e288a | ||
|
|
ec5fbded4f | ||
|
|
f939576311 | ||
|
|
628784da35 | ||
|
|
9ea3231060 | ||
|
|
0b7858494f | ||
|
|
8f98c8a3c9 | ||
|
|
67f9b3a6e0 | ||
|
|
5defc0a87b | ||
|
|
b4bcb09707 | ||
|
|
b2d74f66b3 | ||
|
|
75223e18ee | ||
|
|
4aea9c727d | ||
|
|
7d779afcd4 | ||
|
|
5cb7a69a46 | ||
|
|
0e88bfc570 | ||
|
|
48cf56557b | ||
|
|
9c9354cf38 | ||
|
|
e730ae66ae | ||
|
|
58d6b71808 | ||
|
|
4b9c1c4863 | ||
|
|
e1cdacaebf | ||
|
|
af120248d7 | ||
|
|
3749b327f9 | ||
|
|
017ccd6351 | ||
|
|
cdc860933e | ||
|
|
7b408f338a | ||
|
|
b326c0c9ae | ||
|
|
f06f409f2d | ||
|
|
a0e8b70e6d | ||
|
|
5294178bb7 | ||
|
|
9050d48bc3 | ||
|
|
9d0b54f461 | ||
|
|
4ba848a483 | ||
|
|
0b26e6232a | ||
|
|
88ad827a87 | ||
|
|
0b0f0a959a | ||
|
|
25ee749724 | ||
|
|
204b871fa2 | ||
|
|
f45db6014d | ||
|
|
475850ef94 | ||
|
|
602fe086b9 | ||
|
|
5ad76cf2af | ||
|
|
03e8c56f05 | ||
|
|
d1981967b2 | ||
|
|
c6094ad575 | ||
|
|
93e376ad2f | ||
|
|
6bba3d164a | ||
|
|
b5decffaa2 | ||
|
|
c068ac48d1 | ||
|
|
d4b89803b2 | ||
|
|
d5b73236de | ||
|
|
1e011879b1 | ||
|
|
9c30ff3024 | ||
|
|
035f41b12c | ||
|
|
0bbf1db434 | ||
|
|
639e267392 | ||
|
|
bd5504461e | ||
|
|
c46aa23fdd | ||
|
|
d654e79be3 | ||
|
|
c41877920a | ||
|
|
3f11e3e6a6 | ||
|
|
225e73c8cf | ||
|
|
95ec541a38 | ||
|
|
1941bd36bb | ||
|
|
e1b6d61558 | ||
|
|
c873e4ef42 | ||
|
|
90eb261da6 | ||
|
|
fb46cc9fdf | ||
|
|
2d5a2eb52b | ||
|
|
fa108126bb | ||
|
|
b9540ba2bc | ||
|
|
1992acaf61 | ||
|
|
8c586a34e7 | ||
|
|
44399a03c1 | ||
|
|
3e70af9a57 | ||
|
|
475d20b627 | ||
|
|
69c5c6d6b8 | ||
|
|
2480dc83b2 | ||
|
|
7c8b617f62 | ||
|
|
7377fee8ca | ||
|
|
bdd78b664f | ||
|
|
9272d4725a | ||
|
|
4ae6a8e25d | ||
|
|
6e660140ae | ||
|
|
5315429195 | ||
|
|
abf898e032 | ||
|
|
eef112d83d | ||
|
|
e1784abbeb | ||
|
|
0031ca3159 | ||
|
|
411115523e | ||
|
|
8b206b087c | ||
|
|
0d126106c0 | ||
|
|
0751debff7 | ||
|
|
33a28a64ec | ||
|
|
28e37d8ad2 | ||
|
|
190f571718 | ||
|
|
c7d7dfbd50 | ||
|
|
efb018d2b0 | ||
|
|
cae9a45832 | ||
|
|
3daeab5186 | ||
|
|
83914d5a56 | ||
|
|
0f611eb87b | ||
|
|
f70b2d0839 | ||
|
|
2f33a46e89 | ||
|
|
598c7a5d76 | ||
|
|
8724c12c11 | ||
|
|
22d9020331 | ||
|
|
b4d77080e8 | ||
|
|
e42fc97d03 | ||
|
|
e45648b389 | ||
|
|
085c4ddf09 | ||
|
|
5ddf9bd7ec | ||
|
|
2420af3b6d | ||
|
|
b8fade251b | ||
|
|
8935dc4e31 | ||
|
|
ae61d89494 | ||
|
|
753832d701 | ||
|
|
8926cf777c | ||
|
|
868ea1a1e2 | ||
|
|
1e1707ec0b | ||
|
|
636ac2a56c | ||
|
|
45076b05f7 | ||
|
|
ba9e2101bb | ||
|
|
7301b61cb8 | ||
|
|
ee3f657751 | ||
|
|
e30291966a | ||
|
|
2536bd0988 | ||
|
|
5234350bde | ||
|
|
36e4398bcb | ||
|
|
4b040280c3 | ||
|
|
fdd2300517 | ||
|
|
49913b2258 | ||
|
|
4927b64d27 | ||
|
|
fb2df05e3f | ||
|
|
ab90a93eec | ||
|
|
48c17169b5 | ||
|
|
41cd83f20e | ||
|
|
52dd3f798a | ||
|
|
070efd6951 | ||
|
|
502d82e1c9 | ||
|
|
7760e779ae | ||
|
|
474298c969 | ||
|
|
b2a013c027 | ||
|
|
cca5ef098b | ||
|
|
41b4c28430 | ||
|
|
90962ee056 | ||
|
|
953cff09a0 | ||
|
|
b41a989051 | ||
|
|
4fcd45c1ae | ||
|
|
1f75f0c082 | ||
|
|
c2a95b5bec | ||
|
|
0a246d3de7 | ||
|
|
2d6238d431 | ||
|
|
c4f3dc4434 | ||
|
|
2aea24afdd | ||
|
|
666240f21e | ||
|
|
fb4ab220d6 | ||
|
|
5a882fe37f | ||
|
|
132326136a | ||
|
|
6fc4723d61 | ||
|
|
8564198321 | ||
|
|
4c3f990d4b | ||
|
|
b19c14787e | ||
|
|
f67b79f007 | ||
|
|
daa332aa20 | ||
|
|
c3f538c2f6 | ||
|
|
a0e677ea00 | ||
|
|
343569ba19 | ||
|
|
9096013e13 | ||
|
|
89a2f249c1 | ||
|
|
4b0e094272 | ||
|
|
97713e872a | ||
|
|
f9a7db11eb | ||
|
|
1448d7e6eb | ||
|
|
8e7d5340d7 | ||
|
|
47ecf98e2a | ||
|
|
f8e4e42a36 | ||
|
|
38753c4395 | ||
|
|
b473e13b83 | ||
|
|
9092575186 | ||
|
|
ffe5ac2aad | ||
|
|
0ab6f75410 | ||
|
|
099245f27e | ||
|
|
0a0fe20fa0 | ||
|
|
c2aa5cc994 | ||
|
|
f84e59a7fb | ||
|
|
613c032994 | ||
|
|
7829db97bf | ||
|
|
acdfde6752 | ||
|
|
c673c0b245 | ||
|
|
4bf4e11cee | ||
|
|
770175456f | ||
|
|
0abbf71f15 | ||
|
|
46b0de367a | ||
|
|
30309659d3 | ||
|
|
acadd6bddc | ||
|
|
96c57260cb | ||
|
|
f29f58b2ac | ||
|
|
124a04738c | ||
|
|
3a60c31df9 | ||
|
|
501cf3973c | ||
|
|
c73251e998 | ||
|
|
201fb61bd4 | ||
|
|
f87ae429f4 | ||
|
|
35e8e2df44 | ||
|
|
7c3f80f13d | ||
|
|
17a176ad4e | ||
|
|
ca5eb06de9 | ||
|
|
2378548cf1 | ||
|
|
fdd265f47f | ||
|
|
3e2e1ecddf | ||
|
|
863950963f | ||
|
|
defa1b28a8 | ||
|
|
1f649274d1 | ||
|
|
3ce04de161 | ||
|
|
e798d18e70 | ||
|
|
ed2609d3b3 | ||
|
|
6d2a2632c5 | ||
|
|
dbf95a95a4 | ||
|
|
0e4bd06795 | ||
|
|
4d38280cfa | ||
|
|
75173473ae | ||
|
|
b314b27260 | ||
|
|
cc7e223082 | ||
|
|
79f87d4c20 | ||
|
|
8adbd6720a | ||
|
|
c3973571a7 | ||
|
|
bf63509a6e | ||
|
|
6552fe831b | ||
|
|
05fdf6b93a | ||
|
|
6953c3dbe4 | ||
|
|
55ecda902d | ||
|
|
0495610257 | ||
|
|
301bb2dcfe | ||
|
|
598b8f9980 | ||
|
|
9528f34a25 | ||
|
|
625aed151d | ||
|
|
4ffdf3f9a2 | ||
|
|
0a97e5b7be | ||
|
|
bfeae3a95b | ||
|
|
4ab12663be | ||
|
|
0584c29781 | ||
|
|
a8231d375a | ||
|
|
a86b342ba5 | ||
|
|
0a7a313e5d | ||
|
|
9d4aee5de2 | ||
|
|
faf031ce80 | ||
|
|
e9a2b8f03a | ||
|
|
d89bd0941d | ||
|
|
8d8423b6e0 | ||
|
|
e22669f91d | ||
|
|
b5e5fb7f10 | ||
|
|
2709994ede | ||
|
|
e5bd194b6c | ||
|
|
f01f76dba7 | ||
|
|
289bd41570 | ||
|
|
6a0d6a8faf | ||
|
|
dcc39d954e | ||
|
|
8a67f18cd9 | ||
|
|
2e02304c71 | ||
|
|
ce975c5d93 | ||
|
|
fb4bb54aca | ||
|
|
dae0942d03 | ||
|
|
25b1173db7 | ||
|
|
92d90866ca | ||
|
|
1595e0210a | ||
|
|
ea4ef40a12 | ||
|
|
9986fce8bf | ||
|
|
628f83172a | ||
|
|
c855896221 | ||
|
|
94b5241e70 | ||
|
|
0600f095f5 | ||
|
|
a0a05b676f | ||
|
|
a818975823 | ||
|
|
8e9f31cc32 | ||
|
|
0d4bc4ec2c | ||
|
|
7a0118b31c | ||
|
|
e9a8161811 | ||
|
|
a6bface632 | ||
|
|
48f47351ee | ||
|
|
9247f16add | ||
|
|
d3eab30d74 | ||
|
|
f65ecb9a0f | ||
|
|
312cb9ae70 | ||
|
|
cce60ce101 | ||
|
|
e0a3b8004c | ||
|
|
91239820e3 | ||
|
|
8641a91182 | ||
|
|
84bffd24f2 | ||
|
|
9fb37b1179 | ||
|
|
4eee10b5d5 | ||
|
|
c53456876c | ||
|
|
1a9f31174d | ||
|
|
0493352292 | ||
|
|
13b91193cc | ||
|
|
9a367c76a0 | ||
|
|
f58e7cc154 | ||
|
|
5ee0f15d94 | ||
|
|
250edf26a5 | ||
|
|
7a01376828 | ||
|
|
63b547ea13 | ||
|
|
626689cbe0 | ||
|
|
a44319d815 | ||
|
|
3273af7f40 | ||
|
|
5971ff884e | ||
|
|
cbf33e698b | ||
|
|
868e59bca0 | ||
|
|
04959df194 | ||
|
|
47d687b151 | ||
|
|
2ad6b4fa4e | ||
|
|
8e94688b77 | ||
|
|
fab367f041 | ||
|
|
94617c5ef7 | ||
|
|
4443bc77fd | ||
|
|
d33246612d | ||
|
|
144ab61e07 | ||
|
|
a4c95fd62b | ||
|
|
2245f4690e | ||
|
|
8eaeaa91f9 | ||
|
|
7bd0351ee9 | ||
|
|
811a20f080 | ||
|
|
1decff2114 | ||
|
|
c97968f6c0 | ||
|
|
9deb5adcbf | ||
|
|
91e7c16d90 | ||
|
|
edc81d8e6e | ||
|
|
ed8d553491 | ||
|
|
a64a5e89db | ||
|
|
bd636d59dd | ||
|
|
2d15492190 | ||
|
|
d696f0d081 | ||
|
|
9409e4498f | ||
|
|
541a6417b7 | ||
|
|
f6e9f9011d | ||
|
|
2fe3cb2b22 | ||
|
|
6b9519b56f | ||
|
|
9bbe7564a9 | ||
|
|
58af393968 | ||
|
|
bed4939652 | ||
|
|
ebf6109219 | ||
|
|
0ef232f731 | ||
|
|
ad602f22c8 | ||
|
|
70f44fcaca | ||
|
|
1f32e7cf82 | ||
|
|
00390200ec | ||
|
|
0a11a3afee | ||
|
|
9f77b03643 | ||
|
|
c6dc1675d8 | ||
|
|
e475a4cc7c | ||
|
|
dfc3cdd5d4 | ||
|
|
6974db5fd8 | ||
|
|
32c67c2a02 | ||
|
|
6c585de6d3 | ||
|
|
1056c943d3 | ||
|
|
839f0a3b95 | ||
|
|
b19e9cae23 | ||
|
|
84a15ef54d | ||
|
|
d4b409e166 | ||
|
|
ba1c0ab6fb | ||
|
|
eddafcfdfb | ||
|
|
8a225e279f | ||
|
|
d5cce88108 | ||
|
|
e00c6f2c14 | ||
|
|
0837295bd3 | ||
|
|
f3a005a667 | ||
|
|
d59ffaf0bd | ||
|
|
e133c29b2c | ||
|
|
f64bf7daa0 | ||
|
|
ef24318c17 | ||
|
|
33fe0ffc93 | ||
|
|
243b036ae7 | ||
|
|
06518c209a | ||
|
|
3482474265 | ||
|
|
5debb48265 | ||
|
|
84377eed07 | ||
|
|
dd9589b37a | ||
|
|
7c00099919 | ||
|
|
bc840900a3 | ||
|
|
4429755c09 | ||
|
|
a2967afb55 | ||
|
|
3d03826db5 | ||
|
|
7ff86a2aee | ||
|
|
2a68cc9989 | ||
|
|
855365fba6 | ||
|
|
928303f27b | ||
|
|
df2f69e85f | ||
|
|
ec3407df7e | ||
|
|
5dae074c95 | ||
|
|
a35be6ae57 | ||
|
|
101935ae46 | ||
|
|
d68e731ffd | ||
|
|
bf0dd6946e | ||
|
|
41cbcbc07f | ||
|
|
73f93946b0 | ||
|
|
181d2504e5 | ||
|
|
b0423d987e | ||
|
|
9157fe7323 | ||
|
|
e02718947a | ||
|
|
ebbd47c9cb | ||
|
|
ad810b3740 | ||
|
|
c3e85d747a | ||
|
|
8e092cbe1c | ||
|
|
9a35609bc7 | ||
|
|
7fed92d6b3 | ||
|
|
1f52461cd9 | ||
|
|
fe16743d16 | ||
|
|
7fd8e57bdc | ||
|
|
ed6cd9890a | ||
|
|
f876f9e20e | ||
|
|
62b15f2d6f | ||
|
|
89529f4df5 | ||
|
|
fe18d6e638 | ||
|
|
042519005c | ||
|
|
7e24995afe | ||
|
|
877b165a9a | ||
|
|
64f72ada28 | ||
|
|
171916e1a4 | ||
|
|
dbfc1bb68f | ||
|
|
5d4c067d80 | ||
|
|
3f10a775ba | ||
|
|
2f05a47de3 | ||
|
|
9ca079c95a | ||
|
|
2d37083719 | ||
|
|
0b890e1d70 | ||
|
|
0bb014c965 | ||
|
|
0684449c2a | ||
|
|
a23806d16a | ||
|
|
0b7be94d13 | ||
|
|
4ac996cfe6 | ||
|
|
78c819f976 | ||
|
|
365537f74e | ||
|
|
5c756348a5 | ||
|
|
ed12c2d527 | ||
|
|
82189b0a3c | ||
|
|
23889f7f16 | ||
|
|
45e14bc2f5 | ||
|
|
0746f30645 | ||
|
|
daedfc0a57 | ||
|
|
2cc3372b86 | ||
|
|
e024452610 | ||
|
|
06e4a05e41 | ||
|
|
256514fefc | ||
|
|
3f64ff8194 | ||
|
|
af2cef1bfc | ||
|
|
3be74b1fdd | ||
|
|
e2a705806a | ||
|
|
5c99615edf | ||
|
|
605f168c7e | ||
|
|
b223cf05d9 | ||
|
|
419b98b50f | ||
|
|
b99b3b844a | ||
|
|
d9787e849e | ||
|
|
631e77ce65 | ||
|
|
7ff3a31e72 | ||
|
|
331dfdbab4 | ||
|
|
c83ff2237c | ||
|
|
9972435525 | ||
|
|
409d0e4084 | ||
|
|
18ed255f5a | ||
|
|
4a8e9bf04e | ||
|
|
0b1a302995 | ||
|
|
6978e7439f | ||
|
|
fcb6bec474 | ||
|
|
91690ff99a | ||
|
|
45f930a9e2 | ||
|
|
09a1879f3e | ||
|
|
4bc14dbdd0 | ||
|
|
1627d424e7 | ||
|
|
0aa9da39a9 | ||
|
|
8564c2ba72 | ||
|
|
1c791f240a | ||
|
|
bea0ccaa6c | ||
|
|
05f756963c | ||
|
|
54ad6ad1c7 | ||
|
|
c44ff77e09 | ||
|
|
c77d415893 | ||
|
|
92c9612dee | ||
|
|
b40417fcfe | ||
|
|
13c890b212 | ||
|
|
741ab6e43c | ||
|
|
3db46ecd68 | ||
|
|
a972bb8827 | ||
|
|
2e4e080329 | ||
|
|
5fe4c40ec1 | ||
|
|
d18cb373fc | ||
|
|
1b1771e4eb | ||
|
|
51e450cc4b | ||
|
|
67a97a7e51 | ||
|
|
1e88c86378 | ||
|
|
fcb5f946dd | ||
|
|
2e69e9bef3 | ||
|
|
88623754cf | ||
|
|
886bcef7b0 | ||
|
|
cc414da744 | ||
|
|
6e88ecc2da | ||
|
|
88d2420163 | ||
|
|
7e71ee1aae | ||
|
|
c6d78e27c6 | ||
|
|
b6d06dcfc3 | ||
|
|
756c46c026 | ||
|
|
5afba6e30f | ||
|
|
02a23a65e7 | ||
|
|
e7de16833d | ||
|
|
990c6d1c64 | ||
|
|
f78a8c6fea | ||
|
|
5580d19b75 | ||
|
|
c035e3c7c6 | ||
|
|
1502adfb85 | ||
|
|
3b76b3ddce | ||
|
|
e4a1730a5b | ||
|
|
4aa66170c5 | ||
|
|
24352b56af | ||
|
|
cbea17b4d5 | ||
|
|
4dd0d65db4 | ||
|
|
cdfdcc7d03 | ||
|
|
b4082b2cfa | ||
|
|
fae3cfc58f | ||
|
|
ad038784cc | ||
|
|
7e69dd5ff0 | ||
|
|
8720ec65ab | ||
|
|
3098f99eba | ||
|
|
98b90b82c4 | ||
|
|
01268a37e3 | ||
|
|
0c46f3c205 | ||
|
|
b442cc186a | ||
|
|
2353276aa7 | ||
|
|
8c034d3e78 | ||
|
|
2c25f4a4c0 | ||
|
|
d2a988a715 | ||
|
|
bd12eac145 | ||
|
|
ebc44273c9 | ||
|
|
b781d78cc6 | ||
|
|
3f7c8bdba5 | ||
|
|
fd8e277530 | ||
|
|
6a024b0ced | ||
|
|
a185b2a12a | ||
|
|
2366c2cd94 | ||
|
|
567c01e302 | ||
|
|
b3d2c1a5d1 | ||
|
|
7b3ecb5c2f | ||
|
|
f4abed43ba | ||
|
|
5854202f22 | ||
|
|
d3238de8ab | ||
|
|
09a2705311 | ||
|
|
a4c0861cf4 | ||
|
|
5ba917c5e4 | ||
|
|
83f2fb1e62 | ||
|
|
7bf79675c1 | ||
|
|
f33aa9c71b | ||
|
|
ac1c21c784 | ||
|
|
1757ce23af | ||
|
|
3f1bae3044 | ||
|
|
20bd7ca2cc | ||
|
|
a2b0204a95 | ||
|
|
f7063d03f1 | ||
|
|
9be3043e0f | ||
|
|
4aeeaf185c | ||
|
|
a25d5d98a4 | ||
|
|
973304e0d7 | ||
|
|
590a735f99 | ||
|
|
42185a011b | ||
|
|
e7b872a5df | ||
|
|
2bf75c36e4 | ||
|
|
bcd9005b53 | ||
|
|
cca6297a64 | ||
|
|
39e94d4a5e | ||
|
|
204b1b1963 | ||
|
|
b2e45e8af3 | ||
|
|
27797581ba | ||
|
|
a797801d4c | ||
|
|
d7c09e3493 | ||
|
|
085542c861 | ||
|
|
7ccd74b022 | ||
|
|
047afeebb6 | ||
|
|
a6eab324b8 | ||
|
|
74a11da9bd | ||
|
|
473cd79af1 | ||
|
|
b3c075714c | ||
|
|
f5661b3b1e | ||
|
|
8ce1fd561d | ||
|
|
adb2cf35d4 | ||
|
|
3e32724729 | ||
|
|
b59aa2f3e7 | ||
|
|
44405b250c | ||
|
|
2b547f71f4 | ||
|
|
56e3a1c3b2 | ||
|
|
644a66c983 | ||
|
|
5f62c0d57a | ||
|
|
c440e6f8fa | ||
|
|
706bd6126a | ||
|
|
f8544cf14b | ||
|
|
a8bb992569 | ||
|
|
ddafb96eba | ||
|
|
1dfb5d29c3 | ||
|
|
3c64abceb8 | ||
|
|
3c43b87e9f | ||
|
|
d7fe1150dc | ||
|
|
3e55be910b | ||
|
|
1ae0af56cc | ||
|
|
b329ede52a | ||
|
|
a9f6a06446 | ||
|
|
24383997ef | ||
|
|
48ec0ae44c | ||
|
|
ea49095b0a | ||
|
|
f3ff3920d9 | ||
|
|
3db07f3a26 | ||
|
|
a2ef45e13f | ||
|
|
3e20934e20 | ||
|
|
ec5db5d2c7 | ||
|
|
cc25b5e856 | ||
|
|
c06f232589 | ||
|
|
e1a8184c2e | ||
|
|
fc24eb08cb | ||
|
|
345ab8ea9e | ||
|
|
65547bad87 | ||
|
|
4402442e54 | ||
|
|
5bfd7d5e6c | ||
|
|
09537ec0dd | ||
|
|
5ad4ba6abd | ||
|
|
4decb34f99 | ||
|
|
947014945f | ||
|
|
b6710beadc | ||
|
|
c2c5f07ffa | ||
|
|
95a60adfcc | ||
|
|
59eeb73b60 | ||
|
|
0173a4d7fa | ||
|
|
e2e7ee1893 | ||
|
|
58dfc58622 | ||
|
|
c8bfc52fab | ||
|
|
3e50fe1aa9 | ||
|
|
e7c760e68b | ||
|
|
fa9ca221b4 | ||
|
|
cff76c672c | ||
|
|
2395785337 | ||
|
|
862ea5fa36 | ||
|
|
aa0964d99f | ||
|
|
5b5281e50c | ||
|
|
8bcf7bdade | ||
|
|
57cfafeb34 | ||
|
|
5ea8706ba9 | ||
|
|
68f497517d | ||
|
|
8308bd0039 | ||
|
|
b7f00324bc | ||
|
|
77f5328ab9 | ||
|
|
bb82b9a9d3 | ||
|
|
355fd2b5d7 | ||
|
|
1e6cc95f09 | ||
|
|
bb8b4cae79 | ||
|
|
faef061d74 | ||
|
|
62af7549c7 | ||
|
|
04b4db763c | ||
|
|
c95b4d1305 | ||
|
|
40b8aa42cc | ||
|
|
e66535c572 | ||
|
|
81c8511316 | ||
|
|
b59170078d | ||
|
|
453c653975 | ||
|
|
976e7b6765 | ||
|
|
94a8c42311 | ||
|
|
534b650c10 | ||
|
|
f01c04bb24 | ||
|
|
fcc0449076 | ||
|
|
565b0c5a9c | ||
|
|
c204d37ac7 | ||
|
|
c5adea6993 | ||
|
|
c8648101a7 | ||
|
|
1f9c167bd2 | ||
|
|
cbae7bd500 | ||
|
|
27f97bc55d | ||
|
|
601ba40cb0 | ||
|
|
4632531f2d | ||
|
|
c9e95a9146 | ||
|
|
c48d4ae7df | ||
|
|
4895d389e4 | ||
|
|
92916e42c1 | ||
|
|
1a8499cf26 | ||
|
|
81a912c93f | ||
|
|
989d6eee0f | ||
|
|
a8d371045b | ||
|
|
b80726e942 | ||
|
|
0fc48ea03f | ||
|
|
afc86efe28 | ||
|
|
ab1ebeb7e0 | ||
|
|
6932c7e3e9 | ||
|
|
c04687fdd1 | ||
|
|
7717242112 | ||
|
|
1ad82c22d9 | ||
|
|
8fa88175c1 | ||
|
|
b05fec93bb | ||
|
|
802311a06a | ||
|
|
dc0f26d3d8 | ||
|
|
36c32fd968 | ||
|
|
0001963a04 | ||
|
|
6a264a45e2 | ||
|
|
d897315355 | ||
|
|
d536b9d8c6 | ||
|
|
6af7d4e6e8 | ||
|
|
67dddcb224 | ||
|
|
aad12aa227 | ||
|
|
d71675f3d2 | ||
|
|
88fb35552a | ||
|
|
8a084a05c9 | ||
|
|
90ef7ddacb | ||
|
|
034dfffb85 | ||
|
|
09a15e2e59 | ||
|
|
7859a0d001 | ||
|
|
085e1b6c41 | ||
|
|
5b01664d53 | ||
|
|
03adfd4898 | ||
|
|
1616124fa2 | ||
|
|
2611550ffd | ||
|
|
2989be47cc | ||
|
|
9b7a346380 | ||
|
|
8c8bf8702f | ||
|
|
eb8c2a9277 | ||
|
|
350c91889e | ||
|
|
43018840d1 | ||
|
|
4daf443db8 | ||
|
|
a85f214fdb | ||
|
|
ab77c4e616 | ||
|
|
19315f72a0 | ||
|
|
3683d4a759 | ||
|
|
e9a7722915 | ||
|
|
7794129929 | ||
|
|
2ee2067634 | ||
|
|
ef6ec3fcb8 | ||
|
|
c57ff3ae51 | ||
|
|
aa7f59f88c | ||
|
|
3b4a95ef33 | ||
|
|
09ba14b8ca | ||
|
|
b5dc7281f9 | ||
|
|
deca7726f4 | ||
|
|
058e0279b4 | ||
|
|
5e604950c5 | ||
|
|
af1b81097f | ||
|
|
08e713d381 | ||
|
|
a2c228c09f | ||
|
|
4e6afebf9e | ||
|
|
13d274202e | ||
|
|
f56acac656 | ||
|
|
68d325b5b5 | ||
|
|
40907a2584 | ||
|
|
f4f5d99ec9 | ||
|
|
0475e55518 | ||
|
|
872b390cfa | ||
|
|
dc4c539607 | ||
|
|
69f8c76ce2 | ||
|
|
cbdcca7fd8 | ||
|
|
1e7fa988da | ||
|
|
3cfe771b8b | ||
|
|
8b4d6d0868 | ||
|
|
ba35e1422b | ||
|
|
8db9b09c96 | ||
|
|
78e99d1171 | ||
|
|
6a12f96fdc | ||
|
|
b79a20151c | ||
|
|
c9976020dd | ||
|
|
e8988e82d0 | ||
|
|
ff95570da6 | ||
|
|
6698a000e6 | ||
|
|
b084e3074d | ||
|
|
bc4f9c3442 | ||
|
|
ecf88e7ea1 | ||
|
|
d6cb66cd2f | ||
|
|
bc2241f67a | ||
|
|
3d292aa485 | ||
|
|
21f46a8aea | ||
|
|
ba6d61cc35 | ||
|
|
27b43c11bd | ||
|
|
a5292c4473 | ||
|
|
f3923488a5 | ||
|
|
d964ebfff8 | ||
|
|
e420eeece4 | ||
|
|
f34713545e | ||
|
|
2b513c7d87 | ||
|
|
7e28893613 | ||
|
|
822674ca78 | ||
|
|
35ebf97fdf | ||
|
|
b456c0ca9f | ||
|
|
bae49891be | ||
|
|
dfb4a67d87 | ||
|
|
8e727a3253 | ||
|
|
d6f03d7a07 | ||
|
|
5ada5bf1a0 | ||
|
|
ae9c935c5c | ||
|
|
95618001aa | ||
|
|
40c361968e | ||
|
|
757abda654 | ||
|
|
862807e863 | ||
|
|
d188db887c | ||
|
|
59b6c56262 | ||
|
|
f92658de82 | ||
|
|
f2b3402c17 | ||
|
|
24badf65a4 | ||
|
|
86442f212f | ||
|
|
9b6d6ecc32 | ||
|
|
1f51277004 | ||
|
|
68cc646a3e | ||
|
|
420ca1b3b4 | ||
|
|
a83e68815a | ||
|
|
d87aebb718 | ||
|
|
a9a4d14e8a | ||
|
|
9ed2fc7359 | ||
|
|
caad382a95 | ||
|
|
ea39fc9c48 | ||
|
|
bf7cce52db | ||
|
|
63a15a3359 | ||
|
|
db34392210 | ||
|
|
cc4e0ba6c1 | ||
|
|
38989f9c68 | ||
|
|
c78c92e539 | ||
|
|
31e694f50d | ||
|
|
5368199517 | ||
|
|
6bbb9176fc | ||
|
|
4209eee2f8 | ||
|
|
f65ebb6b71 | ||
|
|
ef8107e56a | ||
|
|
2293a30f19 | ||
|
|
d7678fd355 | ||
|
|
27d8a5cf99 | ||
|
|
03f6c58ac6 | ||
|
|
4fb52dc6fc | ||
|
|
0232ba3f25 | ||
|
|
5987e4c8e1 | ||
|
|
18ce7c8f2f | ||
|
|
177c9da8b5 | ||
|
|
b5f1a8e90f | ||
|
|
494c3dd1bd | ||
|
|
ad8f78d51e | ||
|
|
5112801c37 | ||
|
|
226adfdba2 | ||
|
|
22c0375dca | ||
|
|
66ebfef619 | ||
|
|
7e75513151 | ||
|
|
e77f6c9f6f | ||
|
|
5bd7c0ab8b | ||
|
|
97f7f6f7d2 | ||
|
|
d65fc70f07 | ||
|
|
dcae85eae8 | ||
|
|
686a48298b | ||
|
|
8ca590559d | ||
|
|
70251222cc | ||
|
|
e55c68d27e | ||
|
|
da4f2ef6b3 | ||
|
|
dbf2cabd38 | ||
|
|
72e68a163c | ||
|
|
919a87bf47 | ||
|
|
bea0bbfcdb | ||
|
|
c12d7a8d82 | ||
|
|
711ad1750a | ||
|
|
825567d449 | ||
|
|
800439d29e | ||
|
|
e3517dde13 | ||
|
|
f2da8473a4 | ||
|
|
9cc9a6e9b4 | ||
|
|
873406732f | ||
|
|
14ab950a6c | ||
|
|
6cd8e71f4f | ||
|
|
4aeaec9dc7 | ||
|
|
e318228a08 | ||
|
|
d22efbf745 | ||
|
|
90309d5552 | ||
|
|
72842ecd7a | ||
|
|
a1b32ffca9 | ||
|
|
44d225e6ca | ||
|
|
37ab5f9d7a | ||
|
|
61fdcec511 | ||
|
|
45cc4fd97a | ||
|
|
3228b88312 | ||
|
|
a1d3592d08 | ||
|
|
c686d950d0 | ||
|
|
ca779bb0af | ||
|
|
90f64e2527 | ||
|
|
444d50f751 | ||
|
|
2f9c72c1cf | ||
|
|
1bb81614a5 | ||
|
|
888e13e198 | ||
|
|
8166642ff9 | ||
|
|
51c42790b7 | ||
|
|
f105fd1b2c | ||
|
|
fe78e9a336 | ||
|
|
2fce25b0c8 | ||
|
|
6c0da2ea94 | ||
|
|
a353e69648 | ||
|
|
ac930d5504 | ||
|
|
d4cf8037b7 | ||
|
|
fb1fd851b0 | ||
|
|
2ff8c0b128 | ||
|
|
d232229abf | ||
|
|
490e58fb52 | ||
|
|
a8582be54d | ||
|
|
30bb8449e9 | ||
|
|
adb7132e02 | ||
|
|
4a1e488bd7 | ||
|
|
d200db0eeb | ||
|
|
28e06fa684 | ||
|
|
c4cb9b07cb | ||
|
|
817fc5d4b3 | ||
|
|
2de1e5f71a | ||
|
|
5246d85f11 | ||
|
|
9526ed0258 | ||
|
|
736add031c | ||
|
|
d4042ebaa2 | ||
|
|
54e31be3b2 | ||
|
|
b630be8c8a | ||
|
|
0aca41f9a6 | ||
|
|
a3fed0f84b | ||
|
|
1414ad6d50 | ||
|
|
ed6b4dabf8 | ||
|
|
d9309ebc6e | ||
|
|
c49b7613e0 | ||
|
|
4f88b6dc71 | ||
|
|
5c9e6404cc | ||
|
|
80df494787 | ||
|
|
c0886c2785 | ||
|
|
a83a56eecd | ||
|
|
130eb56d09 | ||
|
|
b60b473e02 | ||
|
|
e0504eb957 | ||
|
|
3886e41e94 | ||
|
|
edc54c7120 | ||
|
|
cef1167ef1 | ||
|
|
f456500f3a | ||
|
|
59328ea44d | ||
|
|
0dc840dc8e | ||
|
|
6700028bd1 | ||
|
|
213b1d1d0d | ||
|
|
feab64b09a | ||
|
|
f9f096cca8 | ||
|
|
535d174c2b | ||
|
|
11d2401970 | ||
|
|
232b36d4ae | ||
|
|
b38b159f4e | ||
|
|
606bf1ff58 | ||
|
|
46cec638dd | ||
|
|
8637397c86 | ||
|
|
7502e1881f | ||
|
|
734d5e50c5 | ||
|
|
052ff6727b | ||
|
|
2962dbd6b8 | ||
|
|
392afd6f33 | ||
|
|
fc3f4dff10 | ||
|
|
24d6889b24 | ||
|
|
27e3e22703 | ||
|
|
7b7f609c47 | ||
|
|
5389c8858a | ||
|
|
c2d2fbba96 | ||
|
|
cfc039dae1 | ||
|
|
edf24dc992 | ||
|
|
97710296ac | ||
|
|
acbfc0bb81 | ||
|
|
e1fe2fb093 | ||
|
|
dd5c1ec9ed | ||
|
|
c7f7614646 | ||
|
|
d604398642 | ||
|
|
d40b1d8937 | ||
|
|
49b4b476dc | ||
|
|
c0ec689be9 | ||
|
|
8e26decb5b | ||
|
|
921efcbf4b | ||
|
|
705ab58bfb | ||
|
|
b42e32a955 | ||
|
|
dec60a0fdd | ||
|
|
fa84d5c502 | ||
|
|
34c763caf5 | ||
|
|
e5709dfabc | ||
|
|
5f4f4a8ab9 | ||
|
|
ca9e71087b | ||
|
|
6da483b3ef | ||
|
|
e300145263 | ||
|
|
eb3f0035fe | ||
|
|
6be3a2a142 | ||
|
|
e23893a419 | ||
|
|
7b4c1dcde0 | ||
|
|
4b7cb2a22a | ||
|
|
344a8a3887 | ||
|
|
0afda5dc27 | ||
|
|
0891ef6d0a | ||
|
|
cdb64ecb19 | ||
|
|
b05ac4f2a4 | ||
|
|
411189a076 | ||
|
|
63cf4d46c9 | ||
|
|
3c683f2192 | ||
|
|
d46e59bcd4 | ||
|
|
c45e76ec31 | ||
|
|
44828707ea | ||
|
|
74c047d249 | ||
|
|
50abfb98fe | ||
|
|
6104657970 | ||
|
|
02116d4c05 | ||
|
|
23f993bb54 | ||
|
|
16aedd61da | ||
|
|
5a2f3ad616 | ||
|
|
54971104f8 | ||
|
|
deeffbf77d | ||
|
|
7e8dd6bba8 | ||
|
|
dc4078d744 | ||
|
|
1eb168be55 | ||
|
|
3c6fd365fb | ||
|
|
53c9184057 | ||
|
|
16c9872571 | ||
|
|
17160bc467 | ||
|
|
3b39e58cf3 | ||
|
|
7db055116c | ||
|
|
0262ff1aac | ||
|
|
14def09ce3 | ||
|
|
96e59da6bc | ||
|
|
8f75b0a0c0 | ||
|
|
70a40cfc45 | ||
|
|
ef6b8b9ebc | ||
|
|
f9dbaa9407 | ||
|
|
ed338668d1 | ||
|
|
1e5d94a958 | ||
|
|
51553c565f | ||
|
|
ae36805aa1 | ||
|
|
22b7445ac4 | ||
|
|
9c278d7d0b | ||
|
|
3a1592692e | ||
|
|
bdec956708 | ||
|
|
d0d8a8a3af | ||
|
|
f787962be8 | ||
|
|
57b9b369b7 | ||
|
|
ccda5bdb7e | ||
|
|
bd5fa83fe0 | ||
|
|
59caf381f7 | ||
|
|
181cb1b1bd | ||
|
|
ba2dd2d872 | ||
|
|
4dc5acd68e | ||
|
|
f57116afbe | ||
|
|
99c41c7e34 | ||
|
|
d7b38d9513 | ||
|
|
2c7aad1dcd | ||
|
|
593dba72a8 | ||
|
|
09f2f2a9e7 | ||
|
|
6c583eedb9 | ||
|
|
1c4d7a6ad1 | ||
|
|
f75ed4bc66 | ||
|
|
f487deb7b9 | ||
|
|
ad30a8476c | ||
|
|
60db807443 | ||
|
|
d1dedff9ca | ||
|
|
9a04506b0d | ||
|
|
a58369fbb1 | ||
|
|
d63b5d71a1 | ||
|
|
360d790282 | ||
|
|
a0dd8f8e0f | ||
|
|
db7c001076 | ||
|
|
c96f905d2b | ||
|
|
4b3f04083b | ||
|
|
8276b6c9a9 | ||
|
|
43d6e788dc | ||
|
|
0c062a8485 | ||
|
|
99b649f24e | ||
|
|
7c6532f145 | ||
|
|
052669a0b0 | ||
|
|
0cf86d3bbc | ||
|
|
56a16b862a | ||
|
|
b2fffb2e23 | ||
|
|
3f7a27cdbb | ||
|
|
58e30b8c88 | ||
|
|
4025e55b95 | ||
|
|
e1e63ebd64 | ||
|
|
8279df48bf | ||
|
|
d86a06fab0 | ||
|
|
90b24dd915 | ||
|
|
bacd2a6893 | ||
|
|
0f059f247d | ||
|
|
e2b76d9c29 | ||
|
|
1107a2f2bc | ||
|
|
efd43013da | ||
|
|
7b8458b47d | ||
|
|
84eed09a17 | ||
|
|
35b1a40d49 | ||
|
|
81d7fe3fdb | ||
|
|
02187fed4e | ||
|
|
019bf013ac | ||
|
|
d6e59a6a0a | ||
|
|
46aa862943 | ||
|
|
0413cab0d9 | ||
|
|
3357ce8f33 | ||
|
|
1776f6e7fd | ||
|
|
edfe5e1156 | ||
|
|
0768992848 | ||
|
|
1224f94879 | ||
|
|
b58c5344b8 | ||
|
|
7175bc0595 | ||
|
|
b7a6f5696d | ||
|
|
abf5b89c28 | ||
|
|
d554444b0e | ||
|
|
16ae0725e6 | ||
|
|
61feced541 | ||
|
|
a1d4db2f1e | ||
|
|
357e9af627 | ||
|
|
a41519be63 | ||
|
|
870e6b07c8 | ||
|
|
6f41759519 | ||
|
|
6727c42f18 | ||
|
|
90c367842f | ||
|
|
a0bb6e370e | ||
|
|
f2910ab9d1 | ||
|
|
b4bfed2ccb | ||
|
|
2fcde61b6d | ||
|
|
ffddf10de5 | ||
|
|
6e3bd5e6f3 | ||
|
|
b21230c4d6 | ||
|
|
0a533b64e1 | ||
|
|
15b0e321bd | ||
|
|
4d749340a2 | ||
|
|
0ef6ffa452 | ||
|
|
d7b1310ba3 | ||
|
|
7408454a75 | ||
|
|
07b71468cc | ||
|
|
522e966194 | ||
|
|
937c60c9cf | ||
|
|
bbb1e22163 | ||
|
|
a16e83200a | ||
|
|
d437521710 | ||
|
|
5cbf4cf352 | ||
|
|
2985e3b75b | ||
|
|
f34a75fc5b | ||
|
|
5aa88714b8 | ||
|
|
ce56a414e0 | ||
|
|
ba4a7dcd45 | ||
|
|
85c648da6c | ||
|
|
483f8eb690 | ||
|
|
93c868d698 | ||
|
|
a14e70e3f4 | ||
|
|
a6ff606cae | ||
|
|
651eb3374c | ||
|
|
68c71adc5a | ||
|
|
0c4ca9c94d | ||
|
|
8c04f5b3f1 | ||
|
|
35b29a0a1e | ||
|
|
d289f432b1 | ||
|
|
e16e269775 | ||
|
|
4e5d0c2e84 | ||
|
|
c9a2034936 | ||
|
|
b70fc1151d | ||
|
|
c11034edcd | ||
|
|
804d9b42a5 | ||
|
|
b1bb4e6758 | ||
|
|
76ed8f0ba2 | ||
|
|
4dde7eaea1 | ||
|
|
2e2149c110 | ||
|
|
70bb9477c5 | ||
|
|
ec5363e9c1 | ||
|
|
dba3b1c559 | ||
|
|
9606e3f80c | ||
|
|
7bc7b500f5 | ||
|
|
c6e804fa10 | ||
|
|
1cbaf9bd9d | ||
|
|
45145685d5 | ||
|
|
2fbec6f21f | ||
|
|
ad29d2765f | ||
|
|
e47e751142 | ||
|
|
c63d4ccf3e | ||
|
|
e5c30cf841 | ||
|
|
c80678aac5 | ||
|
|
1754570057 | ||
|
|
d87b411193 | ||
|
|
8fc6284317 | ||
|
|
eae49d2367 | ||
|
|
69287c5198 | ||
|
|
e6b3984f78 | ||
|
|
547fe888d4 | ||
|
|
3454309cbc | ||
|
|
544c46cd44 | ||
|
|
2c100825cc | ||
|
|
558ecd84a6 | ||
|
|
df24cfff4f | ||
|
|
bd5d93a964 | ||
|
|
ae2ded119f | ||
|
|
abdb80a6be | ||
|
|
2f9cbe2bf1 | ||
|
|
2cca7d60d5 | ||
|
|
3df745d1d2 | ||
|
|
9862083e0b | ||
|
|
7a4976c470 | ||
|
|
8834a19743 | ||
|
|
6e15403f60 | ||
|
|
7e1cf10cb2 | ||
|
|
ee762c3c68 | ||
|
|
32c06414c5 | ||
|
|
e97e1ba4bc | ||
|
|
2f580f7800 | ||
|
|
1ce1459455 | ||
|
|
c26573482e | ||
|
|
414ec08dee | ||
|
|
1cc78191eb | ||
|
|
75c6c6081a | ||
|
|
8d2ebe9718 | ||
|
|
eed974b883 | ||
|
|
ae846dac4d | ||
|
|
0b09c00b50 | ||
|
|
f7a1874cb3 | ||
|
|
28fb04eb7b | ||
|
|
34310cf420 | ||
|
|
e1d61d7190 | ||
|
|
9c14ac84cb | ||
|
|
1d1ea7b6f2 | ||
|
|
92401f5b7c | ||
|
|
38ac9218ec | ||
|
|
48497c749a | ||
|
|
72a1892058 | ||
|
|
f2c328d212 | ||
|
|
e9eafc40a7 | ||
|
|
933ca1bf81 | ||
|
|
b4fc9aa7eb | ||
|
|
dcc475bbef | ||
|
|
1fe35ad0cd | ||
|
|
f1ed1e0f14 | ||
|
|
fcc746fb98 | ||
|
|
95934a5b7a | ||
|
|
d38b101820 | ||
|
|
91d730a7bc | ||
|
|
0cfa77b628 | ||
|
|
ca4881ad51 | ||
|
|
8c2c064fe2 | ||
|
|
10646b9b86 | ||
|
|
967b195946 | ||
|
|
1ae7771290 | ||
|
|
a585fe4d54 | ||
|
|
fa3a9fe70e | ||
|
|
99952a393f | ||
|
|
920a41e3ca | ||
|
|
e5bec957a1 | ||
|
|
41cb765255 | ||
|
|
2d12a3cd7a | ||
|
|
df4fe0176c | ||
|
|
4fcc80719e | ||
|
|
f6c66f6ee4 | ||
|
|
220d137e66 | ||
|
|
425803a1b6 | ||
|
|
c794ea614a | ||
|
|
9000838aab | ||
|
|
2790bda1e9 | ||
|
|
e13d4daa9a | ||
|
|
2f504a4e03 | ||
|
|
598a50a133 | ||
|
|
1b06a5a3e0 | ||
|
|
9f1d3b0269 | ||
|
|
a09543d38b | ||
|
|
2ab3539925 | ||
|
|
23ddf53abe | ||
|
|
d8720d0849 | ||
|
|
6753b55160 | ||
|
|
7f7f48ad56 | ||
|
|
149ca01029 | ||
|
|
5c8133a810 | ||
|
|
2adccdd1b0 | ||
|
|
b91068d658 | ||
|
|
4534cafd3f | ||
|
|
405e79d729 | ||
|
|
4df2349e9d | ||
|
|
a9b61d3e13 | ||
|
|
3767d14e5c | ||
|
|
889a050f25 | ||
|
|
0701fac807 | ||
|
|
9fba91069a | ||
|
|
4f9ce70ff8 | ||
|
|
5e00d4ded7 | ||
|
|
95cd9ee5bb | ||
|
|
40f16f8ef1 | ||
|
|
3d9288f82f | ||
|
|
c51f12f88b | ||
|
|
0618153390 | ||
|
|
a7c066291b | ||
|
|
a69ac372fa | ||
|
|
16b2a54981 | ||
|
|
3f68e0d66f | ||
|
|
12d483fde6 | ||
|
|
96034a9712 | ||
|
|
d2def4479b | ||
|
|
afbbb913e7 | ||
|
|
ad76f239a3 | ||
|
|
e6b096c9e0 | ||
|
|
6e26b4e6c7 | ||
|
|
ea79494b6d | ||
|
|
afb18a3e4d | ||
|
|
f9c9853102 | ||
|
|
b3eb9fb6fa | ||
|
|
d3b97bf51a | ||
|
|
7a2e491199 | ||
|
|
25efaf08b7 | ||
|
|
f893ea6b98 | ||
|
|
500745b62c | ||
|
|
9ebe5bf1a7 | ||
|
|
4aecb86daa | ||
|
|
6924dd6df6 | ||
|
|
431755144e | ||
|
|
d182f81754 | ||
|
|
de0193fffc | ||
|
|
53e86205ad | ||
|
|
aa670efe3a | ||
|
|
e693fe49a7 | ||
|
|
7eaa32d85f | ||
|
|
ab40d2c37a | ||
|
|
784206b39b | ||
|
|
7c8264e221 | ||
|
|
db7195aa30 | ||
|
|
eb7bbc1612 | ||
|
|
ee3792181d | ||
|
|
9804965a20 | ||
|
|
b84842df3d | ||
|
|
fc170d3033 | ||
|
|
8fa4ec7ad8 | ||
|
|
480825ddd7 | ||
|
|
260e328cc1 | ||
|
|
8873428b4b | ||
|
|
ab43c20b8f | ||
|
|
88d9d4f4a3 | ||
|
|
d4840f85c0 | ||
|
|
6f9ddeaed0 | ||
|
|
af5e73c8cb | ||
|
|
333b6e60e1 | ||
|
|
1b61337b75 | ||
|
|
77991896b4 | ||
|
|
60a670ce29 | ||
|
|
c1c69ed22b | ||
|
|
d71c74c6fb | ||
|
|
590aa8b43f | ||
|
|
607e0166f6 | ||
|
|
130c83ee92 | ||
|
|
fd5e418abf | ||
|
|
262d160314 | ||
|
|
9146827590 | ||
|
|
062b108259 | ||
|
|
ba796b6be1 | ||
|
|
3d763235e1 | ||
|
|
c30c6d9f10 | ||
|
|
311716ed18 | ||
|
|
19bb1b4aa4 | ||
|
|
b8749e36b9 | ||
|
|
00b6639155 | ||
|
|
71d7daaef3 | ||
|
|
8654c5d471 | ||
|
|
02124b3d38 | ||
|
|
340dcfb70d | ||
|
|
a37b92223a | ||
|
|
7d2b8cb4fc | ||
|
|
8d7a134cb4 | ||
|
|
4b849d7201 | ||
|
|
e03e185d30 | ||
|
|
7a02df5588 | ||
|
|
19494685ba | ||
|
|
1e26943c3e | ||
|
|
83fa850142 | ||
|
|
968a116d14 | ||
|
|
fb55b494d7 | ||
|
|
59b6a83d7d | ||
|
|
aabc4f0d7b | ||
|
|
391f686173 | ||
|
|
8e6f6d46ec | ||
|
|
2ba7a55439 | ||
|
|
e07df29ab9 | ||
|
|
abf24fe60f | ||
|
|
fad5f5b81f | ||
|
|
6961f49a0c | ||
|
|
6911f8652a | ||
|
|
6658cec6a0 | ||
|
|
14011b9d84 | ||
|
|
bd2d0b6790 | ||
|
|
d36f58230a | ||
|
|
018f950ca3 | ||
|
|
db8db9fae9 | ||
|
|
79ce8d6563 | ||
|
|
13eaa9a35a | ||
|
|
39f0d76b4b | ||
|
|
0a5832ec75 | ||
|
|
6e147b3ed2 | ||
|
|
c162f79daa | ||
|
|
87585be687 | ||
|
|
ea08d6413c | ||
|
|
879905edf6 | ||
|
|
6fd80a5582 | ||
|
|
0dc7333563 | ||
|
|
f61c3168d2 | ||
|
|
9cadd74a96 | ||
|
|
729fa2352b | ||
|
|
b673aaf9f0 | ||
|
|
3132cc6005 | ||
|
|
ac994d3077 | ||
|
|
02d4f7f2da | ||
|
|
d99569f005 | ||
|
|
ec5166249a | ||
|
|
dadd12adb3 | ||
|
|
88b4fb8c2a | ||
|
|
afecae3786 | ||
|
|
d18598bc33 | ||
|
|
794fc05ada | ||
|
|
5daeb7f876 | ||
|
|
53e71c545e | ||
|
|
959a55e36c | ||
|
|
64572b0024 | ||
|
|
9a0c1caa43 | ||
|
|
eed6723147 | ||
|
|
97fabf51b8 | ||
|
|
5e5e2b8aee | ||
|
|
e01071426f | ||
|
|
eed1bfbe50 | ||
|
|
0c3970a266 | ||
|
|
267cfb621e | ||
|
|
0e90febab2 | ||
|
|
31d947837f | ||
|
|
017b11fbba | ||
|
|
3c492062a9 | ||
|
|
b26b49d0ca | ||
|
|
ed08123550 | ||
|
|
add2db5b7a | ||
|
|
f272d7121a | ||
|
|
577556678c | ||
|
|
e146922367 | ||
|
|
6f1548b7f8 | ||
|
|
9e6fe47b44 | ||
|
|
60cfea1126 | ||
|
|
80a4a094af | ||
|
|
70e1560cb3 | ||
|
|
725033659a | ||
|
|
059111fb57 | ||
|
|
d4a5eadf13 | ||
|
|
79cf487ac5 | ||
|
|
52ecbab859 | ||
|
|
adfc79bf92 | ||
|
|
2447bab924 | ||
|
|
1057ca78a6 | ||
|
|
7e7f98fd92 | ||
|
|
64552ce2de | ||
|
|
7506256f42 | ||
|
|
db75230521 | ||
|
|
f8955d5607 | ||
|
|
0bad217b93 | ||
|
|
4da400a136 | ||
|
|
24740bd341 | ||
|
|
3b6a15de84 | ||
|
|
ac1f525a6c | ||
|
|
e3999bdb0c | ||
|
|
ad3d5a30ec | ||
|
|
e4b5847725 | ||
|
|
1a91a245a3 | ||
|
|
229f62d071 | ||
|
|
b96fe16770 | ||
|
|
97750cb5e2 | ||
|
|
e1a2bd11a9 | ||
|
|
229b408252 | ||
|
|
ae929438a5 | ||
|
|
5daaf84e05 | ||
|
|
19b09515a1 | ||
|
|
9ce6078c8b | ||
|
|
51f588f4b1 | ||
|
|
5ee6605703 | ||
|
|
7ef97cfd81 | ||
|
|
f4288f0bd4 | ||
|
|
4a701cb993 | ||
|
|
00dfb07b15 | ||
|
|
5fffa8e9db | ||
|
|
54d187a0ad | ||
|
|
192ce468b7 | ||
|
|
75c0cadb50 | ||
|
|
5d578d4b3b | ||
|
|
325a8889ab | ||
|
|
9cdd78e68c | ||
|
|
3a6770a1ae | ||
|
|
8073924056 | ||
|
|
7b53e1c54b | ||
|
|
c4c0516820 | ||
|
|
8d36f8850e | ||
|
|
abe5f43f3d | ||
|
|
c8d8a8d0b5 | ||
|
|
f60e88573a | ||
|
|
4216671ea2 | ||
|
|
ee3ea7a970 | ||
|
|
2b644dbb01 | ||
|
|
63878e7ffd | ||
|
|
007cd6cff1 | ||
|
|
4375215baa | ||
|
|
8cc5e9db13 | ||
|
|
5685f831a7 | ||
|
|
0cb3d12d94 | ||
|
|
0e38c6751b | ||
|
|
70ad1fb3d8 | ||
|
|
44f27d91a0 | ||
|
|
1bb559c285 | ||
|
|
7a005ef126 | ||
|
|
030c2a740f | ||
|
|
5dcde67ae9 | ||
|
|
ee06fa85f1 | ||
|
|
5b9352a946 | ||
|
|
b7927d8d75 | ||
|
|
c144f30606 | ||
|
|
d2dba3a0db | ||
|
|
2c991583ff | ||
|
|
2e14dec12d | ||
|
|
8826f0ff3c | ||
|
|
9129f7fb33 | ||
|
|
c0ed54406f | ||
|
|
18be257e10 | ||
|
|
615d549494 | ||
|
|
0ce39e7f52 | ||
|
|
3c68cbc955 | ||
|
|
300430e2d5 | ||
|
|
166a07732a | ||
|
|
510b517270 | ||
|
|
dea385384a | ||
|
|
7a1c9101b2 | ||
|
|
2be523cf77 | ||
|
|
c01e334487 | ||
|
|
a2418d1373 | ||
|
|
a697248b26 | ||
|
|
6058939c00 | ||
|
|
318de530e3 | ||
|
|
9e04b7796a | ||
|
|
e8099c4db5 | ||
|
|
bf808811cc | ||
|
|
f0293de1b9 | ||
|
|
810dcb90ce | ||
|
|
a2f2b8fabc | ||
|
|
cbc5f47786 | ||
|
|
3e3886ced7 | ||
|
|
9ce39fd2ba | ||
|
|
5b08cdedf0 | ||
|
|
67e4d40c49 | ||
|
|
537a733157 | ||
|
|
5136e7726d | ||
|
|
6e236ba74d | ||
|
|
374b665089 | ||
|
|
ffecc9a0c7 | ||
|
|
0b997418d3 | ||
|
|
eaad8a4cf5 | ||
|
|
396b4595f4 | ||
|
|
0752aae9ef | ||
|
|
ad2221a677 | ||
|
|
1713d693b1 | ||
|
|
f4f056449f | ||
|
|
6a70e3e45b | ||
|
|
a04cdee33f | ||
|
|
157769eeb4 | ||
|
|
667b66b926 | ||
|
|
c0f7b344d9 | ||
|
|
060c59e97d | ||
|
|
b3461b7134 | ||
|
|
001c450abb | ||
|
|
ceaa5763d4 | ||
|
|
b45fd58944 | ||
|
|
b3149def82 | ||
|
|
378d498402 | ||
|
|
98f52b32a3 | ||
|
|
0ab32a6f84 | ||
|
|
71cc22325d | ||
|
|
e1b2991aa6 | ||
|
|
033bcf80d0 | ||
|
|
103118d558 | ||
|
|
f91b5fa004 | ||
|
|
7179bf7b67 | ||
|
|
a3e6239e6e | ||
|
|
1fa12e56c6 | ||
|
|
4ff834de76 | ||
|
|
6db38ad769 | ||
|
|
293b7b09a9 | ||
|
|
d5945f9ee7 | ||
|
|
d1f5a6fc31 | ||
|
|
e7b9f5e4c3 | ||
|
|
7870749077 | ||
|
|
c5352f443a | ||
|
|
fd8b7aa0f2 | ||
|
|
458ea266ec | ||
|
|
9748eaba25 | ||
|
|
887a3740b2 | ||
|
|
2e7cfe9cd7 | ||
|
|
6dbe156a02 | ||
|
|
2a9ef6d48e | ||
|
|
6717ddbd0b | ||
|
|
47c1aab064 | ||
|
|
eda41658b9 | ||
|
|
7f79363944 | ||
|
|
25967f2a09 | ||
|
|
4d3963ad67 | ||
|
|
f78c5257dc | ||
|
|
ccc6234ac8 | ||
|
|
c81b0200eb | ||
|
|
f039d37c8a | ||
|
|
237975bfef | ||
|
|
015bc7c8c3 | ||
|
|
3da2a00ee9 | ||
|
|
16eca5bebf | ||
|
|
a4483cf255 | ||
|
|
0bf020a1b4 | ||
|
|
d43927a167 | ||
|
|
a62566e8fb | ||
|
|
23a1730106 | ||
|
|
f8ac5e0af3 | ||
|
|
eb48a153d9 | ||
|
|
1a78a6f786 | ||
|
|
f8f60c62fe | ||
|
|
453e507b89 | ||
|
|
022c32f9d5 | ||
|
|
af1a0c3520 | ||
|
|
0a6d9dfcf4 | ||
|
|
5bdedacab1 | ||
|
|
d7a1be2f3c | ||
|
|
d6dcbb63d4 | ||
|
|
b2770f67a1 | ||
|
|
aa2691b153 | ||
|
|
e9a9cbbd07 | ||
|
|
17e2222802 | ||
|
|
58b2970b19 | ||
|
|
fd69961185 | ||
|
|
e5cd813958 | ||
|
|
5b12423d98 | ||
|
|
4141f633a3 | ||
|
|
67854b3ebd | ||
|
|
0c21dbc7c8 | ||
|
|
5925aa50d8 | ||
|
|
852b016111 | ||
|
|
ba77a67ba7 | ||
|
|
c14a9a55d7 | ||
|
|
5203db6c9c | ||
|
|
30eb8dda1d | ||
|
|
69d40b5fe8 | ||
|
|
706e87659e | ||
|
|
5c785e49af | ||
|
|
0974085c6f | ||
|
|
e67ced8848 | ||
|
|
c2dea6b881 | ||
|
|
ee62b2cf31 | ||
|
|
252e06bee6 | ||
|
|
1f0ce88e08 | ||
|
|
7e8fb388a3 | ||
|
|
a3de360878 | ||
|
|
6ee556e386 | ||
|
|
623ed89100 | ||
|
|
8c114cae95 | ||
|
|
93dd58ec59 | ||
|
|
f0bc93ad8e | ||
|
|
27e8aad479 | ||
|
|
6298578db9 | ||
|
|
f079e5dadd | ||
|
|
e6fdead89f | ||
|
|
cfa6e3982c | ||
|
|
e4bc4d9071 | ||
|
|
d372e10f1a | ||
|
|
d1c93754db | ||
|
|
3d54a1abf3 | ||
|
|
06eef5779d | ||
|
|
b4d78376fb | ||
|
|
87a59a6de3 | ||
|
|
1e7741e341 | ||
|
|
ae5e484506 | ||
|
|
c9dd219565 | ||
|
|
55eb662dc9 | ||
|
|
2d202088c7 | ||
|
|
9f7c9180d9 | ||
|
|
fdc5e0a92d | ||
|
|
7f6fef1373 | ||
|
|
e4973f572f | ||
|
|
eb768f2076 | ||
|
|
df9723a011 | ||
|
|
51ad3fdb0b | ||
|
|
8e553e7a93 | ||
|
|
4c70e92293 | ||
|
|
3e983b121e | ||
|
|
9f3c962ea4 | ||
|
|
2a1a3fb1b5 | ||
|
|
bb28cc5c65 | ||
|
|
23b6a38e18 | ||
|
|
715cd9daf5 | ||
|
|
cbfdaec394 | ||
|
|
bb527ac981 | ||
|
|
961c26894d | ||
|
|
693bdebb30 | ||
|
|
353e24f1c5 | ||
|
|
59d1773057 | ||
|
|
93a1368b60 | ||
|
|
3bc0fe5a70 | ||
|
|
973c11a048 | ||
|
|
5094386516 | ||
|
|
64477c6573 | ||
|
|
f052c707e7 | ||
|
|
de0e1d3e10 | ||
|
|
e273da1b5b | ||
|
|
761f6963ab | ||
|
|
e5aff1316a | ||
|
|
8ee0fbe6a3 | ||
|
|
4c6b8b4173 | ||
|
|
6940a75591 | ||
|
|
7ba939b008 | ||
|
|
6918a36bee | ||
|
|
ba132fc411 | ||
|
|
b4a940a8d6 | ||
|
|
5e0dd5c63b | ||
|
|
f19114e530 | ||
|
|
0db40ecf0f | ||
|
|
8289067a4e | ||
|
|
9c5e3d094b | ||
|
|
cb12b19c1e | ||
|
|
5d0b8588f9 | ||
|
|
0c05e1036d | ||
|
|
266087c5f1 | ||
|
|
147b94d936 | ||
|
|
872511ebb9 | ||
|
|
ce8ed5bfeb | ||
|
|
d81838dfc4 | ||
|
|
79ec3594fe | ||
|
|
cdb246697e | ||
|
|
6476e688e5 | ||
|
|
5d1ec6a9c8 | ||
|
|
be8a7e981a | ||
|
|
d59731a678 | ||
|
|
0254510d53 | ||
|
|
9327955891 | ||
|
|
4daf08e20f | ||
|
|
6fc31ddedb | ||
|
|
fac8c9ee4e | ||
|
|
d05f7e2084 | ||
|
|
0a0a6bae0f | ||
|
|
560c063db4 | ||
|
|
54ac2d33e2 | ||
|
|
fb3be8a6a0 | ||
|
|
5a33953b78 | ||
|
|
ba7a8fc796 | ||
|
|
0bdee8219a | ||
|
|
f82951f412 | ||
|
|
35e188b851 | ||
|
|
8990e4666a | ||
|
|
ceff618e5d | ||
|
|
cf3aab9d38 | ||
|
|
a74c70e8a1 | ||
|
|
46817c7664 | ||
|
|
c0c9cab14c | ||
|
|
478a034740 | ||
|
|
01693cb155 | ||
|
|
7a44c9e650 | ||
|
|
70a6a275f4 | ||
|
|
e627ebc127 | ||
|
|
56b81b78c3 | ||
|
|
c304485079 | ||
|
|
df51797c29 | ||
|
|
754339214c | ||
|
|
057ecc3ed9 | ||
|
|
c14f79ebf7 | ||
|
|
71fdff17de | ||
|
|
04b4001277 | ||
|
|
fbfb8a3b41 | ||
|
|
1bee088fe6 | ||
|
|
d2e4d6ecf0 | ||
|
|
5f03f90582 | ||
|
|
e54d46aae1 | ||
|
|
54a3b9900e | ||
|
|
1dc16e900a | ||
|
|
08a7e666b2 | ||
|
|
678fd28f1d | ||
|
|
ff89c3b274 | ||
|
|
cff7aebe55 | ||
|
|
ed3a3d0876 | ||
|
|
425cd9eb26 | ||
|
|
ebe84dd8a4 | ||
|
|
217c4144b5 | ||
|
|
aaeed64621 | ||
|
|
9133a56d2a | ||
|
|
12bd7dc44f | ||
|
|
32fa86adaa | ||
|
|
1811cff1f9 | ||
|
|
2e6a5c0525 | ||
|
|
b68b214d08 | ||
|
|
153d12d93f | ||
|
|
1a62a773ae | ||
|
|
5f0cccb81e | ||
|
|
5c7f4b3df7 | ||
|
|
1248b76b41 | ||
|
|
c4176af1ea | ||
|
|
799c306138 | ||
|
|
c65e3fdf62 | ||
|
|
08712ef4f8 | ||
|
|
d0119f5bf1 | ||
|
|
f3c626c800 | ||
|
|
f1891478d5 | ||
|
|
7a2e6e640d | ||
|
|
890d418639 | ||
|
|
f38c934a6d | ||
|
|
f3540aac0f | ||
|
|
889ce984a9 | ||
|
|
89a437149c | ||
|
|
43f65651ac | ||
|
|
d74d69c1c8 | ||
|
|
fefc85683c | ||
|
|
6f97158c0e | ||
|
|
1320101112 | ||
|
|
031a267394 | ||
|
|
9119030959 | ||
|
|
9b10a8028d | ||
|
|
4be38fcb0e | ||
|
|
9090f4485a | ||
|
|
5749d66ac9 | ||
|
|
4bb4b4eb1d | ||
|
|
103d062f74 | ||
|
|
9893480089 | ||
|
|
5dbd240017 | ||
|
|
e0dce8fd01 | ||
|
|
492139942c | ||
|
|
8ebff1a908 | ||
|
|
44def1f6bc | ||
|
|
8934b9ab5c | ||
|
|
130a6b67bd | ||
|
|
2df32cd9a7 | ||
|
|
d413d58b47 | ||
|
|
9e632aa0bd | ||
|
|
964020ee12 | ||
|
|
672e14d6ea | ||
|
|
54baf04a86 | ||
|
|
64c83460b9 | ||
|
|
74ec3fa7d4 | ||
|
|
0821d7a803 | ||
|
|
55beb3978c | ||
|
|
9b044815de | ||
|
|
0668fea3b7 | ||
|
|
a6677b2e45 | ||
|
|
6f544f56d8 | ||
|
|
5556be9cab | ||
|
|
465c4afe8d | ||
|
|
78dd1e1d81 | ||
|
|
eebfc78ad3 | ||
|
|
4783685fdb | ||
|
|
bfd0363fad | ||
|
|
e9323ba2ec | ||
|
|
dac774c9d2 | ||
|
|
664ee2b433 | ||
|
|
85f283fe2b | ||
|
|
7bd7d66afc | ||
|
|
81b16aa900 | ||
|
|
e07fb34ace | ||
|
|
9303746d80 | ||
|
|
e3e8e67cb4 | ||
|
|
6490027e57 | ||
|
|
6cbe4f2ea7 | ||
|
|
960365a063 | ||
|
|
839d614c9c | ||
|
|
ae13e557a7 | ||
|
|
a245383f8c | ||
|
|
78b8d3e41d | ||
|
|
fcfaa04cc6 | ||
|
|
fe866b2d66 | ||
|
|
e7bbc4ac0c | ||
|
|
3b746c91df | ||
|
|
06f0129b59 | ||
|
|
641e75b8a8 | ||
|
|
35f9fda457 | ||
|
|
de29d69efe | ||
|
|
f587af1005 | ||
|
|
4ed6580e1d | ||
|
|
2f6213c944 | ||
|
|
f365b76cfc | ||
|
|
55921b262f | ||
|
|
3039c97989 | ||
|
|
a1af4f19c5 | ||
|
|
131e4087fd | ||
|
|
ee6471351d | ||
|
|
d93266fee2 | ||
|
|
dbbf39db6d | ||
|
|
d40ea44ae6 | ||
|
|
f0d4847946 | ||
|
|
98a9c766ef | ||
|
|
91393b650b | ||
|
|
49a4b119e1 | ||
|
|
e69fab822b | ||
|
|
45c58cc766 | ||
|
|
ca48f000bd | ||
|
|
21ba1e3958 | ||
|
|
062f3256a7 | ||
|
|
186f565b99 | ||
|
|
5c2b4398d9 | ||
|
|
a9fb61bbd6 | ||
|
|
a51e25dbde | ||
|
|
0a717ae82e | ||
|
|
f9e6751279 | ||
|
|
0306f8ec65 | ||
|
|
66f2e549ce | ||
|
|
9ab413643a | ||
|
|
3a4eeb01b0 | ||
|
|
57a8dcc155 | ||
|
|
2f21476b2a | ||
|
|
9f9e2f3b24 | ||
|
|
f886dfb60c | ||
|
|
c22b014056 | ||
|
|
d899b6a7e1 | ||
|
|
450dde3739 | ||
|
|
2ac40903f3 | ||
|
|
f328b39f57 | ||
|
|
06cc4b07ab | ||
|
|
1c0b68f0e3 | ||
|
|
efcce6a826 | ||
|
|
fa8177d0e5 | ||
|
|
d51cd8df89 | ||
|
|
5530d611b9 | ||
|
|
e73636bef3 | ||
|
|
74ff994281 | ||
|
|
7b28d353ee | ||
|
|
e2a8ca143a | ||
|
|
4e81f98927 | ||
|
|
ab4c994266 | ||
|
|
aea6a434f1 | ||
|
|
6c95d5a2de | ||
|
|
fcaabb2c1e | ||
|
|
66b2722cad | ||
|
|
2e95666939 | ||
|
|
cdfcd99695 | ||
|
|
e71d21fc27 | ||
|
|
e95ebfd6a0 | ||
|
|
e5a875856a | ||
|
|
930218c067 | ||
|
|
ff1362e462 | ||
|
|
01457bbe79 | ||
|
|
8c7da0bdb6 | ||
|
|
6f634c3f13 | ||
|
|
a7f5303eaf | ||
|
|
7159e4fbe2 | ||
|
|
36b243e9d2 | ||
|
|
bd70e00f08 | ||
|
|
0ca96130c8 | ||
|
|
09aa56b63d | ||
|
|
60cd6a455a | ||
|
|
4752ce5250 | ||
|
|
832569a79c | ||
|
|
ecd8cebbef | ||
|
|
3c37efa650 | ||
|
|
21b6ce204d | ||
|
|
337d2970a0 | ||
|
|
3e5bd25c6e | ||
|
|
7f0f68b707 | ||
|
|
ea85482736 | ||
|
|
01160a5361 | ||
|
|
f4b5a02197 | ||
|
|
f724f10a35 | ||
|
|
0c221ba3d7 | ||
|
|
1907aaf32f | ||
|
|
d6f26b3133 | ||
|
|
c97a55e65f | ||
|
|
4a6e38f7da | ||
|
|
845ef42338 | ||
|
|
fde8de8b9e | ||
|
|
88123261ac | ||
|
|
c04b76528b | ||
|
|
04a13c2ebb | ||
|
|
6b3cc62cbe | ||
|
|
8627be07e7 | ||
|
|
5509a5bca3 | ||
|
|
dd52949a2a | ||
|
|
a310ae6566 | ||
|
|
1f8643c538 | ||
|
|
6ea313970d | ||
|
|
13e6b15308 | ||
|
|
0efc2277dd | ||
|
|
381a2740ee | ||
|
|
9e6aecd707 | ||
|
|
f5510ef1b5 | ||
|
|
8b3b16bce4 | ||
|
|
024674eef3 | ||
|
|
c8e6224946 | ||
|
|
bf11300ab3 | ||
|
|
7361a35c94 | ||
|
|
02b2cebb85 | ||
|
|
9b6ae46e92 | ||
|
|
e5e5a42736 | ||
|
|
308d8afe4e | ||
|
|
b7d88b4c0f | ||
|
|
719ca63ec1 | ||
|
|
2100cd77ce | ||
|
|
58b13ae69a | ||
|
|
2cfb416fd0 | ||
|
|
993c9b31bd | ||
|
|
b5d6f0ad36 | ||
|
|
03c05a82e4 | ||
|
|
cc887d25e4 | ||
|
|
80e2d0651b | ||
|
|
50f07f9ef5 | ||
|
|
c517bdd2e1 | ||
|
|
ca3e549dd4 | ||
|
|
51f2ca72b9 | ||
|
|
771950f1de | ||
|
|
c969e9c014 | ||
|
|
658867cb46 | ||
|
|
344692f9f6 | ||
|
|
fd083078fc | ||
|
|
9bacae4b2e | ||
|
|
8f2ad38503 | ||
|
|
76baa6c5f8 | ||
|
|
84c822a0ca | ||
|
|
ddd938fd64 | ||
|
|
e91b30f4c7 | ||
|
|
31fb1801d2 | ||
|
|
117d0f2e38 | ||
|
|
79bb79debc | ||
|
|
11cd022965 | ||
|
|
d7b28a3586 | ||
|
|
dc14245105 | ||
|
|
e772dfaa12 | ||
|
|
4d29cae936 | ||
|
|
71ed0ffe13 | ||
|
|
56d0981cee | ||
|
|
ad43d10ce4 | ||
|
|
fb6618181a | ||
|
|
43a9bc0d7b | ||
|
|
f835b14902 | ||
|
|
c1c591d1eb | ||
|
|
4348549f2d | ||
|
|
e48df87e06 | ||
|
|
e718feb1f7 | ||
|
|
3b6f3f13d4 | ||
|
|
13fabaf6aa | ||
|
|
9cfcdb1c23 | ||
|
|
2800d0dcd3 | ||
|
|
3e2055255e | ||
|
|
64a8857884 | ||
|
|
808b291c2c | ||
|
|
cae3e7136e | ||
|
|
c069a187f8 | ||
|
|
91fa932168 | ||
|
|
188158a29b | ||
|
|
a3d5cb5851 | ||
|
|
0788582528 | ||
|
|
da81abc12e | ||
|
|
81b92111ca | ||
|
|
a809e72704 | ||
|
|
cb0e4b6e87 | ||
|
|
16df8d803c | ||
|
|
ce7ac78b42 | ||
|
|
c21e0755b3 | ||
|
|
e1dc0a576d | ||
|
|
a998db0570 | ||
|
|
c79ec45adb | ||
|
|
72481e8453 | ||
|
|
3753f7d138 | ||
|
|
4d92606562 | ||
|
|
2d0b6bcfcc | ||
|
|
57fb29b600 | ||
|
|
340647cb22 | ||
|
|
a06369dd7b | ||
|
|
95fe103718 | ||
|
|
036297ef36 | ||
|
|
129c055fee | ||
|
|
c688656607 | ||
|
|
b49e8deb3e | ||
|
|
17264e7872 | ||
|
|
022c0c3a89 | ||
|
|
b8539122ed | ||
|
|
4ca906a518 | ||
|
|
7bf67869b0 | ||
|
|
a032164a99 | ||
|
|
f588e7783e | ||
|
|
f8ca6c019f | ||
|
|
f88806fc3c | ||
|
|
ee0880fab7 | ||
|
|
261c674832 | ||
|
|
e95bc82b8e | ||
|
|
6d0cc49ecd | ||
|
|
e108833db2 | ||
|
|
151fdb9bad | ||
|
|
59ca8665fe | ||
|
|
71c101b82e | ||
|
|
860030824e | ||
|
|
46c4bf6e94 | ||
|
|
53ed6e54b5 | ||
|
|
3197c356e9 | ||
|
|
cdad083d7f | ||
|
|
2e076ef3f4 | ||
|
|
46e3a27626 | ||
|
|
1247867187 | ||
|
|
b1f863cc4d | ||
|
|
823b41b7ec | ||
|
|
16a2b3b19b | ||
|
|
0a2e899363 | ||
|
|
65d431c7a0 | ||
|
|
6b617955b7 | ||
|
|
10cf0470cb | ||
|
|
f91ca796de | ||
|
|
7f1fb41d48 | ||
|
|
ceb9c70fba | ||
|
|
5c9d11861e | ||
|
|
706e6c01aa | ||
|
|
64cecb4931 | ||
|
|
31e0dfef76 | ||
|
|
dc85f93423 | ||
|
|
4d5d407655 | ||
|
|
d2424ce540 | ||
|
|
4d5de8176a | ||
|
|
c451d00eb4 | ||
|
|
a8180bddad | ||
|
|
e988364766 | ||
|
|
396697ead2 | ||
|
|
2993bd8c05 | ||
|
|
fc50bb6e57 | ||
|
|
a064066e42 | ||
|
|
a6783e537b | ||
|
|
4b1dad96cd | ||
|
|
6758b51617 | ||
|
|
54fdd2da57 | ||
|
|
3132a4965e | ||
|
|
7ee3f10a81 | ||
|
|
accd65a26a | ||
|
|
e0ada7fc48 | ||
|
|
ad1401854c | ||
|
|
e18189caae | ||
|
|
d601d35a21 | ||
|
|
66fd402f00 | ||
|
|
835a04358c | ||
|
|
af9b4e448d | ||
|
|
39e8ba42ff | ||
|
|
1e52c956a8 | ||
|
|
d261ed074e | ||
|
|
47f9be32ce | ||
|
|
a17390c157 | ||
|
|
5ca5e0d00f | ||
|
|
b0085f2741 | ||
|
|
b983095e13 | ||
|
|
dd6e018e46 | ||
|
|
6f8394a086 | ||
|
|
a0739a18e8 | ||
|
|
27d33f015f | ||
|
|
ffb7ad1417 | ||
|
|
97e6bab6e3 | ||
|
|
b311b7620c | ||
|
|
25ec8fb2ab | ||
|
|
75100cd182 | ||
|
|
50d48ee3ec | ||
|
|
0e330f983b | ||
|
|
9523a929af | ||
|
|
3fcec069ed | ||
|
|
7c2e72aebb | ||
|
|
8d6fbddd67 | ||
|
|
66b5ac8ff1 | ||
|
|
e034fc12eb | ||
|
|
a8317ccacd | ||
|
|
74376586a8 | ||
|
|
ea49296cfe | ||
|
|
992f817fef | ||
|
|
36528fceab | ||
|
|
8323b8af4d | ||
|
|
0e496181a1 | ||
|
|
f47fc7a484 | ||
|
|
0585fb4c80 | ||
|
|
bdfcf6591e | ||
|
|
cad54f0f07 | ||
|
|
a52ab1685e | ||
|
|
3182816965 | ||
|
|
a8da4b0162 | ||
|
|
ab7f6e8300 | ||
|
|
943bf477a0 | ||
|
|
168f4c0056 | ||
|
|
35fef11d2a | ||
|
|
425a8a6412 | ||
|
|
b64495f7a9 | ||
|
|
014861a7f2 | ||
|
|
17edaa0e1f | ||
|
|
bbd0325c10 | ||
|
|
316c276545 | ||
|
|
32ea0213f7 | ||
|
|
86c2f0716e | ||
|
|
68b8d7d7f2 | ||
|
|
43a22f84d9 | ||
|
|
b3a0368b95 | ||
|
|
cd79330c4c | ||
|
|
245e09c723 | ||
|
|
495728593f | ||
|
|
e9c4b0dc01 | ||
|
|
9942bf2124 | ||
|
|
0a8ba068c4 | ||
|
|
a2bb70aaec | ||
|
|
5ed25d8bcb | ||
|
|
cafc068c39 | ||
|
|
b8dde0767b | ||
|
|
f8e5e3b3c0 | ||
|
|
edc19e99a9 | ||
|
|
2b0b3827ab | ||
|
|
8afe5a0087 | ||
|
|
0ecc53f3b6 | ||
|
|
b3f2827961 | ||
|
|
9c96a4d81b | ||
|
|
4f5e363452 | ||
|
|
92572ff919 | ||
|
|
39ddaf49be | ||
|
|
627dc2d4a0 | ||
|
|
42739bbb61 | ||
|
|
261c9eefe1 | ||
|
|
e21e4d2b16 | ||
|
|
8b6b8f0c53 | ||
|
|
5a9feb4411 | ||
|
|
f16128da09 | ||
|
|
48f9997ea9 | ||
|
|
f0e87094d6 | ||
|
|
e0882e9e04 | ||
|
|
d37885ea88 | ||
|
|
d13e5e7e3f | ||
|
|
aa9a024ee1 | ||
|
|
5bbf6d2ae9 | ||
|
|
30299a9f04 | ||
|
|
23d7fe936d | ||
|
|
b50c052222 | ||
|
|
ef9e9809e2 | ||
|
|
f139c3268b | ||
|
|
e869bfd991 | ||
|
|
d5309fcaf5 | ||
|
|
c4fc49553c | ||
|
|
75704899a7 | ||
|
|
70aa3b1ff1 | ||
|
|
6154a8169b | ||
|
|
cf0173e079 | ||
|
|
cc62fc6222 | ||
|
|
6cdadf1b37 | ||
|
|
dc90a66a96 | ||
|
|
4b629d20cf | ||
|
|
437bd13fd0 | ||
|
|
7ce1dc9069 | ||
|
|
5b4e517d9d | ||
|
|
6c3ed5e533 | ||
|
|
ec2762c31a | ||
|
|
29f3158b61 | ||
|
|
d83b7276fd | ||
|
|
1336010bb2 | ||
|
|
5cb3df6db1 | ||
|
|
4be0c1c0eb | ||
|
|
33e5e74228 | ||
|
|
2b06989372 | ||
|
|
cd4da2aca3 | ||
|
|
b335951862 | ||
|
|
e95e084956 | ||
|
|
b7d569de98 | ||
|
|
8320cca5cd | ||
|
|
bad5fec0f1 | ||
|
|
200a3b65ee | ||
|
|
d9fc2a93cc | ||
|
|
94f3533c29 | ||
|
|
77f7ad309e | ||
|
|
cefd270837 | ||
|
|
037e68a376 | ||
|
|
4da0785494 | ||
|
|
53171bafec | ||
|
|
16fe77e472 | ||
|
|
9f147e5b6d | ||
|
|
c43106a744 | ||
|
|
a73eb0377d | ||
|
|
e05514b455 | ||
|
|
901c7be9a8 | ||
|
|
e503cc3003 | ||
|
|
2ff777acb7 | ||
|
|
034d73a4eb | ||
|
|
aec55e50f9 | ||
|
|
1f356a67b2 | ||
|
|
0f60cd480d | ||
|
|
316b8c66db | ||
|
|
744d4ebbaf | ||
|
|
005deaccc8 | ||
|
|
b6f78ce1af | ||
|
|
932b504d82 | ||
|
|
1cca46cf7b | ||
|
|
a8f6d2adf0 | ||
|
|
203de18053 | ||
|
|
ee12b4164b | ||
|
|
b38459439d | ||
|
|
a2eddb3580 | ||
|
|
18adbc6bf0 | ||
|
|
c1ccef25a3 | ||
|
|
b1bea73efb | ||
|
|
4175d29056 | ||
|
|
46c78c33cf | ||
|
|
3fe5a41433 | ||
|
|
f2b1f95521 | ||
|
|
94f81caf28 | ||
|
|
669a4a299c | ||
|
|
afff55045f | ||
|
|
4fbcd2ba5d | ||
|
|
016295dfee | ||
|
|
6d2bc2929a | ||
|
|
23a5e566f2 | ||
|
|
4b387961a4 | ||
|
|
1765a8a7f9 | ||
|
|
837a5b52a7 | ||
|
|
180c4e855e | ||
|
|
01d2af9961 | ||
|
|
7f3cc6269b | ||
|
|
dbc0d54491 | ||
|
|
f843f5ae9d | ||
|
|
962cb290e4 | ||
|
|
7138655dd1 | ||
|
|
93acfc2e38 | ||
|
|
91878c4591 | ||
|
|
05ec1216e0 | ||
|
|
af0e6481f8 | ||
|
|
11a745c4d9 | ||
|
|
2393da4425 | ||
|
|
95ab08e02d | ||
|
|
95fdedf12e | ||
|
|
c73dd776db | ||
|
|
891e5fea3f | ||
|
|
bb2f6f23b5 | ||
|
|
cd9b03bdb9 | ||
|
|
a619269502 | ||
|
|
153b5c028b | ||
|
|
9a33bf2210 | ||
|
|
34b4cd2231 | ||
|
|
6045cbbc62 | ||
|
|
9bbf4044e0 | ||
|
|
fcf8a64d91 | ||
|
|
2c6ab18e41 | ||
|
|
2fea294b13 | ||
|
|
b47ecab1a9 | ||
|
|
b86c294250 | ||
|
|
3eacfb91aa | ||
|
|
94164c2a71 | ||
|
|
d85eb83ea2 | ||
|
|
b2002639db | ||
|
|
347cfe253f | ||
|
|
833e1836e1 | ||
|
|
e4be38b9f7 | ||
|
|
783e7f6939 | ||
|
|
c1c54f4848 | ||
|
|
86be6be2d2 | ||
|
|
35a63e867a | ||
|
|
9c12a417ee | ||
|
|
32a019c0d6 | ||
|
|
b7e4a3c99e | ||
|
|
039062d071 | ||
|
|
83ae3e8371 | ||
|
|
852de8bdfc | ||
|
|
b8acb860aa | ||
|
|
e6849b85d1 | ||
|
|
8fa9657ba6 | ||
|
|
04b038960b | ||
|
|
52507a5a95 | ||
|
|
d8505ba2ab | ||
|
|
fa26c0997e | ||
|
|
5a0aadd2ae | ||
|
|
025549ebf8 | ||
|
|
e85a583f0a | ||
|
|
f7244ddb7a | ||
|
|
d983a519e3 | ||
|
|
ae01070b8f | ||
|
|
0ffb40f4c1 | ||
|
|
8bcffb4ad5 | ||
|
|
b2118602d9 | ||
|
|
9303f3b47b | ||
|
|
e5c43cfc4b | ||
|
|
45fc08e221 | ||
|
|
67e8511106 | ||
|
|
4f7fd0a62b | ||
|
|
88fe454962 | ||
|
|
26f7a9be0a | ||
|
|
9256926bb7 | ||
|
|
2a83318739 | ||
|
|
d6e2535a5e | ||
|
|
2bffb7e22c | ||
|
|
24a162cf86 | ||
|
|
f3104f3bc4 | ||
|
|
45f1bf6709 | ||
|
|
40b2590815 | ||
|
|
dd9ab46b5c | ||
|
|
c2aeadae33 | ||
|
|
1bd9759ab7 | ||
|
|
dcdbb05168 | ||
|
|
ae117c47e9 | ||
|
|
7f7856f0e4 | ||
|
|
aa7b7c8619 | ||
|
|
ee0cbff245 | ||
|
|
c2c18b25d2 | ||
|
|
816c7c95ed | ||
|
|
cb5d65d11a | ||
|
|
75f3f43ba0 | ||
|
|
9a521355ed | ||
|
|
47bfdf0710 | ||
|
|
e1b49c3fb4 | ||
|
|
374dffc5fa | ||
|
|
4f735a5d11 | ||
|
|
94738d8fc4 | ||
|
|
adb4bfa10b | ||
|
|
48e6bbdc97 | ||
|
|
b54d6fea44 | ||
|
|
4462e6339d | ||
|
|
c1581b69f4 | ||
|
|
14284e0cc7 | ||
|
|
de40e733ec | ||
|
|
9d91b6f780 | ||
|
|
6a8b49f9c4 | ||
|
|
445a8a5647 | ||
|
|
83ce4a538a | ||
|
|
35a19d2007 | ||
|
|
505e12c5ea | ||
|
|
b2bfd7f23a | ||
|
|
cdb96e715d | ||
|
|
b3e5f09e3b | ||
|
|
db542d668a | ||
|
|
a8a79a55a4 | ||
|
|
47f62a87a7 | ||
|
|
44f353861a | ||
|
|
a2ef84a4a0 | ||
|
|
12ac20ec43 | ||
|
|
ecfbc7b9fd | ||
|
|
ba2fe0fb1f | ||
|
|
890a20edba | ||
|
|
e6f48c9403 | ||
|
|
909f0afa69 | ||
|
|
5ed2b99b8c | ||
|
|
7848751fd8 | ||
|
|
e593241d75 | ||
|
|
fcdc7b7aeb | ||
|
|
c3c7878f28 | ||
|
|
85f9ae5a0a | ||
|
|
98a97f34f5 | ||
|
|
98d647a3fe | ||
|
|
9a393b4f74 | ||
|
|
88d74235e1 | ||
|
|
36fa470348 | ||
|
|
33dce10bc3 | ||
|
|
feed0b288f | ||
|
|
1b7dc8a509 | ||
|
|
87cc3cf168 | ||
|
|
eac7b1e9f2 | ||
|
|
bb1a42df91 | ||
|
|
ac5ac3e9f1 | ||
|
|
bed25b317c | ||
|
|
1687e6682a | ||
|
|
22572c8ed1 | ||
|
|
8187a339f0 | ||
|
|
382c3930a2 | ||
|
|
a64a30c088 | ||
|
|
dac76a867f | ||
|
|
b2e86e105d | ||
|
|
b8e57c9b6f | ||
|
|
486a1bc9de | ||
|
|
b1b610f4b5 | ||
|
|
68447a6009 | ||
|
|
a55280b941 | ||
|
|
830462d525 | ||
|
|
ce8b29e9d0 | ||
|
|
6ab15f8eb1 | ||
|
|
96eb68e042 | ||
|
|
bf78bdd6d4 | ||
|
|
d998815847 | ||
|
|
00ba7b78ca | ||
|
|
0b735d94f1 | ||
|
|
301989540f | ||
|
|
e26b95a26f | ||
|
|
049c1ddb48 | ||
|
|
2f1c3075a2 | ||
|
|
b1a5068fd6 | ||
|
|
01fbd5d702 | ||
|
|
5916f92f1a | ||
|
|
5e45268f68 | ||
|
|
b8e28e0c12 | ||
|
|
04f824ea36 | ||
|
|
c216bea031 | ||
|
|
e72ef478dc | ||
|
|
897b4ef2cd | ||
|
|
2404899e28 | ||
|
|
a2dfc2cbdc | ||
|
|
92373b25a9 | ||
|
|
ce1840a9ae | ||
|
|
c4f4bdd789 | ||
|
|
ec5068e85b | ||
|
|
1d9d0ddf27 | ||
|
|
e393be90dd | ||
|
|
e633df06e4 | ||
|
|
0ff5f408d6 | ||
|
|
5eda42ff31 | ||
|
|
84168e22d0 | ||
|
|
b722845aff | ||
|
|
fd54682c02 | ||
|
|
f5e287ffa6 | ||
|
|
fb10a546d6 | ||
|
|
006897f1c0 | ||
|
|
968849e52b | ||
|
|
8bee47dc50 | ||
|
|
08250120d1 | ||
|
|
8892b70785 | ||
|
|
534e4cb591 | ||
|
|
489abdcb0b | ||
|
|
f6b6c2e9a3 | ||
|
|
43c016f024 | ||
|
|
c0e7d9cd8b | ||
|
|
5f687a31f8 | ||
|
|
f2d2478dee | ||
|
|
8a98789be1 | ||
|
|
87a5c8894a | ||
|
|
7e92ed4501 | ||
|
|
a57cdfff1e | ||
|
|
d4ff6d4d7a | ||
|
|
63d99d6a57 | ||
|
|
fce7d34171 | ||
|
|
e7df7f69b3 | ||
|
|
94cc18bd71 | ||
|
|
39024ce2ac | ||
|
|
7ac4f45e7b | ||
|
|
f209eebaf8 | ||
|
|
4889db78c9 | ||
|
|
bff200fede | ||
|
|
af6f783043 | ||
|
|
610adcbefc | ||
|
|
1d3631fa04 | ||
|
|
0630504664 | ||
|
|
577d58c92b | ||
|
|
899777632b | ||
|
|
6d5b698c39 | ||
|
|
dd9f1abcea | ||
|
|
b4bd34fb96 | ||
|
|
014971262d | ||
|
|
36ed69b07e | ||
|
|
bbf55ca46e | ||
|
|
3f88b04c4a | ||
|
|
f8910ba136 | ||
|
|
6c95d8b13e | ||
|
|
e6bccaaf4e | ||
|
|
3b8039a580 | ||
|
|
fae3f55010 | ||
|
|
20c877f75b | ||
|
|
8380858a82 | ||
|
|
d2358c399d | ||
|
|
c3af8a77af | ||
|
|
bc5a0b030b | ||
|
|
0b94f1717f | ||
|
|
aaa1249a41 | ||
|
|
ffaa22c49b | ||
|
|
0b78480977 | ||
|
|
ec4fc17e3a | ||
|
|
78b85fb664 | ||
|
|
6b6737613a | ||
|
|
da5d62cc1c | ||
|
|
6a68b63192 | ||
|
|
ff2e79fe7b | ||
|
|
1800e51b19 | ||
|
|
ba9c505249 | ||
|
|
bc9f1c17ed | ||
|
|
74845aed64 | ||
|
|
e49dd0cc6a | ||
|
|
27c45ae24a | ||
|
|
364a14adaf | ||
|
|
5c560b1dd5 | ||
|
|
28b8b88332 | ||
|
|
e39ef0cc9e | ||
|
|
8098d3fec8 | ||
|
|
059ffe09ea | ||
|
|
36a845c29e | ||
|
|
ce6f0dab56 | ||
|
|
f200ab10a4 | ||
|
|
3001688e0e | ||
|
|
a73774099e | ||
|
|
b28676d52c | ||
|
|
eef012b4d1 | ||
|
|
1417a1c020 | ||
|
|
962becb9a5 | ||
|
|
168648e789 | ||
|
|
7f56f57778 | ||
|
|
6cadddc2fc | ||
|
|
15fd54eac4 | ||
|
|
31350e6302 | ||
|
|
8742cdae0a | ||
|
|
4efcb388ff | ||
|
|
2d92e95c8a | ||
|
|
47e5d5684a | ||
|
|
b723e14d98 | ||
|
|
c9d24b8f42 | ||
|
|
43622e7ab1 | ||
|
|
5cfc185ba5 | ||
|
|
4be2635fbe | ||
|
|
0beafb8391 | ||
|
|
1d2654b9fa | ||
|
|
a4bc3673e7 | ||
|
|
fa080537e8 | ||
|
|
bdf67a7db7 | ||
|
|
db4cdc901c | ||
|
|
16a540b89b | ||
|
|
e00ec9ac3f | ||
|
|
fc760afdfc | ||
|
|
cb47bcdb0e | ||
|
|
8d62559ca8 | ||
|
|
dbe9c4dc18 | ||
|
|
1609b4562d | ||
|
|
b6cadb1d65 | ||
|
|
7aafac5b5e | ||
|
|
36f0aacb19 | ||
|
|
0c1a6a918d | ||
|
|
d1f5ff4dba | ||
|
|
77e6df2a1c | ||
|
|
119c037f24 | ||
|
|
97fe1abfd8 | ||
|
|
3a0163f0fb | ||
|
|
d3fab69155 | ||
|
|
9395d2c091 | ||
|
|
b9efb98280 | ||
|
|
60bb264663 | ||
|
|
316dd2f165 | ||
|
|
8a0f700563 | ||
|
|
3d0c6eafec | ||
|
|
46e055833b | ||
|
|
80dfdd1cb9 | ||
|
|
db21678b74 | ||
|
|
09c7fe0565 | ||
|
|
b6dfb2c856 | ||
|
|
ab46ba521f | ||
|
|
4a7670f2aa | ||
|
|
9ba86bc174 | ||
|
|
2ebe5e051c | ||
|
|
24e98abd15 | ||
|
|
b7f1a94ba4 | ||
|
|
70bc7465c9 | ||
|
|
65c2568427 | ||
|
|
186e7bf402 | ||
|
|
e6f1c7d0c3 | ||
|
|
87ad9a3190 | ||
|
|
0ed45f8754 | ||
|
|
116e4401c4 | ||
|
|
c3c0e643d2 | ||
|
|
d5522e7c08 | ||
|
|
658b14ba26 | ||
|
|
38f8469d0b | ||
|
|
ce6750be15 | ||
|
|
f2e600cff9 | ||
|
|
3dab4b0b1e | ||
|
|
5d47aaff29 | ||
|
|
625448fcd1 | ||
|
|
e0258cfc51 | ||
|
|
92993ee105 | ||
|
|
ce579293fb | ||
|
|
d56db14fc7 | ||
|
|
3e98f9e6bd | ||
|
|
e7bd9b6323 | ||
|
|
13699f5c02 | ||
|
|
f1f8341d25 | ||
|
|
796b4899aa | ||
|
|
7dcbed644a | ||
|
|
e6fe01876b | ||
|
|
9b75524d43 | ||
|
|
d98c876f82 | ||
|
|
be7b2fa0a4 | ||
|
|
28719e534c | ||
|
|
620a7c6db2 | ||
|
|
26450aca3a | ||
|
|
b1a3ff6cb1 | ||
|
|
ae2efc7f7b | ||
|
|
2523d039fb | ||
|
|
6dd13c6845 | ||
|
|
dbaa116fe0 | ||
|
|
476071fc1b | ||
|
|
66332ccf76 | ||
|
|
b3e9bb9ddb | ||
|
|
dff87b5fa3 | ||
|
|
7a00df65d0 | ||
|
|
60cc1d8ee5 | ||
|
|
b67ade3610 | ||
|
|
97f47f5415 | ||
|
|
ddef31ecdf | ||
|
|
fa31f1ee26 | ||
|
|
8e477c9d16 | ||
|
|
ce8f0ef9e1 | ||
|
|
4f64738f9e | ||
|
|
c4464455a1 | ||
|
|
254a6c2916 | ||
|
|
c9e1c326f5 | ||
|
|
4532b6cd8c | ||
|
|
53424a5c19 | ||
|
|
bfb47da398 | ||
|
|
cb96d90563 | ||
|
|
b6c02c850a | ||
|
|
c297e076e6 | ||
|
|
20a0800aa7 | ||
|
|
bac25112b7 | ||
|
|
1d2162705d | ||
|
|
1a1f66d2a0 | ||
|
|
a44cde33ed | ||
|
|
a9afd84787 | ||
|
|
ac0224b687 | ||
|
|
8be2992c9a | ||
|
|
e3ed23a0d4 | ||
|
|
377070e3a9 | ||
|
|
6d959051e2 | ||
|
|
0799728000 | ||
|
|
1f02f3b376 | ||
|
|
f7d7244588 | ||
|
|
352703827c | ||
|
|
5f4f55269e | ||
|
|
19e27e8403 | ||
|
|
b41b960ef0 | ||
|
|
d1cc91dd6f | ||
|
|
b0b12856d5 | ||
|
|
fdb19f8c49 | ||
|
|
b8a935ce3d | ||
|
|
ec61b80fd3 | ||
|
|
133863e601 | ||
|
|
3767b85958 | ||
|
|
a9672bc4a2 | ||
|
|
4f1e86d269 | ||
|
|
ae36ff9394 | ||
|
|
a888f38afb | ||
|
|
07e51dc8c6 | ||
|
|
986648479a | ||
|
|
533053f66f | ||
|
|
7a74c60fd1 | ||
|
|
d4d663de38 | ||
|
|
37ae24b879 | ||
|
|
787a06d06e | ||
|
|
43ca879f83 | ||
|
|
19f807b6c4 | ||
|
|
780f5893de | ||
|
|
54eea0ff04 | ||
|
|
480d91e818 | ||
|
|
820871329d | ||
|
|
f7d31fe615 | ||
|
|
8d9648391b | ||
|
|
d14438bf54 |
9
.env-template
Normal file
9
.env-template
Normal file
@@ -0,0 +1,9 @@
|
||||
API_KEY=<LLM api key (for example, open ai key)>
|
||||
LLM_NAME=docsgpt
|
||||
VITE_API_STREAMING=true
|
||||
|
||||
#For Azure (you can delete it if you don't use Azure)
|
||||
OPENAI_API_BASE=
|
||||
OPENAI_API_VERSION=
|
||||
AZURE_DEPLOYMENT_NAME=
|
||||
AZURE_EMBEDDINGS_DEPLOYMENT_NAME=
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: arc53
|
||||
138
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
138
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
name: "🐛 Bug Report"
|
||||
description: "Submit a bug report to help us improve"
|
||||
title: "🐛 Bug Report: "
|
||||
labels: ["type: bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: We value your time and your efforts to submit this bug report is appreciated. 🙏
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "📜 Description"
|
||||
description: "A clear and concise description of what the bug is."
|
||||
placeholder: "It bugs out when ..."
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "👟 Reproduction steps"
|
||||
description: "How do you trigger this bug? Please walk us through it step by step."
|
||||
placeholder: "1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error"
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "👍 Expected behavior"
|
||||
description: "What did you think should happen?"
|
||||
placeholder: "It should ..."
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "👎 Actual Behavior with Screenshots"
|
||||
description: "What did actually happen? Add screenshots, if applicable."
|
||||
placeholder: "It actually ..."
|
||||
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: "💻 Operating system"
|
||||
description: "What OS is your app running on?"
|
||||
options:
|
||||
- Linux
|
||||
- MacOS
|
||||
- Windows
|
||||
- Something else
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Something else
|
||||
|
||||
- type: dropdown
|
||||
id: dev-environment
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "🤖 What development environment are you experiencing this bug on?"
|
||||
options:
|
||||
- Docker
|
||||
- Local dev server
|
||||
|
||||
- type: textarea
|
||||
id: env-vars
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "🔒 Did you set the correct environment variables in the right path? List the environment variable names (not values please!)"
|
||||
description: "Please refer to the [Project setup instructions](https://github.com/arc53/DocsGPT#quickstart) if you are unsure."
|
||||
placeholder: "It actually ..."
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "📃 Provide any additional context for the Bug."
|
||||
description: "Add any other context about the problem here."
|
||||
placeholder: "It actually ..."
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: 📖 Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
- type: checkboxes
|
||||
id: no-duplicate-issues
|
||||
attributes:
|
||||
label: "👀 Have you spent some time to check if this bug has been raised before?"
|
||||
options:
|
||||
- label: "I checked and didn't find similar issue"
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: willing-to-submit-pr
|
||||
attributes:
|
||||
label: 🔗 Are you willing to submit PR?
|
||||
description: This is absolutely not required, but we are happy to guide you in the contribution process.
|
||||
options: # Added options key
|
||||
- "Yes, I am willing to submit a PR!"
|
||||
- "No"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: 🧑⚖️ Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/arc53/DocsGPT/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
54
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: 🚀 Feature
|
||||
description: "Submit a proposal for a new feature"
|
||||
title: "🚀 Feature: "
|
||||
labels: [feature]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: We value your time and your efforts to submit this bug report is appreciated. 🙏
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "🔖 Feature description"
|
||||
description: "A clear and concise description of what the feature is."
|
||||
placeholder: "You should add ..."
|
||||
- type: textarea
|
||||
id: pitch
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "🎤 Why is this feature needed ?"
|
||||
description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable."
|
||||
placeholder: "In my use-case, ..."
|
||||
- type: textarea
|
||||
id: solution
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "✌️ How do you aim to achieve this?"
|
||||
description: "A clear and concise description of what you want to happen."
|
||||
placeholder: "I want this feature to, ..."
|
||||
- type: textarea
|
||||
id: alternative
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "🔄️ Additional Information"
|
||||
description: "A clear and concise description of any alternative solutions or additional solutions you've considered."
|
||||
placeholder: "I tried, ..."
|
||||
- type: checkboxes
|
||||
id: no-duplicate-issues
|
||||
attributes:
|
||||
label: "👀 Have you spent some time to check if this feature request has been raised before?"
|
||||
options:
|
||||
- label: "I checked and didn't find similar issue"
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: willing-to-submit-pr
|
||||
attributes:
|
||||
label: Are you willing to submit PR?
|
||||
description: This is absolutely not required, but we are happy to guide you in the contribution process.
|
||||
options:
|
||||
- "Yes I am willing to submit a PR!"
|
||||
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
- **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
|
||||
|
||||
- **Why was this change needed?** (You can also link to an open issue here)
|
||||
|
||||
- **Other information**:
|
||||
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/application" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/frontend" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
11
.github/holopin.yml
vendored
Normal file
11
.github/holopin.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
organization: docsgpt
|
||||
defaultSticker: cm1ulwkkl180570cl82rtzympu
|
||||
stickers:
|
||||
- id: cm1ulwkkl180570cl82rtzympu
|
||||
alias: contributor2024
|
||||
- id: cm1ureg8o130450cl8c1po6mil
|
||||
alias: api
|
||||
- id: cm1urhmag148240cl8yvqxkthx
|
||||
alias: lpc
|
||||
- id: cm1urlcpq622090cl2tvu4w71y
|
||||
alias: lexeu
|
||||
31
.github/labeler.yml
vendored
Normal file
31
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
repo:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '*'
|
||||
|
||||
github:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '.github/**/*'
|
||||
|
||||
application:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'application/**/*'
|
||||
|
||||
docs:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'docs/**/*'
|
||||
|
||||
extensions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'extensions/**/*'
|
||||
|
||||
frontend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'frontend/**/*'
|
||||
|
||||
scripts:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'scripts/**/*'
|
||||
|
||||
tests:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'tests/**/*'
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -1,44 +1,47 @@
|
||||
name: Build and push DocsGPT Docker image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Build and push Docker images to docker.io and ghcr.io
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: './application/Dockerfile'
|
||||
platforms: linux/amd64
|
||||
context: ./application
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
|
||||
ghcr.io/${{ github.repository_owner }}/docsgpt:latest
|
||||
${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
|
||||
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt:latest
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
|
||||
cache-to: type=inline
|
||||
|
||||
48
.github/workflows/cife.yml
vendored
Normal file
48
.github/workflows/cife.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Build and push DocsGPT-FE Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Build and push Docker images to docker.io and ghcr.io
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: './frontend/Dockerfile'
|
||||
platforms: linux/amd64, linux/arm64
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest
|
||||
ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest
|
||||
cache-to: type=inline
|
||||
49
.github/workflows/docker-develop-build.yml
vendored
Normal file
49
.github/workflows/docker-develop-build.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Build and push DocsGPT Docker image for development
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker images to docker.io and ghcr.io
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: './application/Dockerfile'
|
||||
platforms: linux/amd64
|
||||
context: ./application
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
|
||||
ghcr.io/${{ github.repository_owner }}/docsgpt:develop
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
|
||||
cache-to: type=inline
|
||||
49
.github/workflows/docker-develop-fe-build.yml
vendored
Normal file
49
.github/workflows/docker-develop-fe-build.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Build and push DocsGPT FE Docker image for development
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker images to docker.io and ghcr.io
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: './frontend/Dockerfile'
|
||||
platforms: linux/amd64
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop
|
||||
ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop
|
||||
cache-to: type=inline
|
||||
16
.github/workflows/labeler.yml
vendored
Normal file
16
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# https://github.com/actions/labeler
|
||||
name: Pull Request Labeler
|
||||
on:
|
||||
- pull_request_target
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
sync-labels: true
|
||||
17
.github/workflows/lint.yml
vendored
Normal file
17
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Python linting
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
types: [ opened, synchronize ]
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Lint with Ruff
|
||||
uses: chartboost/ruff-action@v1
|
||||
30
.github/workflows/pytest.yml
vendored
Normal file
30
.github/workflows/pytest.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Run python tests with pytest
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
pytest_and_coverage:
|
||||
name: Run tests and count coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-cov
|
||||
cd application
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Test with pytest and generate coverage report
|
||||
run: |
|
||||
python -m pytest --cov=application --cov-report=xml
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: github.event_name == 'pull_request' && matrix.python-version == '3.12'
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
41
.github/workflows/sync_fork.yaml
vendored
Normal file
41
.github/workflows/sync_fork.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Upstream Sync
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # every hour
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync_latest_from_upstream:
|
||||
name: Sync latest commits from upstream repo
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.repository.fork }}
|
||||
|
||||
steps:
|
||||
# Step 1: run a standard checkout action
|
||||
- name: Checkout target repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Step 2: run the sync action
|
||||
- name: Sync upstream changes
|
||||
id: sync
|
||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||
with:
|
||||
# set your upstream repo and branch
|
||||
upstream_sync_repo: arc53/DocsGPT
|
||||
upstream_sync_branch: main
|
||||
target_sync_branch: main
|
||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||
|
||||
# Set test_mode true to run tests instead of the true action!!
|
||||
test_mode: false
|
||||
|
||||
- name: Sync check
|
||||
if: failure()
|
||||
run: |
|
||||
echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
|
||||
echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
|
||||
exit 1
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -5,7 +5,7 @@ __pycache__/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
*.next
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
@@ -75,6 +75,7 @@ target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
**/*.ipynb
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
@@ -162,3 +163,14 @@ frontend/*.sw?
|
||||
application/vectors/
|
||||
|
||||
**/inputs
|
||||
|
||||
**/indexes
|
||||
|
||||
**/temp
|
||||
|
||||
**/yarn.lock
|
||||
|
||||
node_modules/
|
||||
.vscode/settings.json
|
||||
/models/
|
||||
model/
|
||||
|
||||
2
.ruff.toml
Normal file
2
.ruff.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
# Allow lines to be as long as 120 characters.
|
||||
line-length = 120
|
||||
54
.vscode/launch.json
vendored
Normal file
54
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Docker Debug Frontend",
|
||||
"request": "launch",
|
||||
"type": "chrome",
|
||||
"preLaunchTask": "docker-compose: debug:frontend",
|
||||
"url": "http://127.0.0.1:5173",
|
||||
"webRoot": "${workspaceFolder}/frontend",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flask Debugger",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "flask",
|
||||
"env": {
|
||||
"FLASK_APP": "application/app.py",
|
||||
"PYTHONPATH": "${workspaceFolder}",
|
||||
"FLASK_ENV": "development",
|
||||
"FLASK_DEBUG": "1",
|
||||
"FLASK_RUN_PORT": "7091",
|
||||
"FLASK_RUN_HOST": "0.0.0.0"
|
||||
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
"--no-debugger"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
{
|
||||
"name": "Celery Debugger",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "celery",
|
||||
"env": {
|
||||
"PYTHONPATH": "${workspaceFolder}",
|
||||
},
|
||||
"args": [
|
||||
"-A",
|
||||
"application.app.celery",
|
||||
"worker",
|
||||
"-l",
|
||||
"INFO",
|
||||
"--pool=solo"
|
||||
],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
.vscode/tasks.json
vendored
Normal file
21
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "docker-compose",
|
||||
"label": "docker-compose: debug:frontend",
|
||||
"dockerCompose": {
|
||||
"up": {
|
||||
"detached": true,
|
||||
"services": [
|
||||
"frontend"
|
||||
],
|
||||
"build": true
|
||||
},
|
||||
"files": [
|
||||
"${workspaceFolder}/docker-compose.yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,58 +2,58 @@
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
We as members, contributors and leaders pledge to make participation in our
|
||||
community, a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
nationality, personal appearance, race, religion or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
diverse, inclusive and a healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
Examples of behavior that contribute to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
## Demonstrating empathy and kindness towards other people
|
||||
1. Being respectful and open to differing opinions, viewpoints, and experiences
|
||||
2. Giving and gracefully accepting constructive feedback
|
||||
3. Taking accountability and offering apologies to those who have been impacted by our errors,
|
||||
while also gaining insights from the situation
|
||||
4. Focusing on what is best not just for us as individuals but for the
|
||||
community as a whole
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
1. The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
2. Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
3. Public or private harassment
|
||||
4. Publishing other's private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
5. Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
response to any behavior that they deem inappropriate, threatening, offensive
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
not aligned to this Code of Conduct and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
This Code of Conduct applies within all community spaces and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
posting via an official social media account or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
@@ -63,29 +63,27 @@ reported to the community leaders responsible for enforcement at
|
||||
contact@arc53.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
All community leaders are obligated to be respectful towards the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
the consequences for any action that they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
* **Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community space.
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
* **Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
* **Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
* **Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
@@ -93,23 +91,21 @@ like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
* **Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
* **Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
* **Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior,harassment of an
|
||||
individual or aggression towards or disparagement of classes of individuals.
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
* **Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
135
CONTRIBUTING.md
135
CONTRIBUTING.md
@@ -1,38 +1,129 @@
|
||||
# Welcome to DocsGPT Contributing guideline
|
||||
# Welcome to DocsGPT Contributing Guidelines
|
||||
|
||||
Thank you for choosing this project to contribute to, we are all very grateful!
|
||||
Thank you for choosing to contribute to DocsGPT! We are all very grateful!
|
||||
|
||||
# We accept different types of contributions
|
||||
|
||||
📣 Discussions - where you can start a new topic or answer some questions
|
||||
📣 **Discussions** - Engage in conversations, start new topics, or help answer questions.
|
||||
|
||||
🐞 Issues - Is how we track tasks, sometimes its bugs that need fixing, sometimes its new features
|
||||
🐞 **Issues** - This is where we keep track of tasks. It could be bugs, fixes or suggestions for new features.
|
||||
|
||||
🛠️ Pull requests - Is how you can suggest changes to our repository, to work on existing issue or to add new features
|
||||
🛠️ **Pull requests** - Suggest changes to our repository, either by working on existing issues or adding new features.
|
||||
|
||||
📚 Wiki - where we have our documentation
|
||||
📚 **Wiki** - This is where our documentation resides.
|
||||
|
||||
|
||||
## 🐞 Issues and Pull requests
|
||||
|
||||
We value contributions to our issues in form of discussion or suggestion, we recommend that you check out existing issues and our [Roadmap](https://github.com/orgs/arc53/projects/2)
|
||||
|
||||
If you want to contribute by writing code there are few things that you should know before doing it:
|
||||
We have frontend (React, Vite) and Backend (python)
|
||||
|
||||
### If you are looking to contribute to Frontend (⚛️React, Vite):
|
||||
Current frontend is being migrated from /application to /frontend with a new design, so please contribute to the new on. Check out this [Milestone](https://github.com/arc53/DocsGPT/milestone/1) and its issues also [Figma](https://www.figma.com/file/OXLtrl1EAy885to6S69554/DocsGPT?node-id=0%3A1&t=hjWVuxRg9yi5YkJ9-1)
|
||||
Please try to follow guidelines
|
||||
- We value contributions in the form of discussions or suggestions. We recommend taking a look at existing issues and our [roadmap](https://github.com/orgs/arc53/projects/2).
|
||||
|
||||
|
||||
### If you are looking to contribute to Backend (🐍Python):
|
||||
Check out our issues, and contribute to /application or /scripts (ignore old ingest_rst.py ingest_rst_sphinx.py files, they will be deprecated soon)
|
||||
Currently we don't have any tests(which would be useful😉) but before submitting you PR make sure that after you ingested some test data its queryable
|
||||
- If you're interested in contributing code, here are some important things to know:
|
||||
|
||||
### Workflow:
|
||||
Create a fork, make changes on your forked repository, submit changes in a form of pull request
|
||||
- We have a frontend built on React (Vite) and a backend in Python.
|
||||
|
||||
## Questions / collaboration
|
||||
Please join our [Discord](https://discord.gg/n5BX8dh8rU) don't hesitate, we are very friendly and welcoming to new contributors.
|
||||
|
||||
Before creating issues, please check out how the latest version of our app looks and works by launching it via [Quickstart](https://github.com/arc53/DocsGPT#quickstart) the version on our live demo is slightly modified with login. Your issues should relate to the version you can launch via [Quickstart](https://github.com/arc53/DocsGPT#quickstart).
|
||||
|
||||
# Thank you so much for considering to contribute to DocsGPT!🙏
|
||||
### 👨💻 If you're interested in contributing code, here are some important things to know:
|
||||
|
||||
For instructions on setting up a development environment, please refer to our [Development Deployment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).
|
||||
|
||||
Tech Stack Overview:
|
||||
|
||||
- 🌐 Frontend: Built with React (Vite) ⚛️,
|
||||
|
||||
- 🖥 Backend: Developed in Python 🐍
|
||||
|
||||
### 🌐 If you are looking to contribute to frontend (⚛️React, Vite):
|
||||
|
||||
|
||||
- The updated Figma design can be found [here](https://www.figma.com/file/OXLtrl1EAy885to6S69554/DocsGPT?node-id=0%3A1&t=hjWVuxRg9yi5YkJ9-1).
|
||||
|
||||
Please try to follow the guidelines.
|
||||
|
||||
### 🖥 If you are looking to contribute to Backend (🐍 Python):
|
||||
|
||||
- Review our issues and contribute to [`/application`](https://github.com/arc53/DocsGPT/tree/main/application)
|
||||
- All new code should be covered with unit tests ([pytest](https://github.com/pytest-dev/pytest)). Please find tests under [`/tests`](https://github.com/arc53/DocsGPT/tree/main/tests) folder.
|
||||
- Before submitting your Pull Request, ensure it can be queried after ingesting some test data.
|
||||
|
||||
### Testing
|
||||
|
||||
To run unit tests from the root of the repository, execute:
|
||||
```
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
## Workflow 📈
|
||||
|
||||
Here's a step-by-step guide on how to contribute to DocsGPT:
|
||||
|
||||
1. **Fork the Repository:**
|
||||
- Click the "Fork" button at the top-right of this repository to create your fork.
|
||||
|
||||
2. **Clone the Forked Repository:**
|
||||
- Clone the repository using:
|
||||
``` shell
|
||||
git clone https://github.com/<your-github-username>/DocsGPT.git
|
||||
```
|
||||
|
||||
3. **Keep your Fork in Sync:**
|
||||
- Before you make any changes, make sure that your fork is in sync to avoid merge conflicts using:
|
||||
```shell
|
||||
git remote add upstream https://github.com/arc53/DocsGPT.git
|
||||
git pull upstream main
|
||||
```
|
||||
|
||||
4. **Create and Switch to a New Branch:**
|
||||
- Create a new branch for your contribution using:
|
||||
```shell
|
||||
git checkout -b your-branch-name
|
||||
```
|
||||
|
||||
5. **Make Changes:**
|
||||
- Make the required changes in your branch.
|
||||
|
||||
6. **Add Changes to the Staging Area:**
|
||||
- Add your changes to the staging area using:
|
||||
```shell
|
||||
git add .
|
||||
```
|
||||
|
||||
7. **Commit Your Changes:**
|
||||
- Commit your changes with a descriptive commit message using:
|
||||
```shell
|
||||
git commit -m "Your descriptive commit message"
|
||||
```
|
||||
|
||||
8. **Push Your Changes to the Remote Repository:**
|
||||
- Push your branch with changes to your fork on GitHub using:
|
||||
```shell
|
||||
git push origin your-branch-name
|
||||
```
|
||||
|
||||
9. **Submit a Pull Request (PR):**
|
||||
- Create a Pull Request from your branch to the main repository. Make sure to include a detailed description of your changes and reference any related issues.
|
||||
|
||||
10. **Collaborate:**
|
||||
- Be responsive to comments and feedback on your PR.
|
||||
- Make necessary updates as suggested.
|
||||
- Once your PR is approved, it will be merged into the main repository.
|
||||
|
||||
11. **Testing:**
|
||||
- Before submitting a Pull Request, ensure your code passes all unit tests.
|
||||
- To run unit tests from the root of the repository, execute:
|
||||
```shell
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
*Note: You should run the unit test only after making the changes to the backend code.*
|
||||
|
||||
12. **Questions and Collaboration:**
|
||||
- Feel free to join our Discord. We're very friendly and welcoming to new contributors, so don't hesitate to reach out.
|
||||
|
||||
Thank you for considering contributing to DocsGPT! 🙏
|
||||
|
||||
## Questions/collaboration
|
||||
Feel free to join our [Discord](https://discord.gg/n5BX8dh8rU). We're very friendly and welcoming to new contributors, so don't hesitate to reach out.
|
||||
# Thank you so much for considering to contributing DocsGPT!🙏
|
||||
|
||||
155
README.md
155
README.md
@@ -3,69 +3,150 @@
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Open-Source Documentation Assistant</strong>
|
||||
<strong>Open-Source RAG Assistant</strong>
|
||||
</p>
|
||||
|
||||
<p align="left">
|
||||
<strong>DocsGPT</strong> is a cutting-edge open-source solution that streamlines the process of finding information in project documentation. With its integration of the powerful <strong>GPT</strong> models, developers can easily ask questions about a project and receive accurate answers.
|
||||
|
||||
Say goodbye to time-consuming manual searches, and let <strong>DocsGPT</strong> help you quickly find the information you need. Try it out and see how it revolutionizes your project documentation experience. Contribute to its development and be a part of the future of AI-powered assistance.
|
||||
<strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> is an open-source genAI tool that helps users get reliable answers from any knowledge source, while avoiding hallucinations. It enables quick and reliable information retrieval, with tooling and agentic system capability built in.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://discord.gg/n5BX8dh8rU"></a>
|
||||
<a href="https://discord.gg/n5BX8dh8rU"></a>
|
||||
<a href="https://discord.gg/n5BX8dh8rU"></a>
|
||||
<a href="https://discord.gg/n5BX8dh8rU"></a>
|
||||
|
||||
<a href="https://github.com/arc53/DocsGPT"></a>
|
||||
<a href="https://github.com/arc53/DocsGPT"></a>
|
||||
<a href="https://github.com/arc53/DocsGPT/blob/main/LICENSE"></a>
|
||||
<a href="https://discord.gg/n5BX8dh8rU"></a>
|
||||
<a href="https://twitter.com/docsgptai"></a>
|
||||
|
||||
<br>
|
||||
|
||||
[☁️ Cloud Version](https://app.docsgpt.cloud/) • [💬 Discord](https://discord.gg/n5BX8dh8rU) • [📖 Guides](https://docs.docsgpt.cloud/)
|
||||
<br>
|
||||
[👫 Contribute](https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md) • [🏠 Self-host](https://docs.docsgpt.cloud/Guides/How-to-use-different-LLM) • [⚡️ Quickstart](https://github.com/arc53/DocsGPT#quickstart)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
<img src="https://d3dg1063dc54p9.cloudfront.net/videos/demov7.gif" alt="video-example-of-docs-gpt" width="800" height="450">
|
||||
</div>
|
||||
<h3 align="left">
|
||||
<strong>Key Features:</strong>
|
||||
</h3>
|
||||
<ul align="left">
|
||||
<li><strong>🗂️ Wide Format Support:</strong> Reads PDF, DOCX, CSV, XLSX, EPUB, MD, RST, HTML, MDX, JSON, PPTX, and images.</li>
|
||||
<li><strong>🌐 Web & Data Integration:</strong> Ingests from URLs, sitemaps, Reddit, GitHub and web crawlers.</li>
|
||||
<li><strong>✅ Reliable Answers:</strong> Get accurate, hallucination-free responses with source citations viewable in a clean UI.</li>
|
||||
<li><strong>🔗 Actionable Tooling:</strong> Connect to APIs, tools, and other services to enable LLM actions.</li>
|
||||
<li><strong>🧩 Pre-built Integrations:</strong> Use readily available HTML/React chat widgets, search tools, Discord/Telegram bots, and more.</li>
|
||||
<li><strong>🔌 Flexible Deployment:</strong> Works with major LLMs (OpenAI, Google, Anthropic) and local models (Ollama, llama_cpp).</li>
|
||||
<li><strong>🏢 Secure & Scalable:</strong> Run privately and securely with Kubernetes support, designed for enterprise-grade reliability.</li>
|
||||
</ul>
|
||||
|
||||
## Roadmap
|
||||
|
||||
You can find our [Roadmap](https://github.com/orgs/arc53/projects/2) here, please don't hesitate contributing or creating issues, it helps us make DocsGPT better!
|
||||
- [x] Full GoogleAI compatibility (Jan 2025)
|
||||
- [x] Add tools (Jan 2025)
|
||||
- [ ] Anthropic Tool compatibility
|
||||
- [ ] Add triggerable actions / tools (webhook)
|
||||
- [ ] Add OAuth 2.0 authentication for tools and sources
|
||||
- [ ] Manually updating chunks in the app UI
|
||||
- [ ] Devcontainer for easy development
|
||||
- [ ] Chatbots menu re-design to handle tools, scheduling, and more
|
||||
|
||||
## Preview
|
||||

|
||||
You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT!
|
||||
|
||||
## [Live preview](https://docsgpt.arc53.com/)
|
||||
### Production Support / Help for Companies:
|
||||
|
||||
## [Join Our Discord](https://discord.gg/n5BX8dh8rU)
|
||||
We're eager to provide personalized assistance when deploying your DocsGPT to a live environment.
|
||||
|
||||
[Get a Demo :wave:](https://www.docsgpt.cloud/contact)
|
||||
|
||||
## Project structure
|
||||
- Application - flask app (main application)
|
||||
[Send Email :email:](mailto:support@docsgpt.cloud?subject=DocsGPT%20support%2Fsolutions)
|
||||
|
||||
- Extensions - chrome extension
|
||||
|
||||
- Scripts - script that creates similarity search index and store for other libraries.
|
||||
|
||||
## QuickStart
|
||||
Please note: current vector database uses pandas Python documentation, thus responses will be related to it, if you want to use other docs please follow a guide below
|
||||
|
||||
1. Navigate to `/application` folder
|
||||
2. Install dependencies
|
||||
`pip install -r requirements.txt`
|
||||
3. Prepare .env file
|
||||
Copy .env_sample and create .env with your openai api token
|
||||
4. Run the app
|
||||
`python app.py`
|
||||
> [!Note]
|
||||
> Make sure you have [Docker](https://docs.docker.com/engine/install/) installed
|
||||
|
||||
|
||||
[How to install the Chrome extension](https://github.com/arc53/docsgpt/wiki#launch-chrome-extension)
|
||||
1. Clone the repository and run the following command:
|
||||
```bash
|
||||
git clone https://github.com/arc53/DocsGPT.git
|
||||
cd DocsGPT
|
||||
```
|
||||
|
||||
On Mac OS or Linux, write:
|
||||
|
||||
|
||||
## [Guides](https://github.com/arc53/docsgpt/wiki)
|
||||
2. Run the following command:
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## [Interested in contributing?](https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md)
|
||||
It will install all the dependencies and allow you to download the local model, use OpenAI or use our LLM API.
|
||||
|
||||
## [How to use any other documentation](https://github.com/arc53/docsgpt/wiki/How-to-train-on-other-documentation)
|
||||
Otherwise, refer to this Guide for Windows:
|
||||
|
||||
## [How to host it locally (so all data will stay on-premises)](https://github.com/arc53/DocsGPT/wiki/How-to-use-different-LLM's#hosting-everything-locally)
|
||||
On windows:
|
||||
|
||||
Built with [🦜️🔗 LangChain](https://github.com/hwchase17/langchain)
|
||||
2. Create a `.env` file in your root directory and set the env variables.
|
||||
It should look like this inside:
|
||||
|
||||
```
|
||||
LLM_NAME=[docsgpt or openai or others]
|
||||
API_KEY=[if LLM_NAME is openai]
|
||||
```
|
||||
|
||||
See optional environment variables in the [/application/.env_sample](https://github.com/arc53/DocsGPT/blob/main/application/.env_sample) file.
|
||||
|
||||
3. Run the following command:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
4. Navigate to http://localhost:5173/.
|
||||
|
||||
To stop, just run `Ctrl + C`.
|
||||
|
||||
> [!Note]
|
||||
> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for information about how to get involved. We welcome issues, questions, and pull requests.
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
## Project Structure
|
||||
|
||||
- Application - Flask app (main application).
|
||||
|
||||
- Extensions - Extensions, like react widget or discord bot.
|
||||
|
||||
- Frontend - Frontend uses <a href="https://vitejs.dev/">Vite</a> and <a href="https://react.dev/">React</a>.
|
||||
|
||||
- Scripts - Miscellaneous scripts.
|
||||
|
||||
## Code Of Conduct
|
||||
|
||||
We as members, contributors, and leaders, pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. Please refer to the [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) file for more information about contributing.
|
||||
|
||||
|
||||
## Many Thanks To Our Contributors⚡
|
||||
|
||||
<a href="https://github.com/arc53/DocsGPT/graphs/contributors" alt="View Contributors">
|
||||
<img src="https://contrib.rocks/image?repo=arc53/DocsGPT" alt="Contributors" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
The source code license is [MIT](https://opensource.org/license/mit/), as described in the [LICENSE](LICENSE) file.
|
||||
|
||||
<p>This project is supported by:</p>
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=DocsGPT">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
BIN
Readme Logo.png
BIN
Readme Logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Supported Versions:
|
||||
|
||||
Currently, we support security patches by committing changes and bumping the version published on Github.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Found a vulnerability? Please email us:
|
||||
|
||||
security@arc53.com
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
OPENAI_API_KEY=your_api_key
|
||||
API_KEY=your_api_key
|
||||
EMBEDDINGS_KEY=your_api_key
|
||||
API_URL=http://localhost:7091
|
||||
FLASK_APP=application/app.py
|
||||
FLASK_DEBUG=true
|
||||
|
||||
#For OPENAI on Azure
|
||||
OPENAI_API_BASE=
|
||||
OPENAI_API_VERSION=
|
||||
AZURE_DEPLOYMENT_NAME=
|
||||
AZURE_EMBEDDINGS_DEPLOYMENT_NAME=
|
||||
@@ -1,20 +1,88 @@
|
||||
FROM python:3.10-slim-bullseye as builder
|
||||
# Builder Stage
|
||||
FROM ubuntu:24.04 as builder
|
||||
|
||||
# Tiktoken requires Rust toolchain, so build it in a separate stage
|
||||
RUN apt-get update && apt-get install -y gcc curl
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && apt-get install --reinstall libc6-dev -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN pip install --upgrade pip && pip install tiktoken==0.1.2
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
FROM python:3.10-slim-bullseye
|
||||
# Copy pre-built packages from builder stage
|
||||
COPY --from=builder /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
|
||||
RUN apt-get update && \
|
||||
apt-get install -y software-properties-common && \
|
||||
add-apt-repository ppa:deadsnakes/ppa && \
|
||||
# Install necessary packages and Python
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc wget unzip libc6-dev python3.12 python3.12-venv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Verify Python installation and setup symlink
|
||||
RUN if [ -f /usr/bin/python3.12 ]; then \
|
||||
ln -s /usr/bin/python3.12 /usr/bin/python; \
|
||||
else \
|
||||
echo "Python 3.12 not found"; exit 1; \
|
||||
fi
|
||||
|
||||
# Download and unzip the model
|
||||
RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip && \
|
||||
unzip mpnet-base-v2.zip -d model && \
|
||||
rm mpnet-base-v2.zip
|
||||
|
||||
# Install Rust
|
||||
RUN wget -q -O - https://sh.rustup.rs | sh -s -- -y
|
||||
|
||||
# Clean up to reduce container size
|
||||
RUN apt-get remove --purge -y wget unzip && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements.txt
|
||||
COPY requirements.txt .
|
||||
|
||||
# Setup Python virtual environment
|
||||
RUN python3.12 -m venv /venv
|
||||
|
||||
# Activate virtual environment and install Python packages
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
|
||||
# Install Python packages
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir tiktoken && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Final Stage
|
||||
FROM ubuntu:24.04 as final
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y software-properties-common && \
|
||||
add-apt-repository ppa:deadsnakes/ppa && \
|
||||
# Install Python
|
||||
apt-get update && apt-get install -y --no-install-recommends python3.12 && \
|
||||
ln -s /usr/bin/python3.12 /usr/bin/python && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_DEBUG=true
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
EXPOSE 5001
|
||||
# Create a non-root user: `appuser` (Feel free to choose a name)
|
||||
RUN groupadd -r appuser && \
|
||||
useradd -r -g appuser -d /app -s /sbin/nologin -c "Docker image user" appuser
|
||||
|
||||
CMD ["gunicorn", "-w", "6", "--bind", "0.0.0.0:5001", "wsgi:app"]
|
||||
# Copy the virtual environment and model from the builder stage
|
||||
COPY --from=builder /venv /venv
|
||||
COPY --from=builder /model /app/model
|
||||
|
||||
# Copy your application code
|
||||
COPY . /app/application
|
||||
|
||||
# Change the ownership of the /app directory to the appuser
|
||||
|
||||
RUN mkdir -p /app/application/inputs/local
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=app.py \
|
||||
FLASK_DEBUG=true \
|
||||
PATH="/venv/bin:$PATH"
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 7091
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Start Gunicorn
|
||||
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]
|
||||
0
application/__init__.py
Normal file
0
application/__init__.py
Normal file
0
application/api/__init__.py
Normal file
0
application/api/__init__.py
Normal file
0
application/api/answer/__init__.py
Normal file
0
application/api/answer/__init__.py
Normal file
646
application/api/answer/routes.py
Normal file
646
application/api/answer/routes.py
Normal file
@@ -0,0 +1,646 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from bson.dbref import DBRef
|
||||
from bson.objectid import ObjectId
|
||||
from flask import Blueprint, current_app, make_response, request, Response
|
||||
from flask_restx import fields, Namespace, Resource
|
||||
|
||||
|
||||
from application.core.mongo_db import MongoDB
|
||||
from application.core.settings import settings
|
||||
from application.error import bad_request
|
||||
from application.extensions import api
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.retriever.retriever_creator import RetrieverCreator
|
||||
from application.utils import check_required_fields, limit_chat_history
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
conversations_collection = db["conversations"]
|
||||
sources_collection = db["sources"]
|
||||
prompts_collection = db["prompts"]
|
||||
api_key_collection = db["api_keys"]
|
||||
user_logs_collection = db["user_logs"]
|
||||
|
||||
answer = Blueprint("answer", __name__)
|
||||
answer_ns = Namespace("answer", description="Answer related operations", path="/")
|
||||
api.add_namespace(answer_ns)
|
||||
|
||||
gpt_model = ""
|
||||
# to have some kind of default behaviour
|
||||
if settings.LLM_NAME == "openai":
|
||||
gpt_model = "gpt-4o-mini"
|
||||
elif settings.LLM_NAME == "anthropic":
|
||||
gpt_model = "claude-2"
|
||||
elif settings.LLM_NAME == "groq":
|
||||
gpt_model = "llama3-8b-8192"
|
||||
|
||||
if settings.MODEL_NAME: # in case there is particular model name configured
|
||||
gpt_model = settings.MODEL_NAME
|
||||
|
||||
# load the prompts
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
|
||||
chat_combine_template = f.read()
|
||||
|
||||
with open(os.path.join(current_dir, "prompts", "chat_reduce_prompt.txt"), "r") as f:
|
||||
chat_reduce_template = f.read()
|
||||
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r") as f:
|
||||
chat_combine_creative = f.read()
|
||||
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r") as f:
|
||||
chat_combine_strict = f.read()
|
||||
|
||||
api_key_set = settings.API_KEY is not None
|
||||
embeddings_key_set = settings.EMBEDDINGS_KEY is not None
|
||||
|
||||
|
||||
async def async_generate(chain, question, chat_history):
|
||||
result = await chain.arun({"question": question, "chat_history": chat_history})
|
||||
return result
|
||||
|
||||
|
||||
def run_async_chain(chain, question, chat_history):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
result = {}
|
||||
try:
|
||||
answer = loop.run_until_complete(async_generate(chain, question, chat_history))
|
||||
finally:
|
||||
loop.close()
|
||||
result["answer"] = answer
|
||||
return result
|
||||
|
||||
|
||||
def get_data_from_api_key(api_key):
|
||||
data = api_key_collection.find_one({"key": api_key})
|
||||
# # Raise custom exception if the API key is not found
|
||||
if data is None:
|
||||
raise Exception("Invalid API Key, please generate new key", 401)
|
||||
|
||||
if "retriever" not in data:
|
||||
data["retriever"] = None
|
||||
|
||||
if "source" in data and isinstance(data["source"], DBRef):
|
||||
source_doc = db.dereference(data["source"])
|
||||
data["source"] = str(source_doc["_id"])
|
||||
if "retriever" in source_doc:
|
||||
data["retriever"] = source_doc["retriever"]
|
||||
else:
|
||||
data["source"] = {}
|
||||
return data
|
||||
|
||||
|
||||
def get_retriever(source_id: str):
|
||||
doc = sources_collection.find_one({"_id": ObjectId(source_id)})
|
||||
if doc is None:
|
||||
raise Exception("Source document does not exist", 404)
|
||||
retriever_name = None if "retriever" not in doc else doc["retriever"]
|
||||
return retriever_name
|
||||
|
||||
|
||||
def is_azure_configured():
|
||||
return (
|
||||
settings.OPENAI_API_BASE
|
||||
and settings.OPENAI_API_VERSION
|
||||
and settings.AZURE_DEPLOYMENT_NAME
|
||||
)
|
||||
|
||||
|
||||
def save_conversation(conversation_id, question, response, source_log_docs, llm,index=None):
|
||||
if conversation_id is not None and index is not None:
|
||||
conversations_collection.update_one(
|
||||
{"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}},
|
||||
{
|
||||
"$set": {
|
||||
f"queries.{index}.prompt": question,
|
||||
f"queries.{index}.response": response,
|
||||
f"queries.{index}.sources": source_log_docs,
|
||||
}
|
||||
}
|
||||
)
|
||||
##remove following queries from the array
|
||||
conversations_collection.update_one(
|
||||
{"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}},
|
||||
{
|
||||
"$push":{
|
||||
"queries":{
|
||||
"$each":[],
|
||||
"$slice":index+1
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
elif conversation_id is not None and conversation_id != "None":
|
||||
conversations_collection.update_one(
|
||||
{"_id": ObjectId(conversation_id)},
|
||||
{
|
||||
"$push": {
|
||||
"queries": {
|
||||
"prompt": question,
|
||||
"response": response,
|
||||
"sources": source_log_docs,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
# create new conversation
|
||||
# generate summary
|
||||
messages_summary = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Summarise following conversation in no more than 3 "
|
||||
"words, respond ONLY with the summary, use the same "
|
||||
"language as the system",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Summarise following conversation in no more than 3 words, "
|
||||
"respond ONLY with the summary, use the same language as the "
|
||||
"system \n\nUser: "
|
||||
+ question
|
||||
+ "\n\n"
|
||||
+ "AI: "
|
||||
+ response,
|
||||
},
|
||||
]
|
||||
|
||||
completion = llm.gen(model=gpt_model, messages=messages_summary, max_tokens=30)
|
||||
conversation_id = conversations_collection.insert_one(
|
||||
{
|
||||
"user": "local",
|
||||
"date": datetime.datetime.utcnow(),
|
||||
"name": completion,
|
||||
"queries": [
|
||||
{
|
||||
"prompt": question,
|
||||
"response": response,
|
||||
"sources": source_log_docs,
|
||||
}
|
||||
],
|
||||
}
|
||||
).inserted_id
|
||||
return conversation_id
|
||||
|
||||
|
||||
def get_prompt(prompt_id):
|
||||
if prompt_id == "default":
|
||||
prompt = chat_combine_template
|
||||
elif prompt_id == "creative":
|
||||
prompt = chat_combine_creative
|
||||
elif prompt_id == "strict":
|
||||
prompt = chat_combine_strict
|
||||
else:
|
||||
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})["content"]
|
||||
return prompt
|
||||
|
||||
|
||||
def complete_stream(
|
||||
question, retriever, conversation_id, user_api_key, isNoneDoc=False,index=None
|
||||
):
|
||||
|
||||
try:
|
||||
response_full = ""
|
||||
source_log_docs = []
|
||||
answer = retriever.gen()
|
||||
sources = retriever.search()
|
||||
for source in sources:
|
||||
if "text" in source:
|
||||
source["text"] = source["text"][:100].strip() + "..."
|
||||
if len(sources) > 0:
|
||||
data = json.dumps({"type": "source", "source": sources})
|
||||
yield f"data: {data}\n\n"
|
||||
for line in answer:
|
||||
if "answer" in line:
|
||||
response_full += str(line["answer"])
|
||||
data = json.dumps(line)
|
||||
yield f"data: {data}\n\n"
|
||||
elif "source" in line:
|
||||
source_log_docs.append(line["source"])
|
||||
|
||||
if isNoneDoc:
|
||||
for doc in source_log_docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
if user_api_key is None:
|
||||
conversation_id = save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm,index
|
||||
)
|
||||
# send data.type = "end" to indicate that the stream has ended as json
|
||||
data = json.dumps({"type": "id", "id": str(conversation_id)})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
retriever_params = retriever.get_params()
|
||||
user_logs_collection.insert_one(
|
||||
{
|
||||
"action": "stream_answer",
|
||||
"level": "info",
|
||||
"user": "local",
|
||||
"api_key": user_api_key,
|
||||
"question": question,
|
||||
"response": response_full,
|
||||
"sources": source_log_docs,
|
||||
"retriever_params": retriever_params,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
)
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
except Exception as e:
|
||||
print("\033[91merr", str(e), file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
data = json.dumps(
|
||||
{
|
||||
"type": "error",
|
||||
"error": "Please try again later. We apologize for any inconvenience.",
|
||||
"error_exception": str(e),
|
||||
}
|
||||
)
|
||||
yield f"data: {data}\n\n"
|
||||
return
|
||||
|
||||
|
||||
@answer_ns.route("/stream")
|
||||
class Stream(Resource):
|
||||
stream_model = api.model(
|
||||
"StreamModel",
|
||||
{
|
||||
"question": fields.String(
|
||||
required=True, description="Question to be asked"
|
||||
),
|
||||
"history": fields.List(
|
||||
fields.String, required=False, description="Chat history"
|
||||
),
|
||||
"conversation_id": fields.String(
|
||||
required=False, description="Conversation ID"
|
||||
),
|
||||
"prompt_id": fields.String(
|
||||
required=False, default="default", description="Prompt ID"
|
||||
),
|
||||
"chunks": fields.Integer(
|
||||
required=False, default=2, description="Number of chunks"
|
||||
),
|
||||
"token_limit": fields.Integer(required=False, description="Token limit"),
|
||||
"retriever": fields.String(required=False, description="Retriever type"),
|
||||
"api_key": fields.String(required=False, description="API key"),
|
||||
"active_docs": fields.String(
|
||||
required=False, description="Active documents"
|
||||
),
|
||||
"isNoneDoc": fields.Boolean(
|
||||
required=False, description="Flag indicating if no document is used"
|
||||
),
|
||||
"index":fields.Integer(
|
||||
required=False, description="The position where query is to be updated"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@api.expect(stream_model)
|
||||
@api.doc(description="Stream a response based on the question and retriever")
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = ["question"]
|
||||
if "index" in data:
|
||||
required_fields = ["question","conversation_id"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
|
||||
try:
|
||||
question = data["question"]
|
||||
history = limit_chat_history(json.loads(data.get("history", [])), gpt_model=gpt_model)
|
||||
conversation_id = data.get("conversation_id")
|
||||
prompt_id = data.get("prompt_id", "default")
|
||||
|
||||
index=data.get("index",None)
|
||||
chunks = int(data.get("chunks", 2))
|
||||
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
||||
retriever_name = data.get("retriever", "classic")
|
||||
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
chunks = int(data_key.get("chunks", 2))
|
||||
prompt_id = data_key.get("prompt_id", "default")
|
||||
source = {"active_docs": data_key.get("source")}
|
||||
retriever_name = data_key.get("retriever", retriever_name)
|
||||
user_api_key = data["api_key"]
|
||||
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
retriever_name = get_retriever(data["active_docs"]) or retriever_name
|
||||
user_api_key = None
|
||||
|
||||
else:
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
current_app.logger.info(
|
||||
f"/stream - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})},
|
||||
)
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
if "isNoneDoc" in data and data["isNoneDoc"] is True:
|
||||
chunks = 0
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=history,
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
|
||||
return Response(
|
||||
complete_stream(
|
||||
question=question,
|
||||
retriever=retriever,
|
||||
conversation_id=conversation_id,
|
||||
user_api_key=user_api_key,
|
||||
isNoneDoc=data.get("isNoneDoc"),
|
||||
index=index,
|
||||
),
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
message = "Malformed request body"
|
||||
print("\033[91merr", str(message), file=sys.stderr)
|
||||
return Response(
|
||||
error_stream_generate(message),
|
||||
status=400,
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()},
|
||||
)
|
||||
message = e.args[0]
|
||||
status_code = 400
|
||||
# Custom exceptions with two arguments, index 1 as status code
|
||||
if len(e.args) >= 2:
|
||||
status_code = e.args[1]
|
||||
return Response(
|
||||
error_stream_generate(message),
|
||||
status=status_code,
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
|
||||
|
||||
def error_stream_generate(err_response):
|
||||
data = json.dumps({"type": "error", "error": err_response})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
|
||||
@answer_ns.route("/api/answer")
|
||||
class Answer(Resource):
|
||||
answer_model = api.model(
|
||||
"AnswerModel",
|
||||
{
|
||||
"question": fields.String(
|
||||
required=True, description="The question to answer"
|
||||
),
|
||||
"history": fields.List(
|
||||
fields.String, required=False, description="Conversation history"
|
||||
),
|
||||
"conversation_id": fields.String(
|
||||
required=False, description="Conversation ID"
|
||||
),
|
||||
"prompt_id": fields.String(
|
||||
required=False, default="default", description="Prompt ID"
|
||||
),
|
||||
"chunks": fields.Integer(
|
||||
required=False, default=2, description="Number of chunks"
|
||||
),
|
||||
"token_limit": fields.Integer(required=False, description="Token limit"),
|
||||
"retriever": fields.String(required=False, description="Retriever type"),
|
||||
"api_key": fields.String(required=False, description="API key"),
|
||||
"active_docs": fields.String(
|
||||
required=False, description="Active documents"
|
||||
),
|
||||
"isNoneDoc": fields.Boolean(
|
||||
required=False, description="Flag indicating if no document is used"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@api.expect(answer_model)
|
||||
@api.doc(description="Provide an answer based on the question and retriever")
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = ["question"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
|
||||
try:
|
||||
question = data["question"]
|
||||
history = limit_chat_history(json.loads(data.get("history", [])), gpt_model=gpt_model)
|
||||
conversation_id = data.get("conversation_id")
|
||||
prompt_id = data.get("prompt_id", "default")
|
||||
chunks = int(data.get("chunks", 2))
|
||||
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
||||
retriever_name = data.get("retriever", "classic")
|
||||
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
chunks = int(data_key.get("chunks", 2))
|
||||
prompt_id = data_key.get("prompt_id", "default")
|
||||
source = {"active_docs": data_key.get("source")}
|
||||
retriever_name = data_key.get("retriever", retriever_name)
|
||||
user_api_key = data["api_key"]
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
retriever_name = get_retriever(data["active_docs"]) or retriever_name
|
||||
user_api_key = None
|
||||
else:
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
current_app.logger.info(
|
||||
f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})},
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=history,
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
|
||||
source_log_docs = []
|
||||
response_full = ""
|
||||
for line in retriever.gen():
|
||||
if "source" in line:
|
||||
source_log_docs.append(line["source"])
|
||||
elif "answer" in line:
|
||||
response_full += line["answer"]
|
||||
|
||||
if data.get("isNoneDoc"):
|
||||
for doc in source_log_docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
|
||||
result = {"answer": response_full, "sources": source_log_docs}
|
||||
result["conversation_id"] = str(
|
||||
save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm
|
||||
)
|
||||
)
|
||||
retriever_params = retriever.get_params()
|
||||
user_logs_collection.insert_one(
|
||||
{
|
||||
"action": "api_answer",
|
||||
"level": "info",
|
||||
"user": "local",
|
||||
"api_key": user_api_key,
|
||||
"question": question,
|
||||
"response": response_full,
|
||||
"sources": source_log_docs,
|
||||
"retriever_params": retriever_params,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()},
|
||||
)
|
||||
return bad_request(500, str(e))
|
||||
|
||||
return make_response(result, 200)
|
||||
|
||||
|
||||
@answer_ns.route("/api/search")
|
||||
class Search(Resource):
|
||||
search_model = api.model(
|
||||
"SearchModel",
|
||||
{
|
||||
"question": fields.String(
|
||||
required=True, description="The question to search"
|
||||
),
|
||||
"chunks": fields.Integer(
|
||||
required=False, default=2, description="Number of chunks"
|
||||
),
|
||||
"api_key": fields.String(
|
||||
required=False, description="API key for authentication"
|
||||
),
|
||||
"active_docs": fields.String(
|
||||
required=False, description="Active documents for retrieval"
|
||||
),
|
||||
"retriever": fields.String(required=False, description="Retriever type"),
|
||||
"token_limit": fields.Integer(
|
||||
required=False, description="Limit for tokens"
|
||||
),
|
||||
"isNoneDoc": fields.Boolean(
|
||||
required=False, description="Flag indicating if no document is used"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@api.expect(search_model)
|
||||
@api.doc(
|
||||
description="Search for relevant documents based on the question and retriever"
|
||||
)
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = ["question"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
|
||||
try:
|
||||
question = data["question"]
|
||||
chunks = int(data.get("chunks", 2))
|
||||
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
||||
retriever_name = data.get("retriever", "classic")
|
||||
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
chunks = int(data_key.get("chunks", 2))
|
||||
source = {"active_docs": data_key.get("source")}
|
||||
user_api_key = data["api_key"]
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
user_api_key = None
|
||||
else:
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
current_app.logger.info(
|
||||
f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})},
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=[],
|
||||
prompt="default",
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
|
||||
docs = retriever.search()
|
||||
retriever_params = retriever.get_params()
|
||||
|
||||
user_logs_collection.insert_one(
|
||||
{
|
||||
"action": "api_search",
|
||||
"level": "info",
|
||||
"user": "local",
|
||||
"api_key": user_api_key,
|
||||
"question": question,
|
||||
"sources": docs,
|
||||
"retriever_params": retriever_params,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
)
|
||||
|
||||
if data.get("isNoneDoc"):
|
||||
for doc in docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"/api/search - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()},
|
||||
)
|
||||
return bad_request(500, str(e))
|
||||
|
||||
return make_response(docs, 200)
|
||||
0
application/api/internal/__init__.py
Normal file
0
application/api/internal/__init__.py
Normal file
104
application/api/internal/routes.py
Executable file
104
application/api/internal/routes.py
Executable file
@@ -0,0 +1,104 @@
|
||||
import os
|
||||
import datetime
|
||||
from flask import Blueprint, request, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from application.core.mongo_db import MongoDB
|
||||
from application.core.settings import settings
|
||||
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
conversations_collection = db["conversations"]
|
||||
sources_collection = db["sources"]
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
|
||||
internal = Blueprint("internal", __name__)
|
||||
|
||||
|
||||
@internal.route("/api/download", methods=["get"])
|
||||
def download_file():
|
||||
user = secure_filename(request.args.get("user"))
|
||||
job_name = secure_filename(request.args.get("name"))
|
||||
filename = secure_filename(request.args.get("file"))
|
||||
save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, job_name)
|
||||
return send_from_directory(save_dir, filename, as_attachment=True)
|
||||
|
||||
|
||||
@internal.route("/api/upload_index", methods=["POST"])
|
||||
def upload_index_files():
|
||||
"""Upload two files(index.faiss, index.pkl) to the user's folder."""
|
||||
if "user" not in request.form:
|
||||
return {"status": "no user"}
|
||||
user = secure_filename(request.form["user"])
|
||||
if "name" not in request.form:
|
||||
return {"status": "no name"}
|
||||
job_name = secure_filename(request.form["name"])
|
||||
tokens = secure_filename(request.form["tokens"])
|
||||
retriever = secure_filename(request.form["retriever"])
|
||||
id = secure_filename(request.form["id"])
|
||||
type = secure_filename(request.form["type"])
|
||||
remote_data = request.form["remote_data"] if "remote_data" in request.form else None
|
||||
sync_frequency = secure_filename(request.form["sync_frequency"]) if "sync_frequency" in request.form else None
|
||||
|
||||
save_dir = os.path.join(current_dir, "indexes", str(id))
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
if "file_faiss" not in request.files:
|
||||
print("No file part")
|
||||
return {"status": "no file"}
|
||||
file_faiss = request.files["file_faiss"]
|
||||
if file_faiss.filename == "":
|
||||
return {"status": "no file name"}
|
||||
if "file_pkl" not in request.files:
|
||||
print("No file part")
|
||||
return {"status": "no file"}
|
||||
file_pkl = request.files["file_pkl"]
|
||||
if file_pkl.filename == "":
|
||||
return {"status": "no file name"}
|
||||
# saves index files
|
||||
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
file_faiss.save(os.path.join(save_dir, "index.faiss"))
|
||||
file_pkl.save(os.path.join(save_dir, "index.pkl"))
|
||||
|
||||
existing_entry = sources_collection.find_one({"_id": ObjectId(id)})
|
||||
if existing_entry:
|
||||
sources_collection.update_one(
|
||||
{"_id": ObjectId(id)},
|
||||
{
|
||||
"$set": {
|
||||
"user": user,
|
||||
"name": job_name,
|
||||
"language": job_name,
|
||||
"date": datetime.datetime.now(),
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"type": type,
|
||||
"tokens": tokens,
|
||||
"retriever": retriever,
|
||||
"remote_data": remote_data,
|
||||
"sync_frequency": sync_frequency,
|
||||
}
|
||||
},
|
||||
)
|
||||
else:
|
||||
sources_collection.insert_one(
|
||||
{
|
||||
"_id": ObjectId(id),
|
||||
"user": user,
|
||||
"name": job_name,
|
||||
"language": job_name,
|
||||
"date": datetime.datetime.now(),
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"type": type,
|
||||
"tokens": tokens,
|
||||
"retriever": retriever,
|
||||
"remote_data": remote_data,
|
||||
"sync_frequency": sync_frequency,
|
||||
}
|
||||
)
|
||||
return {"status": "ok"}
|
||||
0
application/api/user/__init__.py
Normal file
0
application/api/user/__init__.py
Normal file
2033
application/api/user/routes.py
Normal file
2033
application/api/user/routes.py
Normal file
File diff suppressed because it is too large
Load Diff
38
application/api/user/tasks.py
Normal file
38
application/api/user/tasks.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from application.celery_init import celery
|
||||
from application.worker import ingest_worker, remote_worker, sync_worker
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def ingest(self, directory, formats, name_job, filename, user):
|
||||
resp = ingest_worker(self, directory, formats, name_job, filename, user)
|
||||
return resp
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def ingest_remote(self, source_data, job_name, user, loader):
|
||||
resp = remote_worker(self, source_data, job_name, user, loader)
|
||||
return resp
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def schedule_syncs(self, frequency):
|
||||
resp = sync_worker(self, frequency)
|
||||
return resp
|
||||
|
||||
|
||||
@celery.on_after_configure.connect
|
||||
def setup_periodic_tasks(sender, **kwargs):
|
||||
sender.add_periodic_task(
|
||||
timedelta(days=1),
|
||||
schedule_syncs.s("daily"),
|
||||
)
|
||||
sender.add_periodic_task(
|
||||
timedelta(weeks=1),
|
||||
schedule_syncs.s("weekly"),
|
||||
)
|
||||
sender.add_periodic_task(
|
||||
timedelta(days=30),
|
||||
schedule_syncs.s("monthly"),
|
||||
)
|
||||
@@ -1,202 +1,53 @@
|
||||
import os
|
||||
import json
|
||||
import traceback
|
||||
import platform
|
||||
|
||||
import dotenv
|
||||
import requests
|
||||
from flask import Flask, request, render_template
|
||||
from langchain import FAISS
|
||||
from langchain.llms import OpenAIChat
|
||||
from langchain import VectorDBQA, HuggingFaceHub, Cohere, OpenAI
|
||||
from langchain.chains.question_answering import load_qa_chain
|
||||
from langchain.embeddings import OpenAIEmbeddings, HuggingFaceHubEmbeddings, CohereEmbeddings, \
|
||||
HuggingFaceInstructEmbeddings
|
||||
from langchain.prompts import PromptTemplate
|
||||
from error import bad_request
|
||||
from flask import Flask, redirect, request
|
||||
|
||||
os.environ["LANGCHAIN_HANDLER"] = "langchain"
|
||||
|
||||
if os.getenv("LLM_NAME") is not None:
|
||||
llm_choice = os.getenv("LLM_NAME")
|
||||
else:
|
||||
llm_choice = "openai"
|
||||
|
||||
if os.getenv("EMBEDDINGS_NAME") is not None:
|
||||
embeddings_choice = os.getenv("EMBEDDINGS_NAME")
|
||||
else:
|
||||
embeddings_choice = "openai_text-embedding-ada-002"
|
||||
|
||||
if llm_choice == "manifest":
|
||||
from manifest import Manifest
|
||||
from langchain.llms.manifest import ManifestWrapper
|
||||
|
||||
manifest = Manifest(
|
||||
client_name="huggingface",
|
||||
client_connection="http://127.0.0.1:5000"
|
||||
)
|
||||
|
||||
# Redirect PosixPath to WindowsPath on Windows
|
||||
import platform
|
||||
from application.api.answer.routes import answer
|
||||
from application.api.internal.routes import internal
|
||||
from application.api.user.routes import user
|
||||
from application.celery_init import celery
|
||||
from application.core.logging_config import setup_logging
|
||||
from application.core.settings import settings
|
||||
from application.extensions import api
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import pathlib
|
||||
|
||||
temp = pathlib.PosixPath
|
||||
pathlib.PosixPath = pathlib.WindowsPath
|
||||
|
||||
# loading the .env file
|
||||
dotenv.load_dotenv()
|
||||
|
||||
with open("combine_prompt.txt", "r") as f:
|
||||
template = f.read()
|
||||
|
||||
with open("combine_prompt_hist.txt", "r") as f:
|
||||
template_hist = f.read()
|
||||
|
||||
with open("question_prompt.txt", "r") as f:
|
||||
template_quest = f.read()
|
||||
|
||||
if os.getenv("API_KEY") is not None:
|
||||
api_key_set = True
|
||||
else:
|
||||
api_key_set = False
|
||||
if os.getenv("EMBEDDINGS_KEY") is not None:
|
||||
embeddings_key_set = True
|
||||
else:
|
||||
embeddings_key_set = False
|
||||
setup_logging()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(user)
|
||||
app.register_blueprint(answer)
|
||||
app.register_blueprint(internal)
|
||||
app.config.update(
|
||||
UPLOAD_FOLDER="inputs",
|
||||
CELERY_BROKER_URL=settings.CELERY_BROKER_URL,
|
||||
CELERY_RESULT_BACKEND=settings.CELERY_RESULT_BACKEND,
|
||||
MONGO_URI=settings.MONGO_URI,
|
||||
)
|
||||
celery.config_from_object("application.celeryconfig")
|
||||
api.init_app(app)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return render_template("index.html", api_key_set=api_key_set, llm_choice=llm_choice,
|
||||
embeddings_choice=embeddings_choice)
|
||||
|
||||
|
||||
@app.route("/api/answer", methods=["POST"])
|
||||
def api_answer():
|
||||
data = request.get_json()
|
||||
question = data["question"]
|
||||
history = data["history"]
|
||||
print('-' * 5)
|
||||
if not api_key_set:
|
||||
api_key = data["api_key"]
|
||||
if request.remote_addr in ("0.0.0.0", "127.0.0.1", "localhost", "172.18.0.1"):
|
||||
return redirect("http://localhost:5173")
|
||||
else:
|
||||
api_key = os.getenv("API_KEY")
|
||||
if not embeddings_key_set:
|
||||
embeddings_key = data["embeddings_key"]
|
||||
else:
|
||||
embeddings_key = os.getenv("EMBEDDINGS_KEY")
|
||||
|
||||
# use try and except to check for exception
|
||||
try:
|
||||
# check if the vectorstore is set
|
||||
if "active_docs" in data:
|
||||
vectorstore = "vectors/" + data["active_docs"]
|
||||
if data['active_docs'] == "default":
|
||||
vectorstore = ""
|
||||
else:
|
||||
vectorstore = ""
|
||||
#vectorstore = "outputs/inputs/"
|
||||
# loading the index and the store and the prompt template
|
||||
# Note if you have used other embeddings than OpenAI, you need to change the embeddings
|
||||
if embeddings_choice == "openai_text-embedding-ada-002":
|
||||
docsearch = FAISS.load_local(vectorstore, OpenAIEmbeddings(openai_api_key=embeddings_key))
|
||||
elif embeddings_choice == "huggingface_sentence-transformers/all-mpnet-base-v2":
|
||||
docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings())
|
||||
elif embeddings_choice == "huggingface_hkunlp/instructor-large":
|
||||
docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings())
|
||||
elif embeddings_choice == "cohere_medium":
|
||||
docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key))
|
||||
|
||||
# create a prompt template
|
||||
if history:
|
||||
history = json.loads(history)
|
||||
template_temp = template_hist.replace("{historyquestion}", history[0]).replace("{historyanswer}",
|
||||
history[1])
|
||||
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template_temp,
|
||||
template_format="jinja2")
|
||||
else:
|
||||
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template,
|
||||
template_format="jinja2")
|
||||
|
||||
q_prompt = PromptTemplate(input_variables=["context", "question"], template=template_quest,
|
||||
template_format="jinja2")
|
||||
if llm_choice == "openai":
|
||||
llm = OpenAIChat(openai_api_key=api_key, temperature=0)
|
||||
#llm = OpenAI(openai_api_key=api_key, temperature=0)
|
||||
elif llm_choice == "manifest":
|
||||
llm = ManifestWrapper(client=manifest, llm_kwargs={"temperature": 0.001, "max_tokens": 2048})
|
||||
elif llm_choice == "huggingface":
|
||||
llm = HuggingFaceHub(repo_id="bigscience/bloom", huggingfacehub_api_token=api_key)
|
||||
elif llm_choice == "cohere":
|
||||
llm = Cohere(model="command-xlarge-nightly", cohere_api_key=api_key)
|
||||
|
||||
qa_chain = load_qa_chain(llm=llm, chain_type="map_reduce",
|
||||
combine_prompt=c_prompt, question_prompt=q_prompt)
|
||||
|
||||
chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=docsearch, k=10)
|
||||
|
||||
# fetch the answer
|
||||
result = chain({"query": question})
|
||||
|
||||
# some formatting for the frontend
|
||||
result['answer'] = result['result']
|
||||
result['answer'] = result['answer'].replace("\\n", "\n")
|
||||
try:
|
||||
result['answer'] = result['answer'].split("SOURCES:")[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
# mock result
|
||||
# result = {
|
||||
# "answer": "The answer is 42",
|
||||
# "sources": ["https://en.wikipedia.org/wiki/42_(number)", "https://en.wikipedia.org/wiki/42_(number)"]
|
||||
# }
|
||||
return result
|
||||
except Exception as e:
|
||||
# print whole traceback
|
||||
traceback.print_exc()
|
||||
print(str(e))
|
||||
return bad_request(500, str(e))
|
||||
return "Welcome to DocsGPT Backend!"
|
||||
|
||||
|
||||
@app.route("/api/docs_check", methods=["POST"])
|
||||
def check_docs():
|
||||
# check if docs exist in a vectorstore folder
|
||||
data = request.get_json()
|
||||
vectorstore = "vectors/" + data["docs"]
|
||||
base_path = 'https://raw.githubusercontent.com/arc53/DocsHUB/main/'
|
||||
if os.path.exists(vectorstore) or data["docs"] == "default":
|
||||
return {"status": 'exists'}
|
||||
else:
|
||||
r = requests.get(base_path + vectorstore + "index.faiss")
|
||||
|
||||
if r.status_code != 200:
|
||||
return {"status": 'null'}
|
||||
else:
|
||||
if not os.path.exists(vectorstore):
|
||||
os.makedirs(vectorstore)
|
||||
with open(vectorstore + "index.faiss", "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
# download the store
|
||||
r = requests.get(base_path + vectorstore + "index.pkl")
|
||||
with open(vectorstore + "index.pkl", "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
return {"status": 'loaded'}
|
||||
|
||||
|
||||
# handling CORS
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
|
||||
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||
response.headers.add("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, port=5001)
|
||||
app.run(debug=settings.FLASK_DEBUG_MODE, port=7091)
|
||||
|
||||
103
application/cache.py
Normal file
103
application/cache.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from threading import Lock
|
||||
|
||||
import redis
|
||||
|
||||
from application.core.settings import settings
|
||||
from application.utils import get_hash
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_redis_instance = None
|
||||
_instance_lock = Lock()
|
||||
|
||||
|
||||
def get_redis_instance():
|
||||
global _redis_instance
|
||||
if _redis_instance is None:
|
||||
with _instance_lock:
|
||||
if _redis_instance is None:
|
||||
try:
|
||||
_redis_instance = redis.Redis.from_url(
|
||||
settings.CACHE_REDIS_URL, socket_connect_timeout=2
|
||||
)
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
_redis_instance = None
|
||||
return _redis_instance
|
||||
|
||||
|
||||
def gen_cache_key(messages, model="docgpt", tools=None):
|
||||
if not all(isinstance(msg, dict) for msg in messages):
|
||||
raise ValueError("All messages must be dictionaries.")
|
||||
messages_str = json.dumps(messages)
|
||||
tools_str = json.dumps(str(tools)) if tools else ""
|
||||
combined = f"{model}_{messages_str}_{tools_str}"
|
||||
cache_key = get_hash(combined)
|
||||
return cache_key
|
||||
|
||||
|
||||
def gen_cache(func):
|
||||
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
|
||||
try:
|
||||
cache_key = gen_cache_key(messages, model, tools)
|
||||
redis_client = get_redis_instance()
|
||||
if redis_client:
|
||||
try:
|
||||
cached_response = redis_client.get(cache_key)
|
||||
if cached_response:
|
||||
return cached_response.decode("utf-8")
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
result = func(self, model, messages, stream, tools, *args, **kwargs)
|
||||
if redis_client and isinstance(result, str):
|
||||
try:
|
||||
redis_client.set(cache_key, result, ex=1800)
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
return result
|
||||
except ValueError as e:
|
||||
logger.error(e)
|
||||
return "Error: No user message found in the conversation to generate a cache key."
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def stream_cache(func):
|
||||
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
|
||||
cache_key = gen_cache_key(messages, model, tools)
|
||||
logger.info(f"Stream cache key: {cache_key}")
|
||||
|
||||
redis_client = get_redis_instance()
|
||||
if redis_client:
|
||||
try:
|
||||
cached_response = redis_client.get(cache_key)
|
||||
if cached_response:
|
||||
logger.info(f"Cache hit for stream key: {cache_key}")
|
||||
cached_response = json.loads(cached_response.decode("utf-8"))
|
||||
for chunk in cached_response:
|
||||
yield chunk
|
||||
time.sleep(0.03)
|
||||
return
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
result = func(self, model, messages, stream, tools=tools, *args, **kwargs)
|
||||
stream_cache_data = []
|
||||
|
||||
for chunk in result:
|
||||
stream_cache_data.append(chunk)
|
||||
yield chunk
|
||||
|
||||
if redis_client:
|
||||
try:
|
||||
redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800)
|
||||
logger.info(f"Stream cache saved for key: {cache_key}")
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
return wrapper
|
||||
23
application/celery_init.py
Normal file
23
application/celery_init.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from celery import Celery
|
||||
from application.core.settings import settings
|
||||
from celery.signals import setup_logging
|
||||
|
||||
|
||||
def make_celery(app_name=__name__):
|
||||
celery = Celery(
|
||||
app_name,
|
||||
broker=settings.CELERY_BROKER_URL,
|
||||
backend=settings.CELERY_RESULT_BACKEND,
|
||||
)
|
||||
celery.conf.update(settings)
|
||||
return celery
|
||||
|
||||
|
||||
@setup_logging.connect
|
||||
def config_loggers(*args, **kwargs):
|
||||
from application.core.logging_config import setup_logging
|
||||
|
||||
setup_logging()
|
||||
|
||||
|
||||
celery = make_celery()
|
||||
8
application/celeryconfig.py
Normal file
8
application/celeryconfig.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
broker_url = os.getenv("CELERY_BROKER_URL")
|
||||
result_backend = os.getenv("CELERY_RESULT_BACKEND")
|
||||
|
||||
task_serializer = 'json'
|
||||
result_serializer = 'json'
|
||||
accept_content = ['json']
|
||||
@@ -1,25 +0,0 @@
|
||||
You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible.
|
||||
|
||||
QUESTION: How to merge tables in pandas?
|
||||
=========
|
||||
Content: pandas provides various facilities for easily combining together Series or DataFrame with various kinds of set logic for the indexes and relational algebra functionality in the case of join / merge-type operations.
|
||||
Source: 28-pl
|
||||
Content: pandas provides a single function, merge(), as the entry point for all standard database join operations between DataFrame or named Series objects: \n\npandas.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)
|
||||
Source: 30-pl
|
||||
=========
|
||||
FINAL ANSWER: To merge two tables in pandas, you can use the pd.merge() function. The basic syntax is: \n\npd.merge(left, right, on, how) \n\nwhere left and right are the two tables to merge, on is the column to merge on, and how is the type of merge to perform. \n\nFor example, to merge the two tables df1 and df2 on the column 'id', you can use: \n\npd.merge(df1, df2, on='id', how='inner')
|
||||
SOURCES: 28-pl 30-pl
|
||||
|
||||
QUESTION: How are you?
|
||||
=========
|
||||
CONTENT:
|
||||
SOURCE:
|
||||
=========
|
||||
FINAL ANSWER: I am fine, thank you. How are you?
|
||||
SOURCES:
|
||||
|
||||
QUESTION: {{ question }}
|
||||
=========
|
||||
{{ summaries }}
|
||||
=========
|
||||
FINAL ANSWER:
|
||||
@@ -1,33 +0,0 @@
|
||||
You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible.
|
||||
|
||||
QUESTION: How to merge tables in pandas?
|
||||
=========
|
||||
Content: pandas provides various facilities for easily combining together Series or DataFrame with various kinds of set logic for the indexes and relational algebra functionality in the case of join / merge-type operations.
|
||||
Source: 28-pl
|
||||
Content: pandas provides a single function, merge(), as the entry point for all standard database join operations between DataFrame or named Series objects: \n\npandas.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)
|
||||
Source: 30-pl
|
||||
=========
|
||||
FINAL ANSWER: To merge two tables in pandas, you can use the pd.merge() function. The basic syntax is: \n\npd.merge(left, right, on, how) \n\nwhere left and right are the two tables to merge, on is the column to merge on, and how is the type of merge to perform. \n\nFor example, to merge the two tables df1 and df2 on the column 'id', you can use: \n\npd.merge(df1, df2, on='id', how='inner')
|
||||
SOURCES: 28-pl 30-pl
|
||||
|
||||
QUESTION: How are you?
|
||||
=========
|
||||
CONTENT:
|
||||
SOURCE:
|
||||
=========
|
||||
FINAL ANSWER: I am fine, thank you. How are you?
|
||||
SOURCES:
|
||||
|
||||
QUESTION: {{ historyquestion }}
|
||||
=========
|
||||
CONTENT:
|
||||
SOURCE:
|
||||
=========
|
||||
FINAL ANSWER: {{ historyanswer }}
|
||||
SOURCES:
|
||||
|
||||
QUESTION: {{ question }}
|
||||
=========
|
||||
{{ summaries }}
|
||||
=========
|
||||
FINAL ANSWER:
|
||||
0
application/core/__init__.py
Normal file
0
application/core/__init__.py
Normal file
22
application/core/logging_config.py
Normal file
22
application/core/logging_config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from logging.config import dictConfig
|
||||
|
||||
def setup_logging():
|
||||
dictConfig({
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'default': {
|
||||
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
'root': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['console'],
|
||||
},
|
||||
})
|
||||
24
application/core/mongo_db.py
Normal file
24
application/core/mongo_db.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from application.core.settings import settings
|
||||
from pymongo import MongoClient
|
||||
|
||||
|
||||
class MongoDB:
|
||||
_client = None
|
||||
|
||||
@classmethod
|
||||
def get_client(cls):
|
||||
"""
|
||||
Get the MongoDB client instance, creating it if necessary.
|
||||
"""
|
||||
if cls._client is None:
|
||||
cls._client = MongoClient(settings.MONGO_URI)
|
||||
return cls._client
|
||||
|
||||
@classmethod
|
||||
def close_client(cls):
|
||||
"""
|
||||
Close the MongoDB client connection.
|
||||
"""
|
||||
if cls._client is not None:
|
||||
cls._client.close()
|
||||
cls._client = None
|
||||
83
application/core/settings.py
Normal file
83
application/core/settings.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
LLM_NAME: str = "docsgpt"
|
||||
MODEL_NAME: Optional[str] = None # if LLM_NAME is openai, MODEL_NAME can be gpt-4 or gpt-3.5-turbo
|
||||
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
||||
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
|
||||
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
|
||||
DEFAULT_MAX_HISTORY: int = 150
|
||||
MODEL_TOKEN_LIMITS: dict = {"gpt-4o-mini": 128000, "gpt-3.5-turbo": 4096, "claude-2": 1e5}
|
||||
UPLOAD_FOLDER: str = "inputs"
|
||||
PARSE_PDF_AS_IMAGE: bool = False
|
||||
VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb"
|
||||
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
||||
|
||||
# LLM Cache
|
||||
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
|
||||
|
||||
API_URL: str = "http://localhost:7091" # backend url for celery worker
|
||||
|
||||
API_KEY: Optional[str] = None # LLM api key
|
||||
EMBEDDINGS_KEY: Optional[str] = None # api key for embeddings (if using openai, just copy API_KEY)
|
||||
OPENAI_API_BASE: Optional[str] = None # azure openai api base url
|
||||
OPENAI_API_VERSION: Optional[str] = None # azure openai api version
|
||||
AZURE_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for answering
|
||||
AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for embeddings
|
||||
OPENAI_BASE_URL: Optional[str] = None # openai base url for open ai compatable models
|
||||
|
||||
# elasticsearch
|
||||
ELASTIC_CLOUD_ID: Optional[str] = None # cloud id for elasticsearch
|
||||
ELASTIC_USERNAME: Optional[str] = None # username for elasticsearch
|
||||
ELASTIC_PASSWORD: Optional[str] = None # password for elasticsearch
|
||||
ELASTIC_URL: Optional[str] = None # url for elasticsearch
|
||||
ELASTIC_INDEX: Optional[str] = "docsgpt" # index name for elasticsearch
|
||||
|
||||
# SageMaker config
|
||||
SAGEMAKER_ENDPOINT: Optional[str] = None # SageMaker endpoint name
|
||||
SAGEMAKER_REGION: Optional[str] = None # SageMaker region name
|
||||
SAGEMAKER_ACCESS_KEY: Optional[str] = None # SageMaker access key
|
||||
SAGEMAKER_SECRET_KEY: Optional[str] = None # SageMaker secret key
|
||||
|
||||
# prem ai project id
|
||||
PREMAI_PROJECT_ID: Optional[str] = None
|
||||
|
||||
# Qdrant vectorstore config
|
||||
QDRANT_COLLECTION_NAME: Optional[str] = "docsgpt"
|
||||
QDRANT_LOCATION: Optional[str] = None
|
||||
QDRANT_URL: Optional[str] = None
|
||||
QDRANT_PORT: Optional[int] = 6333
|
||||
QDRANT_GRPC_PORT: int = 6334
|
||||
QDRANT_PREFER_GRPC: bool = False
|
||||
QDRANT_HTTPS: Optional[bool] = None
|
||||
QDRANT_API_KEY: Optional[str] = None
|
||||
QDRANT_PREFIX: Optional[str] = None
|
||||
QDRANT_TIMEOUT: Optional[float] = None
|
||||
QDRANT_HOST: Optional[str] = None
|
||||
QDRANT_PATH: Optional[str] = None
|
||||
QDRANT_DISTANCE_FUNC: str = "Cosine"
|
||||
|
||||
# Milvus vectorstore config
|
||||
MILVUS_COLLECTION_NAME: Optional[str] = "docsgpt"
|
||||
MILVUS_URI: Optional[str] = "./milvus_local.db" # milvus lite version as default
|
||||
MILVUS_TOKEN: Optional[str] = ""
|
||||
|
||||
# LanceDB vectorstore config
|
||||
LANCEDB_PATH: str = "/tmp/lancedb" # Path where LanceDB stores its local data
|
||||
LANCEDB_TABLE_NAME: Optional[str] = "docsgpts" # Name of the table to use for storing vectors
|
||||
BRAVE_SEARCH_API_KEY: Optional[str] = None
|
||||
|
||||
FLASK_DEBUG_MODE: bool = False
|
||||
|
||||
|
||||
path = Path(__file__).parent.parent.absolute()
|
||||
settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")
|
||||
@@ -1,13 +1,15 @@
|
||||
from flask import jsonify
|
||||
from werkzeug.http import HTTP_STATUS_CODES
|
||||
|
||||
def response_error(code_status,message=None):
|
||||
payload = {'error':HTTP_STATUS_CODES.get(code_status,"something went wrong")}
|
||||
|
||||
def response_error(code_status, message=None):
|
||||
payload = {'error': HTTP_STATUS_CODES.get(code_status, "something went wrong")}
|
||||
if message:
|
||||
payload['message'] = message
|
||||
response = jsonify(payload)
|
||||
response.status_code = code_status
|
||||
return response
|
||||
|
||||
def bad_request(status_code=400,message=''):
|
||||
return response_error(code_status=status_code,message=message)
|
||||
|
||||
def bad_request(status_code=400, message=''):
|
||||
return response_error(code_status=status_code, message=message)
|
||||
|
||||
7
application/extensions.py
Normal file
7
application/extensions.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from flask_restx import Api
|
||||
|
||||
api = Api(
|
||||
version="1.0",
|
||||
title="DocsGPT API",
|
||||
description="API for DocsGPT",
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
0
application/llm/__init__.py
Normal file
0
application/llm/__init__.py
Normal file
50
application/llm/anthropic.py
Normal file
50
application/llm/anthropic.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
|
||||
|
||||
class AnthropicLLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = (
|
||||
api_key or settings.ANTHROPIC_API_KEY
|
||||
) # If not provided, use a default from settings
|
||||
self.user_api_key = user_api_key
|
||||
self.anthropic = Anthropic(api_key=self.api_key)
|
||||
self.HUMAN_PROMPT = HUMAN_PROMPT
|
||||
self.AI_PROMPT = AI_PROMPT
|
||||
|
||||
def _raw_gen(
|
||||
self, baseself, model, messages, stream=False, tools=None, max_tokens=300, **kwargs
|
||||
):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Context \n {context} \n ### Question \n {user_question}"
|
||||
if stream:
|
||||
return self.gen_stream(model, prompt, stream, max_tokens, **kwargs)
|
||||
|
||||
completion = self.anthropic.completions.create(
|
||||
model=model,
|
||||
max_tokens_to_sample=max_tokens,
|
||||
stream=stream,
|
||||
prompt=f"{self.HUMAN_PROMPT} {prompt}{self.AI_PROMPT}",
|
||||
)
|
||||
return completion.completion
|
||||
|
||||
def _raw_gen_stream(
|
||||
self, baseself, model, messages, stream=True, tools=None, max_tokens=300, **kwargs
|
||||
):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Context \n {context} \n ### Question \n {user_question}"
|
||||
stream_response = self.anthropic.completions.create(
|
||||
model=model,
|
||||
prompt=f"{self.HUMAN_PROMPT} {prompt}{self.AI_PROMPT}",
|
||||
max_tokens_to_sample=max_tokens,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for completion in stream_response:
|
||||
yield completion.completion
|
||||
56
application/llm/base.py
Normal file
56
application/llm/base.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from application.cache import gen_cache, stream_cache
|
||||
from application.usage import gen_token_usage, stream_token_usage
|
||||
|
||||
|
||||
class BaseLLM(ABC):
|
||||
def __init__(self):
|
||||
self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0}
|
||||
|
||||
def _apply_decorator(self, method, decorators, *args, **kwargs):
|
||||
for decorator in decorators:
|
||||
method = decorator(method)
|
||||
return method(self, *args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def _raw_gen(self, model, messages, stream, tools, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def gen(self, model, messages, stream=False, tools=None, *args, **kwargs):
|
||||
decorators = [gen_token_usage, gen_cache]
|
||||
return self._apply_decorator(
|
||||
self._raw_gen,
|
||||
decorators=decorators,
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
tools=tools,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def _raw_gen_stream(self, model, messages, stream, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs):
|
||||
decorators = [stream_cache, stream_token_usage]
|
||||
return self._apply_decorator(
|
||||
self._raw_gen_stream,
|
||||
decorators=decorators,
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
tools=tools,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def supports_tools(self):
|
||||
return hasattr(self, "_supports_tools") and callable(
|
||||
getattr(self, "_supports_tools")
|
||||
)
|
||||
|
||||
def _supports_tools(self):
|
||||
raise NotImplementedError("Subclass must implement _supports_tools method")
|
||||
34
application/llm/docsgpt_provider.py
Normal file
34
application/llm/docsgpt_provider.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from application.llm.base import BaseLLM
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
||||
class DocsGPTAPILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.endpoint = "https://llm.arc53.com"
|
||||
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, *args, **kwargs):
|
||||
response = requests.post(
|
||||
f"{self.endpoint}/answer", json={"messages": messages, "max_new_tokens": 30}
|
||||
)
|
||||
response_clean = response.json()["a"].replace("###", "")
|
||||
|
||||
return response_clean
|
||||
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs):
|
||||
response = requests.post(
|
||||
f"{self.endpoint}/stream",
|
||||
json={"messages": messages, "max_new_tokens": 256},
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
data_str = line.decode("utf-8")
|
||||
if data_str.startswith("data: "):
|
||||
data = json.loads(data_str[6:])
|
||||
yield data["a"]
|
||||
159
application/llm/google_ai.py
Normal file
159
application/llm/google_ai.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
from application.llm.base import BaseLLM
|
||||
|
||||
|
||||
class GoogleLLM(BaseLLM):
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _clean_messages_google(self, messages):
|
||||
cleaned_messages = []
|
||||
for message in messages:
|
||||
role = message.get("role")
|
||||
content = message.get("content")
|
||||
|
||||
if role == "assistant":
|
||||
role = "model"
|
||||
|
||||
parts = []
|
||||
if role and content is not None:
|
||||
if isinstance(content, str):
|
||||
parts = [types.Part.from_text(content)]
|
||||
elif isinstance(content, list):
|
||||
for item in content:
|
||||
if "text" in item:
|
||||
parts.append(types.Part.from_text(item["text"]))
|
||||
elif "function_call" in item:
|
||||
parts.append(
|
||||
types.Part.from_function_call(
|
||||
name=item["function_call"]["name"],
|
||||
args=item["function_call"]["args"],
|
||||
)
|
||||
)
|
||||
elif "function_response" in item:
|
||||
parts.append(
|
||||
types.Part.from_function_response(
|
||||
name=item["function_response"]["name"],
|
||||
response=item["function_response"]["response"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected content dictionary format:{item}"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unexpected content type: {type(content)}")
|
||||
|
||||
cleaned_messages.append(types.Content(role=role, parts=parts))
|
||||
|
||||
return cleaned_messages
|
||||
|
||||
def _clean_tools_format(self, tools_list):
|
||||
genai_tools = []
|
||||
for tool_data in tools_list:
|
||||
if tool_data["type"] == "function":
|
||||
function = tool_data["function"]
|
||||
parameters = function["parameters"]
|
||||
properties = parameters.get("properties", {})
|
||||
|
||||
if properties:
|
||||
genai_function = dict(
|
||||
name=function["name"],
|
||||
description=function["description"],
|
||||
parameters={
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
k: {
|
||||
**v,
|
||||
"type": v["type"].upper() if v["type"] else None,
|
||||
}
|
||||
for k, v in properties.items()
|
||||
},
|
||||
"required": (
|
||||
parameters["required"]
|
||||
if "required" in parameters
|
||||
else []
|
||||
),
|
||||
},
|
||||
)
|
||||
else:
|
||||
genai_function = dict(
|
||||
name=function["name"],
|
||||
description=function["description"],
|
||||
)
|
||||
|
||||
genai_tool = types.Tool(function_declarations=[genai_function])
|
||||
genai_tools.append(genai_tool)
|
||||
|
||||
return genai_tools
|
||||
|
||||
def _raw_gen(
|
||||
self,
|
||||
baseself,
|
||||
model,
|
||||
messages,
|
||||
stream=False,
|
||||
tools=None,
|
||||
formatting="openai",
|
||||
**kwargs,
|
||||
):
|
||||
client = genai.Client(api_key=self.api_key)
|
||||
if formatting == "openai":
|
||||
messages = self._clean_messages_google(messages)
|
||||
config = types.GenerateContentConfig()
|
||||
if messages[0].role == "system":
|
||||
config.system_instruction = messages[0].parts[0].text
|
||||
messages = messages[1:]
|
||||
|
||||
if tools:
|
||||
cleaned_tools = self._clean_tools_format(tools)
|
||||
config.tools = cleaned_tools
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=messages,
|
||||
config=config,
|
||||
)
|
||||
return response
|
||||
else:
|
||||
response = client.models.generate_content(
|
||||
model=model, contents=messages, config=config
|
||||
)
|
||||
return response.text
|
||||
|
||||
def _raw_gen_stream(
|
||||
self,
|
||||
baseself,
|
||||
model,
|
||||
messages,
|
||||
stream=True,
|
||||
tools=None,
|
||||
formatting="openai",
|
||||
**kwargs,
|
||||
):
|
||||
client = genai.Client(api_key=self.api_key)
|
||||
if formatting == "openai":
|
||||
messages = self._clean_messages_google(messages)
|
||||
config = types.GenerateContentConfig()
|
||||
if messages[0].role == "system":
|
||||
config.system_instruction = messages[0].parts[0].text
|
||||
messages = messages[1:]
|
||||
|
||||
if tools:
|
||||
cleaned_tools = self._clean_tools_format(tools)
|
||||
config.tools = cleaned_tools
|
||||
|
||||
response = client.models.generate_content_stream(
|
||||
model=model,
|
||||
contents=messages,
|
||||
config=config,
|
||||
)
|
||||
for chunk in response:
|
||||
if chunk.text is not None:
|
||||
yield chunk.text
|
||||
|
||||
def _supports_tools(self):
|
||||
return True
|
||||
32
application/llm/groq.py
Normal file
32
application/llm/groq.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
class GroqLLM(BaseLLM):
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = OpenAI(api_key=api_key, base_url="https://api.groq.com/openai/v1")
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs):
|
||||
if tools:
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, tools=tools, **kwargs
|
||||
)
|
||||
return response.choices[0]
|
||||
else:
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
def _raw_gen_stream(
|
||||
self, baseself, model, messages, stream=True, tools=None, **kwargs
|
||||
):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
for line in response:
|
||||
if line.choices[0].delta.content is not None:
|
||||
yield line.choices[0].delta.content
|
||||
68
application/llm/huggingface.py
Normal file
68
application/llm/huggingface.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from application.llm.base import BaseLLM
|
||||
|
||||
|
||||
class HuggingFaceLLM(BaseLLM):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key=None,
|
||||
user_api_key=None,
|
||||
llm_name="Arc53/DocsGPT-7B",
|
||||
q=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
global hf
|
||||
|
||||
from langchain.llms import HuggingFacePipeline
|
||||
|
||||
if q:
|
||||
import torch
|
||||
from transformers import (
|
||||
AutoModelForCausalLM,
|
||||
AutoTokenizer,
|
||||
pipeline,
|
||||
BitsAndBytesConfig,
|
||||
)
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(llm_name)
|
||||
bnb_config = BitsAndBytesConfig(
|
||||
load_in_4bit=True,
|
||||
bnb_4bit_use_double_quant=True,
|
||||
bnb_4bit_quant_type="nf4",
|
||||
bnb_4bit_compute_dtype=torch.bfloat16,
|
||||
)
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
llm_name, quantization_config=bnb_config
|
||||
)
|
||||
else:
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(llm_name)
|
||||
model = AutoModelForCausalLM.from_pretrained(llm_name)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
pipe = pipeline(
|
||||
"text-generation",
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
max_new_tokens=2000,
|
||||
device_map="auto",
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
)
|
||||
hf = HuggingFacePipeline(pipeline=pipe)
|
||||
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
result = hf(prompt)
|
||||
|
||||
return result.content
|
||||
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
|
||||
raise NotImplementedError("HuggingFaceLLM Streaming is not implemented yet.")
|
||||
55
application/llm/llama_cpp.py
Normal file
55
application/llm/llama_cpp.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
import threading
|
||||
|
||||
class LlamaSingleton:
|
||||
_instances = {}
|
||||
_lock = threading.Lock() # Add a lock for thread synchronization
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, llm_name):
|
||||
if llm_name not in cls._instances:
|
||||
try:
|
||||
from llama_cpp import Llama
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install llama_cpp using pip install llama-cpp-python"
|
||||
)
|
||||
cls._instances[llm_name] = Llama(model_path=llm_name, n_ctx=2048)
|
||||
return cls._instances[llm_name]
|
||||
|
||||
@classmethod
|
||||
def query_model(cls, llm, prompt, **kwargs):
|
||||
with cls._lock:
|
||||
return llm(prompt, **kwargs)
|
||||
|
||||
|
||||
class LlamaCpp(BaseLLM):
|
||||
def __init__(
|
||||
self,
|
||||
api_key=None,
|
||||
user_api_key=None,
|
||||
llm_name=settings.MODEL_PATH,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.llama = LlamaSingleton.get_instance(llm_name)
|
||||
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False)
|
||||
return result["choices"][0]["text"].split("### Answer \n")[-1]
|
||||
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False, stream=stream)
|
||||
for item in result:
|
||||
for choice in item["choices"]:
|
||||
yield choice["text"]
|
||||
31
application/llm/llm_creator.py
Normal file
31
application/llm/llm_creator.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from application.llm.groq import GroqLLM
|
||||
from application.llm.openai import OpenAILLM, AzureOpenAILLM
|
||||
from application.llm.sagemaker import SagemakerAPILLM
|
||||
from application.llm.huggingface import HuggingFaceLLM
|
||||
from application.llm.llama_cpp import LlamaCpp
|
||||
from application.llm.anthropic import AnthropicLLM
|
||||
from application.llm.docsgpt_provider import DocsGPTAPILLM
|
||||
from application.llm.premai import PremAILLM
|
||||
from application.llm.google_ai import GoogleLLM
|
||||
|
||||
|
||||
class LLMCreator:
|
||||
llms = {
|
||||
"openai": OpenAILLM,
|
||||
"azure_openai": AzureOpenAILLM,
|
||||
"sagemaker": SagemakerAPILLM,
|
||||
"huggingface": HuggingFaceLLM,
|
||||
"llama.cpp": LlamaCpp,
|
||||
"anthropic": AnthropicLLM,
|
||||
"docsgpt": DocsGPTAPILLM,
|
||||
"premai": PremAILLM,
|
||||
"groq": GroqLLM,
|
||||
"google": GoogleLLM
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_llm(cls, type, api_key, user_api_key, *args, **kwargs):
|
||||
llm_class = cls.llms.get(type.lower())
|
||||
if not llm_class:
|
||||
raise ValueError(f"No LLM class found for type {type}")
|
||||
return llm_class(api_key, user_api_key, *args, **kwargs)
|
||||
77
application/llm/openai.py
Normal file
77
application/llm/openai.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from application.core.settings import settings
|
||||
from application.llm.base import BaseLLM
|
||||
|
||||
|
||||
class OpenAILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
from openai import OpenAI
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if settings.OPENAI_BASE_URL:
|
||||
self.client = OpenAI(api_key=api_key, base_url=settings.OPENAI_BASE_URL)
|
||||
else:
|
||||
self.client = OpenAI(api_key=api_key)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _raw_gen(
|
||||
self,
|
||||
baseself,
|
||||
model,
|
||||
messages,
|
||||
stream=False,
|
||||
tools=None,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
**kwargs,
|
||||
):
|
||||
if tools:
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, tools=tools, **kwargs
|
||||
)
|
||||
return response.choices[0]
|
||||
else:
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
def _raw_gen_stream(
|
||||
self,
|
||||
baseself,
|
||||
model,
|
||||
messages,
|
||||
stream=True,
|
||||
tools=None,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
**kwargs,
|
||||
):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
|
||||
for line in response:
|
||||
if line.choices[0].delta.content is not None:
|
||||
yield line.choices[0].delta.content
|
||||
|
||||
def _supports_tools(self):
|
||||
return True
|
||||
|
||||
|
||||
class AzureOpenAILLM(OpenAILLM):
|
||||
|
||||
def __init__(
|
||||
self, openai_api_key, openai_api_base, openai_api_version, deployment_name
|
||||
):
|
||||
super().__init__(openai_api_key)
|
||||
self.api_base = (settings.OPENAI_API_BASE,)
|
||||
self.api_version = (settings.OPENAI_API_VERSION,)
|
||||
self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,)
|
||||
from openai import AzureOpenAI
|
||||
|
||||
self.client = AzureOpenAI(
|
||||
api_key=openai_api_key,
|
||||
api_version=settings.OPENAI_API_VERSION,
|
||||
api_base=settings.OPENAI_API_BASE,
|
||||
deployment_name=settings.AZURE_DEPLOYMENT_NAME,
|
||||
)
|
||||
38
application/llm/premai.py
Normal file
38
application/llm/premai.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
|
||||
|
||||
class PremAILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
from premai import Prem
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = Prem(api_key=api_key)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.project_id = settings.PREMAI_PROJECT_ID
|
||||
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model,
|
||||
project_id=self.project_id,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return response.choices[0].message["content"]
|
||||
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model,
|
||||
project_id=self.project_id,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
for line in response:
|
||||
if line.choices[0].delta["content"] is not None:
|
||||
yield line.choices[0].delta["content"]
|
||||
140
application/llm/sagemaker.py
Normal file
140
application/llm/sagemaker.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
import json
|
||||
import io
|
||||
|
||||
|
||||
class LineIterator:
|
||||
"""
|
||||
A helper class for parsing the byte stream input.
|
||||
|
||||
The output of the model will be in the following format:
|
||||
```
|
||||
b'{"outputs": [" a"]}\n'
|
||||
b'{"outputs": [" challenging"]}\n'
|
||||
b'{"outputs": [" problem"]}\n'
|
||||
...
|
||||
```
|
||||
|
||||
While usually each PayloadPart event from the event stream will contain a byte array
|
||||
with a full json, this is not guaranteed and some of the json objects may be split across
|
||||
PayloadPart events. For example:
|
||||
```
|
||||
{'PayloadPart': {'Bytes': b'{"outputs": '}}
|
||||
{'PayloadPart': {'Bytes': b'[" problem"]}\n'}}
|
||||
```
|
||||
|
||||
This class accounts for this by concatenating bytes written via the 'write' function
|
||||
and then exposing a method which will return lines (ending with a '\n' character) within
|
||||
the buffer via the 'scan_lines' function. It maintains the position of the last read
|
||||
position to ensure that previous bytes are not exposed again.
|
||||
"""
|
||||
|
||||
def __init__(self, stream):
|
||||
self.byte_iterator = iter(stream)
|
||||
self.buffer = io.BytesIO()
|
||||
self.read_pos = 0
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
while True:
|
||||
self.buffer.seek(self.read_pos)
|
||||
line = self.buffer.readline()
|
||||
if line and line[-1] == ord("\n"):
|
||||
self.read_pos += len(line)
|
||||
return line[:-1]
|
||||
try:
|
||||
chunk = next(self.byte_iterator)
|
||||
except StopIteration:
|
||||
if self.read_pos < self.buffer.getbuffer().nbytes:
|
||||
continue
|
||||
raise
|
||||
if "PayloadPart" not in chunk:
|
||||
print("Unknown event type:" + chunk)
|
||||
continue
|
||||
self.buffer.seek(0, io.SEEK_END)
|
||||
self.buffer.write(chunk["PayloadPart"]["Bytes"])
|
||||
|
||||
|
||||
class SagemakerAPILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
import boto3
|
||||
|
||||
runtime = boto3.client(
|
||||
"runtime.sagemaker",
|
||||
aws_access_key_id="xxx",
|
||||
aws_secret_access_key="xxx",
|
||||
region_name="us-west-2",
|
||||
)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.endpoint = settings.SAGEMAKER_ENDPOINT
|
||||
self.runtime = runtime
|
||||
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
# Construct payload for endpoint
|
||||
payload = {
|
||||
"inputs": prompt,
|
||||
"stream": False,
|
||||
"parameters": {
|
||||
"do_sample": True,
|
||||
"temperature": 0.1,
|
||||
"max_new_tokens": 30,
|
||||
"repetition_penalty": 1.03,
|
||||
"stop": ["</s>", "###"],
|
||||
},
|
||||
}
|
||||
body_bytes = json.dumps(payload).encode("utf-8")
|
||||
|
||||
# Invoke the endpoint
|
||||
response = self.runtime.invoke_endpoint(
|
||||
EndpointName=self.endpoint, ContentType="application/json", Body=body_bytes
|
||||
)
|
||||
result = json.loads(response["Body"].read().decode())
|
||||
import sys
|
||||
|
||||
print(result[0]["generated_text"], file=sys.stderr)
|
||||
return result[0]["generated_text"][len(prompt) :]
|
||||
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, tools=None, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
# Construct payload for endpoint
|
||||
payload = {
|
||||
"inputs": prompt,
|
||||
"stream": True,
|
||||
"parameters": {
|
||||
"do_sample": True,
|
||||
"temperature": 0.1,
|
||||
"max_new_tokens": 512,
|
||||
"repetition_penalty": 1.03,
|
||||
"stop": ["</s>", "###"],
|
||||
},
|
||||
}
|
||||
body_bytes = json.dumps(payload).encode("utf-8")
|
||||
|
||||
# Invoke the endpoint
|
||||
response = self.runtime.invoke_endpoint_with_response_stream(
|
||||
EndpointName=self.endpoint, ContentType="application/json", Body=body_bytes
|
||||
)
|
||||
# result = json.loads(response['Body'].read().decode())
|
||||
event_stream = response["Body"]
|
||||
start_json = b"{"
|
||||
for line in LineIterator(event_stream):
|
||||
if line != b"" and start_json in line:
|
||||
# print(line)
|
||||
data = json.loads(line[line.find(start_json) :].decode("utf-8"))
|
||||
if data["token"]["text"] not in ["</s>", "###"]:
|
||||
print(data["token"]["text"], end="")
|
||||
yield data["token"]["text"]
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.2.4"
|
||||
}
|
||||
}
|
||||
1
application/parser/__init__.py
Normal file
1
application/parser/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
118
application/parser/chunking.py
Normal file
118
application/parser/chunking.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
import logging
|
||||
from application.parser.schema.base import Document
|
||||
from application.utils import get_encoding
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Chunker:
|
||||
def __init__(
|
||||
self,
|
||||
chunking_strategy: str = "classic_chunk",
|
||||
max_tokens: int = 2000,
|
||||
min_tokens: int = 150,
|
||||
duplicate_headers: bool = False,
|
||||
):
|
||||
if chunking_strategy not in ["classic_chunk"]:
|
||||
raise ValueError(f"Unsupported chunking strategy: {chunking_strategy}")
|
||||
self.chunking_strategy = chunking_strategy
|
||||
self.max_tokens = max_tokens
|
||||
self.min_tokens = min_tokens
|
||||
self.duplicate_headers = duplicate_headers
|
||||
self.encoding = get_encoding()
|
||||
|
||||
def separate_header_and_body(self, text: str) -> Tuple[str, str]:
|
||||
header_pattern = r"^(.*?\n){3}"
|
||||
match = re.match(header_pattern, text)
|
||||
if match:
|
||||
header = match.group(0)
|
||||
body = text[len(header):]
|
||||
else:
|
||||
header, body = "", text # No header, treat entire text as body
|
||||
return header, body
|
||||
|
||||
def combine_documents(self, doc: Document, next_doc: Document) -> Document:
|
||||
combined_text = doc.text + " " + next_doc.text
|
||||
combined_token_count = len(self.encoding.encode(combined_text))
|
||||
new_doc = Document(
|
||||
text=combined_text,
|
||||
doc_id=doc.doc_id,
|
||||
embedding=doc.embedding,
|
||||
extra_info={**(doc.extra_info or {}), "token_count": combined_token_count}
|
||||
)
|
||||
return new_doc
|
||||
|
||||
def split_document(self, doc: Document) -> List[Document]:
|
||||
split_docs = []
|
||||
header, body = self.separate_header_and_body(doc.text)
|
||||
header_tokens = self.encoding.encode(header) if header else []
|
||||
body_tokens = self.encoding.encode(body)
|
||||
|
||||
current_position = 0
|
||||
part_index = 0
|
||||
while current_position < len(body_tokens):
|
||||
end_position = current_position + self.max_tokens - len(header_tokens)
|
||||
chunk_tokens = (header_tokens + body_tokens[current_position:end_position]
|
||||
if self.duplicate_headers or part_index == 0 else body_tokens[current_position:end_position])
|
||||
chunk_text = self.encoding.decode(chunk_tokens)
|
||||
new_doc = Document(
|
||||
text=chunk_text,
|
||||
doc_id=f"{doc.doc_id}-{part_index}",
|
||||
embedding=doc.embedding,
|
||||
extra_info={**(doc.extra_info or {}), "token_count": len(chunk_tokens)}
|
||||
)
|
||||
split_docs.append(new_doc)
|
||||
current_position = end_position
|
||||
part_index += 1
|
||||
header_tokens = []
|
||||
return split_docs
|
||||
|
||||
def classic_chunk(self, documents: List[Document]) -> List[Document]:
|
||||
processed_docs = []
|
||||
i = 0
|
||||
while i < len(documents):
|
||||
doc = documents[i]
|
||||
tokens = self.encoding.encode(doc.text)
|
||||
token_count = len(tokens)
|
||||
|
||||
if self.min_tokens <= token_count <= self.max_tokens:
|
||||
doc.extra_info = doc.extra_info or {}
|
||||
doc.extra_info["token_count"] = token_count
|
||||
processed_docs.append(doc)
|
||||
i += 1
|
||||
elif token_count < self.min_tokens:
|
||||
if i + 1 < len(documents):
|
||||
next_doc = documents[i + 1]
|
||||
next_tokens = self.encoding.encode(next_doc.text)
|
||||
if token_count + len(next_tokens) <= self.max_tokens:
|
||||
# Combine small documents
|
||||
combined_doc = self.combine_documents(doc, next_doc)
|
||||
processed_docs.append(combined_doc)
|
||||
i += 2
|
||||
else:
|
||||
# Keep the small document as is if adding next_doc would exceed max_tokens
|
||||
doc.extra_info = doc.extra_info or {}
|
||||
doc.extra_info["token_count"] = token_count
|
||||
processed_docs.append(doc)
|
||||
i += 1
|
||||
else:
|
||||
# No next document to combine with; add the small document as is
|
||||
doc.extra_info = doc.extra_info or {}
|
||||
doc.extra_info["token_count"] = token_count
|
||||
processed_docs.append(doc)
|
||||
i += 1
|
||||
else:
|
||||
# Split large documents
|
||||
processed_docs.extend(self.split_document(doc))
|
||||
i += 1
|
||||
return processed_docs
|
||||
|
||||
def chunk(
|
||||
self,
|
||||
documents: List[Document]
|
||||
) -> List[Document]:
|
||||
if self.chunking_strategy == "classic_chunk":
|
||||
return self.classic_chunk(documents)
|
||||
else:
|
||||
raise ValueError("Unsupported chunking strategy")
|
||||
86
application/parser/embedding_pipeline.py
Executable file
86
application/parser/embedding_pipeline.py
Executable file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import logging
|
||||
from retry import retry
|
||||
from tqdm import tqdm
|
||||
from application.core.settings import settings
|
||||
from application.vectorstore.vector_creator import VectorCreator
|
||||
|
||||
|
||||
@retry(tries=10, delay=60)
|
||||
def add_text_to_store_with_retry(store, doc, source_id):
|
||||
"""
|
||||
Add a document's text and metadata to the vector store with retry logic.
|
||||
Args:
|
||||
store: The vector store object.
|
||||
doc: The document to be added.
|
||||
source_id: Unique identifier for the source.
|
||||
"""
|
||||
try:
|
||||
doc.metadata["source_id"] = str(source_id)
|
||||
store.add_texts([doc.page_content], metadatas=[doc.metadata])
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to add document with retry: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def embed_and_store_documents(docs, folder_name, source_id, task_status):
|
||||
"""
|
||||
Embeds documents and stores them in a vector store.
|
||||
|
||||
Args:
|
||||
docs (list): List of documents to be embedded and stored.
|
||||
folder_name (str): Directory to save the vector store.
|
||||
source_id (str): Unique identifier for the source.
|
||||
task_status: Task state manager for progress updates.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Ensure the folder exists
|
||||
if not os.path.exists(folder_name):
|
||||
os.makedirs(folder_name)
|
||||
|
||||
# Initialize vector store
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
docs_init = [docs.pop(0)]
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
docs_init=docs_init,
|
||||
source_id=folder_name,
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
)
|
||||
else:
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
source_id=source_id,
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
)
|
||||
store.delete_index()
|
||||
|
||||
total_docs = len(docs)
|
||||
|
||||
# Process and embed documents
|
||||
for idx, doc in tqdm(
|
||||
enumerate(docs),
|
||||
desc="Embedding 🦖",
|
||||
unit="docs",
|
||||
total=total_docs,
|
||||
bar_format="{l_bar}{bar}| Time Left: {remaining}",
|
||||
):
|
||||
try:
|
||||
# Update task status for progress tracking
|
||||
progress = int(((idx + 1) / total_docs) * 100)
|
||||
task_status.update_state(state="PROGRESS", meta={"current": progress})
|
||||
|
||||
# Add document to vector store
|
||||
add_text_to_store_with_retry(store, doc, source_id)
|
||||
except Exception as e:
|
||||
logging.error(f"Error embedding document {idx}: {e}")
|
||||
logging.info(f"Saving progress at document {idx} out of {total_docs}")
|
||||
store.save_local(folder_name)
|
||||
break
|
||||
|
||||
# Save the vector store
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
store.save_local(folder_name)
|
||||
logging.info("Vector store saved successfully.")
|
||||
1
application/parser/file/__init__.py
Normal file
1
application/parser/file/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
19
application/parser/file/base.py
Normal file
19
application/parser/file/base.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Base reader class."""
|
||||
from abc import abstractmethod
|
||||
from typing import Any, List
|
||||
|
||||
from langchain.docstore.document import Document as LCDocument
|
||||
from application.parser.schema.base import Document
|
||||
|
||||
|
||||
class BaseReader:
|
||||
"""Utilities for loading data from a directory."""
|
||||
|
||||
@abstractmethod
|
||||
def load_data(self, *args: Any, **load_kwargs: Any) -> List[Document]:
|
||||
"""Load data from the input directory."""
|
||||
|
||||
def load_langchain_documents(self, **load_kwargs: Any) -> List[LCDocument]:
|
||||
"""Load data in LangChain document format."""
|
||||
docs = self.load_data(**load_kwargs)
|
||||
return [d.to_langchain_format() for d in docs]
|
||||
38
application/parser/file/base_parser.py
Normal file
38
application/parser/file/base_parser.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Base parser and config class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
|
||||
class BaseParser:
|
||||
"""Base class for all parsers."""
|
||||
|
||||
def __init__(self, parser_config: Optional[Dict] = None):
|
||||
"""Init params."""
|
||||
self._parser_config = parser_config
|
||||
|
||||
def init_parser(self) -> None:
|
||||
"""Init parser and store it."""
|
||||
parser_config = self._init_parser()
|
||||
self._parser_config = parser_config
|
||||
|
||||
@property
|
||||
def parser_config_set(self) -> bool:
|
||||
"""Check if parser config is set."""
|
||||
return self._parser_config is not None
|
||||
|
||||
@property
|
||||
def parser_config(self) -> Dict:
|
||||
"""Check if parser config is set."""
|
||||
if self._parser_config is None:
|
||||
raise ValueError("Parser config not set.")
|
||||
return self._parser_config
|
||||
|
||||
@abstractmethod
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Initialize the parser with the config."""
|
||||
|
||||
@abstractmethod
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
||||
"""Parse file."""
|
||||
183
application/parser/file/bulk.py
Normal file
183
application/parser/file/bulk.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Simple reader that reads files of different formats from a directory."""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
|
||||
from application.parser.file.base import BaseReader
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
from application.parser.file.docs_parser import DocxParser, PDFParser
|
||||
from application.parser.file.epub_parser import EpubParser
|
||||
from application.parser.file.html_parser import HTMLParser
|
||||
from application.parser.file.markdown_parser import MarkdownParser
|
||||
from application.parser.file.rst_parser import RstParser
|
||||
from application.parser.file.tabular_parser import PandasCSVParser,ExcelParser
|
||||
from application.parser.file.json_parser import JSONParser
|
||||
from application.parser.file.pptx_parser import PPTXParser
|
||||
from application.parser.file.image_parser import ImageParser
|
||||
from application.parser.schema.base import Document
|
||||
|
||||
DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = {
|
||||
".pdf": PDFParser(),
|
||||
".docx": DocxParser(),
|
||||
".csv": PandasCSVParser(),
|
||||
".xlsx":ExcelParser(),
|
||||
".epub": EpubParser(),
|
||||
".md": MarkdownParser(),
|
||||
".rst": RstParser(),
|
||||
".html": HTMLParser(),
|
||||
".mdx": MarkdownParser(),
|
||||
".json":JSONParser(),
|
||||
".pptx":PPTXParser(),
|
||||
".png": ImageParser(),
|
||||
".jpg": ImageParser(),
|
||||
".jpeg": ImageParser(),
|
||||
}
|
||||
|
||||
|
||||
class SimpleDirectoryReader(BaseReader):
|
||||
"""Simple directory reader.
|
||||
|
||||
Can read files into separate documents, or concatenates
|
||||
files into one document text.
|
||||
|
||||
Args:
|
||||
input_dir (str): Path to the directory.
|
||||
input_files (List): List of file paths to read (Optional; overrides input_dir)
|
||||
exclude_hidden (bool): Whether to exclude hidden files (dotfiles).
|
||||
errors (str): how encoding and decoding errors are to be handled,
|
||||
see https://docs.python.org/3/library/functions.html#open
|
||||
recursive (bool): Whether to recursively search in subdirectories.
|
||||
False by default.
|
||||
required_exts (Optional[List[str]]): List of required extensions.
|
||||
Default is None.
|
||||
file_extractor (Optional[Dict[str, BaseParser]]): A mapping of file
|
||||
extension to a BaseParser class that specifies how to convert that file
|
||||
to text. See DEFAULT_FILE_EXTRACTOR.
|
||||
num_files_limit (Optional[int]): Maximum number of files to read.
|
||||
Default is None.
|
||||
file_metadata (Optional[Callable[str, Dict]]): A function that takes
|
||||
in a filename and returns a Dict of metadata for the Document.
|
||||
Default is None.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_dir: Optional[str] = None,
|
||||
input_files: Optional[List] = None,
|
||||
exclude_hidden: bool = True,
|
||||
errors: str = "ignore",
|
||||
recursive: bool = True,
|
||||
required_exts: Optional[List[str]] = None,
|
||||
file_extractor: Optional[Dict[str, BaseParser]] = None,
|
||||
num_files_limit: Optional[int] = None,
|
||||
file_metadata: Optional[Callable[[str], Dict]] = None,
|
||||
) -> None:
|
||||
"""Initialize with parameters."""
|
||||
super().__init__()
|
||||
|
||||
if not input_dir and not input_files:
|
||||
raise ValueError("Must provide either `input_dir` or `input_files`.")
|
||||
|
||||
self.errors = errors
|
||||
|
||||
self.recursive = recursive
|
||||
self.exclude_hidden = exclude_hidden
|
||||
self.required_exts = required_exts
|
||||
self.num_files_limit = num_files_limit
|
||||
|
||||
if input_files:
|
||||
self.input_files = []
|
||||
for path in input_files:
|
||||
print(path)
|
||||
input_file = Path(path)
|
||||
self.input_files.append(input_file)
|
||||
elif input_dir:
|
||||
self.input_dir = Path(input_dir)
|
||||
self.input_files = self._add_files(self.input_dir)
|
||||
|
||||
self.file_extractor = file_extractor or DEFAULT_FILE_EXTRACTOR
|
||||
self.file_metadata = file_metadata
|
||||
|
||||
def _add_files(self, input_dir: Path) -> List[Path]:
|
||||
"""Add files."""
|
||||
input_files = sorted(input_dir.iterdir())
|
||||
new_input_files = []
|
||||
dirs_to_explore = []
|
||||
for input_file in input_files:
|
||||
if input_file.is_dir():
|
||||
if self.recursive:
|
||||
dirs_to_explore.append(input_file)
|
||||
elif self.exclude_hidden and input_file.name.startswith("."):
|
||||
continue
|
||||
elif (
|
||||
self.required_exts is not None
|
||||
and input_file.suffix not in self.required_exts
|
||||
):
|
||||
continue
|
||||
else:
|
||||
new_input_files.append(input_file)
|
||||
|
||||
for dir_to_explore in dirs_to_explore:
|
||||
sub_input_files = self._add_files(dir_to_explore)
|
||||
new_input_files.extend(sub_input_files)
|
||||
|
||||
if self.num_files_limit is not None and self.num_files_limit > 0:
|
||||
new_input_files = new_input_files[0: self.num_files_limit]
|
||||
|
||||
# print total number of files added
|
||||
logging.debug(
|
||||
f"> [SimpleDirectoryReader] Total files added: {len(new_input_files)}"
|
||||
)
|
||||
|
||||
return new_input_files
|
||||
|
||||
def load_data(self, concatenate: bool = False) -> List[Document]:
|
||||
"""Load data from the input directory.
|
||||
|
||||
Args:
|
||||
concatenate (bool): whether to concatenate all files into one document.
|
||||
If set to True, file metadata is ignored.
|
||||
False by default.
|
||||
|
||||
Returns:
|
||||
List[Document]: A list of documents.
|
||||
|
||||
"""
|
||||
data: Union[str, List[str]] = ""
|
||||
data_list: List[str] = []
|
||||
metadata_list = []
|
||||
for input_file in self.input_files:
|
||||
if input_file.suffix in self.file_extractor:
|
||||
parser = self.file_extractor[input_file.suffix]
|
||||
if not parser.parser_config_set:
|
||||
parser.init_parser()
|
||||
data = parser.parse_file(input_file, errors=self.errors)
|
||||
else:
|
||||
# do standard read
|
||||
with open(input_file, "r", errors=self.errors) as f:
|
||||
data = f.read()
|
||||
# Prepare metadata for this file
|
||||
if self.file_metadata is not None:
|
||||
file_metadata = self.file_metadata(str(input_file))
|
||||
else:
|
||||
# Provide a default empty metadata
|
||||
file_metadata = {'title': '', 'store': ''}
|
||||
# TODO: Find a case with no metadata and check if breaks anything
|
||||
|
||||
if isinstance(data, List):
|
||||
# Extend data_list with each item in the data list
|
||||
data_list.extend([str(d) for d in data])
|
||||
# For each item in the data list, add the file's metadata to metadata_list
|
||||
metadata_list.extend([file_metadata for _ in data])
|
||||
else:
|
||||
# Add the single piece of data to data_list
|
||||
data_list.append(str(data))
|
||||
# Add the file's metadata to metadata_list
|
||||
metadata_list.append(file_metadata)
|
||||
|
||||
if concatenate:
|
||||
return [Document("\n".join(data_list))]
|
||||
elif self.file_metadata is not None:
|
||||
return [Document(d, extra_info=m) for d, m in zip(data_list, metadata_list)]
|
||||
else:
|
||||
return [Document(d) for d in data_list]
|
||||
69
application/parser/file/docs_parser.py
Normal file
69
application/parser/file/docs_parser.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Docs parser.
|
||||
|
||||
Contains parsers for docx, pdf files.
|
||||
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
from application.core.settings import settings
|
||||
import requests
|
||||
|
||||
class PDFParser(BaseParser):
|
||||
"""PDF parser."""
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
||||
"""Parse file."""
|
||||
if settings.PARSE_PDF_AS_IMAGE:
|
||||
doc2md_service = "https://llm.arc53.com/doc2md"
|
||||
# alternatively you can use local vision capable LLM
|
||||
with open(file, "rb") as file_loaded:
|
||||
files = {'file': file_loaded}
|
||||
response = requests.post(doc2md_service, files=files)
|
||||
data = response.json()["markdown"]
|
||||
return data
|
||||
|
||||
try:
|
||||
import PyPDF2
|
||||
except ImportError:
|
||||
raise ValueError("PyPDF2 is required to read PDF files.")
|
||||
text_list = []
|
||||
with open(file, "rb") as fp:
|
||||
# Create a PDF object
|
||||
pdf = PyPDF2.PdfReader(fp)
|
||||
|
||||
# Get the number of pages in the PDF document
|
||||
num_pages = len(pdf.pages)
|
||||
|
||||
# Iterate over every page
|
||||
for page in range(num_pages):
|
||||
# Extract the text from the page
|
||||
page_text = pdf.pages[page].extract_text()
|
||||
text_list.append(page_text)
|
||||
text = "\n".join(text_list)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
class DocxParser(BaseParser):
|
||||
"""Docx parser."""
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
||||
"""Parse file."""
|
||||
try:
|
||||
import docx2txt
|
||||
except ImportError:
|
||||
raise ValueError("docx2txt is required to read Microsoft Word files.")
|
||||
|
||||
text = docx2txt.process(file)
|
||||
|
||||
return text
|
||||
43
application/parser/file/epub_parser.py
Normal file
43
application/parser/file/epub_parser.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Epub parser.
|
||||
|
||||
Contains parsers for epub files.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
|
||||
class EpubParser(BaseParser):
|
||||
"""Epub Parser."""
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
||||
"""Parse file."""
|
||||
try:
|
||||
import ebooklib
|
||||
from ebooklib import epub
|
||||
except ImportError:
|
||||
raise ValueError("`EbookLib` is required to read Epub files.")
|
||||
try:
|
||||
import html2text
|
||||
except ImportError:
|
||||
raise ValueError("`html2text` is required to parse Epub files.")
|
||||
|
||||
text_list = []
|
||||
book = epub.read_epub(file, options={"ignore_ncx": True})
|
||||
|
||||
# Iterate through all chapters.
|
||||
for item in book.get_items():
|
||||
# Chapters are typically located in epub documents items.
|
||||
if item.get_type() == ebooklib.ITEM_DOCUMENT:
|
||||
text_list.append(
|
||||
html2text.html2text(item.get_content().decode("utf-8"))
|
||||
)
|
||||
|
||||
text = "\n".join(text_list)
|
||||
return text
|
||||
24
application/parser/file/html_parser.py
Normal file
24
application/parser/file/html_parser.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""HTML parser.
|
||||
|
||||
Contains parser for html files.
|
||||
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Dict, Union
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
|
||||
class HTMLParser(BaseParser):
|
||||
"""HTML parser."""
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]:
|
||||
from langchain_community.document_loaders import BSHTMLLoader
|
||||
|
||||
loader = BSHTMLLoader(file)
|
||||
data = loader.load()
|
||||
return data
|
||||
27
application/parser/file/image_parser.py
Normal file
27
application/parser/file/image_parser.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Image parser.
|
||||
|
||||
Contains parser for .png, .jpg, .jpeg files.
|
||||
|
||||
"""
|
||||
from pathlib import Path
|
||||
import requests
|
||||
from typing import Dict, Union
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
|
||||
class ImageParser(BaseParser):
|
||||
"""Image parser."""
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]:
|
||||
doc2md_service = "https://llm.arc53.com/doc2md"
|
||||
# alternatively you can use local vision capable LLM
|
||||
with open(file, "rb") as file_loaded:
|
||||
files = {'file': file_loaded}
|
||||
response = requests.post(doc2md_service, files=files)
|
||||
data = response.json()["markdown"]
|
||||
return data
|
||||
57
application/parser/file/json_parser.py
Normal file
57
application/parser/file/json_parser.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import json
|
||||
from typing import Any, Dict, List, Union
|
||||
from pathlib import Path
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
class JSONParser(BaseParser):
|
||||
r"""JSON (.json) parser.
|
||||
|
||||
Parses JSON files into a list of strings or a concatenated document.
|
||||
It handles both JSON objects (dictionaries) and arrays (lists).
|
||||
|
||||
Args:
|
||||
concat_rows (bool): Whether to concatenate all rows into one document.
|
||||
If set to False, a Document will be created for each item in the JSON.
|
||||
True by default.
|
||||
|
||||
row_joiner (str): Separator to use for joining each row.
|
||||
Only used when `concat_rows=True`.
|
||||
Set to "\n" by default.
|
||||
|
||||
json_config (dict): Options for parsing JSON. Can be used to specify options like
|
||||
custom decoding or formatting. Set to empty dict by default.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
concat_rows: bool = True,
|
||||
row_joiner: str = "\n",
|
||||
json_config: dict = {},
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._concat_rows = concat_rows
|
||||
self._row_joiner = row_joiner
|
||||
self._json_config = json_config
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
||||
"""Parse JSON file."""
|
||||
|
||||
with open(file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f, **self._json_config)
|
||||
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
|
||||
if self._concat_rows:
|
||||
return self._row_joiner.join([str(item) for item in data])
|
||||
else:
|
||||
return data
|
||||
145
application/parser/file/markdown_parser.py
Normal file
145
application/parser/file/markdown_parser.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Markdown parser.
|
||||
|
||||
Contains parser for md files.
|
||||
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
||||
|
||||
import tiktoken
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
|
||||
class MarkdownParser(BaseParser):
|
||||
"""Markdown parser.
|
||||
|
||||
Extract text from markdown files.
|
||||
Returns dictionary with keys as headers and values as the text between headers.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
remove_hyperlinks: bool = True,
|
||||
remove_images: bool = True,
|
||||
max_tokens: int = 2048,
|
||||
# remove_tables: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._remove_hyperlinks = remove_hyperlinks
|
||||
self._remove_images = remove_images
|
||||
self._max_tokens = max_tokens
|
||||
# self._remove_tables = remove_tables
|
||||
|
||||
def tups_chunk_append(self, tups: List[Tuple[Optional[str], str]], current_header: Optional[str],
|
||||
current_text: str):
|
||||
"""Append to tups chunk."""
|
||||
num_tokens = len(tiktoken.get_encoding("cl100k_base").encode(current_text))
|
||||
if num_tokens > self._max_tokens:
|
||||
chunks = [current_text[i:i + self._max_tokens] for i in range(0, len(current_text), self._max_tokens)]
|
||||
for chunk in chunks:
|
||||
tups.append((current_header, chunk))
|
||||
else:
|
||||
tups.append((current_header, current_text))
|
||||
return tups
|
||||
|
||||
def markdown_to_tups(self, markdown_text: str) -> List[Tuple[Optional[str], str]]:
|
||||
"""Convert a markdown file to a dictionary.
|
||||
|
||||
The keys are the headers and the values are the text under each header.
|
||||
|
||||
"""
|
||||
markdown_tups: List[Tuple[Optional[str], str]] = []
|
||||
lines = markdown_text.split("\n")
|
||||
|
||||
current_header = None
|
||||
current_text = ""
|
||||
|
||||
for line in lines:
|
||||
header_match = re.match(r"^#+\s", line)
|
||||
if header_match:
|
||||
if current_header is not None:
|
||||
if current_text == "" or None:
|
||||
continue
|
||||
markdown_tups = self.tups_chunk_append(markdown_tups, current_header, current_text)
|
||||
|
||||
current_header = line
|
||||
current_text = ""
|
||||
else:
|
||||
current_text += line + "\n"
|
||||
markdown_tups = self.tups_chunk_append(markdown_tups, current_header, current_text)
|
||||
|
||||
if current_header is not None:
|
||||
# pass linting, assert keys are defined
|
||||
markdown_tups = [
|
||||
(re.sub(r"#", "", cast(str, key)).strip(), re.sub(r"<.*?>", "", value))
|
||||
for key, value in markdown_tups
|
||||
]
|
||||
else:
|
||||
markdown_tups = [
|
||||
(key, re.sub("\n", "", value)) for key, value in markdown_tups
|
||||
]
|
||||
|
||||
return markdown_tups
|
||||
|
||||
def remove_images(self, content: str) -> str:
|
||||
"""Get a dictionary of a markdown file from its path."""
|
||||
pattern = r"!{1}\[\[(.*)\]\]"
|
||||
content = re.sub(pattern, "", content)
|
||||
return content
|
||||
|
||||
# def remove_tables(self, content: str) -> List[List[str]]:
|
||||
# """Convert markdown tables to nested lists."""
|
||||
# table_rows_pattern = r"((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)"
|
||||
# table_cells_pattern = r"([^\|\r\n]*)\|"
|
||||
#
|
||||
# table_rows = re.findall(table_rows_pattern, content, re.MULTILINE)
|
||||
# table_lists = []
|
||||
# for row in table_rows:
|
||||
# cells = re.findall(table_cells_pattern, row[2])
|
||||
# cells = [cell.strip() for cell in cells if cell.strip()]
|
||||
# table_lists.append(cells)
|
||||
# return str(table_lists)
|
||||
|
||||
def remove_hyperlinks(self, content: str) -> str:
|
||||
"""Get a dictionary of a markdown file from its path."""
|
||||
pattern = r"\[(.*?)\]\((.*?)\)"
|
||||
content = re.sub(pattern, r"\1", content)
|
||||
return content
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Initialize the parser with the config."""
|
||||
return {}
|
||||
|
||||
def parse_tups(
|
||||
self, filepath: Path, errors: str = "ignore"
|
||||
) -> List[Tuple[Optional[str], str]]:
|
||||
"""Parse file into tuples."""
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
if self._remove_hyperlinks:
|
||||
content = self.remove_hyperlinks(content)
|
||||
if self._remove_images:
|
||||
content = self.remove_images(content)
|
||||
# if self._remove_tables:
|
||||
# content = self.remove_tables(content)
|
||||
markdown_tups = self.markdown_to_tups(content)
|
||||
return markdown_tups
|
||||
|
||||
def parse_file(
|
||||
self, filepath: Path, errors: str = "ignore"
|
||||
) -> Union[str, List[str]]:
|
||||
"""Parse file into string."""
|
||||
tups = self.parse_tups(filepath, errors=errors)
|
||||
results = []
|
||||
# TODO: don't include headers right now
|
||||
for header, value in tups:
|
||||
if header is None:
|
||||
results.append(value)
|
||||
else:
|
||||
results.append(f"\n\n{header}\n{value}")
|
||||
return results
|
||||
51
application/parser/file/openapi3_parser.py
Normal file
51
application/parser/file/openapi3_parser.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from openapi_parser import parse
|
||||
|
||||
try:
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
except ModuleNotFoundError:
|
||||
from base_parser import BaseParser
|
||||
|
||||
|
||||
class OpenAPI3Parser(BaseParser):
|
||||
def init_parser(self) -> None:
|
||||
return super().init_parser()
|
||||
|
||||
def get_base_urls(self, urls):
|
||||
base_urls = []
|
||||
for i in urls:
|
||||
parsed_url = urlparse(i)
|
||||
base_url = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
if base_url not in base_urls:
|
||||
base_urls.append(base_url)
|
||||
return base_urls
|
||||
|
||||
def get_info_from_paths(self, path):
|
||||
info = ""
|
||||
if path.operations:
|
||||
for operation in path.operations:
|
||||
info += (
|
||||
f"\n{operation.method.value}="
|
||||
f"{operation.responses[0].description}"
|
||||
)
|
||||
return info
|
||||
|
||||
def parse_file(self, file_path):
|
||||
data = parse(file_path)
|
||||
results = ""
|
||||
base_urls = self.get_base_urls(link.url for link in data.servers)
|
||||
base_urls = ",".join([base_url for base_url in base_urls])
|
||||
results += f"Base URL:{base_urls}\n"
|
||||
i = 1
|
||||
for path in data.paths:
|
||||
info = self.get_info_from_paths(path)
|
||||
results += (
|
||||
f"Path{i}: {path.url}\n"
|
||||
f"description: {path.description}\n"
|
||||
f"parameters: {path.parameters}\nmethods: {info}\n"
|
||||
)
|
||||
i += 1
|
||||
with open("results.txt", "w") as f:
|
||||
f.write(results)
|
||||
return results
|
||||
75
application/parser/file/pptx_parser.py
Normal file
75
application/parser/file/pptx_parser.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""PPT parser.
|
||||
Contains parsers for presentation (.pptx) files to extract slide text.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
class PPTXParser(BaseParser):
|
||||
r"""PPTX (.pptx) parser for extracting text from PowerPoint slides.
|
||||
Args:
|
||||
concat_slides (bool): Specifies whether to concatenate all slide text into one document.
|
||||
- If True, slide texts will be joined together as a single string.
|
||||
- If False, each slide's text will be stored as a separate entry in a list.
|
||||
Set to True by default.
|
||||
slide_separator (str): Separator used to join slides' text content.
|
||||
Only used when `concat_slides=True`. Default is "\n".
|
||||
Refer to https://python-pptx.readthedocs.io/en/latest/ for more information.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
concat_slides: bool = True,
|
||||
slide_separator: str = "\n",
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._concat_slides = concat_slides
|
||||
self._slide_separator = slide_separator
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
||||
r"""
|
||||
Parse a .pptx file and extract text from each slide.
|
||||
Args:
|
||||
file (Path): Path to the .pptx file.
|
||||
errors (str): Error handling policy ('ignore' by default).
|
||||
Returns:
|
||||
Union[str, List[str]]: Concatenated text if concat_slides is True,
|
||||
otherwise a list of slide texts.
|
||||
"""
|
||||
|
||||
try:
|
||||
from pptx import Presentation
|
||||
except ImportError:
|
||||
raise ImportError("pptx module is required to read .PPTX files.")
|
||||
|
||||
try:
|
||||
presentation = Presentation(file)
|
||||
slide_texts=[]
|
||||
|
||||
# Iterate over each slide in the presentation
|
||||
for slide in presentation.slides:
|
||||
slide_text=""
|
||||
|
||||
# Iterate over each shape in the slide
|
||||
for shape in slide.shapes:
|
||||
# Check if the shape has a 'text' attribute and append that to the slide_text
|
||||
if hasattr(shape,"text"):
|
||||
slide_text+=shape.text
|
||||
|
||||
slide_texts.append(slide_text.strip())
|
||||
|
||||
if self._concat_slides:
|
||||
return self._slide_separator.join(slide_texts)
|
||||
else:
|
||||
return slide_texts
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
201
application/parser/file/rst_parser.py
Normal file
201
application/parser/file/rst_parser.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""reStructuredText parser.
|
||||
|
||||
Contains parser for md files.
|
||||
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
|
||||
class RstParser(BaseParser):
|
||||
"""reStructuredText parser.
|
||||
|
||||
Extract text from .rst files.
|
||||
Returns dictionary with keys as headers and values as the text between headers.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
remove_hyperlinks: bool = True,
|
||||
remove_images: bool = True,
|
||||
remove_table_excess: bool = True,
|
||||
remove_interpreters: bool = True,
|
||||
remove_directives: bool = True,
|
||||
remove_whitespaces_excess: bool = True,
|
||||
# Be careful with remove_characters_excess, might cause data loss
|
||||
remove_characters_excess: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._remove_hyperlinks = remove_hyperlinks
|
||||
self._remove_images = remove_images
|
||||
self._remove_table_excess = remove_table_excess
|
||||
self._remove_interpreters = remove_interpreters
|
||||
self._remove_directives = remove_directives
|
||||
self._remove_whitespaces_excess = remove_whitespaces_excess
|
||||
self._remove_characters_excess = remove_characters_excess
|
||||
|
||||
def rst_to_tups(self, rst_text: str) -> List[Tuple[Optional[str], str]]:
|
||||
"""Convert a reStructuredText file to a dictionary.
|
||||
|
||||
The keys are the headers and the values are the text under each header.
|
||||
|
||||
"""
|
||||
rst_tups: List[Tuple[Optional[str], str]] = []
|
||||
lines = rst_text.split("\n")
|
||||
|
||||
current_header = None
|
||||
current_text = ""
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
header_match = re.match(r"^[^\S\n]*[-=]+[^\S\n]*$", line)
|
||||
if header_match and i > 0 and (
|
||||
len(lines[i - 1].strip()) == len(header_match.group().strip()) or lines[i - 2] == lines[i - 2]):
|
||||
if current_header is not None:
|
||||
if current_text == "" or None:
|
||||
continue
|
||||
# removes the next heading from current Document
|
||||
if current_text.endswith(lines[i - 1] + "\n"):
|
||||
current_text = current_text[:len(current_text) - len(lines[i - 1] + "\n")]
|
||||
rst_tups.append((current_header, current_text))
|
||||
|
||||
current_header = lines[i - 1]
|
||||
current_text = ""
|
||||
else:
|
||||
current_text += line + "\n"
|
||||
|
||||
rst_tups.append((current_header, current_text))
|
||||
|
||||
# TODO: Format for rst
|
||||
#
|
||||
# if current_header is not None:
|
||||
# # pass linting, assert keys are defined
|
||||
# rst_tups = [
|
||||
# (re.sub(r"#", "", cast(str, key)).strip(), re.sub(r"<.*?>", "", value))
|
||||
# for key, value in rst_tups
|
||||
# ]
|
||||
# else:
|
||||
# rst_tups = [
|
||||
# (key, re.sub("\n", "", value)) for key, value in rst_tups
|
||||
# ]
|
||||
|
||||
if current_header is None:
|
||||
rst_tups = [
|
||||
(key, re.sub("\n", "", value)) for key, value in rst_tups
|
||||
]
|
||||
return rst_tups
|
||||
|
||||
def chunk_by_token_count(self, text: str, max_tokens: int = 100) -> List[str]:
|
||||
"""Chunk text by token count."""
|
||||
|
||||
avg_token_length = 5
|
||||
|
||||
chunk_size = max_tokens * avg_token_length
|
||||
|
||||
chunks = []
|
||||
for i in range(0, len(text), chunk_size):
|
||||
chunk = text[i:i+chunk_size]
|
||||
if i + chunk_size < len(text):
|
||||
last_space = chunk.rfind(' ')
|
||||
if last_space != -1:
|
||||
chunk = chunk[:last_space]
|
||||
|
||||
chunks.append(chunk.strip())
|
||||
|
||||
return chunks
|
||||
|
||||
def remove_images(self, content: str) -> str:
|
||||
pattern = r"\.\. image:: (.*)"
|
||||
content = re.sub(pattern, "", content)
|
||||
return content
|
||||
|
||||
def remove_hyperlinks(self, content: str) -> str:
|
||||
pattern = r"`(.*?) <(.*?)>`_"
|
||||
content = re.sub(pattern, r"\1", content)
|
||||
return content
|
||||
|
||||
def remove_directives(self, content: str) -> str:
|
||||
"""Removes reStructuredText Directives"""
|
||||
pattern = r"`\.\.([^:]+)::"
|
||||
content = re.sub(pattern, "", content)
|
||||
return content
|
||||
|
||||
def remove_interpreters(self, content: str) -> str:
|
||||
"""Removes reStructuredText Interpreted Text Roles"""
|
||||
pattern = r":(\w+):"
|
||||
content = re.sub(pattern, "", content)
|
||||
return content
|
||||
|
||||
def remove_table_excess(self, content: str) -> str:
|
||||
"""Pattern to remove grid table separators"""
|
||||
pattern = r"^\+[-]+\+[-]+\+$"
|
||||
content = re.sub(pattern, "", content, flags=re.MULTILINE)
|
||||
return content
|
||||
|
||||
def remove_whitespaces_excess(self, content: List[Tuple[str, Any]]) -> List[Tuple[str, Any]]:
|
||||
"""Pattern to match 2 or more consecutive whitespaces"""
|
||||
pattern = r"\s{2,}"
|
||||
content = [(key, re.sub(pattern, " ", value)) for key, value in content]
|
||||
return content
|
||||
|
||||
def remove_characters_excess(self, content: List[Tuple[str, Any]]) -> List[Tuple[str, Any]]:
|
||||
"""Pattern to match 2 or more consecutive characters"""
|
||||
pattern = r"(\S)\1{2,}"
|
||||
content = [(key, re.sub(pattern, r"\1\1\1", value, flags=re.MULTILINE)) for key, value in content]
|
||||
return content
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Initialize the parser with the config."""
|
||||
return {}
|
||||
|
||||
def parse_tups(
|
||||
self, filepath: Path, errors: str = "ignore",max_tokens: Optional[int] = 1000
|
||||
) -> List[Tuple[Optional[str], str]]:
|
||||
"""Parse file into tuples."""
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
if self._remove_hyperlinks:
|
||||
content = self.remove_hyperlinks(content)
|
||||
if self._remove_images:
|
||||
content = self.remove_images(content)
|
||||
if self._remove_table_excess:
|
||||
content = self.remove_table_excess(content)
|
||||
if self._remove_directives:
|
||||
content = self.remove_directives(content)
|
||||
if self._remove_interpreters:
|
||||
content = self.remove_interpreters(content)
|
||||
rst_tups = self.rst_to_tups(content)
|
||||
if self._remove_whitespaces_excess:
|
||||
rst_tups = self.remove_whitespaces_excess(rst_tups)
|
||||
if self._remove_characters_excess:
|
||||
rst_tups = self.remove_characters_excess(rst_tups)
|
||||
|
||||
# Apply chunking if max_tokens is provided
|
||||
if max_tokens is not None:
|
||||
chunked_tups = []
|
||||
for header, text in rst_tups:
|
||||
chunks = self.chunk_by_token_count(text, max_tokens)
|
||||
for idx, chunk in enumerate(chunks):
|
||||
chunked_tups.append((f"{header} - Chunk {idx + 1}", chunk))
|
||||
return chunked_tups
|
||||
return rst_tups
|
||||
|
||||
def parse_file(
|
||||
self, filepath: Path, errors: str = "ignore"
|
||||
) -> Union[str, List[str]]:
|
||||
"""Parse file into string."""
|
||||
tups = self.parse_tups(filepath, errors=errors)
|
||||
results = []
|
||||
# TODO: don't include headers right now
|
||||
for header, value in tups:
|
||||
if header is None:
|
||||
results.append(value)
|
||||
else:
|
||||
results.append(f"\n\n{header}\n{value}")
|
||||
return results
|
||||
180
application/parser/file/tabular_parser.py
Normal file
180
application/parser/file/tabular_parser.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Tabular parser.
|
||||
|
||||
Contains parsers for tabular data files.
|
||||
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from application.parser.file.base_parser import BaseParser
|
||||
|
||||
|
||||
class CSVParser(BaseParser):
|
||||
"""CSV parser.
|
||||
|
||||
Args:
|
||||
concat_rows (bool): whether to concatenate all rows into one document.
|
||||
If set to False, a Document will be created for each row.
|
||||
True by default.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Any, concat_rows: bool = True, **kwargs: Any) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._concat_rows = concat_rows
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
||||
"""Parse file.
|
||||
|
||||
Returns:
|
||||
Union[str, List[str]]: a string or a List of strings.
|
||||
|
||||
"""
|
||||
try:
|
||||
import csv
|
||||
except ImportError:
|
||||
raise ValueError("csv module is required to read CSV files.")
|
||||
text_list = []
|
||||
with open(file, "r") as fp:
|
||||
csv_reader = csv.reader(fp)
|
||||
for row in csv_reader:
|
||||
text_list.append(", ".join(row))
|
||||
if self._concat_rows:
|
||||
return "\n".join(text_list)
|
||||
else:
|
||||
return text_list
|
||||
|
||||
|
||||
class PandasCSVParser(BaseParser):
|
||||
r"""Pandas-based CSV parser.
|
||||
|
||||
Parses CSVs using the separator detection from Pandas `read_csv`function.
|
||||
If special parameters are required, use the `pandas_config` dict.
|
||||
|
||||
Args:
|
||||
concat_rows (bool): whether to concatenate all rows into one document.
|
||||
If set to False, a Document will be created for each row.
|
||||
True by default.
|
||||
|
||||
col_joiner (str): Separator to use for joining cols per row.
|
||||
Set to ", " by default.
|
||||
|
||||
row_joiner (str): Separator to use for joining each row.
|
||||
Only used when `concat_rows=True`.
|
||||
Set to "\n" by default.
|
||||
|
||||
pandas_config (dict): Options for the `pandas.read_csv` function call.
|
||||
Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html
|
||||
for more information.
|
||||
Set to empty dict by default, this means pandas will try to figure
|
||||
out the separators, table head, etc. on its own.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
concat_rows: bool = True,
|
||||
col_joiner: str = ", ",
|
||||
row_joiner: str = "\n",
|
||||
pandas_config: dict = {},
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._concat_rows = concat_rows
|
||||
self._col_joiner = col_joiner
|
||||
self._row_joiner = row_joiner
|
||||
self._pandas_config = pandas_config
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
||||
"""Parse file."""
|
||||
try:
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
raise ValueError("pandas module is required to read CSV files.")
|
||||
|
||||
df = pd.read_csv(file, **self._pandas_config)
|
||||
|
||||
text_list = df.apply(
|
||||
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
|
||||
).tolist()
|
||||
|
||||
if self._concat_rows:
|
||||
return (self._row_joiner).join(text_list)
|
||||
else:
|
||||
return text_list
|
||||
|
||||
|
||||
class ExcelParser(BaseParser):
|
||||
r"""Excel (.xlsx) parser.
|
||||
|
||||
Parses Excel files using Pandas `read_excel` function.
|
||||
If special parameters are required, use the `pandas_config` dict.
|
||||
|
||||
Args:
|
||||
concat_rows (bool): whether to concatenate all rows into one document.
|
||||
If set to False, a Document will be created for each row.
|
||||
True by default.
|
||||
|
||||
col_joiner (str): Separator to use for joining cols per row.
|
||||
Set to ", " by default.
|
||||
|
||||
row_joiner (str): Separator to use for joining each row.
|
||||
Only used when `concat_rows=True`.
|
||||
Set to "\n" by default.
|
||||
|
||||
pandas_config (dict): Options for the `pandas.read_excel` function call.
|
||||
Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html
|
||||
for more information.
|
||||
Set to empty dict by default, this means pandas will try to figure
|
||||
out the table structure on its own.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
concat_rows: bool = True,
|
||||
col_joiner: str = ", ",
|
||||
row_joiner: str = "\n",
|
||||
pandas_config: dict = {},
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._concat_rows = concat_rows
|
||||
self._col_joiner = col_joiner
|
||||
self._row_joiner = row_joiner
|
||||
self._pandas_config = pandas_config
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
||||
"""Parse file."""
|
||||
try:
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
raise ValueError("pandas module is required to read Excel files.")
|
||||
|
||||
df = pd.read_excel(file, **self._pandas_config)
|
||||
|
||||
text_list = df.apply(
|
||||
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
|
||||
).tolist()
|
||||
|
||||
if self._concat_rows:
|
||||
return (self._row_joiner).join(text_list)
|
||||
else:
|
||||
return text_list
|
||||
19
application/parser/remote/base.py
Normal file
19
application/parser/remote/base.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Base reader class."""
|
||||
from abc import abstractmethod
|
||||
from typing import Any, List
|
||||
|
||||
from langchain.docstore.document import Document as LCDocument
|
||||
from application.parser.schema.base import Document
|
||||
|
||||
|
||||
class BaseRemote:
|
||||
"""Utilities for loading data from a directory."""
|
||||
|
||||
@abstractmethod
|
||||
def load_data(self, *args: Any, **load_kwargs: Any) -> List[Document]:
|
||||
"""Load data from the input directory."""
|
||||
|
||||
def load_langchain_documents(self, **load_kwargs: Any) -> List[LCDocument]:
|
||||
"""Load data in LangChain document format."""
|
||||
docs = self.load_data(**load_kwargs)
|
||||
return [d.to_langchain_format() for d in docs]
|
||||
64
application/parser/remote/crawler_loader.py
Normal file
64
application/parser/remote/crawler_loader.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import requests
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from bs4 import BeautifulSoup
|
||||
from application.parser.remote.base import BaseRemote
|
||||
from application.parser.schema.base import Document
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
|
||||
class CrawlerLoader(BaseRemote):
|
||||
def __init__(self, limit=10):
|
||||
self.loader = WebBaseLoader # Initialize the document loader
|
||||
self.limit = limit # Set the limit for the number of pages to scrape
|
||||
|
||||
def load_data(self, inputs):
|
||||
url = inputs
|
||||
if isinstance(url, list) and url:
|
||||
url = url[0]
|
||||
|
||||
# Check if the URL scheme is provided, if not, assume http
|
||||
if not urlparse(url).scheme:
|
||||
url = "http://" + url
|
||||
|
||||
visited_urls = set()
|
||||
base_url = urlparse(url).scheme + "://" + urlparse(url).hostname
|
||||
urls_to_visit = [url]
|
||||
loaded_content = []
|
||||
|
||||
while urls_to_visit:
|
||||
current_url = urls_to_visit.pop(0)
|
||||
visited_urls.add(current_url)
|
||||
|
||||
try:
|
||||
response = requests.get(current_url)
|
||||
response.raise_for_status()
|
||||
loader = self.loader([current_url])
|
||||
docs = loader.load()
|
||||
# Convert the loaded documents to your Document schema
|
||||
for doc in docs:
|
||||
loaded_content.append(
|
||||
Document(
|
||||
doc.page_content,
|
||||
extra_info=doc.metadata
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error processing URL {current_url}: {e}")
|
||||
continue
|
||||
|
||||
# Parse the HTML content to extract all links
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
all_links = [
|
||||
urljoin(current_url, a['href'])
|
||||
for a in soup.find_all('a', href=True)
|
||||
if base_url in urljoin(current_url, a['href'])
|
||||
]
|
||||
|
||||
# Add new links to the list of URLs to visit if they haven't been visited yet
|
||||
urls_to_visit.extend([link for link in all_links if link not in visited_urls])
|
||||
urls_to_visit = list(set(urls_to_visit))
|
||||
|
||||
# Stop crawling if the limit of pages to scrape is reached
|
||||
if self.limit is not None and len(visited_urls) >= self.limit:
|
||||
break
|
||||
|
||||
return loaded_content
|
||||
139
application/parser/remote/crawler_markdown.py
Normal file
139
application/parser/remote/crawler_markdown.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import requests
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from bs4 import BeautifulSoup
|
||||
from application.parser.remote.base import BaseRemote
|
||||
import re
|
||||
from markdownify import markdownify
|
||||
from application.parser.schema.base import Document
|
||||
import tldextract
|
||||
|
||||
class CrawlerLoader(BaseRemote):
|
||||
def __init__(self, limit=10, allow_subdomains=False):
|
||||
"""
|
||||
Given a URL crawl web pages up to `self.limit`,
|
||||
convert HTML content to Markdown, and returning a list of Document objects.
|
||||
|
||||
:param limit: The maximum number of pages to crawl.
|
||||
:param allow_subdomains: If True, crawl pages on subdomains of the base domain.
|
||||
"""
|
||||
self.limit = limit
|
||||
self.allow_subdomains = allow_subdomains
|
||||
self.session = requests.Session()
|
||||
|
||||
def load_data(self, inputs):
|
||||
url = inputs
|
||||
if isinstance(url, list) and url:
|
||||
url = url[0]
|
||||
|
||||
# Ensure the URL has a scheme (if not, default to http)
|
||||
if not urlparse(url).scheme:
|
||||
url = "http://" + url
|
||||
|
||||
# Keep track of visited URLs to avoid revisiting the same page
|
||||
visited_urls = set()
|
||||
|
||||
# Determine the base domain for link filtering using tldextract
|
||||
base_domain = self._get_base_domain(url)
|
||||
urls_to_visit = {url}
|
||||
documents = []
|
||||
|
||||
while urls_to_visit:
|
||||
current_url = urls_to_visit.pop()
|
||||
|
||||
# Skip if already visited
|
||||
if current_url in visited_urls:
|
||||
continue
|
||||
visited_urls.add(current_url)
|
||||
|
||||
# Fetch the page content
|
||||
html_content = self._fetch_page(current_url)
|
||||
if html_content is None:
|
||||
continue
|
||||
|
||||
# Convert the HTML to Markdown for cleaner text formatting
|
||||
title, language, processed_markdown = self._process_html_to_markdown(html_content, current_url)
|
||||
if processed_markdown:
|
||||
# Create a Document for each visited page
|
||||
documents.append(
|
||||
Document(
|
||||
processed_markdown, # content
|
||||
None, # doc_id
|
||||
None, # embedding
|
||||
{"source": current_url, "title": title, "language": language} # extra_info
|
||||
)
|
||||
)
|
||||
|
||||
# Extract links and filter them according to domain rules
|
||||
new_links = self._extract_links(html_content, current_url)
|
||||
filtered_links = self._filter_links(new_links, base_domain)
|
||||
|
||||
# Add any new, not-yet-visited links to the queue
|
||||
urls_to_visit.update(link for link in filtered_links if link not in visited_urls)
|
||||
|
||||
# If we've reached the limit, stop crawling
|
||||
if self.limit is not None and len(visited_urls) >= self.limit:
|
||||
break
|
||||
|
||||
return documents
|
||||
|
||||
def _fetch_page(self, url):
|
||||
try:
|
||||
response = self.session.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching URL {url}: {e}")
|
||||
return None
|
||||
|
||||
def _process_html_to_markdown(self, html_content, current_url):
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
title_tag = soup.find('title')
|
||||
title = title_tag.text.strip() if title_tag else "No Title"
|
||||
|
||||
# Extract language
|
||||
language_tag = soup.find('html')
|
||||
language = language_tag.get('lang', 'en') if language_tag else "en"
|
||||
|
||||
markdownified = markdownify(html_content, heading_style="ATX", newline_style="BACKSLASH")
|
||||
# Reduce sequences of more than two newlines to exactly three
|
||||
markdownified = re.sub(r'\n{3,}', '\n\n\n', markdownified)
|
||||
return title, language, markdownified
|
||||
|
||||
def _extract_links(self, html_content, current_url):
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
links = []
|
||||
for a in soup.find_all('a', href=True):
|
||||
full_url = urljoin(current_url, a['href'])
|
||||
links.append((full_url, a.text.strip()))
|
||||
return links
|
||||
|
||||
def _get_base_domain(self, url):
|
||||
extracted = tldextract.extract(url)
|
||||
# Reconstruct the domain as domain.suffix
|
||||
base_domain = f"{extracted.domain}.{extracted.suffix}"
|
||||
return base_domain
|
||||
|
||||
def _filter_links(self, links, base_domain):
|
||||
"""
|
||||
Filter the extracted links to only include those that match the crawling criteria:
|
||||
- If allow_subdomains is True, allow any link whose domain ends with the base_domain.
|
||||
- If allow_subdomains is False, only allow exact matches of the base_domain.
|
||||
"""
|
||||
filtered = []
|
||||
for link, _ in links:
|
||||
parsed_link = urlparse(link)
|
||||
if not parsed_link.netloc:
|
||||
continue
|
||||
|
||||
extracted = tldextract.extract(parsed_link.netloc)
|
||||
link_base = f"{extracted.domain}.{extracted.suffix}"
|
||||
|
||||
if self.allow_subdomains:
|
||||
# For subdomains: sub.example.com ends with example.com
|
||||
if link_base == base_domain or link_base.endswith("." + base_domain):
|
||||
filtered.append(link)
|
||||
else:
|
||||
# Exact domain match
|
||||
if link_base == base_domain:
|
||||
filtered.append(link)
|
||||
return filtered
|
||||
58
application/parser/remote/github_loader.py
Normal file
58
application/parser/remote/github_loader.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import base64
|
||||
import requests
|
||||
from typing import List
|
||||
from application.parser.remote.base import BaseRemote
|
||||
from langchain_core.documents import Document
|
||||
import mimetypes
|
||||
|
||||
class GitHubLoader(BaseRemote):
|
||||
def __init__(self):
|
||||
self.access_token = None
|
||||
self.headers = {
|
||||
"Authorization": f"token {self.access_token}"
|
||||
} if self.access_token else {}
|
||||
return
|
||||
|
||||
def fetch_file_content(self, repo_url: str, file_path: str) -> str:
|
||||
url = f"https://api.github.com/repos/{repo_url}/contents/{file_path}"
|
||||
response = requests.get(url, headers=self.headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
content = response.json()
|
||||
mime_type, _ = mimetypes.guess_type(file_path) # Guess the MIME type based on the file extension
|
||||
|
||||
if content.get("encoding") == "base64":
|
||||
if mime_type and mime_type.startswith("text"): # Handle only text files
|
||||
try:
|
||||
decoded_content = base64.b64decode(content["content"]).decode("utf-8")
|
||||
return f"Filename: {file_path}\n\n{decoded_content}"
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
return f"Filename: {file_path} is a binary file and was skipped."
|
||||
else:
|
||||
return f"Filename: {file_path}\n\n{content['content']}"
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
def fetch_repo_files(self, repo_url: str, path: str = "") -> List[str]:
|
||||
url = f"https://api.github.com/repos/{repo_url}/contents/{path}"
|
||||
response = requests.get(url, headers={**self.headers, "Accept": "application/vnd.github.v3.raw"})
|
||||
contents = response.json()
|
||||
files = []
|
||||
for item in contents:
|
||||
if item["type"] == "file":
|
||||
files.append(item["path"])
|
||||
elif item["type"] == "dir":
|
||||
files.extend(self.fetch_repo_files(repo_url, item["path"]))
|
||||
return files
|
||||
|
||||
def load_data(self, repo_url: str) -> List[Document]:
|
||||
repo_name = repo_url.split("github.com/")[-1]
|
||||
files = self.fetch_repo_files(repo_name)
|
||||
documents = []
|
||||
for file_path in files:
|
||||
content = self.fetch_file_content(repo_name, file_path)
|
||||
documents.append(Document(page_content=content, metadata={"title": file_path,
|
||||
"source": f"https://github.com/{repo_name}/blob/main/{file_path}"}))
|
||||
return documents
|
||||
35
application/parser/remote/reddit_loader.py
Normal file
35
application/parser/remote/reddit_loader.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from application.parser.remote.base import BaseRemote
|
||||
from langchain_community.document_loaders import RedditPostsLoader
|
||||
import json
|
||||
|
||||
|
||||
class RedditPostsLoaderRemote(BaseRemote):
|
||||
def load_data(self, inputs):
|
||||
try:
|
||||
data = json.loads(inputs)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON input: {e}")
|
||||
|
||||
required_fields = ["client_id", "client_secret", "user_agent", "search_queries"]
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
if missing_fields:
|
||||
raise ValueError(f"Missing required fields: {', '.join(missing_fields)}")
|
||||
client_id = data.get("client_id")
|
||||
client_secret = data.get("client_secret")
|
||||
user_agent = data.get("user_agent")
|
||||
categories = data.get("categories", ["new", "hot"])
|
||||
mode = data.get("mode", "subreddit")
|
||||
search_queries = data.get("search_queries")
|
||||
number_posts = data.get("number_posts", 10)
|
||||
self.loader = RedditPostsLoader(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user_agent=user_agent,
|
||||
categories=categories,
|
||||
mode=mode,
|
||||
search_queries=search_queries,
|
||||
number_posts=number_posts,
|
||||
)
|
||||
documents = self.loader.load()
|
||||
print(f"Loaded {len(documents)} documents from Reddit")
|
||||
return documents
|
||||
22
application/parser/remote/remote_creator.py
Normal file
22
application/parser/remote/remote_creator.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from application.parser.remote.sitemap_loader import SitemapLoader
|
||||
from application.parser.remote.crawler_loader import CrawlerLoader
|
||||
from application.parser.remote.web_loader import WebLoader
|
||||
from application.parser.remote.reddit_loader import RedditPostsLoaderRemote
|
||||
from application.parser.remote.github_loader import GitHubLoader
|
||||
|
||||
|
||||
class RemoteCreator:
|
||||
loaders = {
|
||||
"url": WebLoader,
|
||||
"sitemap": SitemapLoader,
|
||||
"crawler": CrawlerLoader,
|
||||
"reddit": RedditPostsLoaderRemote,
|
||||
"github": GitHubLoader,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_loader(cls, type, *args, **kwargs):
|
||||
loader_class = cls.loaders.get(type.lower())
|
||||
if not loader_class:
|
||||
raise ValueError(f"No LLM class found for type {type}")
|
||||
return loader_class(*args, **kwargs)
|
||||
81
application/parser/remote/sitemap_loader.py
Normal file
81
application/parser/remote/sitemap_loader.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import requests
|
||||
import re # Import regular expression library
|
||||
import xml.etree.ElementTree as ET
|
||||
from application.parser.remote.base import BaseRemote
|
||||
|
||||
class SitemapLoader(BaseRemote):
|
||||
def __init__(self, limit=20):
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
self.loader = WebBaseLoader
|
||||
self.limit = limit # Adding limit to control the number of URLs to process
|
||||
|
||||
def load_data(self, inputs):
|
||||
sitemap_url= inputs
|
||||
# Check if the input is a list and if it is, use the first element
|
||||
if isinstance(sitemap_url, list) and sitemap_url:
|
||||
url = sitemap_url[0]
|
||||
|
||||
urls = self._extract_urls(sitemap_url)
|
||||
if not urls:
|
||||
print(f"No URLs found in the sitemap: {sitemap_url}")
|
||||
return []
|
||||
|
||||
# Load content of extracted URLs
|
||||
documents = []
|
||||
processed_urls = 0 # Counter for processed URLs
|
||||
for url in urls:
|
||||
if self.limit is not None and processed_urls >= self.limit:
|
||||
break # Stop processing if the limit is reached
|
||||
|
||||
try:
|
||||
loader = self.loader([url])
|
||||
documents.extend(loader.load())
|
||||
processed_urls += 1 # Increment the counter after processing each URL
|
||||
except Exception as e:
|
||||
print(f"Error processing URL {url}: {e}")
|
||||
continue
|
||||
|
||||
return documents
|
||||
|
||||
def _extract_urls(self, sitemap_url):
|
||||
try:
|
||||
response = requests.get(sitemap_url)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError) as e:
|
||||
print(f"Failed to fetch sitemap: {sitemap_url}. Error: {e}")
|
||||
return []
|
||||
|
||||
# Determine if this is a sitemap or a URL
|
||||
if self._is_sitemap(response):
|
||||
# It's a sitemap, so parse it and extract URLs
|
||||
return self._parse_sitemap(response.content)
|
||||
else:
|
||||
# It's not a sitemap, return the URL itself
|
||||
return [sitemap_url]
|
||||
|
||||
def _is_sitemap(self, response):
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
if 'xml' in content_type or response.url.endswith('.xml'):
|
||||
return True
|
||||
|
||||
if '<sitemapindex' in response.text or '<urlset' in response.text:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _parse_sitemap(self, sitemap_content):
|
||||
# Remove namespaces
|
||||
sitemap_content = re.sub(' xmlns="[^"]+"', '', sitemap_content.decode('utf-8'), count=1)
|
||||
|
||||
root = ET.fromstring(sitemap_content)
|
||||
|
||||
urls = []
|
||||
for loc in root.findall('.//url/loc'):
|
||||
urls.append(loc.text)
|
||||
|
||||
# Check for nested sitemaps
|
||||
for sitemap in root.findall('.//sitemap/loc'):
|
||||
nested_sitemap_url = sitemap.text
|
||||
urls.extend(self._extract_urls(nested_sitemap_url))
|
||||
|
||||
return urls
|
||||
11
application/parser/remote/telegram.py
Normal file
11
application/parser/remote/telegram.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from langchain.document_loader import TelegramChatApiLoader
|
||||
from application.parser.remote.base import BaseRemote
|
||||
|
||||
class TelegramChatApiRemote(BaseRemote):
|
||||
def _init_parser(self, *args, **load_kwargs):
|
||||
self.loader = TelegramChatApiLoader(**load_kwargs)
|
||||
return {}
|
||||
|
||||
def parse_file(self, *args, **load_kwargs):
|
||||
|
||||
return
|
||||
44
application/parser/remote/web_loader.py
Normal file
44
application/parser/remote/web_loader.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from application.parser.remote.base import BaseRemote
|
||||
from application.parser.schema.base import Document
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
from urllib.parse import urlparse
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*"
|
||||
";q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": "https://www.google.com/",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
}
|
||||
|
||||
|
||||
class WebLoader(BaseRemote):
|
||||
def __init__(self):
|
||||
self.loader = WebBaseLoader
|
||||
|
||||
def load_data(self, inputs):
|
||||
urls = inputs
|
||||
if isinstance(urls, str):
|
||||
urls = [urls]
|
||||
documents = []
|
||||
for url in urls:
|
||||
# Check if the URL scheme is provided, if not, assume http
|
||||
if not urlparse(url).scheme:
|
||||
url = "http://" + url
|
||||
try:
|
||||
loader = self.loader([url], header_template=headers)
|
||||
loaded_docs = loader.load()
|
||||
for doc in loaded_docs:
|
||||
documents.append(
|
||||
Document(
|
||||
doc.page_content,
|
||||
extra_info=doc.metadata,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error processing URL {url}: {e}")
|
||||
continue
|
||||
return documents
|
||||
1
application/parser/schema/__init__.py
Normal file
1
application/parser/schema/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
34
application/parser/schema/base.py
Normal file
34
application/parser/schema/base.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Base schema for readers."""
|
||||
from dataclasses import dataclass
|
||||
|
||||
from langchain.docstore.document import Document as LCDocument
|
||||
from application.parser.schema.schema import BaseDocument
|
||||
|
||||
|
||||
@dataclass
|
||||
class Document(BaseDocument):
|
||||
"""Generic interface for a data document.
|
||||
|
||||
This document connects to data sources.
|
||||
|
||||
"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Post init."""
|
||||
if self.text is None:
|
||||
raise ValueError("text field not set.")
|
||||
|
||||
@classmethod
|
||||
def get_type(cls) -> str:
|
||||
"""Get Document type."""
|
||||
return "Document"
|
||||
|
||||
def to_langchain_format(self) -> LCDocument:
|
||||
"""Convert struct to LangChain document format."""
|
||||
metadata = self.extra_info or {}
|
||||
return LCDocument(page_content=self.text, metadata=metadata)
|
||||
|
||||
@classmethod
|
||||
def from_langchain_format(cls, doc: LCDocument) -> "Document":
|
||||
"""Convert struct from LangChain document format."""
|
||||
return cls(text=doc.page_content, extra_info=doc.metadata)
|
||||
64
application/parser/schema/schema.py
Normal file
64
application/parser/schema/schema.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Base schema for data structures."""
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from dataclasses_json import DataClassJsonMixin
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseDocument(DataClassJsonMixin):
|
||||
"""Base document.
|
||||
|
||||
Generic abstract interfaces that captures both index structs
|
||||
as well as documents.
|
||||
|
||||
"""
|
||||
|
||||
# TODO: consolidate fields from Document/IndexStruct into base class
|
||||
text: Optional[str] = None
|
||||
doc_id: Optional[str] = None
|
||||
embedding: Optional[List[float]] = None
|
||||
|
||||
# extra fields
|
||||
extra_info: Optional[Dict[str, Any]] = None
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_type(cls) -> str:
|
||||
"""Get Document type."""
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""Get text."""
|
||||
if self.text is None:
|
||||
raise ValueError("text field not set.")
|
||||
return self.text
|
||||
|
||||
def get_doc_id(self) -> str:
|
||||
"""Get doc_id."""
|
||||
if self.doc_id is None:
|
||||
raise ValueError("doc_id not set.")
|
||||
return self.doc_id
|
||||
|
||||
@property
|
||||
def is_doc_id_none(self) -> bool:
|
||||
"""Check if doc_id is None."""
|
||||
return self.doc_id is None
|
||||
|
||||
def get_embedding(self) -> List[float]:
|
||||
"""Get embedding.
|
||||
|
||||
Errors if embedding is None.
|
||||
|
||||
"""
|
||||
if self.embedding is None:
|
||||
raise ValueError("embedding not set.")
|
||||
return self.embedding
|
||||
|
||||
@property
|
||||
def extra_info_str(self) -> Optional[str]:
|
||||
"""Extra info string."""
|
||||
if self.extra_info is None:
|
||||
return None
|
||||
|
||||
return "\n".join([f"{k}: {str(v)}" for k, v in self.extra_info.items()])
|
||||
9
application/prompts/chat_combine_creative.txt
Normal file
9
application/prompts/chat_combine_creative.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible.
|
||||
Use the following pieces of context to help answer the users question. If its not relevant to the question, provide friendly responses.
|
||||
You have access to chat history, and can use it to help answer the question.
|
||||
When using code examples, use the following format:
|
||||
```(language)
|
||||
(code)
|
||||
```
|
||||
----------------
|
||||
{summaries}
|
||||
9
application/prompts/chat_combine_default.txt
Normal file
9
application/prompts/chat_combine_default.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
You are a helpful AI assistant, DocsGPT, specializing in document assistance, designed to offer detailed and informative responses.
|
||||
If appropriate, your answers can include code examples, formatted as follows:
|
||||
```(language)
|
||||
(code)
|
||||
```
|
||||
You effectively utilize chat history, ensuring relevant and tailored responses.
|
||||
If a question doesn't align with your context, you provide friendly and helpful replies.
|
||||
----------------
|
||||
{summaries}
|
||||
13
application/prompts/chat_combine_strict.txt
Normal file
13
application/prompts/chat_combine_strict.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
You are an AI Assistant, DocsGPT, adept at offering document assistance.
|
||||
Your expertise lies in providing answer on top of provided context.
|
||||
You can leverage the chat history if needed.
|
||||
Answer the question based on the context below.
|
||||
Keep the answer concise. Respond "Irrelevant context" if not sure about the answer.
|
||||
If question is not related to the context, respond "Irrelevant context".
|
||||
When using code examples, use the following format:
|
||||
```(language)
|
||||
(code)
|
||||
```
|
||||
----------------
|
||||
Context:
|
||||
{summaries}
|
||||
3
application/prompts/chat_reduce_prompt.txt
Normal file
3
application/prompts/chat_reduce_prompt.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Use the following pieces of context to help answer the users question. If its not relevant to the question, respond with "-"
|
||||
----------------
|
||||
{context}
|
||||
@@ -1,4 +0,0 @@
|
||||
Use the following portion of a long document to see if any of the text is relevant to answer the question.
|
||||
{{ context }}
|
||||
Question: {{ question }}
|
||||
Provide all relevant text to the question verbatim. Summarize if needed. If nothing relevant return "-".
|
||||
@@ -1,126 +1,95 @@
|
||||
aiodns==3.0.0
|
||||
aiohttp==3.8.3
|
||||
aiohttp-retry==2.8.3
|
||||
aiosignal==1.3.1
|
||||
alabaster==0.7.13
|
||||
aleph-alpha-client==2.16.0
|
||||
anyio==3.6.2
|
||||
argilla==1.3.0
|
||||
async-timeout==4.0.2
|
||||
attrs==22.2.0
|
||||
Babel==2.11.0
|
||||
backoff==2.2.1
|
||||
blobfile==2.0.1
|
||||
boto3==1.26.82
|
||||
botocore==1.29.82
|
||||
cffi==1.15.1
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
cohere==3.4.0
|
||||
dataclasses-json==0.5.7
|
||||
decorator==5.1.1
|
||||
deeplake==3.2.12
|
||||
Deprecated==1.2.13
|
||||
dill==0.3.6
|
||||
docutils==0.19
|
||||
anthropic==0.40.0
|
||||
boto3==1.35.97
|
||||
beautifulsoup4==4.12.3
|
||||
celery==5.4.0
|
||||
dataclasses-json==0.6.7
|
||||
docx2txt==0.8
|
||||
entrypoints==0.4
|
||||
escodegen==1.0.10
|
||||
duckduckgo-search==6.3.0
|
||||
ebooklib==0.18
|
||||
elastic-transport==8.17.0
|
||||
elasticsearch==8.17.0
|
||||
escodegen==1.0.11
|
||||
esprima==4.0.1
|
||||
esutils==1.0.1
|
||||
et-xmlfile==1.1.0
|
||||
faiss-cpu==1.7.3
|
||||
filelock==3.9.0
|
||||
Flask==2.2.2
|
||||
frozenlist==1.3.3
|
||||
greenlet==2.0.2
|
||||
gunicorn==20.1.0
|
||||
h11==0.14.0
|
||||
httpcore==0.16.3
|
||||
httpx==0.23.3
|
||||
hub==3.0.1
|
||||
huggingface-hub==0.12.0
|
||||
humbug==0.2.8
|
||||
idna==3.4
|
||||
imagesize==1.4.1
|
||||
itsdangerous==2.1.2
|
||||
Flask==3.1.0
|
||||
faiss-cpu==1.9.0.post1
|
||||
flask-restx==1.3.0
|
||||
google-genai==0.5.0
|
||||
google-generativeai==0.8.3
|
||||
gTTS==2.5.4
|
||||
gunicorn==23.0.0
|
||||
html2text==2024.2.26
|
||||
javalang==0.13.0
|
||||
Jinja2==3.1.2
|
||||
jinja2==3.1.5
|
||||
jiter==0.8.2
|
||||
jmespath==1.0.1
|
||||
joblib==1.2.0
|
||||
langchain==0.0.98
|
||||
lxml==4.9.2
|
||||
manifest-ml==0.1.1
|
||||
MarkupSafe==2.1.2
|
||||
marshmallow==3.19.0
|
||||
marshmallow-enum==1.5.1
|
||||
monotonic==1.6
|
||||
multidict==6.0.4
|
||||
multiprocess==0.70.14
|
||||
mypy-extensions==0.4.3
|
||||
nltk==3.8.1
|
||||
numcodecs==0.11.0
|
||||
numpy==1.23.5
|
||||
openai==0.27.0
|
||||
openpyxl==3.1.1
|
||||
packaging==23.0
|
||||
pandas==1.5.3
|
||||
pathos==0.3.0
|
||||
Pillow==9.4.0
|
||||
pox==0.3.2
|
||||
ppft==1.7.6.6
|
||||
joblib==1.4.2
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
jsonschema==4.23.0
|
||||
jsonschema-spec==0.2.4
|
||||
jsonschema-specifications==2023.7.1
|
||||
kombu==5.4.2
|
||||
langchain==0.3.14
|
||||
langchain-community==0.3.14
|
||||
langchain-core==0.3.29
|
||||
langchain-openai==0.3.0
|
||||
langchain-text-splitters==0.3.5
|
||||
langsmith==0.2.10
|
||||
lazy-object-proxy==1.10.0
|
||||
lxml==5.3.0
|
||||
markupsafe==3.0.2
|
||||
marshmallow==3.24.1
|
||||
mpmath==1.3.0
|
||||
multidict==6.1.0
|
||||
mypy-extensions==1.0.0
|
||||
networkx==3.4.2
|
||||
numpy==2.2.1
|
||||
openai==1.59.5
|
||||
openapi-schema-validator==0.6.2
|
||||
openapi-spec-validator==0.6.0
|
||||
openapi3-parser==1.1.19
|
||||
orjson==3.10.14
|
||||
packaging==24.1
|
||||
pandas==2.2.3
|
||||
openpyxl==3.1.5
|
||||
pathable==0.4.4
|
||||
pillow==11.1.0
|
||||
portalocker==2.10.1
|
||||
prance==23.6.21.0
|
||||
primp==0.10.0
|
||||
prompt-toolkit==3.0.48
|
||||
protobuf==5.29.3
|
||||
psycopg2-binary==2.9.10
|
||||
py==1.11.0
|
||||
pycares==4.3.0
|
||||
pycparser==2.21
|
||||
pycryptodomex==3.17
|
||||
pydantic==1.10.4
|
||||
Pygments==2.14.0
|
||||
PyJWT==2.6.0
|
||||
PyPDF2==3.0.1
|
||||
python-dateutil==2.8.2
|
||||
python-docx==0.8.11
|
||||
python-dotenv==0.21.1
|
||||
python-magic==0.4.27
|
||||
python-pptx==0.6.21
|
||||
pytz==2022.7.1
|
||||
PyYAML==6.0
|
||||
redis==4.5.1
|
||||
regex==2022.10.31
|
||||
requests==2.28.2
|
||||
pydantic==2.10.4
|
||||
pydantic-core==2.27.2
|
||||
pydantic-settings==2.7.1
|
||||
pymongo==4.10.1
|
||||
pypdf2==3.0.1
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.1
|
||||
python-pptx==1.0.2
|
||||
qdrant-client==1.12.2
|
||||
redis==5.2.1
|
||||
referencing==0.30.2
|
||||
regex==2024.11.6
|
||||
requests==2.32.3
|
||||
retry==0.9.2
|
||||
rfc3986==1.5.0
|
||||
s3transfer==0.6.0
|
||||
scikit-learn==1.2.1
|
||||
scipy==1.10.0
|
||||
sentence-transformers==2.2.2
|
||||
sentencepiece==0.1.97
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
snowballstemmer==2.2.0
|
||||
Sphinx==6.1.3
|
||||
sphinxcontrib-applehelp==1.0.4
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
sphinxcontrib-htmlhelp==2.0.1
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
SQLAlchemy==1.4.46
|
||||
sqlitedict==2.1.0
|
||||
tenacity==8.2.1
|
||||
threadpoolctl==3.1.0
|
||||
tiktoken==0.1.2
|
||||
tokenizers==0.13.2
|
||||
torch==1.13.1
|
||||
torchvision==0.14.1
|
||||
tqdm==4.64.1
|
||||
transformers==4.26.0
|
||||
typer==0.7.0
|
||||
typing-inspect==0.8.0
|
||||
typing_extensions==4.4.0
|
||||
unstructured==0.4.11
|
||||
urllib3==1.26.14
|
||||
Werkzeug==2.2.3
|
||||
wrapt==1.14.1
|
||||
XlsxWriter==3.0.8
|
||||
xxhash==3.2.0
|
||||
yarl==1.8.2
|
||||
sentence-transformers==3.3.1
|
||||
tiktoken==0.8.0
|
||||
tokenizers==0.21.0
|
||||
torch==2.5.1
|
||||
tqdm==4.67.1
|
||||
transformers==4.48.0
|
||||
typing-extensions==4.12.2
|
||||
typing-inspect==0.9.0
|
||||
tzdata==2024.2
|
||||
urllib3==2.3.0
|
||||
vine==5.1.0
|
||||
wcwidth==0.2.13
|
||||
werkzeug==3.1.3
|
||||
yarl==1.18.3
|
||||
markdownify==0.14.1
|
||||
tldextract==5.1.3
|
||||
websockets==14.1
|
||||
|
||||
0
application/retriever/__init__.py
Normal file
0
application/retriever/__init__.py
Normal file
18
application/retriever/base.py
Normal file
18
application/retriever/base.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseRetriever(ABC):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def gen(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_params(self):
|
||||
pass
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user