mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 22:38:10 -05:00
Compare commits
1568 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e892f38e4 | ||
|
|
aa0fe90258 | ||
|
|
c59c7553b3 | ||
|
|
703756d0b0 | ||
|
|
50d22f8e77 | ||
|
|
fde2efd4ce | ||
|
|
0150c3a37d | ||
|
|
2a0216b9aa | ||
|
|
a6d14d86b8 | ||
|
|
a9374c128b | ||
|
|
f32b9f81da | ||
|
|
bf3af8e98e | ||
|
|
095c295ee5 | ||
|
|
93ecc9cfea | ||
|
|
e7aaa23fc2 | ||
|
|
197d3454f8 | ||
|
|
50a4f8b491 | ||
|
|
894b4967dd | ||
|
|
9837bd6664 | ||
|
|
6ca1b5dac4 | ||
|
|
c85135c04e | ||
|
|
31e4e42a94 | ||
|
|
196db04fc2 | ||
|
|
b3b1b5a471 | ||
|
|
892439a177 | ||
|
|
ba2e178e03 | ||
|
|
ed298bcedd | ||
|
|
6b04e6e674 | ||
|
|
04c0f6a0a5 | ||
|
|
486ff42b59 | ||
|
|
f7ab484510 | ||
|
|
f50a14305a | ||
|
|
d5f0cd7616 | ||
|
|
67c658f5b4 | ||
|
|
ef3bc03343 | ||
|
|
e31cb2b46a | ||
|
|
6ca7142ea4 | ||
|
|
8b2174897a | ||
|
|
ac5eab0563 | ||
|
|
65414dcc1c | ||
|
|
5db352f5be | ||
|
|
9e7830ff77 | ||
|
|
5945c0e16b | ||
|
|
29ee141822 | ||
|
|
8a69621e87 | ||
|
|
1645b0c4ea | ||
|
|
45205574d5 | ||
|
|
71a5e0394a | ||
|
|
f286936c23 | ||
|
|
9000f92a55 | ||
|
|
8c84d4b3c8 | ||
|
|
1d77afcc44 | ||
|
|
835bc6044b | ||
|
|
ef895a1ab9 | ||
|
|
82039cedaf | ||
|
|
973df61dfd | ||
|
|
661c85d7a6 | ||
|
|
4638f67fb7 | ||
|
|
ab71dbcd4f | ||
|
|
2abdabc100 | ||
|
|
9f78a2c8e1 | ||
|
|
76f78601f2 | ||
|
|
4eaba2dc56 | ||
|
|
2dcd9cb5f7 | ||
|
|
2943872bde | ||
|
|
b901542a48 | ||
|
|
c122ff8960 | ||
|
|
e128d818c4 | ||
|
|
5e9d6d0a91 | ||
|
|
70edf9cbe3 | ||
|
|
e61a0a9391 | ||
|
|
f8ddf98404 | ||
|
|
55219467f3 | ||
|
|
74d4be1ac6 | ||
|
|
9e57f8c6f1 | ||
|
|
3d2903cb47 | ||
|
|
13e9d22ec6 | ||
|
|
01d12c47cf | ||
|
|
c3258a2c3f | ||
|
|
746885e263 | ||
|
|
b25895c1d2 | ||
|
|
e40b1c1f66 | ||
|
|
ef2ec8bffe | ||
|
|
589991e6a6 | ||
|
|
965392ebbd | ||
|
|
6f615baf53 | ||
|
|
b60bad7799 | ||
|
|
234d1303ad | ||
|
|
cd74a96be2 | ||
|
|
ceaa90a7c7 | ||
|
|
15a2eeadc9 | ||
|
|
8d02f5b21d | ||
|
|
0f8f0b6b39 | ||
|
|
fd58b6d410 | ||
|
|
2579d37c16 | ||
|
|
4f28d85e96 | ||
|
|
f529b8bb80 | ||
|
|
71437605e1 | ||
|
|
cf5753a186 | ||
|
|
433c83fe2c | ||
|
|
01770cc6e3 | ||
|
|
55fda5e025 | ||
|
|
daad5f986e | ||
|
|
3785d0a5fa | ||
|
|
8a326e9cfb | ||
|
|
5f5822f1c6 | ||
|
|
111482e46e | ||
|
|
9b56e0e996 | ||
|
|
9b830f9801 | ||
|
|
dda73d3333 | ||
|
|
40c26d9c9e | ||
|
|
cdd86b0ed9 | ||
|
|
c2e84d6db9 | ||
|
|
4208a02191 | ||
|
|
943b26eeef | ||
|
|
d6ceae9efd | ||
|
|
f57dc6d681 | ||
|
|
4e4bfc9d5d | ||
|
|
ea137c1525 | ||
|
|
f0be1d4735 | ||
|
|
e3975b9364 | ||
|
|
cd48802ea0 | ||
|
|
dceccd8e72 | ||
|
|
0813ad9c39 | ||
|
|
e391132167 | ||
|
|
c7f86d3a0c | ||
|
|
f0d92f9424 | ||
|
|
4a9bdb1479 | ||
|
|
7eed80710e | ||
|
|
fbd62be47d | ||
|
|
85cc7b8a9d | ||
|
|
1fe8afd329 | ||
|
|
e89ccf5e97 | ||
|
|
0eee89140c | ||
|
|
5571e6fafd | ||
|
|
9a4e920618 | ||
|
|
6e479999b1 | ||
|
|
f65f2501b4 | ||
|
|
4b12bd2a61 | ||
|
|
d83a3beeeb | ||
|
|
7428c8017f | ||
|
|
008ed76d37 | ||
|
|
ce9d4ad831 | ||
|
|
657bcab48c | ||
|
|
cd11dcc7a9 | ||
|
|
22040a42f2 | ||
|
|
705ccd750b | ||
|
|
db7c2b70cb | ||
|
|
9dc9bfa1d5 | ||
|
|
6b93658191 | ||
|
|
ea7a425a26 | ||
|
|
9582978adb | ||
|
|
453d8e75e4 | ||
|
|
901a010efd | ||
|
|
b5c2d069f2 | ||
|
|
f744e25b39 | ||
|
|
096f40df68 | ||
|
|
a227e61952 | ||
|
|
29f4534001 | ||
|
|
ed9324d611 | ||
|
|
438b3c5211 | ||
|
|
efeeb7a796 | ||
|
|
6b1ff0ab21 | ||
|
|
acb925f5a9 | ||
|
|
761293ede7 | ||
|
|
e004e50037 | ||
|
|
44a6c03bc8 | ||
|
|
d794afe405 | ||
|
|
e4ac322227 | ||
|
|
1fc19da19f | ||
|
|
b213068680 | ||
|
|
bf3af207b9 | ||
|
|
e28ba224b5 | ||
|
|
5b7697c5ab | ||
|
|
0701b7d263 | ||
|
|
aac29025fb | ||
|
|
6928f9a312 | ||
|
|
ef2e985d3f | ||
|
|
1df945556d | ||
|
|
b6f313db8f | ||
|
|
cb00f2026e | ||
|
|
bd39d98ffc | ||
|
|
36b0afa692 | ||
|
|
53d09d8a5a | ||
|
|
703144edad | ||
|
|
717e50e570 | ||
|
|
4af6d5eeed | ||
|
|
de356ddeb5 | ||
|
|
59c50def2a | ||
|
|
1dafb09e07 | ||
|
|
e8caf9fc10 | ||
|
|
59537c4bf5 | ||
|
|
231516917d | ||
|
|
58d17fd0ec | ||
|
|
8bd4aa6d1a | ||
|
|
629c1b3e11 | ||
|
|
2f4569177d | ||
|
|
f2b85af0ea | ||
|
|
36be4c747c | ||
|
|
7ac9b862f5 | ||
|
|
1515139dd6 | ||
|
|
be6049e577 | ||
|
|
a8ae09d4d6 | ||
|
|
30059c46a0 | ||
|
|
2d10c71e39 | ||
|
|
02e12b028c | ||
|
|
6686b83fc8 | ||
|
|
d53b0b678f | ||
|
|
0d5454372e | ||
|
|
2f040f94c3 | ||
|
|
0b29ebd14b | ||
|
|
1dad903199 | ||
|
|
0bec53360e | ||
|
|
cf637e4137 | ||
|
|
9507c2cca1 | ||
|
|
fa575638d1 | ||
|
|
51220c40d9 | ||
|
|
d1d62fcc4c | ||
|
|
96e6a56e5f | ||
|
|
0d7514ea0e | ||
|
|
a74da4acff | ||
|
|
6d8c3eb6e2 | ||
|
|
adbfa2f6ba | ||
|
|
f5776637d9 | ||
|
|
34db384265 | ||
|
|
1f765c5b53 | ||
|
|
f9395fa108 | ||
|
|
22d2a3ee19 | ||
|
|
b64178c292 | ||
|
|
f7d38fb51f | ||
|
|
ea6c0b9025 | ||
|
|
30fa5ee575 | ||
|
|
1e345af0bc | ||
|
|
952f584158 | ||
|
|
b23b20f540 | ||
|
|
1980edbe1c | ||
|
|
bf618f4a25 | ||
|
|
e4617190d8 | ||
|
|
49fe59f403 | ||
|
|
821faa0894 | ||
|
|
af39e38394 | ||
|
|
8774971b98 | ||
|
|
1286afeb76 | ||
|
|
4725a94f00 | ||
|
|
15ac5351cf | ||
|
|
f69cda8fab | ||
|
|
e9e6549528 | ||
|
|
f1550e1d1d | ||
|
|
1fe00633c4 | ||
|
|
a0e1f7204d | ||
|
|
bb1d4f9ca4 | ||
|
|
942771af60 | ||
|
|
1f0bf7b58b | ||
|
|
d56dcb8b16 | ||
|
|
ca83506089 | ||
|
|
267562e1d2 | ||
|
|
0ac8924b0d | ||
|
|
540186acca | ||
|
|
b26d466394 | ||
|
|
12603b619b | ||
|
|
9af69d456e | ||
|
|
61f0b5848c | ||
|
|
cbc82ec045 | ||
|
|
c8af946c87 | ||
|
|
bc216fdfef | ||
|
|
8befac61af | ||
|
|
bebc8c20b5 | ||
|
|
9a1a46e203 | ||
|
|
a5ab81b5c8 | ||
|
|
31be01f3b3 | ||
|
|
52e2995c55 | ||
|
|
f314671f65 | ||
|
|
292fd75699 | ||
|
|
0a07072be0 | ||
|
|
5d31e90650 | ||
|
|
8bff9764f8 | ||
|
|
40c4cb46be | ||
|
|
8a0f9814e6 | ||
|
|
717eb585b5 | ||
|
|
e10a2c9b09 | ||
|
|
c6ebfd3ad7 | ||
|
|
0369087b91 | ||
|
|
d8a415698c | ||
|
|
2bfb087b55 | ||
|
|
8782f78178 | ||
|
|
90c4f244ae | ||
|
|
2331d011c1 | ||
|
|
2568204395 | ||
|
|
eb56ead927 | ||
|
|
98fe1fbae2 | ||
|
|
8e10a72f1d | ||
|
|
5246a9699a | ||
|
|
7a678dc175 | ||
|
|
b2e2784cf4 | ||
|
|
111e8c786a | ||
|
|
b8b9cdfdae | ||
|
|
bfcbe6f06a | ||
|
|
02c28ad8b8 | ||
|
|
f3a1982e30 | ||
|
|
c4b629fe03 | ||
|
|
f962104a2d | ||
|
|
cf32bdc012 | ||
|
|
1ccbb22866 | ||
|
|
d5a2008c44 | ||
|
|
ff33c33ea5 | ||
|
|
731ecc6b3c | ||
|
|
31df56add8 | ||
|
|
0f8a403dba | ||
|
|
8b33b9946e | ||
|
|
a77efada0e | ||
|
|
3e8aaed268 | ||
|
|
d25be21939 | ||
|
|
c2fad4de80 | ||
|
|
6e0f7b5192 | ||
|
|
f522f6b3bd | ||
|
|
e558d535df | ||
|
|
1c05b37c76 | ||
|
|
e46c588b9c | ||
|
|
3bf6b7b000 | ||
|
|
82db18a8aa | ||
|
|
5a765bd8fc | ||
|
|
339e1e6790 | ||
|
|
a106e6de27 | ||
|
|
86eddbeb0a | ||
|
|
2daf0d90ce | ||
|
|
03dfa03f46 | ||
|
|
92bbbfe88b | ||
|
|
fb2dc00b9c | ||
|
|
0014a53c6e | ||
|
|
021d2738e4 | ||
|
|
f312ad0364 | ||
|
|
02aa41e6aa | ||
|
|
1f8039d996 | ||
|
|
977d902cdd | ||
|
|
710df90361 | ||
|
|
f5d94bfde6 | ||
|
|
1629f36c59 | ||
|
|
12e4611d9a | ||
|
|
46a77de9e8 | ||
|
|
87b55148fa | ||
|
|
3931098aad | ||
|
|
2aebc84c66 | ||
|
|
c107cce22e | ||
|
|
71b049bffd | ||
|
|
d3e8ce5120 | ||
|
|
ce7fc78076 | ||
|
|
f911de41b5 | ||
|
|
7288001a01 | ||
|
|
7f808bcf43 | ||
|
|
025dc8ed13 | ||
|
|
b4b8b96260 | ||
|
|
b07054adea | ||
|
|
fc0fd00e16 | ||
|
|
a3da84f459 | ||
|
|
ff21c60661 | ||
|
|
58a6f0404a | ||
|
|
643403192a | ||
|
|
416cee4f54 | ||
|
|
e42be19347 | ||
|
|
78bae7a6e7 | ||
|
|
ec31f11abf | ||
|
|
2d3ebcd09c | ||
|
|
5da749f994 | ||
|
|
85891f0106 | ||
|
|
229287510a | ||
|
|
d42ba42bb2 | ||
|
|
574bb2c450 | ||
|
|
3797b7ac6a | ||
|
|
ed7c28958f | ||
|
|
74a134eec0 | ||
|
|
4094296a4c | ||
|
|
00a706eb36 | ||
|
|
dfc0efbb67 | ||
|
|
d79449be4a | ||
|
|
5c6b84e4ec | ||
|
|
0fcd4945fb | ||
|
|
c10ae1ddd2 | ||
|
|
9774692b67 | ||
|
|
f8f39b92c3 | ||
|
|
eb8d40dfb6 | ||
|
|
343cbba5ec | ||
|
|
ac3e0b5ba0 | ||
|
|
55c11a3861 | ||
|
|
013c6cb1e5 | ||
|
|
fc54f0e32e | ||
|
|
5a63c6b260 | ||
|
|
157b0a6109 | ||
|
|
b10455ff76 | ||
|
|
a7b4a7160a | ||
|
|
65bb9fee84 | ||
|
|
b701c767fc | ||
|
|
2a450cf1be | ||
|
|
1f1b51edcf | ||
|
|
e45f24c6fd | ||
|
|
eae691aa8c | ||
|
|
9d8d5ca924 | ||
|
|
84e3ff9386 | ||
|
|
002e87ffbb | ||
|
|
4be9cf42b4 | ||
|
|
75aad67a22 | ||
|
|
b8a285bbbc | ||
|
|
fb416c26ea | ||
|
|
e858700976 | ||
|
|
525b89be71 | ||
|
|
e15280d25d | ||
|
|
7a26012457 | ||
|
|
a5929fcad6 | ||
|
|
ad561248fd | ||
|
|
f8f892bfe0 | ||
|
|
8c68ebc0ee | ||
|
|
cbd2ffe81d | ||
|
|
86b76faa5b | ||
|
|
edb4490c86 | ||
|
|
70c9746bcb | ||
|
|
ba774d26c6 | ||
|
|
2e2177e26b | ||
|
|
72ec02bfd4 | ||
|
|
9b94518e20 | ||
|
|
b550936e72 | ||
|
|
ce2d6def36 | ||
|
|
1977c6260a | ||
|
|
811e4c84ab | ||
|
|
104513f72b | ||
|
|
e434999802 | ||
|
|
fce06b5294 | ||
|
|
c53f160ab8 | ||
|
|
4100ace659 | ||
|
|
1e7ae9790c | ||
|
|
ac1fc4b1d6 | ||
|
|
79b23f3106 | ||
|
|
6d00405eb6 | ||
|
|
65285fdef0 | ||
|
|
89edd7152a | ||
|
|
5527dc8db5 | ||
|
|
f5ac7fd92c | ||
|
|
61027f30a4 | ||
|
|
575f83954d | ||
|
|
ae18e9d1c7 | ||
|
|
76d18e2f04 | ||
|
|
978731f385 | ||
|
|
103388ecec | ||
|
|
53ea7ab126 | ||
|
|
b008d17b6e | ||
|
|
2ba294f4d6 | ||
|
|
a7ed257fe3 | ||
|
|
9a9990f78c | ||
|
|
95f0c95832 | ||
|
|
3b1b0385e1 | ||
|
|
621b64c89f | ||
|
|
1ce5bd4447 | ||
|
|
634cd3f484 | ||
|
|
9b38c8d5aa | ||
|
|
8f4aab4f61 | ||
|
|
12284ad3db | ||
|
|
f180e8fc6b | ||
|
|
89153dd235 | ||
|
|
aa2881f3c2 | ||
|
|
82379ee6ec | ||
|
|
e795055d13 | ||
|
|
5b6d7e27b6 | ||
|
|
c6dc13ef7f | ||
|
|
7e6a760623 | ||
|
|
01519d7486 | ||
|
|
4c0ed0a5f0 | ||
|
|
0bc220949a | ||
|
|
5fb18077eb | ||
|
|
fcf073febd | ||
|
|
565fea97cf | ||
|
|
daf1259556 | ||
|
|
0eab786030 | ||
|
|
9dfb911d4a | ||
|
|
197f0e5c0d | ||
|
|
aef4a1a5d4 | ||
|
|
f5f50cc4c9 | ||
|
|
9226e95d18 | ||
|
|
2d8b46b878 | ||
|
|
fbd6083079 | ||
|
|
0320e17652 | ||
|
|
09fb913279 | ||
|
|
ec5ed689bb | ||
|
|
373c1d0858 | ||
|
|
ca55f2375d | ||
|
|
d8671ea03a | ||
|
|
2579d4e87d | ||
|
|
f4885c5cdd | ||
|
|
c49f47ecab | ||
|
|
43ca0dccf7 | ||
|
|
fcfcf55610 | ||
|
|
188235efc5 | ||
|
|
79b27253cd | ||
|
|
6deb4d69c0 | ||
|
|
1b97a57cba | ||
|
|
0302e49ebd | ||
|
|
b9a5501f9d | ||
|
|
faa83f9a49 | ||
|
|
4888f8cb78 | ||
|
|
fdd1d614b2 | ||
|
|
6fc75282e8 | ||
|
|
f33ebb7e25 | ||
|
|
fc67dea243 | ||
|
|
efd363d5fb | ||
|
|
a7d6de1661 | ||
|
|
d17afc1fba | ||
|
|
da6f974887 | ||
|
|
db2ba46099 | ||
|
|
744ec0824b | ||
|
|
b31f094e9b | ||
|
|
43597e4168 | ||
|
|
160703210b | ||
|
|
c0ade48648 | ||
|
|
7fd4fa4742 | ||
|
|
41b2e66c5c | ||
|
|
ed657383fb | ||
|
|
4d5d8d8b30 | ||
|
|
e9a75528ab | ||
|
|
c5ec4b548a | ||
|
|
8e87529638 | ||
|
|
ca33208fa1 | ||
|
|
3f8bca8728 | ||
|
|
ba56c33cf6 | ||
|
|
6ee4fdd366 | ||
|
|
30af189ae3 | ||
|
|
be998ff588 | ||
|
|
6bb3238e6d | ||
|
|
dfcd29593d | ||
|
|
63b357168e | ||
|
|
317a4309f7 | ||
|
|
eceb10b725 | ||
|
|
34f508fd82 | ||
|
|
9fa8634083 | ||
|
|
a3ea63c1f9 | ||
|
|
097b3eb0ba | ||
|
|
30f37ea633 | ||
|
|
23b495c8f7 | ||
|
|
e7f2d48437 | ||
|
|
7043f78f1f | ||
|
|
f2cc718f49 | ||
|
|
edb814c9f0 | ||
|
|
21de69b7d9 | ||
|
|
d4b5c3b8d5 | ||
|
|
afb5857699 | ||
|
|
153b8217fd | ||
|
|
beeba6989a | ||
|
|
666a1d32a3 | ||
|
|
4ed512b8d4 | ||
|
|
af16494be1 | ||
|
|
9afa397c27 | ||
|
|
58f9d3c89c | ||
|
|
7732b6fe55 | ||
|
|
0d5f15edda | ||
|
|
4e2aa1b6d8 | ||
|
|
b6eb969b3a | ||
|
|
4c22965f4b | ||
|
|
7d28c95f48 | ||
|
|
94b713e3a5 | ||
|
|
dccc92e8e0 | ||
|
|
590a9e452d | ||
|
|
56322aaeb5 | ||
|
|
3684031f44 | ||
|
|
005f2b7db5 | ||
|
|
67840605fc | ||
|
|
d475e7b568 | ||
|
|
1f07ea25a2 | ||
|
|
08f4e28342 | ||
|
|
97666d9537 | ||
|
|
f7733f932b | ||
|
|
20a039a8ab | ||
|
|
29856e4749 | ||
|
|
47a797e884 | ||
|
|
d4079aa543 | ||
|
|
62eb837422 | ||
|
|
8d81f8d3aa | ||
|
|
e8acf9ca07 | ||
|
|
af4752d324 | ||
|
|
fbd1fbfc67 | ||
|
|
d1fe826f14 | ||
|
|
b758a27b93 | ||
|
|
2ae26dc2a6 | ||
|
|
81d765a34c | ||
|
|
c396288ca7 | ||
|
|
125e7a341f | ||
|
|
064ab9ba85 | ||
|
|
f0ee8287a7 | ||
|
|
47ccc33dfc | ||
|
|
ceb735482a | ||
|
|
473a20c0f6 | ||
|
|
a337e81a81 | ||
|
|
7d773b51d0 | ||
|
|
bca10ddf7c | ||
|
|
9756c575f3 | ||
|
|
d02fb3e34d | ||
|
|
988ff88a15 | ||
|
|
5de85c3da5 | ||
|
|
5907f9dbac | ||
|
|
1293e37525 | ||
|
|
0a55e6c742 | ||
|
|
ff3b18485f | ||
|
|
2fec6e2e52 | ||
|
|
9250f19d15 | ||
|
|
1e7c5c3b6a | ||
|
|
0289b67a84 | ||
|
|
8934dbaa42 | ||
|
|
75c3d7ea6a | ||
|
|
a94ad620bc | ||
|
|
c6ca1a60d1 | ||
|
|
4321c9d518 | ||
|
|
6cd86639ce | ||
|
|
3e6ad1029c | ||
|
|
1cf967582d | ||
|
|
5e1b4e87e7 | ||
|
|
76d6788231 | ||
|
|
73a0e38af6 | ||
|
|
ff0ee4f111 | ||
|
|
de61e56fda | ||
|
|
79b03c681a | ||
|
|
b8de34e539 | ||
|
|
6377f951d8 | ||
|
|
86b702bf46 | ||
|
|
232847b218 | ||
|
|
44dae97784 | ||
|
|
d8e3860e49 | ||
|
|
e01a84b21d | ||
|
|
97c5341bc1 | ||
|
|
de30df446d | ||
|
|
b5b45c8474 | ||
|
|
c5483276e5 | ||
|
|
263b1cb187 | ||
|
|
203add15e5 | ||
|
|
b98316a705 | ||
|
|
f2d9e0e8ea | ||
|
|
f5abaac8b7 | ||
|
|
0bb4f58222 | ||
|
|
4453afba89 | ||
|
|
96c8117135 | ||
|
|
1830ae2321 | ||
|
|
7af94a9d2a | ||
|
|
6d10c26c5d | ||
|
|
681f1a49a5 | ||
|
|
b750171593 | ||
|
|
02a019632b | ||
|
|
385d381cf1 | ||
|
|
48e8d76f21 | ||
|
|
d5336b2796 | ||
|
|
cb1b2bf5ca | ||
|
|
6c38cd360b | ||
|
|
4c2ca22cb2 | ||
|
|
cd177ff476 | ||
|
|
1ea3b4b3c5 | ||
|
|
8df3a9227f | ||
|
|
583695c228 | ||
|
|
455215290f | ||
|
|
5373345a3c | ||
|
|
e17b96d864 | ||
|
|
3ec4d274c4 | ||
|
|
611f8789da | ||
|
|
8e01d62150 | ||
|
|
f55662300a | ||
|
|
8de6ec27b8 | ||
|
|
2b8b626f69 | ||
|
|
d81fdb0f9c | ||
|
|
38406ee586 | ||
|
|
f2fdd6e6d3 | ||
|
|
ee3668006d | ||
|
|
7b4265470a | ||
|
|
9c9897706b | ||
|
|
9e8ad44cdf | ||
|
|
a6d82e0fc3 | ||
|
|
62ae3de488 | ||
|
|
dff094301a | ||
|
|
69aefc16f6 | ||
|
|
0a2ae30034 | ||
|
|
8b5be309fe | ||
|
|
e16eec8680 | ||
|
|
60f4606c9d | ||
|
|
1fe4b7ae2a | ||
|
|
00b2f90c65 | ||
|
|
c67fe04d3c | ||
|
|
daa57388e7 | ||
|
|
758a8c0540 | ||
|
|
6c1ecf4b4b | ||
|
|
ac6ae9439f | ||
|
|
9b4db98ed9 | ||
|
|
47d2b438aa | ||
|
|
f8841b606e | ||
|
|
05e8e99c89 | ||
|
|
b4e439e817 | ||
|
|
e4fd7b23fd | ||
|
|
2eb96fa4df | ||
|
|
62bc783d14 | ||
|
|
721f6515ed | ||
|
|
022011fb0d | ||
|
|
1837ca3715 | ||
|
|
ef6e49a6c9 | ||
|
|
703cd07210 | ||
|
|
5c8b59fa2b | ||
|
|
da5ccea93e | ||
|
|
3a1bf7314c | ||
|
|
9abb410271 | ||
|
|
865820ece8 | ||
|
|
b7e47d510c | ||
|
|
61e72eb7fe | ||
|
|
23ff16a039 | ||
|
|
3c2280bc42 | ||
|
|
90dbab6376 | ||
|
|
819021b7ba | ||
|
|
ef3c043f77 | ||
|
|
7139ad013d | ||
|
|
151c58d0ef | ||
|
|
d65375da7b | ||
|
|
21186097e4 | ||
|
|
a97302d791 | ||
|
|
aaddc95ec0 | ||
|
|
1b7a1fa652 | ||
|
|
8a3d63ef48 | ||
|
|
609df943dd | ||
|
|
8941551f5a | ||
|
|
61f66f88e3 | ||
|
|
15de33107b | ||
|
|
81f9b1dabb | ||
|
|
888342c119 | ||
|
|
12d83dad6d | ||
|
|
14ef9fd41c | ||
|
|
584e0c8547 | ||
|
|
914b312c2e | ||
|
|
8153d690cc | ||
|
|
d03bdbeb9b | ||
|
|
87730043b5 | ||
|
|
3285b8e330 | ||
|
|
686d039392 | ||
|
|
d7683e4c39 | ||
|
|
56f995afb4 | ||
|
|
17bde814cc | ||
|
|
525f972d22 | ||
|
|
161ce65ae6 | ||
|
|
72f1429db9 | ||
|
|
b5fe3f2ad8 | ||
|
|
2155ff9fc0 | ||
|
|
d33175e5b6 | ||
|
|
6dbd24e541 | ||
|
|
d1c527c421 | ||
|
|
c0bd61ba49 | ||
|
|
8f0cc85742 | ||
|
|
7275dfbd6b | ||
|
|
9f29642635 | ||
|
|
9f94cfb718 | ||
|
|
e1fa674a3f | ||
|
|
dd063f42bb | ||
|
|
ef4e7aa89a | ||
|
|
34f804be3a | ||
|
|
83cd8a1912 | ||
|
|
d347ef0dcc | ||
|
|
44cc9bbcac | ||
|
|
4aa0b99c74 | ||
|
|
d90e8081ac | ||
|
|
70d43c5252 | ||
|
|
fd8216f1bd | ||
|
|
ae0c9009d9 | ||
|
|
4104317b34 | ||
|
|
401f0da689 | ||
|
|
6efe7960cd | ||
|
|
a6d63f4d0e | ||
|
|
8f3928f4b2 | ||
|
|
3380972df1 | ||
|
|
bad01040e3 | ||
|
|
0b26b930f9 | ||
|
|
5dbaf4f28f | ||
|
|
1d9cce5adf | ||
|
|
76622105e2 | ||
|
|
e6e3e34f53 | ||
|
|
7192ddbaa6 | ||
|
|
b5206d1923 | ||
|
|
81d60b4292 | ||
|
|
ca6660585d | ||
|
|
d70e7c570d | ||
|
|
4fb965ec9d | ||
|
|
a5544cb67c | ||
|
|
679a852c1c | ||
|
|
c685b4f810 | ||
|
|
9452d6bd2a | ||
|
|
a6eeff2c91 | ||
|
|
3eb314320e | ||
|
|
14d02073ab | ||
|
|
17a6020c2d | ||
|
|
e8d5d75286 | ||
|
|
eb6430295f | ||
|
|
c0adf73056 | ||
|
|
e373c5d5b8 | ||
|
|
0ed45d34cb | ||
|
|
6fadd428c0 | ||
|
|
87d2acbb43 | ||
|
|
3253cd284a | ||
|
|
bb72fe59d1 | ||
|
|
c88ef3d507 | ||
|
|
550a06c9ed | ||
|
|
3b7b9e07e7 | ||
|
|
bedcc08869 | ||
|
|
4b6f7ddf27 | ||
|
|
c96a4931d1 | ||
|
|
110eb37ffa | ||
|
|
fc3aa9c8d6 | ||
|
|
8e2c26d1a0 | ||
|
|
3766bab44a | ||
|
|
2eed7ec935 | ||
|
|
134f54d372 | ||
|
|
0ccf34ea8c | ||
|
|
7e9d7f6f32 | ||
|
|
d6d721737b | ||
|
|
2d102c1a02 | ||
|
|
6c185c25f6 | ||
|
|
b53005a6f1 | ||
|
|
e4d34f4937 | ||
|
|
689292f7f6 | ||
|
|
857b23c3fd | ||
|
|
42b5cb4413 | ||
|
|
3990fe013b | ||
|
|
81b4bf3756 | ||
|
|
ee1e7d399d | ||
|
|
859ef049d2 | ||
|
|
54825ec8d8 | ||
|
|
99e2e7da57 | ||
|
|
ed34ccfc73 | ||
|
|
c92bcc24da | ||
|
|
6ec0ea4f80 | ||
|
|
3c56d23b03 | ||
|
|
bcafea3b98 | ||
|
|
33df431993 | ||
|
|
d6551bed60 | ||
|
|
fe620ed278 | ||
|
|
7b8ea76239 | ||
|
|
9a55514bc0 | ||
|
|
5012621721 | ||
|
|
9e676fdcd8 | ||
|
|
e0489803f4 | ||
|
|
6c927e23b5 | ||
|
|
8fb7f19fb1 | ||
|
|
a0ba207db9 | ||
|
|
58f8d21ec0 | ||
|
|
2b87109e52 | ||
|
|
f54a052533 | ||
|
|
a67dd0555a | ||
|
|
19b568b075 | ||
|
|
5d3e0dae49 | ||
|
|
be37d889a0 | ||
|
|
be9046aee9 | ||
|
|
7e63a16f28 | ||
|
|
98292df3cc | ||
|
|
cc8711cc07 | ||
|
|
211c0fece0 | ||
|
|
7d6cb5604f | ||
|
|
f774c95865 | ||
|
|
8445b6aad7 | ||
|
|
2f881a2c06 | ||
|
|
6fc4d91a29 | ||
|
|
6126a14c9f | ||
|
|
9aa2e00b16 | ||
|
|
3a7e1cf527 | ||
|
|
913210d2a9 | ||
|
|
b2a2ea0c03 | ||
|
|
ad6260fab5 | ||
|
|
522c1038fb | ||
|
|
cab365f496 | ||
|
|
e714b6c5bf | ||
|
|
d8c3c29ff8 | ||
|
|
273ba3e943 | ||
|
|
70c5aacb45 | ||
|
|
c5db39a06d | ||
|
|
fa457e7812 | ||
|
|
dd6cd06b5a | ||
|
|
41b711f1ca | ||
|
|
8f7acac2b1 | ||
|
|
ea323c12ff | ||
|
|
dd83d7faca | ||
|
|
28937bb8ca | ||
|
|
a1c81c41cb | ||
|
|
24816f25d1 | ||
|
|
fcddedfe72 | ||
|
|
77c7323a39 | ||
|
|
7322d249e2 | ||
|
|
cfae9efcbb | ||
|
|
70aac4e5f9 | ||
|
|
4a34355c3b | ||
|
|
c51c4c8d3e | ||
|
|
a619c915e1 | ||
|
|
f4044cde7e | ||
|
|
a31af9fa80 | ||
|
|
a61590efeb | ||
|
|
26fccfe18e | ||
|
|
2b79a058de | ||
|
|
64da74e25a | ||
|
|
23cb9a9ee8 | ||
|
|
c2a15f6aa1 | ||
|
|
c293c6137b | ||
|
|
d2e2a6537e | ||
|
|
a6fc13dbdc | ||
|
|
4534ef6544 | ||
|
|
ebc0239339 | ||
|
|
bd33795f72 | ||
|
|
39aade44f6 | ||
|
|
3ae503c969 | ||
|
|
780fc42aa0 | ||
|
|
2967cfd1d4 | ||
|
|
0b28847e5d | ||
|
|
3a62c12791 | ||
|
|
75c1e84e08 | ||
|
|
147da29c1a | ||
|
|
329c843567 | ||
|
|
3bf1c95dc2 | ||
|
|
0bc7cd9785 | ||
|
|
680129e370 | ||
|
|
d574670084 | ||
|
|
5ad9943462 | ||
|
|
19a0b8a1d6 | ||
|
|
38d4b459bd | ||
|
|
37401fcfb4 | ||
|
|
4294489c1c | ||
|
|
48076450d7 | ||
|
|
b96415d445 | ||
|
|
d25ea0e88b | ||
|
|
069cf0ff3d | ||
|
|
6fdf17b466 | ||
|
|
e8de263e7f | ||
|
|
357feb7a2f | ||
|
|
4653ebb8e3 | ||
|
|
25377ebd48 | ||
|
|
9b3943e1a3 | ||
|
|
18147de7ee | ||
|
|
e572a9ef6c | ||
|
|
afa0cdecc1 | ||
|
|
7e182acbc6 | ||
|
|
63b73bc745 | ||
|
|
9597580185 | ||
|
|
9aebafb795 | ||
|
|
30fbe8ff28 | ||
|
|
da320e9058 | ||
|
|
1bafb4df0d | ||
|
|
23de7db544 | ||
|
|
3880a61713 | ||
|
|
4d35d4369a | ||
|
|
ae4459b844 | ||
|
|
bc4aa86328 | ||
|
|
26ef123c79 | ||
|
|
1306efb6a5 | ||
|
|
8e61f9b2cd | ||
|
|
04d4f29751 | ||
|
|
c93b840a1c | ||
|
|
0266a6f36d | ||
|
|
6f116ca527 | ||
|
|
e50414f045 | ||
|
|
c79a2915b3 | ||
|
|
bac7d87390 | ||
|
|
dcfafe5ce1 | ||
|
|
13d68570e9 | ||
|
|
9a3b34118e | ||
|
|
43ae2c657e | ||
|
|
ce47429712 | ||
|
|
5a9efba473 | ||
|
|
0608e65d6d | ||
|
|
452114470d | ||
|
|
822749c0f4 | ||
|
|
ce3280a320 | ||
|
|
db66bfc8c6 | ||
|
|
f1c4c60469 | ||
|
|
2343eaa3a3 | ||
|
|
e1d9bd599a | ||
|
|
731e800177 | ||
|
|
44d47395cc | ||
|
|
4e82b27424 | ||
|
|
5da056b87a | ||
|
|
272b21237c | ||
|
|
824e2eb190 | ||
|
|
29f9de7f56 | ||
|
|
1ef492449d | ||
|
|
7103c9adf6 | ||
|
|
d6552f5811 | ||
|
|
a921b77f5a | ||
|
|
fb9bb89da7 | ||
|
|
502cdfeb9b | ||
|
|
660b31aed5 | ||
|
|
c76564b85c | ||
|
|
33632030f6 | ||
|
|
e26d72c2f0 | ||
|
|
feabd565dc | ||
|
|
19cd12029e | ||
|
|
9b8871f25b | ||
|
|
5f773396df | ||
|
|
730366aa5f | ||
|
|
1bf8aa65c1 | ||
|
|
a4b7364230 | ||
|
|
7079c9cb23 | ||
|
|
7d3bf8c3a2 | ||
|
|
e7fd450dad | ||
|
|
b025e69875 | ||
|
|
4006f3f417 | ||
|
|
25845f5d5a | ||
|
|
a259bd30cb | ||
|
|
21f4b5f774 | ||
|
|
83a1fd104d | ||
|
|
ef4cfa94de | ||
|
|
0d3c2749f1 | ||
|
|
206254ea6d | ||
|
|
38b7ab7a26 | ||
|
|
035a8a2781 | ||
|
|
3baa454c80 | ||
|
|
8cd0887c82 | ||
|
|
21007b7e93 | ||
|
|
bacc49c25a | ||
|
|
78aa378ab8 | ||
|
|
a93acd3afc | ||
|
|
60e5b536b4 | ||
|
|
c5f926ba0c | ||
|
|
083cf4c82c | ||
|
|
b6b86bb2c2 | ||
|
|
7cbd49375a | ||
|
|
e8d5fba256 | ||
|
|
9a0444db7e | ||
|
|
a67dca253a | ||
|
|
3726386b9a | ||
|
|
0ef5b54808 | ||
|
|
e915c93885 | ||
|
|
d77dcdf343 | ||
|
|
e071445882 | ||
|
|
58c2c26bff | ||
|
|
ff5fe2fa47 | ||
|
|
12781a48c1 | ||
|
|
5d870e1c3b | ||
|
|
58c9af6aac | ||
|
|
7258ed6a14 | ||
|
|
f60b2ceb63 | ||
|
|
8cfe0309f5 | ||
|
|
839c468b2f | ||
|
|
92e4960eee | ||
|
|
a7eab84517 | ||
|
|
0549e0e7f0 | ||
|
|
4b3afb3c8e | ||
|
|
6996278c8f | ||
|
|
4d77ed30e9 | ||
|
|
69375f2fbc | ||
|
|
cf0b9d2c3d | ||
|
|
0ef4e465e4 | ||
|
|
151fff8f8d | ||
|
|
b2be94f2f8 | ||
|
|
9b4c20dd19 | ||
|
|
c7449c68b7 | ||
|
|
a81e5be74b | ||
|
|
e12dc5b0e8 | ||
|
|
de2ab4c7c5 | ||
|
|
ff97b85497 | ||
|
|
1bafde09b6 | ||
|
|
e01d355d1b | ||
|
|
ebe0135d5b | ||
|
|
3d8c4985e8 | ||
|
|
9acfc3ef92 | ||
|
|
9eb70b8d80 | ||
|
|
576daab07b | ||
|
|
b71a0584ca | ||
|
|
5c8f15e6fa | ||
|
|
452c64f1b3 | ||
|
|
597f2c2b34 | ||
|
|
60d9393c87 | ||
|
|
4473b68d9b | ||
|
|
035c0bb0ca | ||
|
|
872ad3de4a | ||
|
|
adc57b14ed | ||
|
|
d301892e3b | ||
|
|
077824f6a7 | ||
|
|
02f90361e5 | ||
|
|
8a39866513 | ||
|
|
30248ed149 | ||
|
|
d56f9f880e | ||
|
|
aeb5299054 | ||
|
|
a51a565cdc | ||
|
|
9988c5cefc | ||
|
|
c7f038e41e | ||
|
|
55621a6963 | ||
|
|
cd66d88e2d | ||
|
|
d87333fea1 | ||
|
|
bbdca0f91f | ||
|
|
cd7dfb9171 | ||
|
|
fab9cb29da | ||
|
|
1a458c18b0 | ||
|
|
e5cc90b24b | ||
|
|
dfca9bd014 | ||
|
|
3b35d88611 | ||
|
|
92e32b926d | ||
|
|
01169cf71d | ||
|
|
845b4003e7 | ||
|
|
8bf32b1894 | ||
|
|
75ee3ac5e4 | ||
|
|
54e5076857 | ||
|
|
b1a9797201 | ||
|
|
346b051aec | ||
|
|
02a9f19ec0 | ||
|
|
cabdb8d524 | ||
|
|
7b02fa41ec | ||
|
|
55fe665261 | ||
|
|
312435bea2 | ||
|
|
dc4a75e5d6 | ||
|
|
ca496040de | ||
|
|
b01bedb54a | ||
|
|
f054a3c40c | ||
|
|
ca567b4923 | ||
|
|
e593def52a | ||
|
|
3ea896bd9d | ||
|
|
0d2e62d38b | ||
|
|
9b5b152a24 | ||
|
|
7c1a4a9d8a | ||
|
|
0c684bd79f | ||
|
|
f21b7c9cb8 | ||
|
|
7399d84446 | ||
|
|
85e3a4d485 | ||
|
|
e957dea462 | ||
|
|
13ddebe27d | ||
|
|
b92c13772b | ||
|
|
58ba8e51f4 | ||
|
|
a5eefb2b5c | ||
|
|
0b23461272 | ||
|
|
08782c8f68 | ||
|
|
280eaa1dff | ||
|
|
2f2a70053b | ||
|
|
2f9bbd60d6 | ||
|
|
e9ef0415d3 | ||
|
|
a78b5dcc54 | ||
|
|
e72bf4d7cf | ||
|
|
d6c64b68ee | ||
|
|
8d8a3659d5 | ||
|
|
15372ca8ad | ||
|
|
84b3307f73 | ||
|
|
7c2a48c323 | ||
|
|
9bbb167737 | ||
|
|
6aa52582dd | ||
|
|
86ddf894d0 | ||
|
|
630b28e932 | ||
|
|
dda9782fa8 | ||
|
|
8eeac47f99 | ||
|
|
d7b6027c65 | ||
|
|
358f78d455 | ||
|
|
7db5b9fbf1 | ||
|
|
45fcc547d5 | ||
|
|
4ed18437de | ||
|
|
d75ea473e6 | ||
|
|
5f378431ac | ||
|
|
5c98269d25 | ||
|
|
56c2a5e1a3 | ||
|
|
54f4761262 | ||
|
|
78ec3d5f16 | ||
|
|
f8e4cad339 | ||
|
|
ec36fbd0d1 | ||
|
|
95b58e9e09 | ||
|
|
0fcc097a56 | ||
|
|
bba3182d57 | ||
|
|
9ef8e42473 | ||
|
|
dd4b896f4d | ||
|
|
4b8b6bc127 | ||
|
|
aeff6ec6ec | ||
|
|
b16561df02 | ||
|
|
9a7514e38a | ||
|
|
52d2599e81 | ||
|
|
3f495af0a6 | ||
|
|
099af547d9 | ||
|
|
421a2dde9e | ||
|
|
90f9cee3f1 | ||
|
|
937a260328 | ||
|
|
f031972594 | ||
|
|
3d1a55e4eb | ||
|
|
a7f7265214 | ||
|
|
45d2643234 | ||
|
|
df34b73a27 | ||
|
|
42e17b0fe0 | ||
|
|
4088f7cbf3 | ||
|
|
3972665da9 | ||
|
|
7ad53cfde2 | ||
|
|
b9b4d028a2 | ||
|
|
ed0fd2243c | ||
|
|
04ac87eb50 | ||
|
|
54c4d32764 | ||
|
|
2795175c3c | ||
|
|
bf03693a2f | ||
|
|
a03c22a771 | ||
|
|
7f1efb2ac5 | ||
|
|
f77baa8501 | ||
|
|
c543672ba3 | ||
|
|
b518a76831 | ||
|
|
19df135c7c | ||
|
|
b86f682c84 | ||
|
|
e54e4b5274 | ||
|
|
8379e1c7a9 | ||
|
|
d0b7ca5740 | ||
|
|
739d9051c6 | ||
|
|
6dbc7a380f | ||
|
|
6df49a44ad | ||
|
|
b08a053092 | ||
|
|
b672abba88 | ||
|
|
9ba7b1059e | ||
|
|
219a423330 | ||
|
|
1b0338bbe8 | ||
|
|
669abde081 | ||
|
|
f927cf24ec | ||
|
|
1da9b0bb79 | ||
|
|
1c937fc03b | ||
|
|
896223985f | ||
|
|
a2842fd1af | ||
|
|
ad70c01c14 | ||
|
|
1aca359098 | ||
|
|
33e0eddee6 | ||
|
|
e705aaa1b2 | ||
|
|
e1fc5517f5 | ||
|
|
dad02cda33 | ||
|
|
1dd3bbfdf3 | ||
|
|
1f809a4e29 | ||
|
|
b7c1193f72 | ||
|
|
6c0e8a9b3a | ||
|
|
6d83cc1e70 | ||
|
|
18a325658a | ||
|
|
f19ceaf16a | ||
|
|
3ee642bc14 | ||
|
|
7575b91723 | ||
|
|
d7657829ed | ||
|
|
56a30aedcc | ||
|
|
5ad6594514 | ||
|
|
578b4ef80b | ||
|
|
012600a67d | ||
|
|
42d31ecbfe | ||
|
|
a20f2856f6 | ||
|
|
ad2ff8cad8 | ||
|
|
5427cc96ec | ||
|
|
5fd8fbbc44 | ||
|
|
aa57dc25ff | ||
|
|
486a20ee7b | ||
|
|
4baacdf0ae | ||
|
|
2d701bc25e | ||
|
|
c8c7dedacd | ||
|
|
358730e8cc | ||
|
|
f649a05442 | ||
|
|
f036581d0f | ||
|
|
63fbea1023 | ||
|
|
8127a2b236 | ||
|
|
bcf6bb92f0 | ||
|
|
da342447e9 | ||
|
|
ad523fb15c | ||
|
|
089b005377 | ||
|
|
a08e644a50 | ||
|
|
d5bda3045b | ||
|
|
759be82f70 | ||
|
|
47da41c3d7 | ||
|
|
3657682935 | ||
|
|
4b0b33e3af | ||
|
|
d2f42e0563 | ||
|
|
8a40924a88 | ||
|
|
82b99e9b13 | ||
|
|
dfa6c96115 | ||
|
|
52a39efa8d | ||
|
|
47ee8c5446 | ||
|
|
2244750d13 | ||
|
|
43434ba31d | ||
|
|
9b0a22e0f8 | ||
|
|
08776859c6 | ||
|
|
6787ebb984 | ||
|
|
b5a554591b | ||
|
|
dbe6f14988 | ||
|
|
326496de4f | ||
|
|
55033290f3 | ||
|
|
5f75128234 | ||
|
|
545c6599e8 | ||
|
|
e63e4ea44b | ||
|
|
01bb935910 | ||
|
|
c3b146a0f9 | ||
|
|
47ee5c47b2 | ||
|
|
34ddf69750 | ||
|
|
9aae1e71e9 | ||
|
|
dd9699f86d | ||
|
|
726824dcae | ||
|
|
9a56a26ca4 | ||
|
|
62d3ba9fe2 | ||
|
|
b002c632f2 | ||
|
|
e5ec6d59e8 | ||
|
|
b0c6913f6d | ||
|
|
497a048b59 | ||
|
|
0674be7b64 | ||
|
|
4f6a02c8b8 | ||
|
|
74a7960af8 | ||
|
|
ecf8b8ebe9 | ||
|
|
c66887c2a6 | ||
|
|
793e17baf1 | ||
|
|
7332244baa | ||
|
|
c33845134a | ||
|
|
837b8ad7c9 | ||
|
|
eb107de764 | ||
|
|
2f2ee31aaf | ||
|
|
12d96982b4 | ||
|
|
9555228daf | ||
|
|
a4065c51b4 | ||
|
|
3e624ded2f | ||
|
|
1629cad256 | ||
|
|
da369e3961 | ||
|
|
f416aed9f8 | ||
|
|
62b4ecfc43 | ||
|
|
d3c8976b11 | ||
|
|
b460b34bde | ||
|
|
72939b31b9 | ||
|
|
f8e0270237 | ||
|
|
dfb9d59d65 | ||
|
|
7b68650e78 | ||
|
|
8d3bf9dc41 | ||
|
|
78cb3986fd | ||
|
|
7efba77c78 | ||
|
|
382925bf83 | ||
|
|
f88d25d848 | ||
|
|
e6d468ee24 | ||
|
|
4980d60b33 | ||
|
|
ca9fb0f65a | ||
|
|
e2231b3504 | ||
|
|
4ccba83dd3 | ||
|
|
7622f70025 | ||
|
|
d6e7a728b3 | ||
|
|
c1f2fff176 | ||
|
|
6cfe330976 | ||
|
|
dff033e08a | ||
|
|
d5035bd27b | ||
|
|
dcd7fc4220 | ||
|
|
bf563260a6 | ||
|
|
1f57c01b5b | ||
|
|
7354e8d961 | ||
|
|
ded8e300b7 | ||
|
|
4e7652188a | ||
|
|
316be98428 | ||
|
|
3fd22448d3 | ||
|
|
fcd05ac70e | ||
|
|
f4fd2c516f | ||
|
|
42f58b47eb | ||
|
|
2184d4d7e8 | ||
|
|
fffbd81c80 | ||
|
|
d9d46bd662 | ||
|
|
7c0ec8ede2 | ||
|
|
d549e5826a | ||
|
|
55318811fe | ||
|
|
3bdaba968d | ||
|
|
f39a3d80cb | ||
|
|
29d0f02842 | ||
|
|
159272ac74 | ||
|
|
f92cbe9713 | ||
|
|
8bc2e3daa3 | ||
|
|
9ef3b3a1cb | ||
|
|
c5a73df517 | ||
|
|
5a522cda87 | ||
|
|
37ea6da3b2 | ||
|
|
80bac308ea | ||
|
|
184a205c33 | ||
|
|
51522ed6a1 | ||
|
|
6a7b9c381a | ||
|
|
02306b97a8 | ||
|
|
36ccc67eae | ||
|
|
90ecbde180 | ||
|
|
60d441a5e4 | ||
|
|
053e27e732 | ||
|
|
587c9c97bd | ||
|
|
a220d97048 | ||
|
|
053e973b7c | ||
|
|
08c5c6e2c5 | ||
|
|
85b6103688 | ||
|
|
2ad7246a27 | ||
|
|
4a02468b56 | ||
|
|
e02a220275 | ||
|
|
efa8df2dcd | ||
|
|
1f66ccc710 | ||
|
|
54f8bfa3fe | ||
|
|
8da28a90fd | ||
|
|
e123a92976 | ||
|
|
b9f7e3bde6 | ||
|
|
f49bb4f431 | ||
|
|
3357ba3f0d | ||
|
|
5bc4223984 | ||
|
|
d42afed9b9 | ||
|
|
d5b57bbabc | ||
|
|
c62d864249 | ||
|
|
a26b29ddec | ||
|
|
9299f711ff | ||
|
|
d952cd280f | ||
|
|
0942af46bf | ||
|
|
b61ca20c8b | ||
|
|
989cb9b8d4 | ||
|
|
b5ee3d38a3 | ||
|
|
017945f484 | ||
|
|
ce532ca9d8 | ||
|
|
449fda1052 | ||
|
|
eaa1667821 | ||
|
|
4fc2fa1be3 | ||
|
|
005ef438c9 | ||
|
|
b46b0c3fe7 | ||
|
|
aefd86e88c | ||
|
|
161495ed7d | ||
|
|
198ba8c9ee | ||
|
|
c5dd2f300d | ||
|
|
05ba1675b8 | ||
|
|
f09dc76c61 | ||
|
|
24063ef70d | ||
|
|
14a0c5d9f2 | ||
|
|
90fbfeb525 | ||
|
|
46d417f167 | ||
|
|
6946a19f94 | ||
|
|
6bc0a18b0e | ||
|
|
3713ad7d4f | ||
|
|
f1afd24d12 | ||
|
|
c0f464c13c | ||
|
|
403167c886 | ||
|
|
ca4ed26b92 | ||
|
|
f93d8bb3c0 | ||
|
|
f13bd5a0a4 | ||
|
|
18acd5a319 | ||
|
|
06aa8cab28 | ||
|
|
eafc2df48c | ||
|
|
17fce1bea5 | ||
|
|
d6850726d4 | ||
|
|
8934deabd9 | ||
|
|
5c117c45f6 | ||
|
|
24f44b41f2 | ||
|
|
ac80af3d7f | ||
|
|
1dcfb7525e | ||
|
|
5df1ec1cf8 | ||
|
|
e7fc9689b2 | ||
|
|
f56cf9ff70 | ||
|
|
5e8f0d4f56 | ||
|
|
13799ecc2f | ||
|
|
2b3cc6bede | ||
|
|
5fe047bc20 | ||
|
|
5a4ae78caf | ||
|
|
8dadd4b8db | ||
|
|
f30559bc63 | ||
|
|
d7ca76cc5c | ||
|
|
fda9e9866d | ||
|
|
7e3e38ee18 | ||
|
|
7eb5f953d7 | ||
|
|
c5e75568d4 | ||
|
|
3121730102 | ||
|
|
fe74efde71 | ||
|
|
d1b59367bd | ||
|
|
6b9f5d04fe | ||
|
|
a5c9836f9e | ||
|
|
8a3a344800 | ||
|
|
9d9ca714d6 | ||
|
|
8759d0819f | ||
|
|
a5aee1ae17 | ||
|
|
d42a310ec8 | ||
|
|
4d48f299ee | ||
|
|
0320cceee7 | ||
|
|
59cef2fe49 | ||
|
|
aa8295779a | ||
|
|
1ef4c086b3 | ||
|
|
cb71913a80 | ||
|
|
1c47d97976 | ||
|
|
07e96f122d | ||
|
|
04bfffee6c | ||
|
|
1a00152526 | ||
|
|
5dc9cfa0a1 | ||
|
|
ec28f3f47c | ||
|
|
c9b808ddf2 | ||
|
|
5c3ddaccab | ||
|
|
1ee1555a11 | ||
|
|
08e8fd8c37 | ||
|
|
a3400a3b1c | ||
|
|
c424ddd68c | ||
|
|
3dfaa9c738 | ||
|
|
60a7638d0a | ||
|
|
6e0efc92ee | ||
|
|
d1f6a5b9d7 | ||
|
|
36aadeb0f5 | ||
|
|
4a753ab0e1 | ||
|
|
7338411a7d | ||
|
|
be78527707 | ||
|
|
82e3c0a521 | ||
|
|
3f202f4d53 | ||
|
|
27d620f7c1 | ||
|
|
080138196a | ||
|
|
11b373f49e | ||
|
|
d34831dbd6 | ||
|
|
9c89e0cf2b | ||
|
|
0b4c26f31b | ||
|
|
c5092e6596 | ||
|
|
c38be83e4b | ||
|
|
91e509d3c9 | ||
|
|
3ee440ea4d | ||
|
|
a23cb54e45 | ||
|
|
6a361b46e8 | ||
|
|
ce7175cbaa | ||
|
|
257ac16e94 | ||
|
|
c3df1e7eca | ||
|
|
7af62d7464 | ||
|
|
8c540c2ce7 | ||
|
|
9c85fd1025 | ||
|
|
8a1e13051a | ||
|
|
0a8d85be41 | ||
|
|
7c76097e7c | ||
|
|
3fc4abc10f | ||
|
|
5d02154c55 | ||
|
|
de9d622e6e | ||
|
|
be813b2b60 | ||
|
|
e2ad03b121 | ||
|
|
adfab11eb6 | ||
|
|
38d9665baf | ||
|
|
5257f076ee | ||
|
|
439d8bc0a4 | ||
|
|
5a4096c4a2 | ||
|
|
d0a54901a1 | ||
|
|
45fafb02f2 | ||
|
|
0188c915a7 | ||
|
|
606891dbbd | ||
|
|
0c41f3f140 | ||
|
|
e2024fb401 | ||
|
|
87492f5af7 | ||
|
|
65a776cbd6 | ||
|
|
00a94f6a42 | ||
|
|
de3919e6a1 | ||
|
|
0d254ef212 | ||
|
|
22cd7c3fe5 | ||
|
|
0056221a5d | ||
|
|
8dd72ff546 | ||
|
|
121f2fe3b9 | ||
|
|
ff84eb373c | ||
|
|
28c0c56b69 | ||
|
|
559fa7158c | ||
|
|
01c5b7c340 | ||
|
|
dc9ab679aa | ||
|
|
d8c9ad0e0b | ||
|
|
94e736a13c | ||
|
|
53bd3a19a9 | ||
|
|
94e2ddb937 | ||
|
|
52b77a809b | ||
|
|
bba5ef0345 | ||
|
|
1fa85d9275 | ||
|
|
1de0422b18 | ||
|
|
a63de21e73 | ||
|
|
7b644cf84c | ||
|
|
5501fd8c16 | ||
|
|
baf5c67cc2 | ||
|
|
915dd596e9 | ||
|
|
433595c1da | ||
|
|
70e92a96ed | ||
|
|
63d9ab2cba | ||
|
|
642493c965 | ||
|
|
e6df0f93f0 | ||
|
|
e0d2361aab | ||
|
|
6ab4d976e5 | ||
|
|
322b8362b9 | ||
|
|
44ead0f988 | ||
|
|
91064dd11b | ||
|
|
0cc9da74ef | ||
|
|
9d96248834 | ||
|
|
0f8df54e57 | ||
|
|
4bee5ecd76 | ||
|
|
8b0649460f | ||
|
|
3fc263f655 | ||
|
|
92e327baeb | ||
|
|
70a7f7ad0c | ||
|
|
371f16fac9 | ||
|
|
059a737938 | ||
|
|
df5d045e36 | ||
|
|
42d9fb6bd6 | ||
|
|
164fe205de | ||
|
|
e72dbcc3e1 | ||
|
|
bf7cf84d08 | ||
|
|
fd574f4f84 | ||
|
|
a0c1f03441 | ||
|
|
1111aea461 | ||
|
|
20f1e1cdfe | ||
|
|
0a682b4a8b | ||
|
|
2e9fa45d48 | ||
|
|
290ebe01a1 | ||
|
|
8a84d5a5a3 | ||
|
|
89ca14b0b4 |
5
.devcontainer/devcontainer.json
Normal file
5
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
}
|
||||
}
|
||||
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
README.md
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -34,4 +34,4 @@ body:
|
||||
id: screens
|
||||
attributes:
|
||||
label: Relevant screenshots (optional)
|
||||
description: Please upload any screenshots that may help us reproduce and/or understand the issue.
|
||||
description: Please upload any screenshots that may help us reproduce and/or understand the issue.
|
||||
|
||||
37
.github/workflows/ci.yml
vendored
Normal file
37
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Go Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- 'patterns/**'
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- 'patterns/**'
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Check Formatting
|
||||
run: nix flake check
|
||||
33
.github/workflows/patterns.yaml
vendored
Normal file
33
.github/workflows/patterns.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Patterns Artifact
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "patterns/**" # Trigger only on changes to files in the patterns folder
|
||||
|
||||
jobs:
|
||||
zip-and-upload:
|
||||
name: Zip and Upload Patterns Folder
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Verify Changes in Patterns Folder
|
||||
run: |
|
||||
git fetch origin
|
||||
if git diff --quiet HEAD~1 -- patterns; then
|
||||
echo "No changes detected in patterns folder."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Zip the Patterns Folder
|
||||
run: zip -r patterns.zip patterns/
|
||||
|
||||
- name: Upload Patterns Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patterns
|
||||
path: patterns.zip
|
||||
128
.github/workflows/release.yml
vendored
Normal file
128
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
name: Go Release
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [ tag_created ]
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
build:
|
||||
name: Build binaries for Windows, macOS, and Linux
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
arch: [amd64, arm64]
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Determine OS Name
|
||||
id: os-name
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||
echo "OS=linux" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
echo "OS=darwin" >> $GITHUB_ENV
|
||||
else
|
||||
echo "OS=windows" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build binary on Linux and macOS
|
||||
if: matrix.os != 'windows-latest'
|
||||
env:
|
||||
GOOS: ${{ env.OS }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
go build -o fabric-${OS}-${{ matrix.arch }} .
|
||||
|
||||
- name: Build binary on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
go build -o fabric-windows-${{ matrix.arch }}.exe .
|
||||
|
||||
- name: Upload build artifact
|
||||
if: matrix.os != 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fabric-${OS}-${{ matrix.arch }}
|
||||
path: fabric-${OS}-${{ matrix.arch }}
|
||||
|
||||
- name: Upload build artifact
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fabric-windows-${{ matrix.arch }}.exe
|
||||
path: fabric-windows-${{ matrix.arch }}.exe
|
||||
|
||||
- name: Get latest tag
|
||||
if: matrix.os != 'windows-latest'
|
||||
id: get_latest_tag
|
||||
run: |
|
||||
latest_tag=$(git tag --sort=-creatordate | head -n 1)
|
||||
echo "latest_tag=$latest_tag" >> $GITHUB_ENV
|
||||
|
||||
- name: Get latest tag
|
||||
if: matrix.os == 'windows-latest'
|
||||
id: get_latest_tag_windows
|
||||
run: |
|
||||
$latest_tag = git tag --sort=-creatordate | Select-Object -First 1
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "latest_tag=$latest_tag"
|
||||
|
||||
- name: Create release if it doesn't exist
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release view ${{ env.latest_tag }} || gh release create ${{ env.latest_tag }} --title "Release ${{ env.latest_tag }}" --notes "Automated release for ${{ env.latest_tag }}"
|
||||
|
||||
- name: Upload release artifact
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ env.latest_tag }} fabric-windows-${{ matrix.arch }}.exe
|
||||
|
||||
- name: Upload release artifact
|
||||
if: matrix.os != 'windows-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ env.latest_tag }} fabric-${OS}-${{ matrix.arch }}
|
||||
106
.github/workflows/update-version-and-create-tag.yml
vendored
Normal file
106
.github/workflows/update-version-and-create-tag.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Update Version File and Create Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Monitor the main branch
|
||||
paths-ignore:
|
||||
- 'patterns/**'
|
||||
- '**/*.md'
|
||||
|
||||
permissions:
|
||||
contents: write # Ensure the workflow has write permissions
|
||||
|
||||
jobs:
|
||||
update-version:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Get the latest tag
|
||||
id: get_latest_tag
|
||||
run: |
|
||||
latest_tag=$(git tag --sort=-creatordate | head -n 1)
|
||||
echo "Latest tag is: $latest_tag"
|
||||
echo "tag=$latest_tag" >> $GITHUB_ENV # Save the latest tag to environment file
|
||||
|
||||
- name: Increment patch version
|
||||
id: increment_version
|
||||
run: |
|
||||
latest_tag=${{ env.tag }}
|
||||
major=$(echo "$latest_tag" | cut -d. -f1 | sed 's/v//')
|
||||
minor=$(echo "$latest_tag" | cut -d. -f2)
|
||||
patch=$(echo "$latest_tag" | cut -d. -f3)
|
||||
new_patch=$((patch + 1))
|
||||
new_version="${major}.${minor}.${new_patch}"
|
||||
new_tag="v${new_version}"
|
||||
echo "New version is: $new_version"
|
||||
echo "new_version=$new_version" >> $GITHUB_ENV # Save the new version to environment file
|
||||
echo "New tag is: $new_tag"
|
||||
echo "new_tag=$new_tag" >> $GITHUB_ENV # Save the new tag to environment file
|
||||
|
||||
- name: Update version.go file
|
||||
run: |
|
||||
echo "package main" > version.go
|
||||
echo "" >> version.go
|
||||
echo "var version = \"${{ env.new_tag }}\"" >> version.go
|
||||
|
||||
- name: Update version.nix file
|
||||
run: |
|
||||
echo "\"${{ env.new_version }}\"" > nix/pkgs/fabric/version.nix
|
||||
|
||||
- name: Format source code
|
||||
run: |
|
||||
nix fmt
|
||||
|
||||
- name: Update gomod2nix.toml file
|
||||
run: |
|
||||
nix run .#gomod2nix -- --outdir nix/pkgs/fabric
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git add version.go
|
||||
git add nix/pkgs/fabric/version.nix
|
||||
git add nix/pkgs/fabric/gomod2nix.toml
|
||||
git add .
|
||||
if ! git diff --staged --quiet; then
|
||||
git commit -m "Update version to ${{ env.new_tag }} and commit $commit_hash"
|
||||
else
|
||||
echo "No changes to commit."
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN to authenticate the push
|
||||
run: |
|
||||
git push origin main # Push changes to the main branch
|
||||
|
||||
- name: Create a new tag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.TAG_PAT }}
|
||||
run: |
|
||||
git tag ${{ env.new_tag }}
|
||||
git push origin ${{ env.new_tag }} # Push the new tag
|
||||
|
||||
- name: Dispatch event to trigger release workflow
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN to authenticate the dispatch
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
https://api.github.com/repos/${{ github.repository }}/dispatches \
|
||||
-d '{"event_type": "tag_created", "client_payload": {"tag": "${{ env.new_tag }}"}}'
|
||||
194
.gitignore
vendored
194
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
# Nix
|
||||
.direnv
|
||||
result
|
||||
|
||||
# macOS local stores
|
||||
.DS_Store
|
||||
|
||||
@@ -11,13 +15,14 @@ __pycache__/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.idea
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
@@ -53,6 +58,7 @@ coverage.xml
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
coverage.out
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@@ -160,4 +166,188 @@ cython_debug/
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
#.idea/
|
||||
|
||||
patterns/dialog_with_socrates/Apology by Plato.txt
|
||||
patterns/dialog_with_socrates/Phaedrus by Plato.txt
|
||||
patterns/dialog_with_socrates/Symposium by Plato.txt
|
||||
patterns/dialog_with_socrates/The Economist by Xenophon.txt
|
||||
patterns/dialog_with_socrates/The Memorabilia by Xenophon.txt
|
||||
patterns/dialog_with_socrates/The Memorable Thoughts of Socrates by Xenophon.txt
|
||||
patterns/dialog_with_socrates/The Republic by Plato.txt
|
||||
patterns/dialog_with_socrates/The Symposium by Xenophon.txt
|
||||
|
||||
web/node_modules
|
||||
|
||||
# Output
|
||||
web/.output
|
||||
web/.vercel
|
||||
web/.svelte-kit
|
||||
web/build
|
||||
|
||||
# OS
|
||||
web/.DS_Store
|
||||
web/Thumbs.db
|
||||
|
||||
# Env
|
||||
web/.env
|
||||
web/.env.*
|
||||
web/!.env.example
|
||||
web/!.env.test
|
||||
|
||||
# Vite
|
||||
web/vite.config.js.timestamp-*
|
||||
web/vite.config.ts.timestamp-*
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
web/logs
|
||||
web/*.log
|
||||
web/npm-debug.log*
|
||||
web/yarn-debug.log*
|
||||
web/yarn-error.log*
|
||||
web/lerna-debug.log*
|
||||
web/.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
web/report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
web/pids
|
||||
web/*.pid
|
||||
web/*.seed
|
||||
web/*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
web/lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
web/coverage
|
||||
web/*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
web/.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
web/.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
web/bower_components
|
||||
|
||||
# node-waf configuration
|
||||
web/.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
web/node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web/web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
web/.npm
|
||||
|
||||
# Optional eslint cache
|
||||
web/.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
web/.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
web/.rpt2_cache/
|
||||
web/.rts2_cache_cjs/
|
||||
web/.rts2_cache_es/
|
||||
web/.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
web/.env
|
||||
web/.env.development.local
|
||||
web/.env.test.local
|
||||
web/.env.production.local
|
||||
web/.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
web/.next
|
||||
web/out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
web/.nuxt
|
||||
web/dist
|
||||
|
||||
# Gatsby files
|
||||
web/.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
web/.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
web/.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
web/.vscode-test
|
||||
|
||||
# yarn v2
|
||||
web/.yarn/cache
|
||||
web/.yarn/unplugged
|
||||
web/.yarn/build-state.yml
|
||||
web/.yarn/install-state.gz
|
||||
web/.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
web/.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
web/.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
||||
web/myfiles/Obsidian_perso_not_share/
|
||||
ENV
|
||||
web/package-lock.json
|
||||
.gitignore_backup
|
||||
web/static/*.png
|
||||
|
||||
# Local VSCode project settings
|
||||
.vscode/
|
||||
|
||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# Use official golang image as builder
|
||||
FROM golang:1.24.2-alpine AS builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod and sum files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o fabric
|
||||
|
||||
# Use scratch as final base image
|
||||
FROM alpine:latest
|
||||
|
||||
# Copy the binary from builder
|
||||
COPY --from=builder /app/fabric /fabric
|
||||
|
||||
# Copy patterns directory
|
||||
COPY patterns /patterns
|
||||
|
||||
# Ensure clean config directory and copy ENV file
|
||||
RUN rm -rf /root/.config/fabric && \
|
||||
mkdir -p /root/.config/fabric
|
||||
COPY ENV /root/.config/fabric/.env
|
||||
|
||||
# Add debug commands
|
||||
RUN ls -la /root/.config/fabric/
|
||||
|
||||
# Expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the binary with debug output
|
||||
ENTRYPOINT ["/fabric"]
|
||||
CMD ["--serve"]
|
||||
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
33
NOTES.md
Normal file
33
NOTES.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## Notes on some refactoring.
|
||||
|
||||
- The goal is to bring more encapsulation of the models management and simplified configuration management to bring increased flexibility, transparency on the overall flow, and simplicity in adding new model.
|
||||
- We need to differentiate:
|
||||
- Vendors: the producer of models (like OpenAI, Azure, Anthropric, Ollama, ..etc) and their associated APIs
|
||||
- Models: the LLM models these vendors are making public
|
||||
- Each vendor and operations allowed by the vendor needs to be encapsulated. This includes:
|
||||
- The questions needed to setup the model (like the API key, or the URL)
|
||||
- The listing of all models supported by the vendor
|
||||
- The actions performed with a given model
|
||||
|
||||
- The configuration flow works like this for an **initial** call:
|
||||
- The available vendors are called one by one, each of them being responsible for the data they collect. They return a set of environment variables under the form of a list of strings, or an empty list if the user does not want to setup this vendor. As we do not want each vendor to know which way the data they need will be collected (e.g., read from the command line, or a GUI), they will be asked for a list of questions, the configuration will inquire the user, and send back the questions with the collected answers to the Vendor. The Vendor is then either instantiating an instance (Vendor configured) and returning it, or returning `nil` if the Vendor should not be set up.
|
||||
- the `.env` file is created, using the information returned by the vendors
|
||||
- A list of patterns is downloaded from the main site
|
||||
|
||||
- When the system is configured, the configuration flows:
|
||||
- Read the `.env` file using the godotenv library
|
||||
- It configures a structure that contains the various vendors selected as well as the preferred model. This structure will be completed with some of the command line values (i.e, context, session, etc..)
|
||||
|
||||
- To get the list of all supported models:
|
||||
- Each configured model (part of the configuration structure) is asked, using a goroutine, to return the list of model
|
||||
|
||||
- Order when building message: session + context + pattern + user input (role "user)
|
||||
|
||||
|
||||
## TODO:
|
||||
- Check if we need to read the system.md for every patterns when running the ListAllPatterns
|
||||
- Context management seems more complex than the one in the original fabric. Probably needs some work (at least to make it clear how it works)
|
||||
- models on command line: give as well vendor (like `--model openai/gpt-4o`). If the vendor is not given, get it by retrieving all possible models and searching from that.
|
||||
- if user gives the ollama url on command line, we need to update/init an ollama vendor.
|
||||
- The db should host only things related to access and storage in ~/.config/fabric
|
||||
- The interaction part of the Setup function should be in the cli (and perhaps all the Setup)
|
||||
124
Pattern_Descriptions/README_Pattern_Descriptions_and_Tags_MGT.md
Normal file
124
Pattern_Descriptions/README_Pattern_Descriptions_and_Tags_MGT.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Pattern Descriptions and Tags Management
|
||||
|
||||
This document explains the complete workflow for managing pattern descriptions and tags, including how to process new patterns and maintain metadata.
|
||||
|
||||
## System Overview
|
||||
|
||||
The pattern system follows this hierarchy:
|
||||
1. `~/.config/fabric/patterns/` directory: The source of truth for available patterns
|
||||
2. `pattern_extracts.json`: Contains first 500 words of each pattern for reference
|
||||
3. `pattern_descriptions.json`: Stores pattern metadata (descriptions and tags)
|
||||
4. `web/static/data/pattern_descriptions.json`: Web-accessible copy for the interface
|
||||
|
||||
## Pattern Processing Workflow
|
||||
|
||||
### 1. Adding New Patterns
|
||||
- Add patterns to `~/.config/fabric/patterns/`
|
||||
- Run extract_patterns.py to process new additions:
|
||||
```bash
|
||||
python extract_patterns.py
|
||||
|
||||
The Python Script automatically:
|
||||
- Creates pattern extracts for reference
|
||||
- Adds placeholder entries in descriptions file
|
||||
- Syncs to web interface
|
||||
|
||||
### 2. Pattern Extract Creation
|
||||
The script extracts first 500 words from each pattern's system.md file to:
|
||||
|
||||
- Provide context for writing descriptions
|
||||
- Maintain reference material
|
||||
- Aid in pattern categorization
|
||||
|
||||
### 3. Description and Tag Management
|
||||
Pattern descriptions and tags are managed in pattern_descriptions.json:
|
||||
|
||||
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"patternName": "pattern_name",
|
||||
"description": "[Description pending]",
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
## Completing Pattern Metadata
|
||||
|
||||
### Writing Descriptions
|
||||
1. Check pattern_descriptions.json for "[Description pending]" entries
|
||||
2. Reference pattern_extracts.json for context
|
||||
|
||||
3. How to update Pattern short descriptions (one sentence).
|
||||
|
||||
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (prefered approach).
|
||||
|
||||
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
|
||||
|
||||
### Managing Tags
|
||||
1. Add appropriate tags to new patterns
|
||||
2. Update existing tags as needed
|
||||
3. Tags are stored as arrays: ["TAG1", "TAG2"]
|
||||
4. Edit pattern_descriptions.json directly to modify tags
|
||||
5. Make tags your own. You can delete, replace, amend existing tags.
|
||||
|
||||
## File Synchronization
|
||||
|
||||
The script maintains synchronization between:
|
||||
- Local pattern_descriptions.json
|
||||
- Web interface copy in static/data/
|
||||
- No manual file copying needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Run extract_patterns.py when:
|
||||
- Adding new patterns
|
||||
- Updating existing patterns
|
||||
- Modifying pattern structure
|
||||
|
||||
2. Description Writing:
|
||||
- Use pattern extracts for context
|
||||
- Keep descriptions clear and concise
|
||||
- Focus on pattern purpose and usage
|
||||
|
||||
3. Tag Management:
|
||||
- Use consistent tag categories
|
||||
- Apply multiple tags when relevant
|
||||
- Update tags to reflect pattern evolution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If patterns are not showing in the web interface:
|
||||
1. Verify pattern_descriptions.json format
|
||||
2. Check web static copy exists
|
||||
3. Ensure proper file permissions
|
||||
4. Run extract_patterns.py to resync
|
||||
|
||||
## File Structure
|
||||
|
||||
fabric/
|
||||
├── patterns/ # Pattern source files
|
||||
├── PATTERN_DESCRIPTIONS/
|
||||
│ ├── extract_patterns.py # Pattern processing script
|
||||
│ ├── pattern_extracts.json # Pattern content references
|
||||
│ └── pattern_descriptions.json # Pattern metadata
|
||||
└── web/
|
||||
└── static/
|
||||
└── data/
|
||||
└── pattern_descriptions.json # Web interface copy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
114
Pattern_Descriptions/extract_patterns.py
Normal file
114
Pattern_Descriptions/extract_patterns.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
|
||||
def load_existing_file(filepath):
|
||||
"""Load existing JSON file or return default structure"""
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {"patterns": []}
|
||||
|
||||
def get_pattern_extract(pattern_path):
|
||||
"""Extract first 500 words from pattern's system.md file"""
|
||||
system_md_path = os.path.join(pattern_path, "system.md")
|
||||
with open(system_md_path, 'r', encoding='utf-8') as f:
|
||||
content = ' '.join(f.read().split()[:500])
|
||||
return content
|
||||
|
||||
def extract_pattern_info():
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
patterns_dir = os.path.expanduser("~/.config/fabric/patterns")
|
||||
print(f"\nScanning patterns directory: {patterns_dir}")
|
||||
|
||||
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
|
||||
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
|
||||
|
||||
existing_extracts = load_existing_file(extracts_path)
|
||||
existing_descriptions = load_existing_file(descriptions_path)
|
||||
|
||||
existing_extract_names = {p["patternName"] for p in existing_extracts["patterns"]}
|
||||
existing_description_names = {p["patternName"] for p in existing_descriptions["patterns"]}
|
||||
print(f"Found existing patterns: {len(existing_extract_names)}")
|
||||
|
||||
new_extracts = []
|
||||
new_descriptions = []
|
||||
|
||||
|
||||
for dirname in sorted(os.listdir(patterns_dir)):
|
||||
# Only log new pattern processing
|
||||
if dirname not in existing_extract_names:
|
||||
print(f"Processing new pattern: {dirname}")
|
||||
|
||||
|
||||
pattern_path = os.path.join(patterns_dir, dirname)
|
||||
system_md_path = os.path.join(pattern_path, "system.md")
|
||||
print(f"Checking system.md at: {system_md_path}")
|
||||
|
||||
if os.path.isdir(pattern_path) and os.path.exists(system_md_path):
|
||||
print(f"Valid pattern directory found: {dirname}")
|
||||
try:
|
||||
if dirname not in existing_extract_names:
|
||||
print(f"Creating new extract for: {dirname}")
|
||||
pattern_extract = get_pattern_extract(pattern_path) # Pass directory path
|
||||
new_extracts.append({
|
||||
"patternName": dirname,
|
||||
"pattern_extract": pattern_extract
|
||||
})
|
||||
|
||||
if dirname not in existing_description_names:
|
||||
print(f"Creating new description for: {dirname}")
|
||||
new_descriptions.append({
|
||||
"patternName": dirname,
|
||||
"description": "[Description pending]",
|
||||
"tags": []
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {dirname}: {str(e)}")
|
||||
else:
|
||||
print(f"Invalid pattern directory or missing system.md: {dirname}")
|
||||
|
||||
print(f"\nProcessing summary:")
|
||||
print(f"New extracts created: {len(new_extracts)}")
|
||||
print(f"New descriptions added: {len(new_descriptions)}")
|
||||
|
||||
existing_extracts["patterns"].extend(new_extracts)
|
||||
existing_descriptions["patterns"].extend(new_descriptions)
|
||||
|
||||
return existing_extracts, existing_descriptions, len(new_descriptions)
|
||||
|
||||
|
||||
def update_web_static(descriptions_path):
|
||||
"""Copy pattern descriptions to web static directory"""
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
static_dir = os.path.join(script_dir, "..", "web", "static", "data")
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
static_path = os.path.join(static_dir, "pattern_descriptions.json")
|
||||
shutil.copy2(descriptions_path, static_path)
|
||||
|
||||
def save_pattern_files():
|
||||
"""Save both pattern files and sync to web"""
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
|
||||
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
|
||||
|
||||
pattern_extracts, pattern_descriptions, new_count = extract_pattern_info()
|
||||
|
||||
# Save files
|
||||
with open(extracts_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(pattern_extracts, f, indent=2, ensure_ascii=False)
|
||||
|
||||
with open(descriptions_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(pattern_descriptions, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Update web static
|
||||
update_web_static(descriptions_path)
|
||||
|
||||
print(f"\nProcessing complete:")
|
||||
print(f"Total patterns: {len(pattern_descriptions['patterns'])}")
|
||||
print(f"New patterns added: {new_count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
save_pattern_files()
|
||||
|
||||
1779
Pattern_Descriptions/pattern_descriptions.json
Normal file
1779
Pattern_Descriptions/pattern_descriptions.json
Normal file
File diff suppressed because it is too large
Load Diff
868
Pattern_Descriptions/pattern_extracts.json
Normal file
868
Pattern_Descriptions/pattern_extracts.json
Normal file
File diff suppressed because one or more lines are too long
215
Web Interface MOD Readme Files/WEB V2 Install Guide.md
Normal file
215
Web Interface MOD Readme Files/WEB V2 Install Guide.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# How to Install the Web Interface and PDF-to-Markdown
|
||||
|
||||
If Fabric is already installed and you see fabric/web, go to step 3
|
||||
|
||||
If fabric is not installed, ensure Go is installed https://go.dev/doc/install and node / npm for web https://nodejs.org/en/download.
|
||||
|
||||
There are many ways to install fabric. Here's one approach that usually works well:
|
||||
|
||||
## Step 1: clone the repo
|
||||
In terminal, from the parent directory where you want to install fabric:
|
||||
git clone https://github.com/danielmiessler/fabric.git
|
||||
|
||||
## Step 2 : Install Fabric
|
||||
cd fabric
|
||||
go install github.com/danielmiessler/fabric@latest
|
||||
|
||||
## Step 3: Install GUI
|
||||
Navigate to the web directory and install dependencies:
|
||||
|
||||
cd web
|
||||
|
||||
npm install
|
||||
|
||||
npx svelte-kit sync
|
||||
|
||||
## Step 4: Install PDF-to-Markdown
|
||||
Install the PDF conversion components in the correct order:
|
||||
cd web
|
||||
# Install dependencies in this specific order
|
||||
|
||||
npm install -D patch-package
|
||||
|
||||
npm install -D pdfjs-dist@2.5.207
|
||||
|
||||
npm install -D github:jzillmann/pdf-to-markdown#modularize
|
||||
|
||||
|
||||
No build step is required after installation.
|
||||
|
||||
## Step 5: Update Shell Configuration if not already done from your fabric installation
|
||||
For Mac/Linux users:
|
||||
|
||||
Add environment variables to your ~/.bashrc (Linux) or ~/.zshrc (Mac) file:
|
||||
|
||||
# For Intel-based Macs or Linux
|
||||
export GOROOT=/usr/local/go
|
||||
export GOPATH=$HOME/go
|
||||
export PATH=$GOPATH/bin:$GOROOT/bin:$HOME/.local/bin:$PATH
|
||||
|
||||
# For Apple Silicon Macs
|
||||
export GOROOT=$(brew --prefix go)/libexec
|
||||
export GOPATH=$HOME/go
|
||||
export PATH=$GOPATH/bin:$GOROOT/bin:$HOME/.local/bin:$PATH
|
||||
|
||||
REFER TO OFFICIAL FABRIC README.MD FILE FOR OTHER OPERATING SYSTEMS
|
||||
|
||||
Step 5: Create Aliases for Patterns
|
||||
Add the following to your .zshrc or .bashrc file to create shorter commands:
|
||||
|
||||
```bash
|
||||
|
||||
# The following three lines of code are path examples, replace with your actual path.
|
||||
|
||||
# Add fabric to PATH
|
||||
export PATH="/Users/USERNAME/Documents/fabric:$PATH"
|
||||
|
||||
# Define the base directory for Obsidian notes
|
||||
obsidian_base="/Users/USERNAME/Documents/fabric/web/myfiles/Fabric_obsidian"
|
||||
|
||||
# Define the patterns directory
|
||||
patterns_dir="/Users/USERNAME/Documents/fabric/patterns"
|
||||
|
||||
|
||||
# Loop through all files in the ~/.config/fabric/patterns directory
|
||||
|
||||
for pattern_file in ~/.config/fabric/patterns/*; do
|
||||
# Get the base name of the file
|
||||
pattern_name=$(basename "$pattern_file")
|
||||
|
||||
# Unalias any existing alias with the same name
|
||||
unalias "$pattern_name" 2>/dev/null
|
||||
|
||||
# Define a function dynamically for each pattern
|
||||
eval "
|
||||
$pattern_name() {
|
||||
local title=\$1
|
||||
local date_stamp=\$(date +'%Y-%m-%d')
|
||||
local output_path=\"\$obsidian_base/\${date_stamp}-\${title}.md\"
|
||||
|
||||
# Check if a title was provided
|
||||
if [ -n \"\$title\" ]; then
|
||||
# If a title is provided, use the output path
|
||||
fabric --pattern \"$pattern_name\" -o \"\$output_path\"
|
||||
else
|
||||
# If no title is provided, use --stream
|
||||
fabric --pattern \"$pattern_name\" --stream
|
||||
fi
|
||||
}
|
||||
"
|
||||
done
|
||||
|
||||
# YouTube shortcut function
|
||||
yt() {
|
||||
local video_link="$1"
|
||||
fabric -y "$video_link" --transcript
|
||||
}
|
||||
|
||||
|
||||
After modifying your shell configuration file, apply the changes:
|
||||
|
||||
source ~/.zshrc # or source ~/.bashrc for Linux
|
||||
|
||||
Step 6: Run Fabric Setup
|
||||
Initialize fabric configuration:
|
||||
|
||||
fabric --setup
|
||||
|
||||
Step 7: Launch the Web Interface
|
||||
Open two terminal windows and navigate to the web folder:
|
||||
|
||||
Terminal 1: Start the Fabric API Server
|
||||
fabric --serve
|
||||
|
||||
Terminal 2: Start the Development Server
|
||||
npm run dev
|
||||
|
||||
|
||||
If you get an ** ERROR **.
|
||||
It would be much appreciated that you copy /paste your error in your favorite LLM before opening a ticket, 90% of the time your llm will point you to the solution.
|
||||
|
||||
Also if you modify patterns, descriptions or tags in Pattern_Descriptions/pattern_descriptions.json, make sure to copy the file over in web/static/data/pattern_descriptions.json
|
||||
|
||||
_____ ______ ______
|
||||
|
||||
OPTIONAL: Create Start/Stop Scripts
|
||||
You can create scripts to start/stop both servers at once.
|
||||
|
||||
### For Mac Users
|
||||
When creating scripts on Mac using TextEdit:
|
||||
|
||||
1. Open TextEdit
|
||||
2. **IMPORTANT:** Select "Format > Make Plain Text" from the menu BEFORE pasting any code
|
||||
3. Paste the script content, follow instructions below ((Mac example)).
|
||||
|
||||
|
||||
### For Windows Users
|
||||
When creating scripts on Windows:
|
||||
|
||||
1. Use Notepad or a code editor like VS Code
|
||||
2. Paste the script content
|
||||
3. Save the file with the appropriate extension
|
||||
4. Ensure line endings are set to LF (not CRLF) for bash scripts
|
||||
|
||||
ACTUAL SCRIPTS (Mac example)
|
||||
|
||||
Start Script
|
||||
1. Create a new file named start-fabric.command on your Desktop:
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Change to the fabric web directory
|
||||
cd "$HOME/Documents/Github/fabric/web"
|
||||
|
||||
# Start fabric serve in the background
|
||||
osascript -e 'tell application "Terminal" to do script "cd '$HOME'/Documents/Github/fabric/web && fabric --serve; exit"'
|
||||
|
||||
# Wait a moment to ensure the fabric server starts
|
||||
sleep 2
|
||||
|
||||
# Start npm development server in a new terminal
|
||||
osascript -e 'tell application "Terminal" to do script "cd '$HOME'/Documents/Github/fabric/web && npm run dev; exit"'
|
||||
|
||||
# Close this script's terminal window after starting servers
|
||||
echo "Fabric servers started!"
|
||||
sleep 1
|
||||
osascript -e 'tell application "Terminal" to close (every window whose name contains ".command")' &
|
||||
exit
|
||||
|
||||
Stop Script
|
||||
|
||||
2. Create a new file named stop-fabric.command on your Desktop:
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Kill the npm dev server
|
||||
pkill -f "node.*dev"
|
||||
|
||||
# Kill the fabric server
|
||||
pkill -f "fabric --serve"
|
||||
|
||||
# Force quit Terminal entirely and restart it
|
||||
osascript <<EOD
|
||||
tell application "Terminal" to quit
|
||||
delay 1
|
||||
tell application "Terminal" to activate
|
||||
EOD
|
||||
|
||||
echo "Fabric servers stopped!"
|
||||
sleep 1
|
||||
|
||||
# This script's terminal will already be closed by the quit command above
|
||||
exit
|
||||
|
||||
3. Make both scripts executable:
|
||||
chmod +x ~/Desktop/start-fabric.command
|
||||
chmod +x ~/Desktop/stop-fabric.command
|
||||
|
||||
You can customize with icons by finding suitable .icns files, right-clicking each .command file, selecting "Get Info", and dragging your icon file onto the small icon in the top-left corner.
|
||||
|
||||
Note: You might need to allow the scripts to execute in your security settings by going to System Preferences → Security & Privacy after trying to run them the first time.
|
||||
|
||||
|
||||
|
||||
## 🎥 Demo Video
|
||||
https://youtu.be/XMzjgqvdltM
|
||||
295
Web Interface MOD Readme Files/pr-1284-update.md
Normal file
295
Web Interface MOD Readme Files/pr-1284-update.md
Normal file
@@ -0,0 +1,295 @@
|
||||
This Cummulative PR adds several Web UI and functionality improvements to make pattern selection more intuitive with the addition of pattern descriptions, ability to save favorite patterns, a Pattern TAG system, powerful multilingual capabilities, PDF-to-markdown functionnalities, a help reference section, more robust Youtube processing and a variety of other ui improvements.
|
||||
|
||||
## 🎥 Demo Video
|
||||
https://youtu.be/XMzjgqvdltM
|
||||
|
||||
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
### 1. Web UI and Pattern Selection Improvements
|
||||
- Pattern Descriptions
|
||||
- Pattern Tags
|
||||
- Pattern Favourites
|
||||
- Pattern Search bar
|
||||
- PDF to markdown (pdf as pattern input)
|
||||
- Better handling of Youtube url
|
||||
- Multilingual Support
|
||||
- Web UI refinements for clearer interaction
|
||||
- Help section via modal
|
||||
|
||||
### 2. Multilingual Support System
|
||||
- Seamless language switching via UI dropdown
|
||||
- Persistent language state management
|
||||
- Pattern processing now use the selected language seamlessly
|
||||
|
||||
### 3. YouTube Integration Enhancement
|
||||
- Robust language handling for YouTube transcript processing
|
||||
- Chunk-based language maintenance for long transcripts
|
||||
- Consistent language output throughout transcript analysis
|
||||
|
||||
### 4. Enhanced Tag Management Integration
|
||||
|
||||
The tag filtering system has been deeply integrated into the Pattern Selection interface through several UI enhancements:
|
||||
|
||||
1. **Dual-Position Tag Panel**
|
||||
- Sliding panel positioned to the right of pattern modal
|
||||
- Dynamic toggle button that adapts position and text based on panel state
|
||||
- Smooth transitions for opening/closing animations
|
||||
|
||||
2. **Tag Selection Visibility**
|
||||
- New dedicated tag display section in pattern modal
|
||||
- Visual separation through subtle background styling
|
||||
- Immediate feedback showing selected tags with comma separation
|
||||
- Inline reset capability for quick tag clearing
|
||||
|
||||
3. **Improved User Experience**
|
||||
- Clear visual hierarchy between pattern list and tag filtering
|
||||
- Multiple ways to manage tags (panel or quick reset)
|
||||
- Consistent styling with existing design language
|
||||
- Space-efficient tag brick layout in 3-column grid
|
||||
|
||||
4. **Technical Implementation**
|
||||
- Reactive tag state management
|
||||
- Efficient tag filtering logic
|
||||
- Proper event dispatching between components
|
||||
- Maintained accessibility standards
|
||||
- Responsive design considerations
|
||||
|
||||
|
||||
5. **PDF to Markdown conversion functionality for the web interface**
|
||||
- Automatic detection and processing of PDF files in chat
|
||||
- Conversion to markdown format for LLM processing
|
||||
- Installation instructions from the pdf-to-markdown repository
|
||||
|
||||
The PDF conversion module has been integrated in the svelte web browser interface. Once installed, it will automatically detect pdf files in the chat interface and convert them to markdown
|
||||
|
||||
|
||||
## HOW TO INSTALL PDF-TO-MARKDOWN
|
||||
If you need to update the web component follow the instructions in "Web Interface MOD Readme Files/WEB V2 Install Guide.md".
|
||||
|
||||
Assuming your web install is up to date and web svelte config complete, you can simply follow these steps to add Pdf-to-mardown.
|
||||
|
||||
# FROM FABRIC ROOT DIRECTORY
|
||||
cd .. web
|
||||
|
||||
# Install in this sequence:
|
||||
# Step 1
|
||||
npm install -D patch-package
|
||||
# Step 2
|
||||
npm install -D pdfjs-dist@2.5.207
|
||||
# Step 3
|
||||
npm install -D github:jzillmann/pdf-to-markdown#modularize
|
||||
|
||||
These enhancements create a more intuitive and efficient pattern discovery experience, allowing users to quickly filter and find relevant patterns while maintaining a clean, modern interface.
|
||||
|
||||
|
||||
## 🛠 Technical Implementation
|
||||
|
||||
### Language Support Architecture
|
||||
```typescript
|
||||
// Language state management
|
||||
export const languageStore = writable<string>('');
|
||||
|
||||
// Chat input language detection
|
||||
if (qualifier === 'fr') {
|
||||
languageStore.set('fr');
|
||||
userInput = userInput.replace(/--fr\s*/, '');
|
||||
}
|
||||
|
||||
// Service layer integration
|
||||
const language = get(languageStore) || 'en';
|
||||
const languageInstruction = language !== 'en'
|
||||
? `. Please use the language '${language}' for the output.`
|
||||
: '';
|
||||
```
|
||||
|
||||
### YouTube Processing Enhancement
|
||||
```typescript
|
||||
// Process stream with language instruction per chunk
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content: string, response?: StreamResponse) => {
|
||||
if (currentLanguage !== 'en') {
|
||||
content = `${content}. Please use the language '${currentLanguage}' for the output.`;
|
||||
}
|
||||
// Update messages...
|
||||
}
|
||||
);
|
||||
```
|
||||
# Pattern Descriptions and Tags Management
|
||||
|
||||
This document explains the complete workflow for managing pattern descriptions and tags, including how to process new patterns and maintain metadata.
|
||||
|
||||
## System Overview
|
||||
|
||||
The pattern system follows this hierarchy:
|
||||
1. `~/.config/fabric/patterns/` directory: The source of truth for available patterns
|
||||
2. `pattern_extracts.json`: Contains first 500 words of each pattern for reference
|
||||
3. `pattern_descriptions.json`: Stores pattern metadata (descriptions and tags)
|
||||
4. `web/static/data/pattern_descriptions.json`: Web-accessible copy for the interface
|
||||
|
||||
## Pattern Processing Workflow
|
||||
|
||||
### 1. Adding New Patterns
|
||||
- Add patterns to `~/.config/fabric/patterns/`
|
||||
- Run extract_patterns.py to process new additions:
|
||||
```bash
|
||||
python extract_patterns.py
|
||||
|
||||
The Python Script automatically:
|
||||
- Creates pattern extracts for reference
|
||||
- Adds placeholder entries in descriptions file
|
||||
- Syncs to web interface
|
||||
|
||||
### 2. Pattern Extract Creation
|
||||
The script extracts first 500 words from each pattern's system.md file to:
|
||||
|
||||
- Provide context for writing descriptions
|
||||
- Maintain reference material
|
||||
- Aid in pattern categorization
|
||||
|
||||
### 3. Description and Tag Management
|
||||
Pattern descriptions and tags are managed in pattern_descriptions.json:
|
||||
|
||||
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"patternName": "pattern_name",
|
||||
"description": "[Description pending]",
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
## Completing Pattern Metadata
|
||||
|
||||
### Writing Descriptions
|
||||
1. Check pattern_descriptions.json for "[Description pending]" entries
|
||||
2. Reference pattern_extracts.json for context
|
||||
|
||||
3. How to update Pattern short descriptions (one sentence).
|
||||
|
||||
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (prefered approach).
|
||||
|
||||
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
|
||||
|
||||
### Managing Tags
|
||||
1. Add appropriate tags to new patterns
|
||||
2. Update existing tags as needed
|
||||
3. Tags are stored as arrays: ["TAG1", "TAG2"]
|
||||
4. Edit pattern_descriptions.json directly to modify tags
|
||||
5. Make tags your own. You can delete, replace, amend existing tags.
|
||||
|
||||
## File Synchronization
|
||||
|
||||
The script maintains synchronization between:
|
||||
- Local pattern_descriptions.json
|
||||
- Web interface copy in static/data/
|
||||
- No manual file copying needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Run extract_patterns.py when:
|
||||
- Adding new patterns
|
||||
- Updating existing patterns
|
||||
- Modifying pattern structure
|
||||
|
||||
2. Description Writing:
|
||||
- Use pattern extracts for context
|
||||
- Keep descriptions clear and concise
|
||||
- Focus on pattern purpose and usage
|
||||
|
||||
3. Tag Management:
|
||||
- Use consistent tag categories
|
||||
- Apply multiple tags when relevant
|
||||
- Update tags to reflect pattern evolution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If patterns are not showing in the web interface:
|
||||
1. Verify pattern_descriptions.json format
|
||||
2. Check web static copy exists
|
||||
3. Ensure proper file permissions
|
||||
4. Run extract_patterns.py to resync
|
||||
|
||||
## File Structure
|
||||
|
||||
fabric/
|
||||
├── patterns/ # Pattern source files
|
||||
├── PATTERN_DESCRIPTIONS/
|
||||
│ ├── extract_patterns.py # Pattern processing script
|
||||
│ ├── pattern_extracts.json # Pattern content references
|
||||
│ └── pattern_descriptions.json # Pattern metadata
|
||||
└── web/
|
||||
└── static/
|
||||
└── data/
|
||||
└── pattern_descriptions.json # Web interface copy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### 1. Using Language Qualifiers
|
||||
```
|
||||
User: What is the weather?
|
||||
AI: The weather information...
|
||||
|
||||
User: --fr What is the weather?
|
||||
AI: Voici les informations météo...
|
||||
```
|
||||
|
||||
### 2. Global Settings
|
||||
1. Select language from dropdown
|
||||
2. All interactions use selected language
|
||||
3. Automatic reset to English after each message
|
||||
|
||||
### 3. YouTube Analysis
|
||||
```
|
||||
User: Analyze this YouTube video --fr
|
||||
AI: [Provides analysis in French, maintaining language throughout the transcript]
|
||||
```
|
||||
|
||||
## 💡 Key Benefits
|
||||
|
||||
1. **Enhanced User Experience**
|
||||
- Intuitive language switching
|
||||
- Consistent language handling
|
||||
- Seamless integration with existing features
|
||||
|
||||
2. **Robust Implementation**
|
||||
- Simple yet powerful design
|
||||
- No complex language detection needed
|
||||
- Direct AI instruction approach
|
||||
|
||||
3. **Maintainable Architecture**
|
||||
- Clean separation of concerns
|
||||
- Stateful language management
|
||||
- Easy to extend for new languages
|
||||
|
||||
4. **YouTube Integration**
|
||||
- Handles long transcripts effectively
|
||||
- Maintains language consistency
|
||||
- Robust chunk processing
|
||||
|
||||
## 🔄 Implementation Notes
|
||||
|
||||
1. **State Management**
|
||||
- Language persists until changed
|
||||
- Resets to English after each message
|
||||
- Handles UI state updates efficiently
|
||||
|
||||
2. **Error Handling**
|
||||
- Invalid qualifiers are ignored
|
||||
- Unknown languages default to English
|
||||
- Proper store reset on errors
|
||||
|
||||
3. **Best Practices**
|
||||
- Clear language instructions
|
||||
- Consistent state management
|
||||
- Robust error handling
|
||||
|
||||
77
cli/README.md
Normal file
77
cli/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# YAML Configuration Support
|
||||
|
||||
## Overview
|
||||
|
||||
Fabric now supports YAML configuration files for commonly used options. This allows users to persist settings and share configurations across multiple runs.
|
||||
|
||||
## Usage
|
||||
|
||||
Use the `--config` flag to specify a YAML configuration file:
|
||||
|
||||
```bash
|
||||
fabric --config ~/.config/fabric/config.yaml "Tell me about APIs"
|
||||
```
|
||||
|
||||
## Configuration Precedence
|
||||
|
||||
1. CLI flags (highest priority)
|
||||
2. YAML config values
|
||||
3. Default values (lowest priority)
|
||||
|
||||
## Supported Configuration Options
|
||||
|
||||
```yaml
|
||||
# Model selection
|
||||
model: gpt-4
|
||||
modelContextLength: 4096
|
||||
|
||||
# Model parameters
|
||||
temperature: 0.7
|
||||
topp: 0.9
|
||||
presencepenalty: 0.0
|
||||
frequencypenalty: 0.0
|
||||
seed: 42
|
||||
|
||||
# Pattern selection
|
||||
pattern: analyze # Use pattern name or filename
|
||||
|
||||
# Feature flags
|
||||
stream: true
|
||||
raw: false
|
||||
```
|
||||
|
||||
## Rules and Behavior
|
||||
|
||||
- Only long flag names are supported in YAML (e.g., `temperature` not `-t`)
|
||||
- CLI flags always override YAML values
|
||||
- Unknown YAML declarations are ignored
|
||||
- If a declaration appears multiple times in YAML, the last one wins
|
||||
- The order of YAML declarations doesn't matter
|
||||
|
||||
## Type Conversions
|
||||
|
||||
The following string-to-type conversions are supported:
|
||||
|
||||
- String to number: `"42"` → `42`
|
||||
- String to float: `"42.5"` → `42.5`
|
||||
- String to boolean: `"true"` → `true`
|
||||
|
||||
## Example Config
|
||||
|
||||
```yaml
|
||||
# ~/.config/fabric/config.yaml
|
||||
model: gpt-4
|
||||
temperature: 0.8
|
||||
pattern: analyze
|
||||
stream: true
|
||||
topp: 0.95
|
||||
presencepenalty: 0.1
|
||||
frequencypenalty: 0.2
|
||||
```
|
||||
|
||||
## CLI Override Example
|
||||
|
||||
```bash
|
||||
# Override temperature from config
|
||||
fabric --config ~/.config/fabric/config.yaml --temperature 0.9 "Query"
|
||||
```
|
||||
356
cli/cli.go
Normal file
356
cli/cli.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/plugins/tools/youtube"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/danielmiessler/fabric/core"
|
||||
"github.com/danielmiessler/fabric/plugins/ai"
|
||||
"github.com/danielmiessler/fabric/plugins/db/fsdb"
|
||||
"github.com/danielmiessler/fabric/plugins/tools/converter"
|
||||
"github.com/danielmiessler/fabric/restapi"
|
||||
)
|
||||
|
||||
// Cli Controls the cli. It takes in the flags and runs the appropriate functions
|
||||
func Cli(version string) (err error) {
|
||||
var currentFlags *Flags
|
||||
if currentFlags, err = Init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.Version {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
var homedir string
|
||||
if homedir, err = os.UserHomeDir(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fabricDb := fsdb.NewDb(filepath.Join(homedir, ".config/fabric"))
|
||||
|
||||
if err = fabricDb.Configure(); err != nil {
|
||||
if !currentFlags.Setup {
|
||||
println(err.Error())
|
||||
currentFlags.Setup = true
|
||||
}
|
||||
}
|
||||
|
||||
var registry *core.PluginRegistry
|
||||
if registry, err = core.NewPluginRegistry(fabricDb); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if the setup flag is set, run the setup function
|
||||
if currentFlags.Setup {
|
||||
err = registry.Setup()
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.Serve {
|
||||
registry.ConfigureVendors()
|
||||
err = restapi.Serve(registry, currentFlags.ServeAddress, currentFlags.ServeAPIKey)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ServeOllama {
|
||||
registry.ConfigureVendors()
|
||||
err = restapi.ServeOllama(registry, currentFlags.ServeAddress, version)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.UpdatePatterns {
|
||||
err = registry.PatternsLoader.PopulateDB()
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ChangeDefaultModel {
|
||||
if err = registry.Defaults.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
err = registry.SaveEnvFile()
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.LatestPatterns != "0" {
|
||||
var parsedToInt int
|
||||
if parsedToInt, err = strconv.Atoi(currentFlags.LatestPatterns); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = fabricDb.Patterns.PrintLatestPatterns(parsedToInt); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ListPatterns {
|
||||
err = fabricDb.Patterns.ListNames(currentFlags.ShellCompleteOutput)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ListAllModels {
|
||||
var models *ai.VendorsModels
|
||||
if models, err = registry.VendorManager.GetModels(); err != nil {
|
||||
return
|
||||
}
|
||||
models.Print(currentFlags.ShellCompleteOutput)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ListAllContexts {
|
||||
err = fabricDb.Contexts.ListNames(currentFlags.ShellCompleteOutput)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ListAllSessions {
|
||||
err = fabricDb.Sessions.ListNames(currentFlags.ShellCompleteOutput)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.WipeContext != "" {
|
||||
err = fabricDb.Contexts.Delete(currentFlags.WipeContext)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.WipeSession != "" {
|
||||
err = fabricDb.Sessions.Delete(currentFlags.WipeSession)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.PrintSession != "" {
|
||||
err = fabricDb.Sessions.PrintSession(currentFlags.PrintSession)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.PrintContext != "" {
|
||||
err = fabricDb.Contexts.PrintContext(currentFlags.PrintContext)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.HtmlReadability {
|
||||
if msg, cleanErr := converter.HtmlReadability(currentFlags.Message); cleanErr != nil {
|
||||
fmt.Println("use original input, because can't apply html readability", err)
|
||||
} else {
|
||||
currentFlags.Message = msg
|
||||
}
|
||||
}
|
||||
|
||||
if currentFlags.ListExtensions {
|
||||
err = registry.TemplateExtensions.ListExtensions()
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.AddExtension != "" {
|
||||
err = registry.TemplateExtensions.RegisterExtension(currentFlags.AddExtension)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.RemoveExtension != "" {
|
||||
err = registry.TemplateExtensions.RemoveExtension(currentFlags.RemoveExtension)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ListStrategies {
|
||||
err = registry.Strategies.ListStrategies(currentFlags.ShellCompleteOutput)
|
||||
return
|
||||
}
|
||||
|
||||
if currentFlags.ListVendors {
|
||||
err = registry.ListVendors(os.Stdout)
|
||||
return
|
||||
}
|
||||
|
||||
// if the interactive flag is set, run the interactive function
|
||||
// if currentFlags.Interactive {
|
||||
// interactive.Interactive()
|
||||
// }
|
||||
|
||||
// if none of the above currentFlags are set, run the initiate chat function
|
||||
|
||||
var messageTools string
|
||||
|
||||
if currentFlags.YouTube != "" {
|
||||
if !registry.YouTube.IsConfigured() {
|
||||
err = fmt.Errorf("YouTube is not configured, please run the setup procedure")
|
||||
return
|
||||
}
|
||||
|
||||
var videoId string
|
||||
var playlistId string
|
||||
if videoId, playlistId, err = registry.YouTube.GetVideoOrPlaylistId(currentFlags.YouTube); err != nil {
|
||||
return
|
||||
} else if (videoId == "" || currentFlags.YouTubePlaylist) && playlistId != "" {
|
||||
if currentFlags.Output != "" {
|
||||
err = registry.YouTube.FetchAndSavePlaylist(playlistId, currentFlags.Output)
|
||||
} else {
|
||||
var videos []*youtube.VideoMeta
|
||||
if videos, err = registry.YouTube.FetchPlaylistVideos(playlistId); err != nil {
|
||||
err = fmt.Errorf("error fetching playlist videos: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, video := range videos {
|
||||
var message string
|
||||
if message, err = processYoutubeVideo(currentFlags, registry, video.Id); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !currentFlags.IsChatRequest() {
|
||||
if err = WriteOutput(message, fmt.Sprintf("%v.md", video.TitleNormalized)); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
messageTools = AppendMessage(messageTools, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if messageTools, err = processYoutubeVideo(currentFlags, registry, videoId); err != nil {
|
||||
return
|
||||
}
|
||||
if !currentFlags.IsChatRequest() {
|
||||
err = currentFlags.WriteOutput(messageTools)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFlags.ScrapeURL != "" || currentFlags.ScrapeQuestion != "") && registry.Jina.IsConfigured() {
|
||||
// Check if the scrape_url flag is set and call ScrapeURL
|
||||
if currentFlags.ScrapeURL != "" {
|
||||
var website string
|
||||
if website, err = registry.Jina.ScrapeURL(currentFlags.ScrapeURL); err != nil {
|
||||
return
|
||||
}
|
||||
messageTools = AppendMessage(messageTools, website)
|
||||
}
|
||||
|
||||
// Check if the scrape_question flag is set and call ScrapeQuestion
|
||||
if currentFlags.ScrapeQuestion != "" {
|
||||
var website string
|
||||
if website, err = registry.Jina.ScrapeQuestion(currentFlags.ScrapeQuestion); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
messageTools = AppendMessage(messageTools, website)
|
||||
}
|
||||
|
||||
if !currentFlags.IsChatRequest() {
|
||||
err = currentFlags.WriteOutput(messageTools)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if messageTools != "" {
|
||||
currentFlags.AppendMessage(messageTools)
|
||||
}
|
||||
|
||||
var chatter *core.Chatter
|
||||
if chatter, err = registry.GetChatter(currentFlags.Model, currentFlags.ModelContextLength, currentFlags.Strategy, currentFlags.Stream, currentFlags.DryRun); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var session *fsdb.Session
|
||||
var chatReq *common.ChatRequest
|
||||
if chatReq, err = currentFlags.BuildChatRequest(strings.Join(os.Args[1:], " ")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if chatReq.Language == "" {
|
||||
chatReq.Language = registry.Language.DefaultLanguage.Value
|
||||
}
|
||||
if session, err = chatter.Send(chatReq, currentFlags.BuildChatOptions()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := session.GetLastMessage().Content
|
||||
|
||||
if !currentFlags.Stream {
|
||||
// print the result if it was not streamed already
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
// if the copy flag is set, copy the message to the clipboard
|
||||
if currentFlags.Copy {
|
||||
if err = CopyToClipboard(result); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if the output flag is set, create an output file
|
||||
if currentFlags.Output != "" {
|
||||
if currentFlags.OutputSession {
|
||||
sessionAsString := session.String()
|
||||
err = CreateOutputFile(sessionAsString, currentFlags.Output)
|
||||
} else {
|
||||
err = CreateOutputFile(result, currentFlags.Output)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func processYoutubeVideo(
|
||||
flags *Flags, registry *core.PluginRegistry, videoId string) (message string, err error) {
|
||||
|
||||
if (!flags.YouTubeComments && !flags.YouTubeMetadata) || flags.YouTubeTranscript || flags.YouTubeTranscriptWithTimestamps {
|
||||
var transcript string
|
||||
var language = "en"
|
||||
if flags.Language != "" || registry.Language.DefaultLanguage.Value != "" {
|
||||
if flags.Language != "" {
|
||||
language = flags.Language
|
||||
} else {
|
||||
language = registry.Language.DefaultLanguage.Value
|
||||
}
|
||||
}
|
||||
if flags.YouTubeTranscriptWithTimestamps {
|
||||
if transcript, err = registry.YouTube.GrabTranscriptWithTimestamps(videoId, language); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if transcript, err = registry.YouTube.GrabTranscript(videoId, language); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
message = AppendMessage(message, transcript)
|
||||
}
|
||||
|
||||
if flags.YouTubeComments {
|
||||
var comments []string
|
||||
if comments, err = registry.YouTube.GrabComments(videoId); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
commentsString := strings.Join(comments, "\n")
|
||||
|
||||
message = AppendMessage(message, commentsString)
|
||||
}
|
||||
|
||||
if flags.YouTubeMetadata {
|
||||
var metadata *youtube.VideoMetadata
|
||||
if metadata, err = registry.YouTube.GrabMetadata(videoId); err != nil {
|
||||
return
|
||||
}
|
||||
metadataJson, _ := json.MarshalIndent(metadata, "", " ")
|
||||
message = AppendMessage(message, string(metadataJson))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func WriteOutput(message string, outputFile string) (err error) {
|
||||
fmt.Println(message)
|
||||
if outputFile != "" {
|
||||
err = CreateOutputFile(message, outputFile)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
cli/cli_test.go
Normal file
21
cli/cli_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/core"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCli(t *testing.T) {
|
||||
t.Skip("Skipping test for now, collision with flag -t")
|
||||
originalArgs := os.Args
|
||||
defer func() { os.Args = originalArgs }()
|
||||
|
||||
os.Args = []string{os.Args[0]}
|
||||
err := Cli("test")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, core.NoSessionPatternUserMessages, err.Error())
|
||||
}
|
||||
21
cli/example.yaml
Normal file
21
cli/example.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
#this is an example yaml config file for fabric
|
||||
|
||||
# use fabric pattern names
|
||||
pattern: ai
|
||||
|
||||
# or use a filename
|
||||
# pattern: ~/testpattern.md
|
||||
|
||||
model: phi3:latest
|
||||
|
||||
# for models that support context length
|
||||
modelContextLength: 2048
|
||||
|
||||
frequencypenalty: 0.5
|
||||
presencepenalty: 0.5
|
||||
topp: 0.67
|
||||
temperature: 0.88
|
||||
seed: 42
|
||||
|
||||
stream: true
|
||||
raw: false
|
||||
361
cli/flags.go
Normal file
361
cli/flags.go
Normal file
@@ -0,0 +1,361 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/jessevdk/go-flags"
|
||||
goopenai "github.com/sashabaranov/go-openai"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Flags create flags struct. the users flags go into this, this will be passed to the chat struct in cli
|
||||
type Flags struct {
|
||||
Pattern string `short:"p" long:"pattern" yaml:"pattern" description:"Choose a pattern from the available patterns" default:""`
|
||||
PatternVariables map[string]string `short:"v" long:"variable" description:"Values for pattern variables, e.g. -v=#role:expert -v=#points:30"`
|
||||
Context string `short:"C" long:"context" description:"Choose a context from the available contexts" default:""`
|
||||
Session string `long:"session" description:"Choose a session from the available sessions"`
|
||||
Attachments []string `short:"a" long:"attachment" description:"Attachment path or URL (e.g. for OpenAI image recognition messages)"`
|
||||
Setup bool `short:"S" long:"setup" description:"Run setup for all reconfigurable parts of fabric"`
|
||||
Temperature float64 `short:"t" long:"temperature" yaml:"temperature" description:"Set temperature" default:"0.7"`
|
||||
TopP float64 `short:"T" long:"topp" yaml:"topp" description:"Set top P" default:"0.9"`
|
||||
Stream bool `short:"s" long:"stream" yaml:"stream" description:"Stream"`
|
||||
PresencePenalty float64 `short:"P" long:"presencepenalty" yaml:"presencepenalty" description:"Set presence penalty" default:"0.0"`
|
||||
Raw bool `short:"r" long:"raw" yaml:"raw" description:"Use the defaults of the model without sending chat options (like temperature etc.) and use the user role instead of the system role for patterns."`
|
||||
FrequencyPenalty float64 `short:"F" long:"frequencypenalty" yaml:"frequencypenalty" description:"Set frequency penalty" default:"0.0"`
|
||||
ListPatterns bool `short:"l" long:"listpatterns" description:"List all patterns"`
|
||||
ListAllModels bool `short:"L" long:"listmodels" description:"List all available models"`
|
||||
ListAllContexts bool `short:"x" long:"listcontexts" description:"List all contexts"`
|
||||
ListAllSessions bool `short:"X" long:"listsessions" description:"List all sessions"`
|
||||
UpdatePatterns bool `short:"U" long:"updatepatterns" description:"Update patterns"`
|
||||
Message string `hidden:"true" description:"Messages to send to chat"`
|
||||
Copy bool `short:"c" long:"copy" description:"Copy to clipboard"`
|
||||
Model string `short:"m" long:"model" yaml:"model" description:"Choose model"`
|
||||
ModelContextLength int `long:"modelContextLength" yaml:"modelContextLength" description:"Model context length (only affects ollama)"`
|
||||
Output string `short:"o" long:"output" description:"Output to file" default:""`
|
||||
OutputSession bool `long:"output-session" description:"Output the entire session (also a temporary one) to the output file"`
|
||||
LatestPatterns string `short:"n" long:"latest" description:"Number of latest patterns to list" default:"0"`
|
||||
ChangeDefaultModel bool `short:"d" long:"changeDefaultModel" description:"Change default model"`
|
||||
YouTube string `short:"y" long:"youtube" description:"YouTube video or play list \"URL\" to grab transcript, comments from it and send to chat or print it put to the console and store it in the output file"`
|
||||
YouTubePlaylist bool `long:"playlist" description:"Prefer playlist over video if both ids are present in the URL"`
|
||||
YouTubeTranscript bool `long:"transcript" description:"Grab transcript from YouTube video and send to chat (it is used per default)."`
|
||||
YouTubeTranscriptWithTimestamps bool `long:"transcript-with-timestamps" description:"Grab transcript from YouTube video with timestamps and send to chat"`
|
||||
YouTubeComments bool `long:"comments" description:"Grab comments from YouTube video and send to chat"`
|
||||
YouTubeMetadata bool `long:"metadata" description:"Output video metadata"`
|
||||
Language string `short:"g" long:"language" description:"Specify the Language Code for the chat, e.g. -g=en -g=zh" default:""`
|
||||
ScrapeURL string `short:"u" long:"scrape_url" description:"Scrape website URL to markdown using Jina AI"`
|
||||
ScrapeQuestion string `short:"q" long:"scrape_question" description:"Search question using Jina AI"`
|
||||
Seed int `short:"e" long:"seed" yaml:"seed" description:"Seed to be used for LMM generation"`
|
||||
WipeContext string `short:"w" long:"wipecontext" description:"Wipe context"`
|
||||
WipeSession string `short:"W" long:"wipesession" description:"Wipe session"`
|
||||
PrintContext string `long:"printcontext" description:"Print context"`
|
||||
PrintSession string `long:"printsession" description:"Print session"`
|
||||
HtmlReadability bool `long:"readability" description:"Convert HTML input into a clean, readable view"`
|
||||
InputHasVars bool `long:"input-has-vars" description:"Apply variables to user input"`
|
||||
DryRun bool `long:"dry-run" description:"Show what would be sent to the model without actually sending it"`
|
||||
Serve bool `long:"serve" description:"Serve the Fabric Rest API"`
|
||||
ServeOllama bool `long:"serveOllama" description:"Serve the Fabric Rest API with ollama endpoints"`
|
||||
ServeAddress string `long:"address" description:"The address to bind the REST API" default:":8080"`
|
||||
ServeAPIKey string `long:"api-key" description:"API key used to secure server routes" default:""`
|
||||
Config string `long:"config" description:"Path to YAML config file"`
|
||||
Version bool `long:"version" description:"Print current version"`
|
||||
ListExtensions bool `long:"listextensions" description:"List all registered extensions"`
|
||||
AddExtension string `long:"addextension" description:"Register a new extension from config file path"`
|
||||
RemoveExtension string `long:"rmextension" description:"Remove a registered extension by name"`
|
||||
Strategy string `long:"strategy" description:"Choose a strategy from the available strategies" default:""`
|
||||
ListStrategies bool `long:"liststrategies" description:"List all strategies"`
|
||||
ListVendors bool `long:"listvendors" description:"List all vendors"`
|
||||
ShellCompleteOutput bool `long:"shell-complete-list" description:"Output raw list without headers/formatting (for shell completion)"`
|
||||
}
|
||||
|
||||
var debug = false
|
||||
|
||||
func Debugf(format string, a ...interface{}) {
|
||||
if debug {
|
||||
fmt.Printf("DEBUG: "+format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
// Init Initialize flags. returns a Flags struct and an error
|
||||
func Init() (ret *Flags, err error) {
|
||||
// Track which yaml-configured flags were set on CLI
|
||||
usedFlags := make(map[string]bool)
|
||||
yamlArgsScan := os.Args[1:]
|
||||
|
||||
// Get list of fields that have yaml tags, could be in yaml config
|
||||
yamlFields := make(map[string]bool)
|
||||
t := reflect.TypeOf(Flags{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if yamlTag := t.Field(i).Tag.Get("yaml"); yamlTag != "" {
|
||||
yamlFields[yamlTag] = true
|
||||
//Debugf("Found yaml-configured field: %s\n", yamlTag)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan args for that are provided by cli and might be in yaml
|
||||
for _, arg := range yamlArgsScan {
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
flag := strings.TrimPrefix(arg, "--")
|
||||
if i := strings.Index(flag, "="); i > 0 {
|
||||
flag = flag[:i]
|
||||
}
|
||||
if yamlFields[flag] {
|
||||
usedFlags[flag] = true
|
||||
Debugf("CLI flag used: %s\n", flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI flags first
|
||||
ret = &Flags{}
|
||||
parser := flags.NewParser(ret, flags.Default)
|
||||
var args []string
|
||||
if args, err = parser.Parse(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If config specified, load and apply YAML for unused flags
|
||||
if ret.Config != "" {
|
||||
var yamlFlags *Flags
|
||||
if yamlFlags, err = loadYAMLConfig(ret.Config); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply YAML values where CLI flags weren't used
|
||||
flagsVal := reflect.ValueOf(ret).Elem()
|
||||
yamlVal := reflect.ValueOf(yamlFlags).Elem()
|
||||
flagsType := flagsVal.Type()
|
||||
|
||||
for i := 0; i < flagsType.NumField(); i++ {
|
||||
field := flagsType.Field(i)
|
||||
if yamlTag := field.Tag.Get("yaml"); yamlTag != "" {
|
||||
if !usedFlags[yamlTag] {
|
||||
flagField := flagsVal.Field(i)
|
||||
yamlField := yamlVal.Field(i)
|
||||
if flagField.CanSet() {
|
||||
if yamlField.Type() != flagField.Type() {
|
||||
if err := assignWithConversion(flagField, yamlField); err != nil {
|
||||
Debugf("Type conversion failed for %s: %v\n", yamlTag, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
flagField.Set(yamlField)
|
||||
}
|
||||
Debugf("Applied YAML value for %s: %v\n", yamlTag, yamlField.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stdin and messages
|
||||
// Handle stdin and messages
|
||||
info, _ := os.Stdin.Stat()
|
||||
pipedToStdin := (info.Mode() & os.ModeCharDevice) == 0
|
||||
|
||||
// Append positional arguments to the message (custom message)
|
||||
if len(args) > 0 {
|
||||
ret.Message = AppendMessage(ret.Message, args[len(args)-1])
|
||||
}
|
||||
|
||||
if pipedToStdin {
|
||||
var pipedMessage string
|
||||
if pipedMessage, err = readStdin(); err != nil {
|
||||
return
|
||||
}
|
||||
ret.Message = AppendMessage(ret.Message, pipedMessage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func assignWithConversion(targetField, sourceField reflect.Value) error {
|
||||
// Handle string source values
|
||||
if sourceField.Kind() == reflect.String {
|
||||
str := sourceField.String()
|
||||
switch targetField.Kind() {
|
||||
case reflect.Int:
|
||||
// Try parsing as float first to handle "42.9" -> 42
|
||||
if val, err := strconv.ParseFloat(str, 64); err == nil {
|
||||
targetField.SetInt(int64(val))
|
||||
return nil
|
||||
}
|
||||
// Try direct int parse
|
||||
if val, err := strconv.ParseInt(str, 10, 64); err == nil {
|
||||
targetField.SetInt(val)
|
||||
return nil
|
||||
}
|
||||
case reflect.Float64:
|
||||
if val, err := strconv.ParseFloat(str, 64); err == nil {
|
||||
targetField.SetFloat(val)
|
||||
return nil
|
||||
}
|
||||
case reflect.Bool:
|
||||
if val, err := strconv.ParseBool(str); err == nil {
|
||||
targetField.SetBool(val)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot convert string %q to %v", str, targetField.Kind())
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported conversion from %v to %v", sourceField.Kind(), targetField.Kind())
|
||||
}
|
||||
|
||||
func loadYAMLConfig(configPath string) (*Flags, error) {
|
||||
absPath, err := common.GetAbsolutePath(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid config path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("config file not found: %s", absPath)
|
||||
}
|
||||
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
|
||||
// Use the existing Flags struct for YAML unmarshal
|
||||
config := &Flags{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file: %w", err)
|
||||
}
|
||||
|
||||
Debugf("Config: %v\n", config)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// readStdin reads from stdin and returns the input as a string or an error
|
||||
func readStdin() (ret string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
var sb strings.Builder
|
||||
for {
|
||||
if line, readErr := reader.ReadString('\n'); readErr != nil {
|
||||
if errors.Is(readErr, io.EOF) {
|
||||
sb.WriteString(line)
|
||||
break
|
||||
}
|
||||
err = fmt.Errorf("error reading piped message from stdin: %w", readErr)
|
||||
return
|
||||
} else {
|
||||
sb.WriteString(line)
|
||||
}
|
||||
}
|
||||
ret = sb.String()
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Flags) BuildChatOptions() (ret *common.ChatOptions) {
|
||||
ret = &common.ChatOptions{
|
||||
Temperature: o.Temperature,
|
||||
TopP: o.TopP,
|
||||
PresencePenalty: o.PresencePenalty,
|
||||
FrequencyPenalty: o.FrequencyPenalty,
|
||||
Raw: o.Raw,
|
||||
Seed: o.Seed,
|
||||
ModelContextLength: o.ModelContextLength,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Flags) BuildChatRequest(Meta string) (ret *common.ChatRequest, err error) {
|
||||
ret = &common.ChatRequest{
|
||||
ContextName: o.Context,
|
||||
SessionName: o.Session,
|
||||
PatternName: o.Pattern,
|
||||
StrategyName: o.Strategy,
|
||||
PatternVariables: o.PatternVariables,
|
||||
InputHasVars: o.InputHasVars,
|
||||
Meta: Meta,
|
||||
}
|
||||
|
||||
var message *goopenai.ChatCompletionMessage
|
||||
if len(o.Attachments) == 0 {
|
||||
if o.Message != "" {
|
||||
message = &goopenai.ChatCompletionMessage{
|
||||
Role: goopenai.ChatMessageRoleUser,
|
||||
Content: strings.TrimSpace(o.Message),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message = &goopenai.ChatCompletionMessage{
|
||||
Role: goopenai.ChatMessageRoleUser,
|
||||
}
|
||||
|
||||
if o.Message != "" {
|
||||
message.MultiContent = append(message.MultiContent, goopenai.ChatMessagePart{
|
||||
Type: goopenai.ChatMessagePartTypeText,
|
||||
Text: strings.TrimSpace(o.Message),
|
||||
})
|
||||
}
|
||||
|
||||
for _, attachmentValue := range o.Attachments {
|
||||
var attachment *common.Attachment
|
||||
if attachment, err = common.NewAttachment(attachmentValue); err != nil {
|
||||
return
|
||||
}
|
||||
url := attachment.URL
|
||||
if url == nil {
|
||||
var base64Image string
|
||||
if base64Image, err = attachment.Base64Content(); err != nil {
|
||||
return
|
||||
}
|
||||
var mimeType string
|
||||
if mimeType, err = attachment.ResolveType(); err != nil {
|
||||
return
|
||||
}
|
||||
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Image)
|
||||
url = &dataURL
|
||||
}
|
||||
message.MultiContent = append(message.MultiContent, goopenai.ChatMessagePart{
|
||||
Type: goopenai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &goopenai.ChatMessageImageURL{
|
||||
URL: *url,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
ret.Message = message
|
||||
|
||||
if o.Language != "" {
|
||||
if langTag, langErr := language.Parse(o.Language); langErr == nil {
|
||||
ret.Language = langTag.String()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Flags) AppendMessage(message string) {
|
||||
o.Message = AppendMessage(o.Message, message)
|
||||
}
|
||||
|
||||
func (o *Flags) IsChatRequest() (ret bool) {
|
||||
ret = o.Message != "" || len(o.Attachments) > 0 || o.Context != "" || o.Session != "" || o.Pattern != ""
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Flags) WriteOutput(message string) (err error) {
|
||||
fmt.Println(message)
|
||||
if o.Output != "" {
|
||||
err = CreateOutputFile(message, o.Output)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AppendMessage(message string, newMessage string) (ret string) {
|
||||
if message != "" {
|
||||
ret = message + "\n" + newMessage
|
||||
} else {
|
||||
ret = newMessage
|
||||
}
|
||||
return
|
||||
}
|
||||
166
cli/flags_test.go
Normal file
166
cli/flags_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
args := []string{"--copy"}
|
||||
expectedFlags := &Flags{Copy: true}
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = append([]string{"cmd"}, args...)
|
||||
|
||||
flags, err := Init()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedFlags.Copy, flags.Copy)
|
||||
}
|
||||
|
||||
func TestReadStdin(t *testing.T) {
|
||||
input := "test input"
|
||||
stdin := io.NopCloser(strings.NewReader(input))
|
||||
// No need to cast stdin to *os.File, pass it as io.ReadCloser directly
|
||||
content, err := ReadStdin(stdin)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if content != input {
|
||||
t.Fatalf("expected %q, got %q", input, content)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadStdin function assuming it's part of `cli` package
|
||||
func ReadStdin(reader io.ReadCloser) (string, error) {
|
||||
defer reader.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := buf.ReadFrom(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func TestBuildChatOptions(t *testing.T) {
|
||||
flags := &Flags{
|
||||
Temperature: 0.8,
|
||||
TopP: 0.9,
|
||||
PresencePenalty: 0.1,
|
||||
FrequencyPenalty: 0.2,
|
||||
Seed: 1,
|
||||
}
|
||||
|
||||
expectedOptions := &common.ChatOptions{
|
||||
Temperature: 0.8,
|
||||
TopP: 0.9,
|
||||
PresencePenalty: 0.1,
|
||||
FrequencyPenalty: 0.2,
|
||||
Raw: false,
|
||||
Seed: 1,
|
||||
}
|
||||
options := flags.BuildChatOptions()
|
||||
assert.Equal(t, expectedOptions, options)
|
||||
}
|
||||
|
||||
func TestBuildChatOptionsDefaultSeed(t *testing.T) {
|
||||
flags := &Flags{
|
||||
Temperature: 0.8,
|
||||
TopP: 0.9,
|
||||
PresencePenalty: 0.1,
|
||||
FrequencyPenalty: 0.2,
|
||||
}
|
||||
|
||||
expectedOptions := &common.ChatOptions{
|
||||
Temperature: 0.8,
|
||||
TopP: 0.9,
|
||||
PresencePenalty: 0.1,
|
||||
FrequencyPenalty: 0.2,
|
||||
Raw: false,
|
||||
Seed: 0,
|
||||
}
|
||||
options := flags.BuildChatOptions()
|
||||
assert.Equal(t, expectedOptions, options)
|
||||
}
|
||||
|
||||
func TestInitWithYAMLConfig(t *testing.T) {
|
||||
// Create a temporary YAML config file
|
||||
configContent := `
|
||||
temperature: 0.9
|
||||
model: gpt-4
|
||||
pattern: analyze
|
||||
stream: true
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config.*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.Write([]byte(configContent)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test 1: Basic YAML loading
|
||||
t.Run("Load YAML config", func(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = []string{"cmd", "--config", tmpfile.Name()}
|
||||
|
||||
flags, err := Init()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0.9, flags.Temperature)
|
||||
assert.Equal(t, "gpt-4", flags.Model)
|
||||
assert.Equal(t, "analyze", flags.Pattern)
|
||||
assert.True(t, flags.Stream)
|
||||
})
|
||||
|
||||
// Test 2: CLI overrides YAML
|
||||
t.Run("CLI overrides YAML", func(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = []string{"cmd", "--config", tmpfile.Name(), "--temperature", "0.7", "--model", "gpt-3.5-turbo"}
|
||||
|
||||
flags, err := Init()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0.7, flags.Temperature)
|
||||
assert.Equal(t, "gpt-3.5-turbo", flags.Model)
|
||||
assert.Equal(t, "analyze", flags.Pattern) // unchanged from YAML
|
||||
assert.True(t, flags.Stream) // unchanged from YAML
|
||||
})
|
||||
|
||||
// Test 3: Invalid YAML config
|
||||
t.Run("Invalid YAML config", func(t *testing.T) {
|
||||
badConfig := `
|
||||
temperature: "not a float"
|
||||
model: 123 # should be string
|
||||
`
|
||||
badfile, err := os.CreateTemp("", "bad-config.*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(badfile.Name())
|
||||
|
||||
if _, err := badfile.Write([]byte(badConfig)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := badfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = []string{"cmd", "--config", badfile.Name()}
|
||||
|
||||
_, err = Init()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
30
cli/output.go
Normal file
30
cli/output.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
)
|
||||
|
||||
func CopyToClipboard(message string) (err error) {
|
||||
if err = clipboard.WriteAll(message); err != nil {
|
||||
err = fmt.Errorf("could not copy to clipboard: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CreateOutputFile(message string, fileName string) (err error) {
|
||||
var file *os.File
|
||||
if file, err = os.Create(fileName); err != nil {
|
||||
err = fmt.Errorf("error creating file: %v", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err = file.WriteString(message); err != nil {
|
||||
err = fmt.Errorf("error writing to file: %v", err)
|
||||
} else {
|
||||
fmt.Printf("\n\n... written to %s\n", fileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
28
cli/output_test.go
Normal file
28
cli/output_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyToClipboard(t *testing.T) {
|
||||
t.Skip("skipping test, because of docker env. in ci.")
|
||||
|
||||
message := "test message"
|
||||
err := CopyToClipboard(message)
|
||||
if err != nil {
|
||||
t.Fatalf("CopyToClipboard() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOutputFile(t *testing.T) {
|
||||
|
||||
fileName := "test_output.txt"
|
||||
message := "test message"
|
||||
err := CreateOutputFile(message, fileName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOutputFile() error = %v", err)
|
||||
}
|
||||
|
||||
defer os.Remove(fileName)
|
||||
}
|
||||
171
common/attachment.go
Normal file
171
common/attachment.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
type Attachment struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Content []byte `json:"content,omitempty"`
|
||||
ID *string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Attachment) GetId() (ret string, err error) {
|
||||
if a.ID == nil {
|
||||
var hash string
|
||||
if a.Content != nil {
|
||||
hash = fmt.Sprintf("%x", sha256.Sum256(a.Content))
|
||||
} else if a.Path != nil {
|
||||
var content []byte
|
||||
if content, err = os.ReadFile(*a.Path); err != nil {
|
||||
return
|
||||
}
|
||||
hash = fmt.Sprintf("%x", sha256.Sum256(content))
|
||||
} else if a.URL != nil {
|
||||
data := map[string]string{"url": *a.URL}
|
||||
var jsonData []byte
|
||||
if jsonData, err = json.Marshal(data); err != nil {
|
||||
return
|
||||
}
|
||||
hash = fmt.Sprintf("%x", sha256.Sum256(jsonData))
|
||||
}
|
||||
a.ID = &hash
|
||||
}
|
||||
ret = *a.ID
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Attachment) ResolveType() (ret string, err error) {
|
||||
if a.Type != nil {
|
||||
ret = *a.Type
|
||||
return
|
||||
}
|
||||
if a.Path != nil {
|
||||
var mime *mimetype.MIME
|
||||
if mime, err = mimetype.DetectFile(*a.Path); err != nil {
|
||||
return
|
||||
}
|
||||
ret = mime.String()
|
||||
return
|
||||
}
|
||||
if a.URL != nil {
|
||||
var resp *http.Response
|
||||
if resp, err = http.Head(*a.URL); err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ret = resp.Header.Get("Content-Type")
|
||||
return
|
||||
}
|
||||
if a.Content != nil {
|
||||
ret = mimetype.Detect(a.Content).String()
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("attachment has no type and no content to derive it from")
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Attachment) ContentBytes() (ret []byte, err error) {
|
||||
if a.Content != nil {
|
||||
ret = a.Content
|
||||
return
|
||||
}
|
||||
if a.Path != nil {
|
||||
if ret, err = os.ReadFile(*a.Path); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if a.URL != nil {
|
||||
var resp *http.Response
|
||||
if resp, err = http.Get(*a.URL); err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if ret, err = io.ReadAll(resp.Body); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("no content available")
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Attachment) Base64Content() (ret string, err error) {
|
||||
var content []byte
|
||||
if content, err = a.ContentBytes(); err != nil {
|
||||
return
|
||||
}
|
||||
ret = base64.StdEncoding.EncodeToString(content)
|
||||
return
|
||||
}
|
||||
|
||||
func NewAttachment(value string) (ret *Attachment, err error) {
|
||||
if isURL(value) {
|
||||
var mimeType string
|
||||
if mimeType, err = detectMimeTypeFromURL(value); err != nil {
|
||||
return
|
||||
}
|
||||
ret = &Attachment{
|
||||
Type: &mimeType,
|
||||
URL: &value,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var absPath string
|
||||
if absPath, err = filepath.Abs(value); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = os.Stat(absPath); os.IsNotExist(err) {
|
||||
err = fmt.Errorf("file %s does not exist", value)
|
||||
return
|
||||
}
|
||||
|
||||
var mimeType string
|
||||
if mimeType, err = detectMimeTypeFromFile(absPath); err != nil {
|
||||
return
|
||||
}
|
||||
ret = &Attachment{
|
||||
Type: &mimeType,
|
||||
Path: &absPath,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func detectMimeTypeFromURL(url string) (string, error) {
|
||||
resp, err := http.Head(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
mimeType := resp.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
return "", fmt.Errorf("could not determine mimetype of URL")
|
||||
}
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
func detectMimeTypeFromFile(path string) (string, error) {
|
||||
mime, err := mimetype.DetectFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return mime.String(), nil
|
||||
}
|
||||
|
||||
func isURL(value string) bool {
|
||||
return bytes.Contains([]byte(value), []byte("://"))
|
||||
}
|
||||
49
common/domain.go
Normal file
49
common/domain.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package common
|
||||
|
||||
import goopenai "github.com/sashabaranov/go-openai"
|
||||
|
||||
const ChatMessageRoleMeta = "meta"
|
||||
|
||||
type ChatRequest struct {
|
||||
ContextName string
|
||||
SessionName string
|
||||
PatternName string
|
||||
PatternVariables map[string]string
|
||||
Message *goopenai.ChatCompletionMessage
|
||||
Language string
|
||||
Meta string
|
||||
InputHasVars bool
|
||||
StrategyName string
|
||||
}
|
||||
|
||||
type ChatOptions struct {
|
||||
Model string
|
||||
Temperature float64
|
||||
TopP float64
|
||||
PresencePenalty float64
|
||||
FrequencyPenalty float64
|
||||
Raw bool
|
||||
Seed int
|
||||
ModelContextLength int
|
||||
}
|
||||
|
||||
// NormalizeMessages remove empty messages and ensure messages order user-assist-user
|
||||
func NormalizeMessages(msgs []*goopenai.ChatCompletionMessage, defaultUserMessage string) (ret []*goopenai.ChatCompletionMessage) {
|
||||
// Iterate over messages to enforce the odd position rule for user messages
|
||||
fullMessageIndex := 0
|
||||
for _, message := range msgs {
|
||||
if message.Content == "" {
|
||||
// Skip empty messages as the anthropic API doesn't accept them
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure, that each odd position shall be a user message
|
||||
if fullMessageIndex%2 == 0 && message.Role != goopenai.ChatMessageRoleUser {
|
||||
ret = append(ret, &goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleUser, Content: defaultUserMessage})
|
||||
fullMessageIndex++
|
||||
}
|
||||
ret = append(ret, message)
|
||||
fullMessageIndex++
|
||||
}
|
||||
return
|
||||
}
|
||||
27
common/domain_test.go
Normal file
27
common/domain_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
goopenai "github.com/sashabaranov/go-openai"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeMessages(t *testing.T) {
|
||||
msgs := []*goopenai.ChatCompletionMessage{
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: "Hello"},
|
||||
{Role: goopenai.ChatMessageRoleAssistant, Content: "Hi there!"},
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: ""},
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: ""},
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: "How are you?"},
|
||||
}
|
||||
|
||||
expected := []*goopenai.ChatCompletionMessage{
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: "Hello"},
|
||||
{Role: goopenai.ChatMessageRoleAssistant, Content: "Hi there!"},
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: "How are you?"},
|
||||
}
|
||||
|
||||
actual := NormalizeMessages(msgs, "default")
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
195
common/file_manager.go
Normal file
195
common/file_manager.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileChangesMarker identifies the start of a file changes section in output
|
||||
const FileChangesMarker = "__CREATE_CODING_FEATURE_FILE_CHANGES__"
|
||||
|
||||
const (
|
||||
// MaxFileSize is the maximum size of a file that can be created (10MB)
|
||||
MaxFileSize = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
// FileChange represents a single file change operation to be performed
|
||||
type FileChange struct {
|
||||
Operation string `json:"operation"` // "create" or "update"
|
||||
Path string `json:"path"` // Relative path from project root
|
||||
Content string `json:"content"` // New file content
|
||||
}
|
||||
|
||||
// ParseFileChanges extracts and parses the file change marker section from LLM output
|
||||
func ParseFileChanges(output string) (changeSummary string, changes []FileChange, err error) {
|
||||
fileChangesStart := strings.Index(output, FileChangesMarker)
|
||||
if fileChangesStart == -1 {
|
||||
return output, nil, nil // No file changes section found
|
||||
}
|
||||
changeSummary = output[:fileChangesStart] // Everything before the marker
|
||||
|
||||
// Extract the JSON part
|
||||
jsonStart := fileChangesStart + len(FileChangesMarker)
|
||||
// Find the first [ after the file changes marker
|
||||
jsonArrayStart := strings.Index(output[jsonStart:], "[")
|
||||
if jsonArrayStart == -1 {
|
||||
return output, nil, fmt.Errorf("invalid %s format: no JSON array found", FileChangesMarker)
|
||||
}
|
||||
jsonStart += jsonArrayStart
|
||||
|
||||
// Find the matching closing bracket for the array with proper bracket counting
|
||||
bracketCount := 0
|
||||
jsonEnd := jsonStart
|
||||
for i := jsonStart; i < len(output); i++ {
|
||||
if output[i] == '[' {
|
||||
bracketCount++
|
||||
} else if output[i] == ']' {
|
||||
bracketCount--
|
||||
if bracketCount == 0 {
|
||||
jsonEnd = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bracketCount != 0 {
|
||||
return output, nil, fmt.Errorf("invalid %s format: unbalanced brackets", FileChangesMarker)
|
||||
}
|
||||
|
||||
// Extract the JSON string and fix escape sequences
|
||||
jsonStr := output[jsonStart:jsonEnd]
|
||||
|
||||
// Fix specific invalid escape sequences
|
||||
// First try with the common \C issue
|
||||
jsonStr = strings.Replace(jsonStr, `\C`, `\\C`, -1)
|
||||
|
||||
// Parse the JSON
|
||||
var fileChanges []FileChange
|
||||
err = json.Unmarshal([]byte(jsonStr), &fileChanges)
|
||||
if err != nil {
|
||||
// If still failing, try a more comprehensive fix
|
||||
jsonStr = fixInvalidEscapes(jsonStr)
|
||||
err = json.Unmarshal([]byte(jsonStr), &fileChanges)
|
||||
if err != nil {
|
||||
return changeSummary, nil, fmt.Errorf("failed to parse %s JSON: %w", FileChangesMarker, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file changes
|
||||
for i, change := range fileChanges {
|
||||
// Validate operation
|
||||
if change.Operation != "create" && change.Operation != "update" {
|
||||
return changeSummary, nil, fmt.Errorf("invalid operation for file change %d: %s", i, change.Operation)
|
||||
}
|
||||
|
||||
// Validate path
|
||||
if change.Path == "" {
|
||||
return changeSummary, nil, fmt.Errorf("empty path for file change %d", i)
|
||||
}
|
||||
|
||||
// Check for suspicious paths (directory traversal)
|
||||
if strings.Contains(change.Path, "..") {
|
||||
return changeSummary, nil, fmt.Errorf("suspicious path for file change %d: %s", i, change.Path)
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if len(change.Content) > MaxFileSize {
|
||||
return changeSummary, nil, fmt.Errorf("file content too large for file change %d: %d bytes", i, len(change.Content))
|
||||
}
|
||||
}
|
||||
|
||||
return changeSummary, fileChanges, nil
|
||||
}
|
||||
|
||||
// fixInvalidEscapes replaces invalid escape sequences in JSON strings
|
||||
func fixInvalidEscapes(jsonStr string) string {
|
||||
validEscapes := []byte{'b', 'f', 'n', 'r', 't', '\\', '/', '"', 'u'}
|
||||
|
||||
var result strings.Builder
|
||||
inQuotes := false
|
||||
i := 0
|
||||
|
||||
for i < len(jsonStr) {
|
||||
ch := jsonStr[i]
|
||||
|
||||
// Track whether we're inside a JSON string
|
||||
if ch == '"' && (i == 0 || jsonStr[i-1] != '\\') {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
|
||||
// Handle actual control characters inside string literals
|
||||
if inQuotes {
|
||||
// Convert literal control characters to proper JSON escape sequences
|
||||
if ch == '\n' {
|
||||
result.WriteString("\\n")
|
||||
i++
|
||||
continue
|
||||
} else if ch == '\r' {
|
||||
result.WriteString("\\r")
|
||||
i++
|
||||
continue
|
||||
} else if ch == '\t' {
|
||||
result.WriteString("\\t")
|
||||
i++
|
||||
continue
|
||||
} else if ch < 32 {
|
||||
// Handle other control characters
|
||||
fmt.Fprintf(&result, "\\u%04x", ch)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check for escape sequences only inside strings
|
||||
if inQuotes && ch == '\\' && i+1 < len(jsonStr) {
|
||||
nextChar := jsonStr[i+1]
|
||||
isValid := false
|
||||
|
||||
for _, validEscape := range validEscapes {
|
||||
if nextChar == validEscape {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
// Invalid escape sequence - add an extra backslash
|
||||
result.WriteByte('\\')
|
||||
result.WriteByte('\\')
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result.WriteByte(ch)
|
||||
i++
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ApplyFileChanges applies the parsed file changes to the file system
|
||||
func ApplyFileChanges(projectRoot string, changes []FileChange) error {
|
||||
for i, change := range changes {
|
||||
// Get the absolute path
|
||||
absPath := filepath.Join(projectRoot, change.Path)
|
||||
|
||||
// Create directories if necessary
|
||||
dir := filepath.Dir(absPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s for file change %d: %w", dir, i, err)
|
||||
}
|
||||
|
||||
// Write the file
|
||||
if err := os.WriteFile(absPath, []byte(change.Content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file %s for file change %d: %w", absPath, i, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Applied %s operation to %s\n", change.Operation, change.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
185
common/file_manager_test.go
Normal file
185
common/file_manager_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFileChanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int // number of expected file changes
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "No " + FileChangesMarker + " section",
|
||||
input: "This is a normal response with no file changes.",
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid " + FileChangesMarker + " section",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "test.txt",
|
||||
"content": "Hello, World!"
|
||||
},
|
||||
{
|
||||
"operation": "update",
|
||||
"path": "other.txt",
|
||||
"content": "Updated content"
|
||||
}
|
||||
]
|
||||
Some text after.`,
|
||||
want: 2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON in " + FileChangesMarker + " section",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "test.txt",
|
||||
"content": "Hello, World!"
|
||||
},
|
||||
{
|
||||
"operation": "invalid",
|
||||
"path": "other.txt"
|
||||
"content": "Updated content"
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid operation",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "delete",
|
||||
"path": "test.txt",
|
||||
"content": ""
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Empty path",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "",
|
||||
"content": "Hello, World!"
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Suspicious path with directory traversal",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "../etc/passwd",
|
||||
"content": "Hello, World!"
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, got, err := ParseFileChanges(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseFileChanges() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && len(got) != tt.want {
|
||||
t.Errorf("ParseFileChanges() got %d file changes, want %d", len(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFileChanges(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "file-manager-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
// Test file changes
|
||||
changes := []FileChange{
|
||||
{
|
||||
Operation: "create",
|
||||
Path: "test.txt",
|
||||
Content: "Hello, World!",
|
||||
},
|
||||
{
|
||||
Operation: "create",
|
||||
Path: "subdir/nested.txt",
|
||||
Content: "Nested content",
|
||||
},
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
if err := ApplyFileChanges(tempDir, changes); err != nil {
|
||||
t.Fatalf("ApplyFileChanges() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify the first file was created correctly
|
||||
content, err := os.ReadFile(filepath.Join(tempDir, "test.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read created file: %v", err)
|
||||
}
|
||||
if string(content) != "Hello, World!" {
|
||||
t.Errorf("File content = %q, want %q", string(content), "Hello, World!")
|
||||
}
|
||||
|
||||
// Verify the nested file was created correctly
|
||||
content, err = os.ReadFile(filepath.Join(tempDir, "subdir/nested.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read created nested file: %v", err)
|
||||
}
|
||||
if string(content) != "Nested content" {
|
||||
t.Errorf("Nested file content = %q, want %q", string(content), "Nested content")
|
||||
}
|
||||
|
||||
// Test updating a file
|
||||
updateChanges := []FileChange{
|
||||
{
|
||||
Operation: "update",
|
||||
Path: "test.txt",
|
||||
Content: "Updated content",
|
||||
},
|
||||
}
|
||||
|
||||
// Apply the update
|
||||
if err := ApplyFileChanges(tempDir, updateChanges); err != nil {
|
||||
t.Fatalf("ApplyFileChanges() error = %v", err)
|
||||
}
|
||||
// Verify the file was updated correctly
|
||||
content, err = os.ReadFile(filepath.Join(tempDir, "test.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
if string(content) != "Updated content" {
|
||||
t.Errorf("Updated file content = %q, want %q", string(content), "Updated content")
|
||||
}
|
||||
}
|
||||
182
common/groups_items.go
Normal file
182
common/groups_items.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func NewGroupsItemsSelector[I any](selectionLabel string,
|
||||
getItemLabel func(I) string) *GroupsItemsSelector[I] {
|
||||
|
||||
return &GroupsItemsSelector[I]{SelectionLabel: selectionLabel,
|
||||
GetItemKey: getItemLabel,
|
||||
GroupsItems: make([]*GroupItems[I], 0),
|
||||
}
|
||||
}
|
||||
|
||||
type GroupItems[I any] struct {
|
||||
Group string
|
||||
Items []I
|
||||
}
|
||||
|
||||
func (o *GroupItems[I]) Count() int {
|
||||
return len(o.Items)
|
||||
}
|
||||
|
||||
func (o *GroupItems[I]) ContainsItemBy(predicate func(item I) bool) (ret bool) {
|
||||
ret = lo.ContainsBy(o.Items, predicate)
|
||||
return
|
||||
}
|
||||
|
||||
type GroupsItemsSelector[I any] struct {
|
||||
SelectionLabel string
|
||||
GetItemKey func(I) string
|
||||
|
||||
GroupsItems []*GroupItems[I]
|
||||
}
|
||||
|
||||
func (o *GroupsItemsSelector[I]) AddGroupItems(group string, items ...I) {
|
||||
o.GroupsItems = append(o.GroupsItems, &GroupItems[I]{group, items})
|
||||
}
|
||||
|
||||
// getSortedGroupsItems returns a new slice of GroupItems with both groups and their items
|
||||
// sorted alphabetically in a case-insensitive manner. The original GroupsItems are not modified.
|
||||
func (o *GroupsItemsSelector[I]) getSortedGroupsItems() []*GroupItems[I] {
|
||||
// Copy and sort groups (case‑insensitive)
|
||||
sortedGroupsItems := make([]*GroupItems[I], len(o.GroupsItems))
|
||||
copy(sortedGroupsItems, o.GroupsItems)
|
||||
sort.SliceStable(sortedGroupsItems, func(i, j int) bool {
|
||||
return strings.ToLower(sortedGroupsItems[i].Group) < strings.ToLower(sortedGroupsItems[j].Group)
|
||||
})
|
||||
|
||||
// For each group, sort its items
|
||||
for i, groupItems := range sortedGroupsItems {
|
||||
sortedItems := make([]I, len(groupItems.Items))
|
||||
copy(sortedItems, groupItems.Items)
|
||||
sort.SliceStable(sortedItems, func(i, j int) bool {
|
||||
return strings.ToLower(o.GetItemKey(sortedItems[i])) < strings.ToLower(o.GetItemKey(sortedItems[j]))
|
||||
})
|
||||
|
||||
// Create a new GroupItems with the sorted items
|
||||
sortedGroupsItems[i] = &GroupItems[I]{
|
||||
Group: groupItems.Group,
|
||||
Items: sortedItems,
|
||||
}
|
||||
}
|
||||
|
||||
return sortedGroupsItems
|
||||
}
|
||||
|
||||
func (o *GroupsItemsSelector[I]) GetGroupAndItemByItemNumber(number int) (group string, item I, err error) {
|
||||
var currentItemNumber int
|
||||
found := false
|
||||
|
||||
sortedGroupsItems := o.getSortedGroupsItems()
|
||||
|
||||
for _, groupItems := range sortedGroupsItems {
|
||||
if currentItemNumber+len(groupItems.Items) < number {
|
||||
currentItemNumber += len(groupItems.Items)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, groupItem := range groupItems.Items {
|
||||
currentItemNumber++
|
||||
if currentItemNumber == number {
|
||||
group = groupItems.Group
|
||||
item = groupItem
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
err = fmt.Errorf("number %d is out of range", number)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *GroupsItemsSelector[I]) Print(shellCompleteList bool) {
|
||||
// Only print the section header if not in plain output mode
|
||||
if !shellCompleteList {
|
||||
fmt.Printf("\n%v:\n", o.SelectionLabel)
|
||||
}
|
||||
|
||||
var currentItemIndex int
|
||||
sortedGroupsItems := o.getSortedGroupsItems()
|
||||
|
||||
for _, groupItems := range sortedGroupsItems {
|
||||
if !shellCompleteList {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s\n\n", groupItems.Group)
|
||||
}
|
||||
|
||||
for _, item := range groupItems.Items {
|
||||
currentItemIndex++
|
||||
if shellCompleteList {
|
||||
// plain mode: "index key"
|
||||
fmt.Printf("%s\n", o.GetItemKey(item))
|
||||
} else {
|
||||
// formatted mode: "[index] key"
|
||||
fmt.Printf("\t[%d]\t%s\n", currentItemIndex, o.GetItemKey(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *GroupsItemsSelector[I]) HasGroup(group string) (ret bool) {
|
||||
for _, groupItems := range o.GroupsItems {
|
||||
if ret = groupItems.Group == group; ret {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *GroupsItemsSelector[I]) FindGroupsByItemFirst(item I) (ret string) {
|
||||
itemKey := o.GetItemKey(item)
|
||||
|
||||
for _, groupItems := range o.GroupsItems {
|
||||
if groupItems.ContainsItemBy(func(groupItem I) bool {
|
||||
groupItemKey := o.GetItemKey(groupItem)
|
||||
return groupItemKey == itemKey
|
||||
}) {
|
||||
ret = groupItems.Group
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *GroupsItemsSelector[I]) FindGroupsByItem(item I) (groups []string) {
|
||||
itemKey := o.GetItemKey(item)
|
||||
|
||||
for _, groupItems := range o.GroupsItems {
|
||||
if groupItems.ContainsItemBy(func(groupItem I) bool {
|
||||
groupItemKey := o.GetItemKey(groupItem)
|
||||
return groupItemKey == itemKey
|
||||
}) {
|
||||
groups = append(groups, groupItems.Group)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ReturnItem(item string) string {
|
||||
return item
|
||||
}
|
||||
|
||||
func NewGroupsItemsSelectorString(selectionLabel string) *GroupsItemsSelectorString {
|
||||
return &GroupsItemsSelectorString{GroupsItemsSelector: NewGroupsItemsSelector(selectionLabel, ReturnItem)}
|
||||
}
|
||||
|
||||
type GroupsItemsSelectorString struct {
|
||||
*GroupsItemsSelector[string]
|
||||
}
|
||||
73
common/utils.go
Normal file
73
common/utils.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetAbsolutePath resolves a given path to its absolute form, handling ~, ./, ../, UNC paths, and symlinks.
|
||||
func GetAbsolutePath(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", errors.New("path is empty")
|
||||
}
|
||||
|
||||
// Handle UNC paths on Windows
|
||||
if runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Handle ~ for home directory expansion
|
||||
if strings.HasPrefix(path, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", errors.New("could not resolve home directory")
|
||||
}
|
||||
path = filepath.Join(home, path[1:])
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", errors.New("could not get absolute path")
|
||||
}
|
||||
|
||||
// Resolve symlinks, but allow non-existent paths
|
||||
resolvedPath, err := filepath.EvalSymlinks(absPath)
|
||||
if err == nil {
|
||||
return resolvedPath, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
// Return the absolute path for non-existent paths
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not resolve symlinks: %w", err)
|
||||
}
|
||||
|
||||
// Helper function to check if a symlink points to a directory
|
||||
func IsSymlinkToDir(path string) bool {
|
||||
fileInfo, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if fileInfo.Mode()&os.ModeSymlink != 0 {
|
||||
resolvedPath, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
fileInfo, err = os.Stat(resolvedPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
|
||||
return false // Regular directories should not be treated as symlinks
|
||||
}
|
||||
111
completions/_fabric
Normal file
111
completions/_fabric
Normal file
@@ -0,0 +1,111 @@
|
||||
#compdef fabric
|
||||
|
||||
# Zsh completion for fabric CLI
|
||||
# Place this file in a directory in your $fpath (e.g. /usr/local/share/zsh/site-functions)
|
||||
|
||||
_fabric_patterns() {
|
||||
local -a patterns
|
||||
patterns=(${(f)"$(fabric --listpatterns --shell-complete-list 2>/dev/null)"})
|
||||
compadd -X "Patterns:" ${patterns}
|
||||
}
|
||||
|
||||
_fabric_models() {
|
||||
local -a models
|
||||
models=(${(f)"$(fabric --listmodels --shell-complete-list 2>/dev/null)"})
|
||||
compadd -X "Models:" ${models}
|
||||
}
|
||||
_fabric_contexts() {
|
||||
local -a contexts
|
||||
contexts=(${(f)"$(fabric --listcontexts --shell-complete-list 2>/dev/null)"})
|
||||
compadd -X "Contexts:" ${contexts}
|
||||
}
|
||||
_fabric_sessions() {
|
||||
local -a sessions
|
||||
sessions=(${(f)"$(fabric --listsessions --shell-complete-list 2>/dev/null)"})
|
||||
compadd -X "Sessions:" ${sessions}
|
||||
}
|
||||
_fabric_strategies() {
|
||||
local -a strategies
|
||||
strategies=(${(f)"$(fabric --liststrategies --shell-complete-list 2>/dev/null)"})
|
||||
compadd -X "Strategies:" ${strategies}
|
||||
}
|
||||
|
||||
_fabric_extensions() {
|
||||
local -a extensions
|
||||
extensions=(${(f)"$(fabric --listextensions --shell-complete-list 2>/dev/null)"})
|
||||
compadd -X "Extensions:" ${extensions}
|
||||
'(-L --listmodels)'{-L,--listmodels}'[List all available models]:list models:_fabric_models' \
|
||||
'(-x --listcontexts)'{-x,--listcontexts}'[List all contexts]:list contexts:_fabric_contexts' \
|
||||
'(-X --listsessions)'{-X,--listsessions}'[List all sessions]:list sessions:_fabric_sessions' \
|
||||
'(--listextensions)--listextensions[List all registered extensions]' \
|
||||
'(--liststrategies)--liststrategies[List all strategies]:list strategies:_fabric_strategies' \
|
||||
'(--listvendors)--listvendors[List all vendors]' \
|
||||
vendors=(${(f)"$(fabric --listvendors 2>/dev/null)"})
|
||||
compadd -X "Vendors:" ${vendors}
|
||||
}
|
||||
|
||||
_fabric() {
|
||||
local curcontext="$curcontext" state line
|
||||
typeset -A opt_args
|
||||
|
||||
_arguments -C \
|
||||
'(-p --pattern)'{-p,--pattern}'[Choose a pattern from the available patterns]:pattern:_fabric_patterns' \
|
||||
'(-v --variable)'{-v,--variable}'[Values for pattern variables, e.g. -v=#role:expert -v=#points:30]:variable:' \
|
||||
'(-C --context)'{-C,--context}'[Choose a context from the available contexts]:context:_fabric_contexts' \
|
||||
'(--session)--session[Choose a session from the available sessions]:session:_fabric_sessions' \
|
||||
'(-a --attachment)'{-a,--attachment}'[Attachment path or URL (e.g. for OpenAI image recognition messages)]:file:_files' \
|
||||
'(-S --setup)'{-S,--setup}'[Run setup for all reconfigurable parts of fabric]' \
|
||||
'(-t --temperature)'{-t,--temperature}'[Set temperature (default: 0.7)]:temperature:' \
|
||||
'(-T --topp)'{-T,--topp}'[Set top P (default: 0.9)]:topp:' \
|
||||
'(-s --stream)'{-s,--stream}'[Stream]' \
|
||||
'(-P --presencepenalty)'{-P,--presencepenalty}'[Set presence penalty (default: 0.0)]:presence penalty:' \
|
||||
'(-r --raw)'{-r,--raw}'[Use the defaults of the model without sending chat options]' \
|
||||
'(-F --frequencypenalty)'{-F,--frequencypenalty}'[Set frequency penalty (default: 0.0)]:frequency penalty:' \
|
||||
'(-l --listpatterns)'{-l,--listpatterns}'[List all patterns]' \
|
||||
'(-L --listmodels)'{-L,--listmodels}'[List all available models]' \
|
||||
'(-x --listcontexts)'{-x,--listcontexts}'[List all contexts]' \
|
||||
'(-X --listsessions)'{-X,--listsessions}'[List all sessions]' \
|
||||
'(-U --updatepatterns)'{-U,--updatepatterns}'[Update patterns]' \
|
||||
'(-c --copy)'{-c,--copy}'[Copy to clipboard]' \
|
||||
'(-m --model)'{-m,--model}'[Choose model]:model:_fabric_models' \
|
||||
'(--modelContextLength)--modelContextLength[Model context length (only affects ollama)]:length:' \
|
||||
'(-o --output)'{-o,--output}'[Output to file]:file:_files' \
|
||||
'(--output-session)--output-session[Output the entire session to the output file]' \
|
||||
'(-n --latest)'{-n,--latest}'[Number of latest patterns to list (default: 0)]:number:' \
|
||||
'(-d --changeDefaultModel)'{-d,--changeDefaultModel}'[Change default model]' \
|
||||
'(-y --youtube)'{-y,--youtube}'[YouTube video or play list URL]:youtube url:' \
|
||||
'(--playlist)--playlist[Prefer playlist over video if both ids are present in the URL]' \
|
||||
'(--transcript)--transcript[Grab transcript from YouTube video and send to chat]' \
|
||||
'(--transcript-with-timestamps)--transcript-with-timestamps[Grab transcript from YouTube video with timestamps]' \
|
||||
'(--comments)--comments[Grab comments from YouTube video and send to chat]' \
|
||||
'(--metadata)--metadata[Output video metadata]' \
|
||||
'(-g --language)'{-g,--language}'[Specify the Language Code for the chat, e.g. -g=en -g=zh]:language:' \
|
||||
'(-u --scrape_url)'{-u,--scrape_url}'[Scrape website URL to markdown using Jina AI]:url:' \
|
||||
'(-q --scrape_question)'{-q,--scrape_question}'[Search question using Jina AI]:question:' \
|
||||
'(-e --seed)'{-e,--seed}'[Seed to be used for LMM generation]:seed:' \
|
||||
'(-w --wipecontext)'{-w,--wipecontext}'[Wipe context]:context:_fabric_contexts' \
|
||||
'(-W --wipesession)'{-W,--wipesession}'[Wipe session]:session:_fabric_sessions' \
|
||||
'(--printcontext)--printcontext[Print context]:context:_fabric_contexts' \
|
||||
'(--printsession)--printsession[Print session]:session:_fabric_sessions' \
|
||||
'(--readability)--readability[Convert HTML input into a clean, readable view]' \
|
||||
'(--input-has-vars)--input-has-vars[Apply variables to user input]' \
|
||||
'(--dry-run)--dry-run[Show what would be sent to the model without actually sending it]' \
|
||||
'(--serve)--serve[Serve the Fabric Rest API]' \
|
||||
'(--serveOllama)--serveOllama[Serve the Fabric Rest API with ollama endpoints]' \
|
||||
'(--address)--address[The address to bind the REST API (default: :8080)]:address:' \
|
||||
'(--api-key)--api-key[API key used to secure server routes]:api-key:' \
|
||||
'(--config)--config[Path to YAML config file]:config file:_files -g "*.yaml *.yml"' \
|
||||
'(--version)--version[Print current version]' \
|
||||
'(--listextensions)--listextensions[List all registered extensions]' \
|
||||
'(--addextension)--addextension[Register a new extension from config file path]:config file:_files -g "*.yaml *.yml"' \
|
||||
'(--rmextension)--rmextension[Remove a registered extension by name]:extension:_fabric_extensions' \
|
||||
'(--strategy)--strategy[Choose a strategy from the available strategies]:strategy:_fabric_strategies' \
|
||||
'(--liststrategies)--liststrategies[List all strategies]' \
|
||||
'(--listvendors)--listvendors[List all vendors]' \
|
||||
'(--shell-complete-list)--shell-complete-list[Output raw list without headers/formatting (for shell completion)]' \
|
||||
'(-h --help)'{-h,--help}'[Show this help message]' \
|
||||
'*:arguments:'
|
||||
}
|
||||
|
||||
_fabric "$@"
|
||||
|
||||
90
completions/fabric.bash
Normal file
90
completions/fabric.bash
Normal file
@@ -0,0 +1,90 @@
|
||||
# Bash completion for fabric CLI
|
||||
#
|
||||
# Installation:
|
||||
# 1. Place this file in a standard completion directory, e.g.,
|
||||
# - /etc/bash_completion.d/
|
||||
# - /usr/local/etc/bash_completion.d/
|
||||
# - ~/.local/share/bash-completion/completions/
|
||||
# 2. Or, source it directly in your ~/.bashrc or ~/.bash_profile:
|
||||
# source /path/to/fabric.bash
|
||||
|
||||
_fabric() {
|
||||
local cur prev words cword
|
||||
_get_comp_words_by_ref -n : cur prev words cword
|
||||
|
||||
# Define all possible options/flags
|
||||
local opts="--pattern -p --variable -v --context -C --session --attachment -a --setup -S --temperature -t --topp -T --stream -s --presencepenalty -P --raw -r --frequencypenalty -F --listpatterns -l --listmodels -L --listcontexts -x --listsessions -X --updatepatterns -U --copy -c --model -m --modelContextLength --output -o --output-session --latest -n --changeDefaultModel -d --youtube -y --playlist --transcript --transcript-with-timestamps --comments --metadata --language -g --scrape_url -u --scrape_question -q --seed -e --wipecontext -w --wipesession -W --printcontext --printsession --readability --input-has-vars --dry-run --serve --serveOllama --address --api-key --config --version --listextensions --addextension --rmextension --strategy --liststrategies --listvendors --shell-complete-list --help -h"
|
||||
|
||||
# Helper function for dynamic completions
|
||||
_fabric_get_list() {
|
||||
fabric "$1" --shell-complete-list 2>/dev/null
|
||||
}
|
||||
|
||||
# Handle completions based on the previous word
|
||||
case "${prev}" in
|
||||
-p | --pattern)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listpatterns)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-C | --context)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listcontexts)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--session)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listsessions)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-m | --model)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listmodels)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-w | --wipecontext)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listcontexts)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-W | --wipesession)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listsessions)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--printcontext)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listcontexts)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--printsession)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listsessions)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--rmextension)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --listextensions)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--strategy)
|
||||
COMPREPLY=($(compgen -W "$(_fabric_get_list --liststrategies)" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
# Options requiring file/directory paths
|
||||
-a | --attachment | -o | --output | --config | --addextension)
|
||||
_filedir
|
||||
return 0
|
||||
;;
|
||||
# Options requiring simple arguments (no specific completion logic here)
|
||||
-v | --variable | -t | --temperature | -T | --topp | -P | --presencepenalty | -F | --frequencypenalty | --modelContextLength | -n | --latest | -y | --youtube | -g | --language | -u | --scrape_url | -q | --scrape_question | -e | --seed | --address | --api-key)
|
||||
# No specific completion suggestions, user types the value
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# If the current word starts with '-', suggest options
|
||||
if [[ "${cur}" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "${opts}" -- "${cur}"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Default: complete files/directories if no other rule matches
|
||||
# _filedir
|
||||
# Or provide no completions if it's not an option or argument following a known flag
|
||||
COMPREPLY=()
|
||||
|
||||
}
|
||||
|
||||
complete -F _fabric fabric
|
||||
94
completions/fabric.fish
Executable file
94
completions/fabric.fish
Executable file
@@ -0,0 +1,94 @@
|
||||
# Fish shell completion for fabric CLI
|
||||
#
|
||||
# Installation:
|
||||
# Copy this file to ~/.config/fish/completions/fabric.fish
|
||||
# or run:
|
||||
# mkdir -p ~/.config/fish/completions
|
||||
# cp completions/fabric.fish ~/.config/fish/completions/
|
||||
|
||||
# Helper functions for dynamic completions
|
||||
function __fabric_get_patterns
|
||||
fabric --listpatterns --shell-complete-list 2>/dev/null
|
||||
end
|
||||
|
||||
function __fabric_get_models
|
||||
fabric --listmodels --shell-complete-list 2>/dev/null
|
||||
end
|
||||
|
||||
function __fabric_get_contexts
|
||||
fabric --listcontexts --shell-complete-list 2>/dev/null
|
||||
end
|
||||
|
||||
function __fabric_get_sessions
|
||||
fabric --listsessions --shell-complete-list 2>/dev/null
|
||||
end
|
||||
|
||||
function __fabric_get_strategies
|
||||
fabric --liststrategies --shell-complete-list 2>/dev/null
|
||||
end
|
||||
|
||||
function __fabric_get_extensions
|
||||
fabric --listextensions --shell-complete-list 2>/dev/null
|
||||
end
|
||||
|
||||
# Main completion function
|
||||
complete -c fabric -f
|
||||
|
||||
# Flag completions with arguments
|
||||
complete -c fabric -s p -l pattern -d "Choose a pattern from the available patterns" -a "(__fabric_get_patterns)"
|
||||
complete -c fabric -s v -l variable -d "Values for pattern variables, e.g. -v=#role:expert -v=#points:30"
|
||||
complete -c fabric -s C -l context -d "Choose a context from the available contexts" -a "(__fabric_get_contexts)"
|
||||
complete -c fabric -l session -d "Choose a session from the available sessions" -a "(__fabric_get_sessions)"
|
||||
complete -c fabric -s a -l attachment -d "Attachment path or URL (e.g. for OpenAI image recognition messages)" -r
|
||||
complete -c fabric -s t -l temperature -d "Set temperature (default: 0.7)"
|
||||
complete -c fabric -s T -l topp -d "Set top P (default: 0.9)"
|
||||
complete -c fabric -s P -l presencepenalty -d "Set presence penalty (default: 0.0)"
|
||||
complete -c fabric -s F -l frequencypenalty -d "Set frequency penalty (default: 0.0)"
|
||||
complete -c fabric -s m -l model -d "Choose model" -a "(__fabric_get_models)"
|
||||
complete -c fabric -l modelContextLength -d "Model context length (only affects ollama)"
|
||||
complete -c fabric -s o -l output -d "Output to file" -r
|
||||
complete -c fabric -s n -l latest -d "Number of latest patterns to list (default: 0)"
|
||||
complete -c fabric -s y -l youtube -d "YouTube video or play list URL to grab transcript, comments from it"
|
||||
complete -c fabric -s g -l language -d "Specify the Language Code for the chat, e.g. -g=en -g=zh"
|
||||
complete -c fabric -s u -l scrape_url -d "Scrape website URL to markdown using Jina AI"
|
||||
complete -c fabric -s q -l scrape_question -d "Search question using Jina AI"
|
||||
complete -c fabric -s e -l seed -d "Seed to be used for LMM generation"
|
||||
complete -c fabric -s w -l wipecontext -d "Wipe context" -a "(__fabric_get_contexts)"
|
||||
complete -c fabric -s W -l wipesession -d "Wipe session" -a "(__fabric_get_sessions)"
|
||||
complete -c fabric -l printcontext -d "Print context" -a "(__fabric_get_contexts)"
|
||||
complete -c fabric -l printsession -d "Print session" -a "(__fabric_get_sessions)"
|
||||
complete -c fabric -l address -d "The address to bind the REST API (default: :8080)"
|
||||
complete -c fabric -l api-key -d "API key used to secure server routes"
|
||||
complete -c fabric -l config -d "Path to YAML config file" -r -a "*.yaml *.yml"
|
||||
complete -c fabric -l addextension -d "Register a new extension from config file path" -r -a "*.yaml *.yml"
|
||||
complete -c fabric -l rmextension -d "Remove a registered extension by name" -a "(__fabric_get_extensions)"
|
||||
complete -c fabric -l strategy -d "Choose a strategy from the available strategies" -a "(__fabric_get_strategies)"
|
||||
|
||||
# Boolean flags (no arguments)
|
||||
complete -c fabric -s S -l setup -d "Run setup for all reconfigurable parts of fabric"
|
||||
complete -c fabric -s s -l stream -d "Stream"
|
||||
complete -c fabric -s r -l raw -d "Use the defaults of the model without sending chat options"
|
||||
complete -c fabric -s l -l listpatterns -d "List all patterns"
|
||||
complete -c fabric -s L -l listmodels -d "List all available models"
|
||||
complete -c fabric -s x -l listcontexts -d "List all contexts"
|
||||
complete -c fabric -s X -l listsessions -d "List all sessions"
|
||||
complete -c fabric -s U -l updatepatterns -d "Update patterns"
|
||||
complete -c fabric -s c -l copy -d "Copy to clipboard"
|
||||
complete -c fabric -l output-session -d "Output the entire session to the output file"
|
||||
complete -c fabric -s d -l changeDefaultModel -d "Change default model"
|
||||
complete -c fabric -l playlist -d "Prefer playlist over video if both ids are present in the URL"
|
||||
complete -c fabric -l transcript -d "Grab transcript from YouTube video and send to chat"
|
||||
complete -c fabric -l transcript-with-timestamps -d "Grab transcript from YouTube video with timestamps"
|
||||
complete -c fabric -l comments -d "Grab comments from YouTube video and send to chat"
|
||||
complete -c fabric -l metadata -d "Output video metadata"
|
||||
complete -c fabric -l readability -d "Convert HTML input into a clean, readable view"
|
||||
complete -c fabric -l input-has-vars -d "Apply variables to user input"
|
||||
complete -c fabric -l dry-run -d "Show what would be sent to the model without actually sending it"
|
||||
complete -c fabric -l serve -d "Serve the Fabric Rest API"
|
||||
complete -c fabric -l serveOllama -d "Serve the Fabric Rest API with ollama endpoints"
|
||||
complete -c fabric -l version -d "Print current version"
|
||||
complete -c fabric -l listextensions -d "List all registered extensions"
|
||||
complete -c fabric -l liststrategies -d "List all strategies"
|
||||
complete -c fabric -l listvendors -d "List all vendors"
|
||||
complete -c fabric -l shell-complete-list -d "Output raw list without headers/formatting (for shell completion)"
|
||||
complete -c fabric -s h -l help -d "Show this help message"
|
||||
222
core/chatter.go
Normal file
222
core/chatter.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
goopenai "github.com/sashabaranov/go-openai"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/danielmiessler/fabric/plugins/ai"
|
||||
"github.com/danielmiessler/fabric/plugins/db/fsdb"
|
||||
"github.com/danielmiessler/fabric/plugins/strategy"
|
||||
"github.com/danielmiessler/fabric/plugins/template"
|
||||
)
|
||||
|
||||
const NoSessionPatternUserMessages = "no session, pattern or user messages provided"
|
||||
|
||||
type Chatter struct {
|
||||
db *fsdb.Db
|
||||
|
||||
Stream bool
|
||||
DryRun bool
|
||||
|
||||
model string
|
||||
modelContextLength int
|
||||
vendor ai.Vendor
|
||||
strategy string
|
||||
}
|
||||
|
||||
// Send processes a chat request and applies any file changes if using the create_coding_feature pattern
|
||||
func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (session *fsdb.Session, err error) {
|
||||
if session, err = o.BuildSession(request, opts.Raw); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vendorMessages := session.GetVendorMessages()
|
||||
if len(vendorMessages) == 0 {
|
||||
if session.Name != "" {
|
||||
err = o.db.Sessions.SaveSession(session)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no messages provided")
|
||||
return
|
||||
}
|
||||
|
||||
if opts.Model == "" {
|
||||
opts.Model = o.model
|
||||
}
|
||||
|
||||
if opts.ModelContextLength == 0 {
|
||||
opts.ModelContextLength = o.modelContextLength
|
||||
}
|
||||
|
||||
message := ""
|
||||
|
||||
if o.Stream {
|
||||
channel := make(chan string)
|
||||
go func() {
|
||||
if streamErr := o.vendor.SendStream(session.GetVendorMessages(), opts, channel); streamErr != nil {
|
||||
channel <- streamErr.Error()
|
||||
}
|
||||
}()
|
||||
|
||||
for response := range channel {
|
||||
message += response
|
||||
fmt.Print(response)
|
||||
}
|
||||
} else {
|
||||
if message, err = o.vendor.Send(context.Background(), session.GetVendorMessages(), opts); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if message == "" {
|
||||
session = nil
|
||||
err = fmt.Errorf("empty response")
|
||||
return
|
||||
}
|
||||
|
||||
// Process file changes if using the create_coding_feature pattern
|
||||
if request.PatternName == "create_coding_feature" {
|
||||
// Look for file changes in the response
|
||||
summary, fileChanges, parseErr := common.ParseFileChanges(message)
|
||||
if parseErr != nil {
|
||||
fmt.Printf("Warning: Failed to parse file changes: %v\n", parseErr)
|
||||
} else if len(fileChanges) > 0 {
|
||||
// Get the project root - use the current directory
|
||||
projectRoot, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to get current directory: %v\n", err)
|
||||
// Continue without applying changes
|
||||
} else {
|
||||
if applyErr := common.ApplyFileChanges(projectRoot, fileChanges); applyErr != nil {
|
||||
fmt.Printf("Warning: Failed to apply file changes: %v\n", applyErr)
|
||||
} else {
|
||||
fmt.Println("Successfully applied file changes.")
|
||||
fmt.Printf("You can review the changes with 'git diff' if you're using git.\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
message = summary
|
||||
}
|
||||
|
||||
session.Append(&goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleAssistant, Content: message})
|
||||
|
||||
if session.Name != "" {
|
||||
err = o.db.Sessions.SaveSession(session)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *fsdb.Session, err error) {
|
||||
// If a session name is provided, retrieve it from the database
|
||||
if request.SessionName != "" {
|
||||
var sess *fsdb.Session
|
||||
if sess, err = o.db.Sessions.Get(request.SessionName); err != nil {
|
||||
err = fmt.Errorf("could not find session %s: %v", request.SessionName, err)
|
||||
return
|
||||
}
|
||||
session = sess
|
||||
} else {
|
||||
session = &fsdb.Session{}
|
||||
}
|
||||
|
||||
if request.Meta != "" {
|
||||
session.Append(&goopenai.ChatCompletionMessage{Role: common.ChatMessageRoleMeta, Content: request.Meta})
|
||||
}
|
||||
|
||||
// if a context name is provided, retrieve it from the database
|
||||
var contextContent string
|
||||
if request.ContextName != "" {
|
||||
var ctx *fsdb.Context
|
||||
if ctx, err = o.db.Contexts.Get(request.ContextName); err != nil {
|
||||
err = fmt.Errorf("could not find context %s: %v", request.ContextName, err)
|
||||
return
|
||||
}
|
||||
contextContent = ctx.Content
|
||||
}
|
||||
|
||||
// Process any template variables in the message content (user input)
|
||||
// Double curly braces {{variable}} indicate template substitution
|
||||
// Ensure we have a message before processing, other wise we'll get an error when we pass to pattern.go
|
||||
if request.Message == nil {
|
||||
request.Message = &goopenai.ChatCompletionMessage{
|
||||
Role: goopenai.ChatMessageRoleUser,
|
||||
Content: " ",
|
||||
}
|
||||
}
|
||||
|
||||
// Now we know request.Message is not nil, process template variables
|
||||
if request.InputHasVars {
|
||||
request.Message.Content, err = template.ApplyTemplate(request.Message.Content, request.PatternVariables, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var patternContent string
|
||||
if request.PatternName != "" {
|
||||
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, request.Message.Content)
|
||||
// pattern will now contain user input, and all variables will be resolved, or errored
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
|
||||
}
|
||||
patternContent = pattern.Pattern
|
||||
}
|
||||
|
||||
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
|
||||
|
||||
// Apply strategy if specified
|
||||
if request.StrategyName != "" {
|
||||
strategy, err := strategy.LoadStrategy(request.StrategyName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load strategy %s: %v", request.StrategyName, err)
|
||||
}
|
||||
if strategy != nil && strategy.Prompt != "" {
|
||||
// prepend the strategy prompt to the system message
|
||||
systemMessage = fmt.Sprintf("%s\n%s", strategy.Prompt, systemMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply refined language instruction if specified
|
||||
if request.Language != "" && request.Language != "en" {
|
||||
// Refined instruction: Execute pattern using user input, then translate the entire response.
|
||||
systemMessage = fmt.Sprintf("%s\n\nIMPORTANT: First, execute the instructions provided in this prompt using the user's input. Second, ensure your entire final response, including any section headers or titles generated as part of executing the instructions, is written ONLY in the %s language.", systemMessage, request.Language)
|
||||
}
|
||||
|
||||
if raw {
|
||||
// In raw mode, combine system message (potentially with strategy) and user message into a single user message
|
||||
if systemMessage != "" {
|
||||
if request.Message != nil {
|
||||
// Prepend system message to user content, ensuring user input is preserved
|
||||
request.Message.Content = fmt.Sprintf("%s\n\n%s", systemMessage, request.Message.Content)
|
||||
request.Message.Role = goopenai.ChatMessageRoleUser // Ensure role is User in raw mode
|
||||
} else {
|
||||
// If no user message, create one with the system content, marked as User role
|
||||
request.Message = &goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleUser, Content: systemMessage}
|
||||
}
|
||||
} // else: no system message, user message (if any) remains unchanged
|
||||
} else {
|
||||
// Not raw mode, append system message separately if it exists
|
||||
if systemMessage != "" {
|
||||
session.Append(&goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleSystem, Content: systemMessage})
|
||||
}
|
||||
}
|
||||
|
||||
if request.Message != nil {
|
||||
session.Append(request.Message)
|
||||
}
|
||||
|
||||
if session.IsEmpty() {
|
||||
session = nil
|
||||
err = errors.New(NoSessionPatternUserMessages)
|
||||
}
|
||||
return
|
||||
}
|
||||
287
core/plugin_registry.go
Normal file
287
core/plugin_registry.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/plugins/ai/exolab"
|
||||
"github.com/danielmiessler/fabric/plugins/strategy"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/danielmiessler/fabric/plugins"
|
||||
"github.com/danielmiessler/fabric/plugins/ai"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/anthropic"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/azure"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/dryrun"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/gemini"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/lmstudio"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/ollama"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/openai"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/openai_compatible"
|
||||
"github.com/danielmiessler/fabric/plugins/db/fsdb"
|
||||
"github.com/danielmiessler/fabric/plugins/template"
|
||||
"github.com/danielmiessler/fabric/plugins/tools"
|
||||
"github.com/danielmiessler/fabric/plugins/tools/jina"
|
||||
"github.com/danielmiessler/fabric/plugins/tools/lang"
|
||||
"github.com/danielmiessler/fabric/plugins/tools/youtube"
|
||||
)
|
||||
|
||||
func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
|
||||
ret = &PluginRegistry{
|
||||
Db: db,
|
||||
VendorManager: ai.NewVendorsManager(),
|
||||
VendorsAll: ai.NewVendorsManager(),
|
||||
PatternsLoader: tools.NewPatternsLoader(db.Patterns),
|
||||
YouTube: youtube.NewYouTube(),
|
||||
Language: lang.NewLanguage(),
|
||||
Jina: jina.NewClient(),
|
||||
Strategies: strategy.NewStrategiesManager(),
|
||||
}
|
||||
|
||||
var homedir string
|
||||
if homedir, err = os.UserHomeDir(); err != nil {
|
||||
return
|
||||
}
|
||||
ret.TemplateExtensions = template.NewExtensionManager(filepath.Join(homedir, ".config/fabric"))
|
||||
|
||||
ret.Defaults = tools.NeeDefaults(ret.GetModels)
|
||||
|
||||
// Create a vendors slice to hold all vendors (order doesn't matter initially)
|
||||
vendors := []ai.Vendor{}
|
||||
|
||||
// Add non-OpenAI compatible clients
|
||||
vendors = append(vendors,
|
||||
openai.NewClient(),
|
||||
ollama.NewClient(),
|
||||
azure.NewClient(),
|
||||
gemini.NewClient(),
|
||||
anthropic.NewClient(),
|
||||
lmstudio.NewClient(),
|
||||
exolab.NewClient(),
|
||||
)
|
||||
|
||||
// Add all OpenAI-compatible providers
|
||||
for providerName := range openai_compatible.ProviderMap {
|
||||
provider, _ := openai_compatible.GetProviderByName(providerName)
|
||||
vendors = append(vendors, openai_compatible.NewClient(provider))
|
||||
}
|
||||
|
||||
// Sort vendors by name for consistent ordering (case-insensitive)
|
||||
sort.Slice(vendors, func(i, j int) bool {
|
||||
return strings.ToLower(vendors[i].GetName()) < strings.ToLower(vendors[j].GetName())
|
||||
})
|
||||
|
||||
// Add all sorted vendors to VendorsAll
|
||||
ret.VendorsAll.AddVendors(vendors...)
|
||||
_ = ret.Configure()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) ListVendors(out io.Writer) error {
|
||||
vendors := lo.Map(o.VendorsAll.Vendors, func(vendor ai.Vendor, _ int) string {
|
||||
return vendor.GetName()
|
||||
})
|
||||
fmt.Fprint(out, "Available Vendors:\n\n")
|
||||
for _, vendor := range vendors {
|
||||
fmt.Fprintf(out, "%s\n", vendor)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PluginRegistry struct {
|
||||
Db *fsdb.Db
|
||||
|
||||
VendorManager *ai.VendorsManager
|
||||
VendorsAll *ai.VendorsManager
|
||||
Defaults *tools.Defaults
|
||||
PatternsLoader *tools.PatternsLoader
|
||||
YouTube *youtube.YouTube
|
||||
Language *lang.Language
|
||||
Jina *jina.Client
|
||||
TemplateExtensions *template.ExtensionManager
|
||||
Strategies *strategy.StrategiesManager
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) SaveEnvFile() (err error) {
|
||||
// Now create the .env with all configured VendorsController info
|
||||
var envFileContent bytes.Buffer
|
||||
|
||||
o.Defaults.Settings.FillEnvFileContent(&envFileContent)
|
||||
o.PatternsLoader.SetupFillEnvFileContent(&envFileContent)
|
||||
o.Strategies.SetupFillEnvFileContent(&envFileContent)
|
||||
|
||||
for _, vendor := range o.VendorManager.Vendors {
|
||||
vendor.SetupFillEnvFileContent(&envFileContent)
|
||||
}
|
||||
|
||||
o.YouTube.SetupFillEnvFileContent(&envFileContent)
|
||||
o.Jina.SetupFillEnvFileContent(&envFileContent)
|
||||
o.Language.SetupFillEnvFileContent(&envFileContent)
|
||||
|
||||
err = o.Db.SaveEnv(envFileContent.String())
|
||||
return
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) Setup() (err error) {
|
||||
setupQuestion := plugins.NewSetupQuestion("Enter the number of the plugin to setup")
|
||||
groupsPlugins := common.NewGroupsItemsSelector("Available plugins (please configure all required plugins):",
|
||||
func(plugin plugins.Plugin) string {
|
||||
var configuredLabel string
|
||||
if plugin.IsConfigured() {
|
||||
configuredLabel = " (configured)"
|
||||
} else {
|
||||
configuredLabel = ""
|
||||
}
|
||||
return fmt.Sprintf("%v%v", plugin.GetSetupDescription(), configuredLabel)
|
||||
})
|
||||
|
||||
groupsPlugins.AddGroupItems("AI Vendors [at least one, required]", lo.Map(o.VendorsAll.Vendors,
|
||||
func(vendor ai.Vendor, _ int) plugins.Plugin {
|
||||
return vendor
|
||||
})...)
|
||||
|
||||
groupsPlugins.AddGroupItems("Tools", o.Defaults, o.Jina, o.Language, o.PatternsLoader, o.Strategies, o.YouTube)
|
||||
|
||||
for {
|
||||
groupsPlugins.Print(false)
|
||||
|
||||
if answerErr := setupQuestion.Ask("Plugin Number"); answerErr != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if setupQuestion.Value == "" {
|
||||
break
|
||||
}
|
||||
number, parseErr := strconv.Atoi(setupQuestion.Value)
|
||||
setupQuestion.Value = ""
|
||||
|
||||
if parseErr == nil {
|
||||
var plugin plugins.Plugin
|
||||
if _, plugin, err = groupsPlugins.GetGroupAndItemByItemNumber(number); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if pluginSetupErr := plugin.Setup(); pluginSetupErr != nil {
|
||||
println(pluginSetupErr.Error())
|
||||
} else {
|
||||
if err = o.SaveEnvFile(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := o.VendorManager.VendorsByName[plugin.GetName()]; !ok {
|
||||
var vendor ai.Vendor
|
||||
if vendor, ok = plugin.(ai.Vendor); ok {
|
||||
o.VendorManager.AddVendors(vendor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = o.SaveEnvFile()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) SetupVendor(vendorName string) (err error) {
|
||||
if err = o.VendorsAll.SetupVendor(vendorName, o.VendorManager.VendorsByName); err != nil {
|
||||
return
|
||||
}
|
||||
err = o.SaveEnvFile()
|
||||
return
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) ConfigureVendors() {
|
||||
o.VendorManager.Clear()
|
||||
for _, vendor := range o.VendorsAll.Vendors {
|
||||
if vendorErr := vendor.Configure(); vendorErr == nil {
|
||||
o.VendorManager.AddVendors(vendor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) GetModels() (ret *ai.VendorsModels, err error) {
|
||||
o.ConfigureVendors()
|
||||
ret, err = o.VendorManager.GetModels()
|
||||
return
|
||||
}
|
||||
|
||||
// Configure buildClient VendorsController based on the environment variables
|
||||
func (o *PluginRegistry) Configure() (err error) {
|
||||
o.ConfigureVendors()
|
||||
_ = o.Defaults.Configure()
|
||||
_ = o.PatternsLoader.Configure()
|
||||
|
||||
//YouTube and Jina are not mandatory, so ignore not configured error
|
||||
_ = o.YouTube.Configure()
|
||||
_ = o.Jina.Configure()
|
||||
_ = o.Language.Configure()
|
||||
return
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) GetChatter(model string, modelContextLength int, strategy string, stream bool, dryRun bool) (ret *Chatter, err error) {
|
||||
ret = &Chatter{
|
||||
db: o.Db,
|
||||
Stream: stream,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
|
||||
defaultModel := o.Defaults.Model.Value
|
||||
defaultModelContextLength, err := strconv.Atoi(o.Defaults.ModelContextLength.Value)
|
||||
defaultVendor := o.Defaults.Vendor.Value
|
||||
vendorManager := o.VendorManager
|
||||
|
||||
if err != nil {
|
||||
defaultModelContextLength = 0
|
||||
err = nil
|
||||
}
|
||||
|
||||
ret.modelContextLength = modelContextLength
|
||||
if ret.modelContextLength == 0 {
|
||||
ret.modelContextLength = defaultModelContextLength
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
ret.vendor = dryrun.NewClient()
|
||||
ret.model = model
|
||||
if ret.model == "" {
|
||||
ret.model = defaultModel
|
||||
}
|
||||
} else if model == "" {
|
||||
ret.vendor = vendorManager.FindByName(defaultVendor)
|
||||
ret.model = defaultModel
|
||||
} else {
|
||||
var models *ai.VendorsModels
|
||||
if models, err = vendorManager.GetModels(); err != nil {
|
||||
return
|
||||
}
|
||||
ret.vendor = vendorManager.FindByName(models.FindGroupsByItemFirst(model))
|
||||
ret.model = model
|
||||
}
|
||||
|
||||
if ret.vendor == nil {
|
||||
var errMsg string
|
||||
if defaultModel == "" || defaultVendor == "" {
|
||||
errMsg = "Please run, fabric --setup, and select default model and vendor."
|
||||
} else {
|
||||
errMsg = "could not find vendor."
|
||||
}
|
||||
err = fmt.Errorf(
|
||||
" Requested Model = %s\n Default Model = %s\n Default Vendor = %s.\n\n%s",
|
||||
model, defaultModel, defaultVendor, errMsg)
|
||||
return
|
||||
}
|
||||
ret.strategy = strategy
|
||||
return
|
||||
}
|
||||
21
core/plugin_registry_test.go
Normal file
21
core/plugin_registry_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/plugins/db/fsdb"
|
||||
)
|
||||
|
||||
func TestSaveEnvFile(t *testing.T) {
|
||||
db := fsdb.NewDb(os.TempDir())
|
||||
registry, err := NewPluginRegistry(db)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPluginRegistry() error = %v", err)
|
||||
}
|
||||
|
||||
err = registry.SaveEnvFile()
|
||||
if err != nil {
|
||||
t.Fatalf("SaveEnvFile() error = %v", err)
|
||||
}
|
||||
}
|
||||
2345
coverage.out
Normal file
2345
coverage.out
Normal file
File diff suppressed because it is too large
Load Diff
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
fabric-api:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./ENV:/root/.config/fabric/.env:ro
|
||||
environment:
|
||||
- GIN_MODE=release
|
||||
119
flake.lock
generated
Normal file
119
flake.lock
generated
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gomod2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1742209644,
|
||||
"narHash": "sha256-jMy1XqXqD0/tJprEbUmKilTkvbDY/C0ZGSsJJH4TNCE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "8f3534eb8f6c5c3fce799376dc3b91bae6b11884",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "gomod2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1745234285,
|
||||
"narHash": "sha256-GfpyMzxwkfgRVN0cTGQSkTC0OHhEkv3Jf6Tcjm//qZ0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c11863f1e964833214b767f4a369c6e6a7aba141",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1744961264,
|
||||
"narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "8d404a69efe76146368885110f29a2ca3700bee6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
80
flake.nix
Normal file
80
flake.nix
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
description = "Fabric is an open-source framework for augmenting humans using AI. It provides a modular framework for solving specific problems using a crowdsourced set of AI prompts that can be used anywhere";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
systems.url = "github:nix-systems/default";
|
||||
|
||||
treefmt-nix = {
|
||||
url = "github:numtide/treefmt-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
gomod2nix = {
|
||||
url = "github:nix-community/gomod2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
systems,
|
||||
treefmt-nix,
|
||||
gomod2nix,
|
||||
...
|
||||
}:
|
||||
let
|
||||
forAllSystems = nixpkgs.lib.genAttrs (import systems);
|
||||
|
||||
getGoVersion = system: nixpkgs.legacyPackages.${system}.go_1_24;
|
||||
|
||||
treefmtEval = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
treefmt-nix.lib.evalModule pkgs ./nix/treefmt.nix
|
||||
);
|
||||
in
|
||||
{
|
||||
formatter = forAllSystems (system: treefmtEval.${system}.config.build.wrapper);
|
||||
|
||||
checks = forAllSystems (system: {
|
||||
formatting = treefmtEval.${system}.config.build.check self;
|
||||
});
|
||||
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
goVersion = getGoVersion system;
|
||||
goEnv = gomod2nix.legacyPackages.${system}.mkGoEnv {
|
||||
pwd = ./.;
|
||||
go = goVersion;
|
||||
};
|
||||
in
|
||||
import ./nix/shell.nix {
|
||||
inherit pkgs goEnv goVersion;
|
||||
inherit (gomod2nix.legacyPackages.${system}) gomod2nix;
|
||||
}
|
||||
);
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
goVersion = getGoVersion system;
|
||||
in
|
||||
{
|
||||
default = self.packages.${system}.fabric;
|
||||
fabric = pkgs.callPackage ./nix/pkgs/fabric {
|
||||
go = goVersion;
|
||||
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
|
||||
};
|
||||
inherit (gomod2nix.legacyPackages.${system}) gomod2nix;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
105
go.mod
Normal file
105
go.mod
Normal file
@@ -0,0 +1,105 @@
|
||||
module github.com/danielmiessler/fabric
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/anaskhan96/soup v1.2.5
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/gabriel-vasile/mimetype v1.4.9
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-git/go-git/v5 v5.16.0
|
||||
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612
|
||||
github.com/google/generative-ai-go v0.19.0
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/ollama/ollama v0.6.6
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/sashabaranov/go-openai v1.38.2
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/text v0.24.0
|
||||
google.golang.org/api v0.230.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.120.1 // indirect
|
||||
cloud.google.com/go/ai v0.10.2 // indirect
|
||||
cloud.google.com/go/auth v0.16.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
||||
google.golang.org/grpc v1.72.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
330
go.sum
Normal file
330
go.sum
Normal file
@@ -0,0 +1,330 @@
|
||||
cloud.google.com/go v0.120.1 h1:Z+5V7yd383+9617XDCyszmK5E4wJRJL+tquMfDj9hLM=
|
||||
cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0rp8=
|
||||
cloud.google.com/go/ai v0.10.2 h1:5NHzmZlRs+3kvlsVdjT0cTnLrjQdROJ/8VOljVfs+8o=
|
||||
cloud.google.com/go/ai v0.10.2/go.mod h1:xZuZuE9d3RgsR132meCnPadiU9XV0qXjpLr+P4J46eE=
|
||||
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
|
||||
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
|
||||
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/anaskhan96/soup v1.2.5 h1:V/FHiusdTrPrdF4iA1YkVxsOpdNcgvqT1hG+YtcZ5hM=
|
||||
github.com/anaskhan96/soup v1.2.5/go.mod h1:6YnEp9A2yywlYdM4EgDz9NEHclocMepEtku7wg6Cq3s=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 h1:b5t1ZJMvV/l99y4jbz7kRFdUp3BSDkI8EhSlHczivtw=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
|
||||
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c h1:wpkoddUomPfHiOziHZixGO5ZBS73cKqVzZipfrLmO1w=
|
||||
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c/go.mod h1:oVDCh3qjJMLVUSILBRwrm+Bc6RNXGZYtoh9xdvf1ffM=
|
||||
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612 h1:BYLNYdZaepitbZreRIa9xeCQZocWmy/wj4cGIH0qyw0=
|
||||
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612/go.mod h1:wgqthQa8SAYs0yyljVeCOQlZ027VW5CmLsbi9jWC08c=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
|
||||
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ollama/ollama v0.6.6 h1:rnCQTSTiRD3Dsvd35dh2j2YB9DlQMFQR/y3XOhWZOmI=
|
||||
github.com/ollama/ollama v0.6.6/go.mod h1:pGgtoNyc9DdM6oZI6yMfI6jTk2Eh4c36c2GpfQCH7PY=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
|
||||
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
|
||||
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/sashabaranov/go-openai v1.38.2 h1:akrssjj+6DY3lWuDwHv6cBvJ8Z+FZDM9XEaaYFt0Auo=
|
||||
github.com/sashabaranov/go-openai v1.38.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
|
||||
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
@@ -1,91 +0,0 @@
|
||||
# Fabric Helpers
|
||||
|
||||
These are helper tools to work with Fabric. Examples include things like getting transcripts from media files, getting metadata about media, etc.
|
||||
|
||||
## yt (YouTube)
|
||||
|
||||
`yt` is a command that uses the YouTube API to pull transcripts, get video duration, and other functions. It's primary function is to get a transcript from a video that can then be stitched (piped) into other Fabric Patterns.
|
||||
|
||||
```bash
|
||||
usage: yt [-h] [--duration] [--transcript] [url]
|
||||
|
||||
vm (video meta) extracts metadata about a video, such as the transcript and the video's duration. By Daniel Miessler.
|
||||
|
||||
positional arguments:
|
||||
url YouTube video URL
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--duration Output only the duration
|
||||
--transcript Output only the transcript
|
||||
```
|
||||
|
||||
## ts (Audio transcriptions)
|
||||
|
||||
'ts' is a command that uses the OpenApi Whisper API to transcribe audio files. Due to the context window, this tool uses pydub to split the files into 10 minute segments. for more information on pydub, please refer https://github.com/jiaaro/pydub
|
||||
|
||||
### installation
|
||||
|
||||
```bash
|
||||
|
||||
mac:
|
||||
brew install ffmpeg
|
||||
|
||||
linux:
|
||||
apt install ffmpeg
|
||||
|
||||
windows:
|
||||
download instructions https://www.ffmpeg.org/download.html
|
||||
```
|
||||
|
||||
````bash
|
||||
ts -h
|
||||
usage: ts [-h] audio_file
|
||||
|
||||
Transcribe an audio file.
|
||||
|
||||
positional arguments:
|
||||
audio_file The path to the audio file to be transcribed.
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
## save
|
||||
|
||||
`save` is a "tee-like" utility to pipeline saving of content, while keeping the output stream intact. Can optionally generate "frontmatter" for PKM utilities like Obsidian via the
|
||||
"FABRIC_FRONTMATTER" environment variable
|
||||
|
||||
|
||||
|
||||
If you'd like to default variables, set them in `~/.config/fabric/.env`. `FABRIC_OUTPUT_PATH` needs to be set so `save` where to write. `FABRIC_FRONTMATTER_TAGS` is optional, but useful for tracking how tags have entered your PKM, if that's important to you.
|
||||
|
||||
### usage
|
||||
```bash
|
||||
usage: save [-h] [-t, TAG] [-n] [-s] [stub]
|
||||
|
||||
save: a "tee-like" utility to pipeline saving of content, while keeping the output stream intact. Can optionally generate "frontmatter" for PKM utilities like Obsidian via the
|
||||
"FABRIC_FRONTMATTER" environment variable
|
||||
|
||||
positional arguments:
|
||||
stub stub to describe your content. Use quotes if you have spaces. Resulting format is YYYY-MM-DD-stub.md by default
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-t, TAG, --tag TAG add an additional frontmatter tag. Use this argument multiple timesfor multiple tags
|
||||
-n, --nofabric don't use the fabric tags, only use tags from --tag
|
||||
-s, --silent don't use STDOUT for output, only save to the file
|
||||
````
|
||||
|
||||
### example
|
||||
|
||||
```bash
|
||||
echo test | save --tag extra-tag stub-for-name
|
||||
test
|
||||
|
||||
$ cat ~/obsidian/Fabric/2024-03-02-stub-for-name.md
|
||||
---
|
||||
generation_date: 2024-03-02 10:43
|
||||
tags: fabric-extraction stub-for-name extra-tag
|
||||
---
|
||||
test
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
from ..installer.client.cli.ts import main as main_ts
|
||||
from ..installer.client.cli.yt import main as main_yt
|
||||
from ..installer.client.cli.save import cli as main_save
|
||||
BIN
images/fabric-summarize.png
Normal file
BIN
images/fabric-summarize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 491 KiB |
@@ -1,5 +0,0 @@
|
||||
from .client.cli import main as cli, main_save, main_ts, main_yt
|
||||
from .server import (
|
||||
run_api_server,
|
||||
run_webui_server,
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
# The `fabric` client
|
||||
|
||||
Please see the main project's README.md for the latest documentation.
|
||||
@@ -1,4 +0,0 @@
|
||||
from .fabric import main
|
||||
from .yt import main as main_yt
|
||||
from .ts import main as main_ts
|
||||
from .save import cli as main_save
|
||||
@@ -1,89 +0,0 @@
|
||||
from crewai import Crew
|
||||
from textwrap import dedent
|
||||
from .trip_agents import TripAgents
|
||||
from .trip_tasks import TripTasks
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
current_directory = os.path.dirname(os.path.realpath(__file__))
|
||||
config_directory = os.path.expanduser("~/.config/fabric")
|
||||
env_file = os.path.join(config_directory, ".env")
|
||||
load_dotenv(env_file)
|
||||
os.environ['OPENAI_MODEL_NAME'] = 'gpt-4-0125-preview'
|
||||
|
||||
|
||||
class TripCrew:
|
||||
|
||||
def __init__(self, origin, cities, date_range, interests):
|
||||
self.cities = cities
|
||||
self.origin = origin
|
||||
self.interests = interests
|
||||
self.date_range = date_range
|
||||
|
||||
def run(self):
|
||||
agents = TripAgents()
|
||||
tasks = TripTasks()
|
||||
|
||||
city_selector_agent = agents.city_selection_agent()
|
||||
local_expert_agent = agents.local_expert()
|
||||
travel_concierge_agent = agents.travel_concierge()
|
||||
|
||||
identify_task = tasks.identify_task(
|
||||
city_selector_agent,
|
||||
self.origin,
|
||||
self.cities,
|
||||
self.interests,
|
||||
self.date_range
|
||||
)
|
||||
gather_task = tasks.gather_task(
|
||||
local_expert_agent,
|
||||
self.origin,
|
||||
self.interests,
|
||||
self.date_range
|
||||
)
|
||||
plan_task = tasks.plan_task(
|
||||
travel_concierge_agent,
|
||||
self.origin,
|
||||
self.interests,
|
||||
self.date_range
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[
|
||||
city_selector_agent, local_expert_agent, travel_concierge_agent
|
||||
],
|
||||
tasks=[identify_task, gather_task, plan_task],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
return result
|
||||
|
||||
|
||||
class planner_cli:
|
||||
def ask(self):
|
||||
print("## Welcome to Trip Planner Crew")
|
||||
print('-------------------------------')
|
||||
location = input(
|
||||
dedent("""
|
||||
From where will you be traveling from?
|
||||
"""))
|
||||
cities = input(
|
||||
dedent("""
|
||||
What are the cities options you are interested in visiting?
|
||||
"""))
|
||||
date_range = input(
|
||||
dedent("""
|
||||
What is the date range you are interested in traveling?
|
||||
"""))
|
||||
interests = input(
|
||||
dedent("""
|
||||
What are some of your high level interests and hobbies?
|
||||
"""))
|
||||
|
||||
trip_crew = TripCrew(location, cities, date_range, interests)
|
||||
result = trip_crew.run()
|
||||
print("\n\n########################")
|
||||
print("## Here is you Trip Plan")
|
||||
print("########################\n")
|
||||
print(result)
|
||||
@@ -1,38 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
from crewai import Agent, Task
|
||||
from langchain.tools import tool
|
||||
from unstructured.partition.html import partition_html
|
||||
|
||||
|
||||
class BrowserTools():
|
||||
|
||||
@tool("Scrape website content")
|
||||
def scrape_and_summarize_website(website):
|
||||
"""Useful to scrape and summarize a website content"""
|
||||
url = f"https://chrome.browserless.io/content?token={os.environ['BROWSERLESS_API_KEY']}"
|
||||
payload = json.dumps({"url": website})
|
||||
headers = {'cache-control': 'no-cache', 'content-type': 'application/json'}
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
elements = partition_html(text=response.text)
|
||||
content = "\n\n".join([str(el) for el in elements])
|
||||
content = [content[i:i + 8000] for i in range(0, len(content), 8000)]
|
||||
summaries = []
|
||||
for chunk in content:
|
||||
agent = Agent(
|
||||
role='Principal Researcher',
|
||||
goal=
|
||||
'Do amazing researches and summaries based on the content you are working with',
|
||||
backstory=
|
||||
"You're a Principal Researcher at a big company and you need to do a research about a given topic.",
|
||||
allow_delegation=False)
|
||||
task = Task(
|
||||
agent=agent,
|
||||
description=
|
||||
f'Analyze and summarize the content bellow, make sure to include the most relevant information in the summary, return only the summary nothing else.\n\nCONTENT\n----------\n{chunk}'
|
||||
)
|
||||
summary = task.execute()
|
||||
summaries.append(summary)
|
||||
return "\n\n".join(summaries)
|
||||
@@ -1,15 +0,0 @@
|
||||
from langchain.tools import tool
|
||||
|
||||
class CalculatorTools():
|
||||
|
||||
@tool("Make a calculation")
|
||||
def calculate(operation):
|
||||
"""Useful to perform any mathematical calculations,
|
||||
like sum, minus, multiplication, division, etc.
|
||||
The input to this tool should be a mathematical
|
||||
expression, a couple examples are `200*7` or `5000/2*10`
|
||||
"""
|
||||
try:
|
||||
return eval(operation)
|
||||
except SyntaxError:
|
||||
return "Error: Invalid syntax in mathematical expression"
|
||||
@@ -1,37 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
from langchain.tools import tool
|
||||
|
||||
|
||||
class SearchTools():
|
||||
|
||||
@tool("Search the internet")
|
||||
def search_internet(query):
|
||||
"""Useful to search the internet
|
||||
about a a given topic and return relevant results"""
|
||||
top_result_to_return = 4
|
||||
url = "https://google.serper.dev/search"
|
||||
payload = json.dumps({"q": query})
|
||||
headers = {
|
||||
'X-API-KEY': os.environ['SERPER_API_KEY'],
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
# check if there is an organic key
|
||||
if 'organic' not in response.json():
|
||||
return "Sorry, I couldn't find anything about that, there could be an error with you serper api key."
|
||||
else:
|
||||
results = response.json()['organic']
|
||||
string = []
|
||||
for result in results[:top_result_to_return]:
|
||||
try:
|
||||
string.append('\n'.join([
|
||||
f"Title: {result['title']}", f"Link: {result['link']}",
|
||||
f"Snippet: {result['snippet']}", "\n-----------------"
|
||||
]))
|
||||
except KeyError:
|
||||
next
|
||||
|
||||
return '\n'.join(string)
|
||||
@@ -1,45 +0,0 @@
|
||||
from crewai import Agent
|
||||
|
||||
from .tools.browser_tools import BrowserTools
|
||||
from .tools.calculator_tools import CalculatorTools
|
||||
from .tools.search_tools import SearchTools
|
||||
|
||||
|
||||
class TripAgents():
|
||||
|
||||
def city_selection_agent(self):
|
||||
return Agent(
|
||||
role='City Selection Expert',
|
||||
goal='Select the best city based on weather, season, and prices',
|
||||
backstory='An expert in analyzing travel data to pick ideal destinations',
|
||||
tools=[
|
||||
SearchTools.search_internet,
|
||||
BrowserTools.scrape_and_summarize_website,
|
||||
],
|
||||
verbose=True)
|
||||
|
||||
def local_expert(self):
|
||||
return Agent(
|
||||
role='Local Expert at this city',
|
||||
goal='Provide the BEST insights about the selected city',
|
||||
backstory="""A knowledgeable local guide with extensive information
|
||||
about the city, it's attractions and customs""",
|
||||
tools=[
|
||||
SearchTools.search_internet,
|
||||
BrowserTools.scrape_and_summarize_website,
|
||||
],
|
||||
verbose=True)
|
||||
|
||||
def travel_concierge(self):
|
||||
return Agent(
|
||||
role='Amazing Travel Concierge',
|
||||
goal="""Create the most amazing travel itineraries with budget and
|
||||
packing suggestions for the city""",
|
||||
backstory="""Specialist in travel planning and logistics with
|
||||
decades of experience""",
|
||||
tools=[
|
||||
SearchTools.search_internet,
|
||||
BrowserTools.scrape_and_summarize_website,
|
||||
CalculatorTools.calculate,
|
||||
],
|
||||
verbose=True)
|
||||
@@ -1,83 +0,0 @@
|
||||
from crewai import Task
|
||||
from textwrap import dedent
|
||||
from datetime import date
|
||||
|
||||
|
||||
class TripTasks():
|
||||
|
||||
def identify_task(self, agent, origin, cities, interests, range):
|
||||
return Task(description=dedent(f"""
|
||||
Analyze and select the best city for the trip based
|
||||
on specific criteria such as weather patterns, seasonal
|
||||
events, and travel costs. This task involves comparing
|
||||
multiple cities, considering factors like current weather
|
||||
conditions, upcoming cultural or seasonal events, and
|
||||
overall travel expenses.
|
||||
|
||||
Your final answer must be a detailed
|
||||
report on the chosen city, and everything you found out
|
||||
about it, including the actual flight costs, weather
|
||||
forecast and attractions.
|
||||
{self.__tip_section()}
|
||||
|
||||
Traveling from: {origin}
|
||||
City Options: {cities}
|
||||
Trip Date: {range}
|
||||
Traveler Interests: {interests}
|
||||
"""),
|
||||
agent=agent)
|
||||
|
||||
def gather_task(self, agent, origin, interests, range):
|
||||
return Task(description=dedent(f"""
|
||||
As a local expert on this city you must compile an
|
||||
in-depth guide for someone traveling there and wanting
|
||||
to have THE BEST trip ever!
|
||||
Gather information about key attractions, local customs,
|
||||
special events, and daily activity recommendations.
|
||||
Find the best spots to go to, the kind of place only a
|
||||
local would know.
|
||||
This guide should provide a thorough overview of what
|
||||
the city has to offer, including hidden gems, cultural
|
||||
hotspots, must-visit landmarks, weather forecasts, and
|
||||
high level costs.
|
||||
|
||||
The final answer must be a comprehensive city guide,
|
||||
rich in cultural insights and practical tips,
|
||||
tailored to enhance the travel experience.
|
||||
{self.__tip_section()}
|
||||
|
||||
Trip Date: {range}
|
||||
Traveling from: {origin}
|
||||
Traveler Interests: {interests}
|
||||
"""),
|
||||
agent=agent)
|
||||
|
||||
def plan_task(self, agent, origin, interests, range):
|
||||
return Task(description=dedent(f"""
|
||||
Expand this guide into a a full 7-day travel
|
||||
itinerary with detailed per-day plans, including
|
||||
weather forecasts, places to eat, packing suggestions,
|
||||
and a budget breakdown.
|
||||
|
||||
You MUST suggest actual places to visit, actual hotels
|
||||
to stay and actual restaurants to go to.
|
||||
|
||||
This itinerary should cover all aspects of the trip,
|
||||
from arrival to departure, integrating the city guide
|
||||
information with practical travel logistics.
|
||||
|
||||
Your final answer MUST be a complete expanded travel plan,
|
||||
formatted as markdown, encompassing a daily schedule,
|
||||
anticipated weather conditions, recommended clothing and
|
||||
items to pack, and a detailed budget, ensuring THE BEST
|
||||
TRIP EVER, Be specific and give it a reason why you picked
|
||||
# up each place, what make them special! {self.__tip_section()}
|
||||
|
||||
Trip Date: {range}
|
||||
Traveling from: {origin}
|
||||
Traveler Interests: {interests}
|
||||
"""),
|
||||
agent=agent)
|
||||
|
||||
def __tip_section(self):
|
||||
return "If you do your BEST WORK, I'll tip you $100!"
|
||||
@@ -1,160 +0,0 @@
|
||||
from .utils import Standalone, Update, Setup, Alias
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
script_directory = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="An open source framework for augmenting humans using AI."
|
||||
)
|
||||
parser.add_argument("--text", "-t", help="Text to extract summary from")
|
||||
parser.add_argument(
|
||||
"--copy", "-C", help="Copy the response to the clipboard", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--agents', '-a', choices=['trip_planner', 'ApiKeys'],
|
||||
help="Use an AI agent to help you with a task. Acceptable values are 'trip_planner' or 'ApiKeys'. This option cannot be used with any other flag."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="Save the response to a file",
|
||||
nargs="?",
|
||||
const="analyzepaper.txt",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stream",
|
||||
"-s",
|
||||
help="Use this option if you want to see the results in realtime. NOTE: You will not be able to pipe the output into another command.",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list", "-l", help="List available patterns", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--update", "-u", help="Update patterns. NOTE: This will revert the default model to gpt4-turbo. please run --changeDefaultModel to once again set default model", action="store_true")
|
||||
parser.add_argument("--pattern", "-p", help="The pattern (prompt) to use")
|
||||
parser.add_argument(
|
||||
"--setup", help="Set up your fabric instance", action="store_true"
|
||||
)
|
||||
parser.add_argument('--changeDefaultModel',
|
||||
help="Change the default model. For a list of available models, use the --listmodels flag.")
|
||||
|
||||
parser.add_argument(
|
||||
"--model", "-m", help="Select the model to use. NOTE: Will not work if you have set a default model. please use --clear to clear persistence before using this flag"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--listmodels", help="List all available models", action="store_true"
|
||||
)
|
||||
parser.add_argument('--remoteOllamaServer',
|
||||
help='The URL of the remote ollamaserver to use. ONLY USE THIS if you are using a local ollama server in an non-deault location or port')
|
||||
parser.add_argument('--context', '-c',
|
||||
help="Use Context file (context.md) to add context to your pattern", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
home_holder = os.path.expanduser("~")
|
||||
config = os.path.join(home_holder, ".config", "fabric")
|
||||
config_patterns_directory = os.path.join(config, "patterns")
|
||||
config_context = os.path.join(config, "context.md")
|
||||
env_file = os.path.join(config, ".env")
|
||||
if not os.path.exists(config):
|
||||
os.makedirs(config)
|
||||
if args.setup:
|
||||
Setup().run()
|
||||
Alias().execute()
|
||||
sys.exit()
|
||||
if not os.path.exists(env_file) or not os.path.exists(config_patterns_directory):
|
||||
print("Please run --setup to set up your API key and download patterns.")
|
||||
sys.exit()
|
||||
if not os.path.exists(config_patterns_directory):
|
||||
Update()
|
||||
Alias()
|
||||
sys.exit()
|
||||
if args.changeDefaultModel:
|
||||
Setup().default_model(args.changeDefaultModel)
|
||||
sys.exit()
|
||||
if args.agents:
|
||||
# Handle the agents logic
|
||||
if args.agents == 'trip_planner':
|
||||
from .agents.trip_planner.main import planner_cli
|
||||
tripcrew = planner_cli()
|
||||
tripcrew.ask()
|
||||
sys.exit()
|
||||
elif args.agents == 'ApiKeys':
|
||||
from .utils import AgentSetup
|
||||
AgentSetup().run()
|
||||
sys.exit()
|
||||
if args.update:
|
||||
Update()
|
||||
Alias()
|
||||
sys.exit()
|
||||
if args.context:
|
||||
if not os.path.exists(os.path.join(config, "context.md")):
|
||||
print("Please create a context.md file in ~/.config/fabric")
|
||||
sys.exit()
|
||||
standalone = Standalone(args, args.pattern)
|
||||
if args.list:
|
||||
try:
|
||||
direct = sorted(os.listdir(config_patterns_directory))
|
||||
for d in direct:
|
||||
print(d)
|
||||
sys.exit()
|
||||
except FileNotFoundError:
|
||||
print("No patterns found")
|
||||
sys.exit()
|
||||
if args.listmodels:
|
||||
gptmodels, localmodels, claudemodels = standalone.fetch_available_models()
|
||||
print("GPT Models:")
|
||||
for model in gptmodels:
|
||||
print(model)
|
||||
print("\nLocal Models:")
|
||||
for model in localmodels:
|
||||
print(model)
|
||||
print("\nClaude Models:")
|
||||
for model in claudemodels:
|
||||
print(model)
|
||||
sys.exit()
|
||||
if args.text is not None:
|
||||
text = args.text
|
||||
else:
|
||||
text = standalone.get_cli_input()
|
||||
if args.stream and not args.context:
|
||||
if args.remoteOllamaServer:
|
||||
standalone.streamMessage(text, host=args.remoteOllamaServer)
|
||||
else:
|
||||
standalone.streamMessage(text)
|
||||
sys.exit()
|
||||
if args.stream and args.context:
|
||||
with open(config_context, "r") as f:
|
||||
context = f.read()
|
||||
if args.remoteOllamaServer:
|
||||
standalone.streamMessage(
|
||||
text, context=context, host=args.remoteOllamaServer)
|
||||
else:
|
||||
standalone.streamMessage(text, context=context)
|
||||
sys.exit()
|
||||
elif args.context:
|
||||
with open(config_context, "r") as f:
|
||||
context = f.read()
|
||||
if args.remoteOllamaServer:
|
||||
standalone.sendMessage(
|
||||
text, context=context, host=args.remoteOllamaServer)
|
||||
else:
|
||||
standalone.sendMessage(text, context=context)
|
||||
sys.exit()
|
||||
else:
|
||||
if args.remoteOllamaServer:
|
||||
standalone.sendMessage(text, host=args.remoteOllamaServer)
|
||||
else:
|
||||
standalone.sendMessage(text)
|
||||
sys.exit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,120 +0,0 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
DEFAULT_CONFIG = "~/.config/fabric/.env"
|
||||
PATH_KEY = "FABRIC_OUTPUT_PATH"
|
||||
FM_KEY = "FABRIC_FRONTMATTER_TAGS"
|
||||
DATE_FORMAT = "%Y-%m-%d"
|
||||
load_dotenv(os.path.expanduser(DEFAULT_CONFIG))
|
||||
|
||||
|
||||
def main(tag, tags, silent, fabric):
|
||||
out = os.getenv(PATH_KEY)
|
||||
if out is None:
|
||||
print(f"'{PATH_KEY}' not set in {DEFAULT_CONFIG} or in your environment.")
|
||||
sys.exit(1)
|
||||
|
||||
out = os.path.expanduser(out)
|
||||
|
||||
if not os.path.isdir(out):
|
||||
print(f"'{out}' does not exist. Create it and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
if not out.endswith("/"):
|
||||
out += "/"
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print(f"'{sys.argv[0]}' takes a single argument to tag your summary")
|
||||
sys.exit(1)
|
||||
|
||||
yyyymmdd = datetime.now().strftime(DATE_FORMAT)
|
||||
target = f"{out}{yyyymmdd}-{tag}.md"
|
||||
|
||||
# don't clobber existing files- add an incremented number to the end instead
|
||||
would_clobber = True
|
||||
inc = 0
|
||||
while would_clobber:
|
||||
if inc > 0:
|
||||
target = f"{out}{yyyymmdd}-{tag}-{inc}.md"
|
||||
if os.path.exists(target):
|
||||
inc += 1
|
||||
else:
|
||||
would_clobber = False
|
||||
|
||||
# YAML frontmatter stubs for things like Obsidian
|
||||
# Prevent a NoneType ending up in the tags
|
||||
frontmatter_tags = ""
|
||||
if fabric:
|
||||
frontmatter_tags = os.getenv(FM_KEY)
|
||||
|
||||
with open(target, "w") as fp:
|
||||
if frontmatter_tags or len(tags) != 0:
|
||||
fp.write("---\n")
|
||||
now = datetime.now().strftime(f"{DATE_FORMAT} %H:%M")
|
||||
fp.write(f"generation_date: {now}\n")
|
||||
fp.write(f"tags: {frontmatter_tags} {tag} {' '.join(tags)}\n")
|
||||
fp.write("---\n")
|
||||
|
||||
# function like 'tee' and split the output to a file and STDOUT
|
||||
for line in sys.stdin:
|
||||
if not silent:
|
||||
print(line, end="")
|
||||
fp.write(line)
|
||||
|
||||
|
||||
def cli():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
'save: a "tee-like" utility to pipeline saving of content, '
|
||||
"while keeping the output stream intact. Can optionally generate "
|
||||
'"frontmatter" for PKM utilities like Obsidian via the '
|
||||
'"FABRIC_FRONTMATTER" environment variable'
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"stub",
|
||||
nargs="?",
|
||||
help=(
|
||||
"stub to describe your content. Use quotes if you have spaces. "
|
||||
"Resulting format is YYYY-MM-DD-stub.md by default"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t,",
|
||||
"--tag",
|
||||
required=False,
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"add an additional frontmatter tag. Use this argument multiple times"
|
||||
"for multiple tags"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--nofabric",
|
||||
required=False,
|
||||
action="store_false",
|
||||
help="don't use the fabric tags, only use tags from --tag",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--silent",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="don't use STDOUT for output, only save to the file",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.stub:
|
||||
main(args.stub, args.tag, args.silent, args.nofabric)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -1,110 +0,0 @@
|
||||
from dotenv import load_dotenv
|
||||
from pydub import AudioSegment
|
||||
from openai import OpenAI
|
||||
import os
|
||||
import argparse
|
||||
|
||||
|
||||
class Whisper:
|
||||
def __init__(self):
|
||||
env_file = os.path.expanduser("~/.config/fabric/.env")
|
||||
load_dotenv(env_file)
|
||||
try:
|
||||
apikey = os.environ["OPENAI_API_KEY"]
|
||||
self.client = OpenAI()
|
||||
self.client.api_key = apikey
|
||||
except KeyError:
|
||||
print("OPENAI_API_KEY not found in environment variables.")
|
||||
|
||||
except FileNotFoundError:
|
||||
print("No API key found. Use the --apikey option to set the key")
|
||||
self.whole_response = []
|
||||
|
||||
def split_audio(self, file_path):
|
||||
"""
|
||||
Splits the audio file into segments of the given length.
|
||||
|
||||
Args:
|
||||
- file_path: The path to the audio file.
|
||||
- segment_length_ms: Length of each segment in milliseconds.
|
||||
|
||||
Returns:
|
||||
- A list of audio segments.
|
||||
"""
|
||||
audio = AudioSegment.from_file(file_path)
|
||||
segments = []
|
||||
segment_length_ms = 10 * 60 * 1000 # 10 minutes in milliseconds
|
||||
for start_ms in range(0, len(audio), segment_length_ms):
|
||||
end_ms = start_ms + segment_length_ms
|
||||
segment = audio[start_ms:end_ms]
|
||||
segments.append(segment)
|
||||
|
||||
return segments
|
||||
|
||||
def process_segment(self, segment):
|
||||
""" Transcribe an audio file and print the transcript.
|
||||
|
||||
Args:
|
||||
audio_file (str): The path to the audio file to be transcribed.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
try:
|
||||
# if audio_file.startswith("http"):
|
||||
# response = requests.get(audio_file)
|
||||
# response.raise_for_status()
|
||||
# with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
# f.write(response.content)
|
||||
# audio_file = f.name
|
||||
audio_file = open(segment, "rb")
|
||||
response = self.client.audio.transcriptions.create(
|
||||
model="whisper-1",
|
||||
file=audio_file
|
||||
)
|
||||
self.whole_response.append(response.text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def process_file(self, audio_file):
|
||||
""" Transcribe an audio file and print the transcript.
|
||||
|
||||
Args:
|
||||
audio_file (str): The path to the audio file to be transcribed.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
try:
|
||||
# if audio_file.startswith("http"):
|
||||
# response = requests.get(audio_file)
|
||||
# response.raise_for_status()
|
||||
# with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
# f.write(response.content)
|
||||
# audio_file = f.name
|
||||
|
||||
segments = self.split_audio(audio_file)
|
||||
for i, segment in enumerate(segments):
|
||||
segment_file_path = f"segment_{i}.mp3"
|
||||
segment.export(segment_file_path, format="mp3")
|
||||
self.process_segment(segment_file_path)
|
||||
print(' '.join(self.whole_response))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Transcribe an audio file.")
|
||||
parser.add_argument(
|
||||
"audio_file", help="The path to the audio file to be transcribed.")
|
||||
args = parser.parse_args()
|
||||
whisper = Whisper()
|
||||
whisper.process_file(args.audio_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,662 +0,0 @@
|
||||
import requests
|
||||
import os
|
||||
from openai import OpenAI, APIConnectionError
|
||||
import asyncio
|
||||
import pyperclip
|
||||
import sys
|
||||
import platform
|
||||
from dotenv import load_dotenv
|
||||
import zipfile
|
||||
import tempfile
|
||||
import re
|
||||
import shutil
|
||||
|
||||
current_directory = os.path.dirname(os.path.realpath(__file__))
|
||||
config_directory = os.path.expanduser("~/.config/fabric")
|
||||
env_file = os.path.join(config_directory, ".env")
|
||||
|
||||
|
||||
class Standalone:
|
||||
def __init__(self, args, pattern="", env_file="~/.config/fabric/.env"):
|
||||
""" Initialize the class with the provided arguments and environment file.
|
||||
|
||||
Args:
|
||||
args: The arguments for initialization.
|
||||
pattern: The pattern to be used (default is an empty string).
|
||||
env_file: The path to the environment file (default is "~/.config/fabric/.env").
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
KeyError: If the "OPENAI_API_KEY" is not found in the environment variables.
|
||||
FileNotFoundError: If no API key is found in the environment variables.
|
||||
"""
|
||||
|
||||
# Expand the tilde to the full path
|
||||
env_file = os.path.expanduser(env_file)
|
||||
load_dotenv(env_file)
|
||||
assert 'OPENAI_API_KEY' in os.environ, "Error: OPENAI_API_KEY not found in environment variables. Please run fabric --setup and add a key."
|
||||
api_key = os.environ['OPENAI_API_KEY']
|
||||
base_url = os.environ.get(
|
||||
'OPENAI_BASE_URL', 'https://api.openai.com/v1/')
|
||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
||||
self.local = False
|
||||
self.config_pattern_directory = config_directory
|
||||
self.pattern = pattern
|
||||
self.args = args
|
||||
self.model = None
|
||||
if args.model:
|
||||
self.model = args.model
|
||||
else:
|
||||
try:
|
||||
self.model = os.environ["DEFAULT_MODEL"]
|
||||
except:
|
||||
self.model = 'gpt-4-turbo-preview'
|
||||
self.claude = False
|
||||
sorted_gpt_models, ollamaList, claudeList = self.fetch_available_models()
|
||||
self.local = self.model in ollamaList
|
||||
self.claude = self.model in claudeList
|
||||
|
||||
async def localChat(self, messages, host=''):
|
||||
from ollama import AsyncClient
|
||||
response = None
|
||||
if host:
|
||||
response = await AsyncClient(host=host).chat(model=self.model, messages=messages, host=host)
|
||||
else:
|
||||
response = await AsyncClient().chat(model=self.model, messages=messages)
|
||||
print(response['message']['content'])
|
||||
copy = self.args.copy
|
||||
if copy:
|
||||
pyperclip.copy(response['message']['content'])
|
||||
|
||||
async def localStream(self, messages, host=''):
|
||||
from ollama import AsyncClient
|
||||
if host:
|
||||
async for part in await AsyncClient(host=host).chat(model=self.model, messages=messages, stream=True, host=host):
|
||||
print(part['message']['content'], end='', flush=True)
|
||||
else:
|
||||
async for part in await AsyncClient().chat(model=self.model, messages=messages, stream=True):
|
||||
print(part['message']['content'], end='', flush=True)
|
||||
|
||||
async def claudeStream(self, system, user):
|
||||
from anthropic import AsyncAnthropic
|
||||
self.claudeApiKey = os.environ["CLAUDE_API_KEY"]
|
||||
Streamingclient = AsyncAnthropic(api_key=self.claudeApiKey)
|
||||
async with Streamingclient.messages.stream(
|
||||
max_tokens=4096,
|
||||
system=system,
|
||||
messages=[user],
|
||||
model=self.model, temperature=0.0, top_p=1.0
|
||||
) as stream:
|
||||
async for text in stream.text_stream:
|
||||
print(text, end="", flush=True)
|
||||
print()
|
||||
|
||||
message = await stream.get_final_message()
|
||||
|
||||
async def claudeChat(self, system, user, copy=False):
|
||||
from anthropic import Anthropic
|
||||
self.claudeApiKey = os.environ["CLAUDE_API_KEY"]
|
||||
client = Anthropic(api_key=self.claudeApiKey)
|
||||
message = client.messages.create(
|
||||
max_tokens=4096,
|
||||
system=system,
|
||||
messages=[user],
|
||||
model=self.model,
|
||||
temperature=0.0, top_p=1.0
|
||||
)
|
||||
print(message.content[0].text)
|
||||
copy = self.args.copy
|
||||
if copy:
|
||||
pyperclip.copy(message.content[0].text)
|
||||
|
||||
def streamMessage(self, input_data: str, context="", host=''):
|
||||
""" Stream a message and handle exceptions.
|
||||
|
||||
Args:
|
||||
input_data (str): The input data for the message.
|
||||
|
||||
Returns:
|
||||
None: If the pattern is not found.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the pattern file is not found.
|
||||
"""
|
||||
|
||||
wisdomFilePath = os.path.join(
|
||||
config_directory, f"patterns/{self.pattern}/system.md"
|
||||
)
|
||||
user_message = {"role": "user", "content": f"{input_data}"}
|
||||
wisdom_File = os.path.join(current_directory, wisdomFilePath)
|
||||
system = ""
|
||||
buffer = ""
|
||||
if self.pattern:
|
||||
try:
|
||||
with open(wisdom_File, "r") as f:
|
||||
if context:
|
||||
system = context + '\n\n' + f.read()
|
||||
else:
|
||||
system = f.read()
|
||||
system_message = {"role": "system", "content": system}
|
||||
messages = [system_message, user_message]
|
||||
except FileNotFoundError:
|
||||
print("pattern not found")
|
||||
return
|
||||
else:
|
||||
if context:
|
||||
messages = [
|
||||
{"role": "system", "content": context}, user_message]
|
||||
else:
|
||||
messages = [user_message]
|
||||
try:
|
||||
if self.local:
|
||||
if host:
|
||||
asyncio.run(self.localStream(messages, host=host))
|
||||
else:
|
||||
asyncio.run(self.localStream(messages))
|
||||
elif self.claude:
|
||||
from anthropic import AsyncAnthropic
|
||||
asyncio.run(self.claudeStream(system, user_message))
|
||||
else:
|
||||
stream = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
top_p=1,
|
||||
frequency_penalty=0.1,
|
||||
presence_penalty=0.1,
|
||||
stream=True,
|
||||
)
|
||||
for chunk in stream:
|
||||
if chunk.choices[0].delta.content is not None:
|
||||
char = chunk.choices[0].delta.content
|
||||
buffer += char
|
||||
if char not in ["\n", " "]:
|
||||
print(char, end="")
|
||||
elif char == " ":
|
||||
print(" ", end="") # Explicitly handle spaces
|
||||
elif char == "\n":
|
||||
print() # Handle newlines
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
if "All connection attempts failed" in str(e):
|
||||
print(
|
||||
"Error: cannot connect to llama2. If you have not already, please visit https://ollama.com for installation instructions")
|
||||
if "CLAUDE_API_KEY" in str(e):
|
||||
print(
|
||||
"Error: CLAUDE_API_KEY not found in environment variables. Please run --setup and add the key")
|
||||
if "overloaded_error" in str(e):
|
||||
print(
|
||||
"Error: Fabric is working fine, but claude is overloaded. Please try again later.")
|
||||
else:
|
||||
print(f"Error: {e}")
|
||||
print(e)
|
||||
if self.args.copy:
|
||||
pyperclip.copy(buffer)
|
||||
if self.args.output:
|
||||
with open(self.args.output, "w") as f:
|
||||
f.write(buffer)
|
||||
|
||||
def sendMessage(self, input_data: str, context="", host=''):
|
||||
""" Send a message using the input data and generate a response.
|
||||
|
||||
Args:
|
||||
input_data (str): The input data to be sent as a message.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the specified pattern file is not found.
|
||||
"""
|
||||
|
||||
wisdomFilePath = os.path.join(
|
||||
config_directory, f"patterns/{self.pattern}/system.md"
|
||||
)
|
||||
user_message = {"role": "user", "content": f"{input_data}"}
|
||||
wisdom_File = os.path.join(current_directory, wisdomFilePath)
|
||||
system = ""
|
||||
if self.pattern:
|
||||
try:
|
||||
with open(wisdom_File, "r") as f:
|
||||
if context:
|
||||
system = context + '\n\n' + f.read()
|
||||
else:
|
||||
system = f.read()
|
||||
system_message = {"role": "system", "content": system}
|
||||
messages = [system_message, user_message]
|
||||
except FileNotFoundError:
|
||||
print("pattern not found")
|
||||
return
|
||||
else:
|
||||
if context:
|
||||
messages = [
|
||||
{'role': 'system', 'content': context}, user_message]
|
||||
else:
|
||||
messages = [user_message]
|
||||
try:
|
||||
if self.local:
|
||||
if host:
|
||||
asyncio.run(self.localChat(messages, host=host))
|
||||
else:
|
||||
asyncio.run(self.localChat(messages))
|
||||
elif self.claude:
|
||||
asyncio.run(self.claudeChat(system, user_message))
|
||||
else:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
top_p=1,
|
||||
frequency_penalty=0.1,
|
||||
presence_penalty=0.1,
|
||||
)
|
||||
print(response.choices[0].message.content)
|
||||
if self.args.copy:
|
||||
pyperclip.copy(response.choices[0].message.content)
|
||||
if self.args.output:
|
||||
with open(self.args.output, "w") as f:
|
||||
f.write(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
if "All connection attempts failed" in str(e):
|
||||
print(
|
||||
"Error: cannot connect to llama2. If you have not already, please visit https://ollama.com for installation instructions")
|
||||
if "CLAUDE_API_KEY" in str(e):
|
||||
print(
|
||||
"Error: CLAUDE_API_KEY not found in environment variables. Please run --setup and add the key")
|
||||
if "overloaded_error" in str(e):
|
||||
print(
|
||||
"Error: Fabric is working fine, but claude is overloaded. Please try again later.")
|
||||
if "Attempted to call a sync iterator on an async stream" in str(e):
|
||||
print("Error: There is a problem connecting fabric with your local ollama installation. Please visit https://ollama.com for installation instructions. It is possible that you have chosen the wrong model. Please run fabric --listmodels to see the available models and choose the right one with fabric --model <model> or fabric --changeDefaultModel. If this does not work. Restart your computer (always a good idea) and try again. If you are still having problems, please visit https://ollama.com for installation instructions.")
|
||||
else:
|
||||
print(f"Error: {e}")
|
||||
print(e)
|
||||
|
||||
def fetch_available_models(self):
|
||||
gptlist = []
|
||||
fullOllamaList = []
|
||||
claudeList = ['claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
'claude-3-haiku-20240307',
|
||||
'claude-2.1']
|
||||
try:
|
||||
models = [model.id.strip()
|
||||
for model in self.client.models.list().data]
|
||||
except APIConnectionError as e:
|
||||
if getattr(e.__cause__, 'args', [''])[0] == "Illegal header value b'Bearer '":
|
||||
print("Error: Cannot connect to the OpenAI API Server because the API key is not set. Please run fabric --setup and add a key.")
|
||||
|
||||
else:
|
||||
print(
|
||||
f"Error: {e.message} trying to access {e.request.url}: {getattr(e.__cause__, 'args', [''])}")
|
||||
sys.exit()
|
||||
except Exception as e:
|
||||
print(f"Error: {getattr(e.__context__, 'args', [''])[0]}")
|
||||
sys.exit()
|
||||
if "/" in models[0] or "\\" in models[0]:
|
||||
# lmstudio returns full paths to models. Iterate and truncate everything before and including the last slash
|
||||
gptlist = [item[item.rfind(
|
||||
"/") + 1:] if "/" in item else item[item.rfind("\\") + 1:] for item in models]
|
||||
else:
|
||||
# Keep items that start with "gpt"
|
||||
gptlist = [item.strip()
|
||||
for item in models if item.startswith("gpt")]
|
||||
gptlist.sort()
|
||||
import ollama
|
||||
try:
|
||||
default_modelollamaList = ollama.list()['models']
|
||||
for model in default_modelollamaList:
|
||||
fullOllamaList.append(model['name'])
|
||||
except:
|
||||
fullOllamaList = []
|
||||
return gptlist, fullOllamaList, claudeList
|
||||
|
||||
def get_cli_input(self):
|
||||
""" aided by ChatGPT; uses platform library
|
||||
accepts either piped input or console input
|
||||
from either Windows or Linux
|
||||
|
||||
Args:
|
||||
none
|
||||
Returns:
|
||||
string from either user or pipe
|
||||
"""
|
||||
system = platform.system()
|
||||
if system == 'Windows':
|
||||
if not sys.stdin.isatty(): # Check if input is being piped
|
||||
return sys.stdin.read().strip() # Read piped input
|
||||
else:
|
||||
# Prompt user for input from console
|
||||
return input("Enter Question: ")
|
||||
else:
|
||||
return sys.stdin.read()
|
||||
|
||||
|
||||
class Update:
|
||||
def __init__(self):
|
||||
"""Initialize the object with default values."""
|
||||
self.repo_zip_url = "https://github.com/danielmiessler/fabric/archive/refs/heads/main.zip"
|
||||
self.config_directory = os.path.expanduser("~/.config/fabric")
|
||||
self.pattern_directory = os.path.join(
|
||||
self.config_directory, "patterns")
|
||||
os.makedirs(self.pattern_directory, exist_ok=True)
|
||||
print("Updating patterns...")
|
||||
self.update_patterns() # Start the update process immediately
|
||||
|
||||
def update_patterns(self):
|
||||
"""Update the patterns by downloading the zip from GitHub and extracting it."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
zip_path = os.path.join(temp_dir, "repo.zip")
|
||||
self.download_zip(self.repo_zip_url, zip_path)
|
||||
extracted_folder_path = self.extract_zip(zip_path, temp_dir)
|
||||
# The patterns folder will be inside "fabric-main" after extraction
|
||||
patterns_source_path = os.path.join(
|
||||
extracted_folder_path, "fabric-main", "patterns")
|
||||
if os.path.exists(patterns_source_path):
|
||||
# If the patterns directory already exists, remove it before copying over the new one
|
||||
if os.path.exists(self.pattern_directory):
|
||||
old_pattern_contents = os.listdir(self.pattern_directory)
|
||||
new_pattern_contents = os.listdir(patterns_source_path)
|
||||
custom_patterns = []
|
||||
for pattern in old_pattern_contents:
|
||||
if pattern not in new_pattern_contents:
|
||||
custom_patterns.append(pattern)
|
||||
if custom_patterns:
|
||||
for pattern in custom_patterns:
|
||||
custom_path = os.path.join(
|
||||
self.pattern_directory, pattern)
|
||||
shutil.move(custom_path, patterns_source_path)
|
||||
shutil.rmtree(self.pattern_directory)
|
||||
shutil.copytree(patterns_source_path, self.pattern_directory)
|
||||
print("Patterns updated successfully.")
|
||||
else:
|
||||
print("Patterns folder not found in the downloaded zip.")
|
||||
|
||||
def download_zip(self, url, save_path):
|
||||
"""Download the zip file from the specified URL."""
|
||||
response = requests.get(url)
|
||||
response.raise_for_status() # Check if the download was successful
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
print("Downloaded zip file successfully.")
|
||||
|
||||
def extract_zip(self, zip_path, extract_to):
|
||||
"""Extract the zip file to the specified directory."""
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_to)
|
||||
print("Extracted zip file successfully.")
|
||||
return extract_to # Return the path to the extracted contents
|
||||
|
||||
|
||||
class Alias:
|
||||
def __init__(self):
|
||||
self.config_files = []
|
||||
self.home_directory = os.path.expanduser("~")
|
||||
patternsFolder = os.path.join(
|
||||
self.home_directory, ".config/fabric/patterns")
|
||||
self.patterns = os.listdir(patternsFolder)
|
||||
|
||||
def execute(self):
|
||||
with open(os.path.join(self.home_directory, ".config/fabric/fabric-bootstrap.inc"), "w") as w:
|
||||
for pattern in self.patterns:
|
||||
w.write(f"alias {pattern}='fabric --pattern {pattern}'\n")
|
||||
|
||||
|
||||
class Setup:
|
||||
def __init__(self):
|
||||
""" Initialize the object.
|
||||
|
||||
Raises:
|
||||
OSError: If there is an error in creating the pattern directory.
|
||||
"""
|
||||
|
||||
self.config_directory = os.path.expanduser("~/.config/fabric")
|
||||
self.pattern_directory = os.path.join(
|
||||
self.config_directory, "patterns")
|
||||
os.makedirs(self.pattern_directory, exist_ok=True)
|
||||
self.shconfigs = []
|
||||
home = os.path.expanduser("~")
|
||||
if os.path.exists(os.path.join(home, ".bashrc")):
|
||||
self.shconfigs.append(os.path.join(home, ".bashrc"))
|
||||
if os.path.exists(os.path.join(home, ".bash_profile")):
|
||||
self.shconfigs.append(os.path.join(home, ".bash_profile"))
|
||||
if os.path.exists(os.path.join(home, ".zshrc")):
|
||||
self.shconfigs.append(os.path.join(home, ".zshrc"))
|
||||
self.env_file = os.path.join(self.config_directory, ".env")
|
||||
self.gptlist = []
|
||||
self.fullOllamaList = []
|
||||
self.claudeList = ['claude-3-opus-20240229']
|
||||
load_dotenv(self.env_file)
|
||||
try:
|
||||
openaiapikey = os.environ["OPENAI_API_KEY"]
|
||||
self.openaiapi_key = openaiapikey
|
||||
except:
|
||||
pass
|
||||
|
||||
def update_shconfigs(self):
|
||||
bootstrap_file = os.path.join(
|
||||
self.config_directory, "fabric-bootstrap.inc")
|
||||
sourceLine = f'if [ -f "{bootstrap_file}" ]; then . "{bootstrap_file}"; fi'
|
||||
for config in self.shconfigs:
|
||||
lines = None
|
||||
with open(config, 'r') as f:
|
||||
lines = f.readlines()
|
||||
with open(config, 'w') as f:
|
||||
for line in lines:
|
||||
if sourceLine not in line:
|
||||
f.write(line)
|
||||
f.write(sourceLine)
|
||||
|
||||
def api_key(self, api_key):
|
||||
""" Set the OpenAI API key in the environment file.
|
||||
|
||||
Args:
|
||||
api_key (str): The API key to be set.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
OSError: If the environment file does not exist or cannot be accessed.
|
||||
"""
|
||||
api_key = api_key.strip()
|
||||
if not os.path.exists(self.env_file) and api_key:
|
||||
with open(self.env_file, "w") as f:
|
||||
f.write(f"OPENAI_API_KEY={api_key}\n")
|
||||
print(f"OpenAI API key set to {api_key}")
|
||||
elif api_key:
|
||||
# erase the line OPENAI_API_KEY=key and write the new key
|
||||
with open(self.env_file, "r") as f:
|
||||
lines = f.readlines()
|
||||
with open(self.env_file, "w") as f:
|
||||
for line in lines:
|
||||
if "OPENAI_API_KEY" not in line:
|
||||
f.write(line)
|
||||
f.write(f"OPENAI_API_KEY={api_key}\n")
|
||||
|
||||
def claude_key(self, claude_key):
|
||||
""" Set the Claude API key in the environment file.
|
||||
|
||||
Args:
|
||||
claude_key (str): The API key to be set.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
OSError: If the environment file does not exist or cannot be accessed.
|
||||
"""
|
||||
claude_key = claude_key.strip()
|
||||
if os.path.exists(self.env_file) and claude_key:
|
||||
with open(self.env_file, "r") as f:
|
||||
lines = f.readlines()
|
||||
with open(self.env_file, "w") as f:
|
||||
for line in lines:
|
||||
if "CLAUDE_API_KEY" not in line:
|
||||
f.write(line)
|
||||
f.write(f"CLAUDE_API_KEY={claude_key}\n")
|
||||
elif claude_key:
|
||||
with open(self.env_file, "w") as f:
|
||||
f.write(f"CLAUDE_API_KEY={claude_key}\n")
|
||||
|
||||
def youtube_key(self, youtube_key):
|
||||
""" Set the YouTube API key in the environment file.
|
||||
|
||||
Args:
|
||||
youtube_key (str): The API key to be set.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
OSError: If the environment file does not exist or cannot be accessed.
|
||||
"""
|
||||
youtube_key = youtube_key.strip()
|
||||
if os.path.exists(self.env_file) and youtube_key:
|
||||
with open(self.env_file, "r") as f:
|
||||
lines = f.readlines()
|
||||
with open(self.env_file, "w") as f:
|
||||
for line in lines:
|
||||
if "YOUTUBE_API_KEY" not in line:
|
||||
f.write(line)
|
||||
f.write(f"YOUTUBE_API_KEY={youtube_key}\n")
|
||||
elif youtube_key:
|
||||
with open(self.env_file, "w") as f:
|
||||
f.write(f"YOUTUBE_API_KEY={youtube_key}\n")
|
||||
|
||||
def default_model(self, model):
|
||||
"""Set the default model in the environment file.
|
||||
|
||||
Args:
|
||||
model (str): The model to be set.
|
||||
"""
|
||||
model = model.strip()
|
||||
env = os.path.expanduser("~/.config/fabric/.env")
|
||||
standalone = Standalone(args=[], pattern="")
|
||||
gpt, ollama, claude = standalone.fetch_available_models()
|
||||
allmodels = gpt + ollama + claude
|
||||
if model not in allmodels:
|
||||
print(
|
||||
f"Error: {model} is not a valid model. Please run fabric --listmodels to see the available models.")
|
||||
sys.exit()
|
||||
|
||||
# Only proceed if the model is not empty
|
||||
if model:
|
||||
if os.path.exists(env):
|
||||
# Initialize a flag to track the presence of DEFAULT_MODEL
|
||||
there = False
|
||||
with open(env, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Open the file again to write the changes
|
||||
with open(env, "w") as f:
|
||||
for line in lines:
|
||||
# Check each line to see if it contains DEFAULT_MODEL
|
||||
if "DEFAULT_MODEL=" in line:
|
||||
# Update the flag and the line with the new model
|
||||
there = True
|
||||
f.write(f'DEFAULT_MODEL={model}\n')
|
||||
else:
|
||||
# If the line does not contain DEFAULT_MODEL, write it unchanged
|
||||
f.write(line)
|
||||
|
||||
# If DEFAULT_MODEL was not found in the file, add it
|
||||
if not there:
|
||||
f.write(f'DEFAULT_MODEL={model}\n')
|
||||
|
||||
print(
|
||||
f"Default model changed to {model}. Please restart your terminal to use it.")
|
||||
else:
|
||||
print("No shell configuration file found.")
|
||||
|
||||
def patterns(self):
|
||||
""" Method to update patterns and exit the system.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
Update()
|
||||
|
||||
def run(self):
|
||||
""" Execute the Fabric program.
|
||||
|
||||
This method prompts the user for their OpenAI API key, sets the API key in the Fabric object, and then calls the patterns method.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
print("Welcome to Fabric. Let's get started.")
|
||||
apikey = input(
|
||||
"Please enter your OpenAI API key. If you do not have one or if you have already entered it, press enter.\n")
|
||||
self.api_key(apikey)
|
||||
print("Please enter your claude API key. If you do not have one, or if you have already entered it, press enter.\n")
|
||||
claudekey = input()
|
||||
self.claude_key(claudekey)
|
||||
print("Please enter your YouTube API key. If you do not have one, or if you have already entered it, press enter.\n")
|
||||
youtubekey = input()
|
||||
self.youtube_key(youtubekey)
|
||||
self.patterns()
|
||||
self.update_shconfigs()
|
||||
|
||||
|
||||
class Transcribe:
|
||||
def youtube(video_id):
|
||||
"""
|
||||
This method gets the transciption
|
||||
of a YouTube video designated with the video_id
|
||||
|
||||
Input:
|
||||
the video id specifying a YouTube video
|
||||
an example url for a video: https://www.youtube.com/watch?v=vF-MQmVxnCs&t=306s
|
||||
the video id is vF-MQmVxnCs&t=306s
|
||||
|
||||
Output:
|
||||
a transcript for the video
|
||||
|
||||
Raises:
|
||||
an exception and prints error
|
||||
|
||||
|
||||
"""
|
||||
try:
|
||||
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
||||
transcript = ""
|
||||
for segment in transcript_list:
|
||||
transcript += segment['text'] + " "
|
||||
return transcript.strip()
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
return None
|
||||
|
||||
|
||||
class AgentSetup:
|
||||
def apiKeys(self):
|
||||
"""Method to set the API keys in the environment file.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
print("Welcome to Fabric. Let's get started.")
|
||||
browserless = input("Please enter your Browserless API key\n").strip()
|
||||
serper = input("Please enter your Serper API key\n").strip()
|
||||
|
||||
# Entries to be added
|
||||
browserless_entry = f"BROWSERLESS_API_KEY={browserless}"
|
||||
serper_entry = f"SERPER_API_KEY={serper}"
|
||||
|
||||
# Check and write to the file
|
||||
with open(env_file, "r+") as f:
|
||||
content = f.read()
|
||||
|
||||
# Determine if the file ends with a newline
|
||||
if content.endswith('\n'):
|
||||
# If it ends with a newline, we directly write the new entries
|
||||
f.write(f"{browserless_entry}\n{serper_entry}\n")
|
||||
else:
|
||||
# If it does not end with a newline, add one before the new entries
|
||||
f.write(f"\n{browserless_entry}\n{serper_entry}\n")
|
||||
@@ -1,85 +0,0 @@
|
||||
import re
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from youtube_transcript_api import YouTubeTranscriptApi
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import json
|
||||
import isodate
|
||||
import argparse
|
||||
|
||||
|
||||
def get_video_id(url):
|
||||
# Extract video ID from URL
|
||||
pattern = r"(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})"
|
||||
match = re.search(pattern, url)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main_function(url, options):
|
||||
# Load environment variables from .env file
|
||||
load_dotenv(os.path.expanduser("~/.config/fabric/.env"))
|
||||
|
||||
# Get YouTube API key from environment variable
|
||||
api_key = os.getenv("YOUTUBE_API_KEY")
|
||||
if not api_key:
|
||||
print("Error: YOUTUBE_API_KEY not found in ~/.config/fabric/.env")
|
||||
return
|
||||
|
||||
# Extract video ID from URL
|
||||
video_id = get_video_id(url)
|
||||
if not video_id:
|
||||
print("Invalid YouTube URL")
|
||||
return
|
||||
|
||||
try:
|
||||
# Initialize the YouTube API client
|
||||
youtube = build("youtube", "v3", developerKey=api_key)
|
||||
|
||||
# Get video details
|
||||
video_response = (
|
||||
youtube.videos().list(id=video_id, part="contentDetails").execute()
|
||||
)
|
||||
|
||||
# Extract video duration and convert to minutes
|
||||
duration_iso = video_response["items"][0]["contentDetails"]["duration"]
|
||||
duration_seconds = isodate.parse_duration(duration_iso).total_seconds()
|
||||
duration_minutes = round(duration_seconds / 60)
|
||||
|
||||
# Get video transcript
|
||||
try:
|
||||
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
||||
transcript_text = " ".join([item["text"]
|
||||
for item in transcript_list])
|
||||
transcript_text = transcript_text.replace("\n", " ")
|
||||
except Exception as e:
|
||||
transcript_text = f"Transcript not available. ({e})"
|
||||
|
||||
# Output based on options
|
||||
if options.duration:
|
||||
print(duration_minutes)
|
||||
elif options.transcript:
|
||||
print(transcript_text)
|
||||
else:
|
||||
# Create JSON object
|
||||
output = {"transcript": transcript_text,
|
||||
"duration": duration_minutes}
|
||||
# Print JSON object
|
||||
print(json.dumps(output))
|
||||
except HttpError as e:
|
||||
|
||||
print(
|
||||
f"Error: Failed to access YouTube API. Please check your YOUTUBE_API_KEY and ensure it is valid: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='yt (video meta) extracts metadata about a video, such as the transcript and the video\'s duration. By Daniel Miessler.')
|
||||
# Ensure 'url' is defined once
|
||||
parser.add_argument('url', help='YouTube video URL')
|
||||
parser.add_argument('--duration', action='store_true',
|
||||
help='Output only the duration')
|
||||
parser.add_argument('--transcript', action='store_true',
|
||||
help='Output only the transcript')
|
||||
args = parser.parse_args()
|
||||
main_function(args.url, args)
|
||||
3
installer/client/gui/.gitignore
vendored
3
installer/client/gui/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
@@ -1,21 +0,0 @@
|
||||
Fabric is not just a tool; it's a transformative step towards integrating the power of GPT prompts into your digital life. With Fabric, you have the ability to create a personal API that brings advanced GPT capabilities into various aspects of your digital environment. Whether you're looking to incorporate powerful GPT prompts into command line operations or extend their functionality to a wider network through a personal API, Fabric is designed to seamlessly blend with your digital ecosystem. This tool is all about augmenting your digital interactions, enhancing productivity, and enabling a more intelligent, GPT-powered experience in every aspect of your online presence.
|
||||
|
||||
## Features
|
||||
|
||||
1. Text Analysis: Easily extract summaries from texts.
|
||||
2. Clipboard Integration: Conveniently copy responses to the clipboard.
|
||||
3. File Output: Save responses to files for later reference.
|
||||
4. Pattern Module: Utilize specific modules for different types of analysis.
|
||||
5. Server Mode: Operate the tool in server mode for expanded capabilities.
|
||||
6. Remote & Standalone Modes: Choose between remote and standalone operations.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Start the application:
|
||||
`npm start`
|
||||
|
||||
Contributing
|
||||
|
||||
We welcome contributions to Fabric! For details on our code of conduct and the process for submitting pull requests, please read the CONTRIBUTING.md.
|
||||
@@ -1,45 +0,0 @@
|
||||
const { OpenAI } = require("openai");
|
||||
require("dotenv").config({
|
||||
path: require("os").homedir() + "/.config/fabric/.env",
|
||||
});
|
||||
|
||||
let openaiClient = null;
|
||||
|
||||
// Function to initialize and get the OpenAI client
|
||||
function getOpenAIClient() {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
throw new Error(
|
||||
"The OPENAI_API_KEY environment variable is missing or empty."
|
||||
);
|
||||
}
|
||||
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
}
|
||||
|
||||
async function queryOpenAI(system, user, callback) {
|
||||
const openai = getOpenAIClient(); // Ensure the client is initialized here
|
||||
const messages = [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: user },
|
||||
];
|
||||
try {
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-4-1106-preview", // Adjust the model as necessary.
|
||||
messages: messages,
|
||||
temperature: 0.0,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0.1,
|
||||
presence_penalty: 0.1,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const message = chunk.choices[0]?.delta?.content || "";
|
||||
callback(message); // Process each chunk of data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error querying OpenAI:", error);
|
||||
callback("Error querying OpenAI. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { queryOpenAI };
|
||||
@@ -1,70 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fabric</title>
|
||||
<link rel="stylesheet" href="static/stylesheet/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="static/stylesheet/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<a class="navbar-brand" href="#">
|
||||
<img
|
||||
src="static/images/fabric-logo-gif.gif"
|
||||
alt="Fabric Logo"
|
||||
height="40"
|
||||
/>
|
||||
</a>
|
||||
<button id="configButton" class="btn btn-outline-success my-2 my-sm-0">
|
||||
Config
|
||||
</button>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbarCollap se"
|
||||
aria-controls="navbarCollapse"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<button
|
||||
id="updatePatternsButton"
|
||||
class="btn btn-outline-success my-2 my-sm-0"
|
||||
>
|
||||
Update Patterns
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse"></div>
|
||||
<div class="m1-auto">
|
||||
<a class="navbar-brand" id="themeChanger" href="#">Dark</a>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="container" id="my-form">
|
||||
<select class="form-control" id="patternSelector"></select>
|
||||
<textarea
|
||||
rows="5"
|
||||
class="form-control"
|
||||
id="userInput"
|
||||
placeholder="start typing or drag a file (.txt, .svg, .pdf and .doc are currently supported)"
|
||||
></textarea>
|
||||
<button class="btn btn-primary" id="submit">Submit</button>
|
||||
</div>
|
||||
<div id="configSection" class="container hidden">
|
||||
<input
|
||||
type="text"
|
||||
id="apiKeyInput"
|
||||
placeholder="Enter OpenAI API Key"
|
||||
class="form-control"
|
||||
/>
|
||||
<button id="saveApiKey" class="btn btn-primary">Save API Key</button>
|
||||
</div>
|
||||
<div class="container hidden" id="responseContainer"></div>
|
||||
</main>
|
||||
<script src="static/js/jquery-3.0.0.slim.min.js"></script>
|
||||
<script src="static/js/bootstrap.min.js"></script>
|
||||
<script src="static/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,300 +0,0 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
||||
const pdfParse = require("pdf-parse");
|
||||
const mammoth = require("mammoth");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const { queryOpenAI } = require("./chatgpt.js");
|
||||
const axios = require("axios");
|
||||
const fsExtra = require("fs-extra");
|
||||
|
||||
let fetch;
|
||||
import("node-fetch").then((module) => {
|
||||
fetch = module.default;
|
||||
});
|
||||
const unzipper = require("unzipper");
|
||||
|
||||
let win;
|
||||
|
||||
function promptUserForApiKey() {
|
||||
// Create a new window to prompt the user for the API key
|
||||
const promptWindow = new BrowserWindow({
|
||||
// Window configuration for the prompt
|
||||
width: 500,
|
||||
height: 200,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false, // Consider security implications
|
||||
},
|
||||
});
|
||||
|
||||
// Handle the API key submission from the prompt window
|
||||
ipcMain.on("submit-api-key", (event, apiKey) => {
|
||||
if (apiKey) {
|
||||
saveApiKey(apiKey);
|
||||
promptWindow.close();
|
||||
createWindow(); // Proceed to create the main window
|
||||
} else {
|
||||
// Handle invalid input or user cancellation
|
||||
promptWindow.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadApiKey() {
|
||||
const configPath = path.join(os.homedir(), ".config", "fabric", ".env");
|
||||
if (fs.existsSync(configPath)) {
|
||||
const envContents = fs.readFileSync(configPath, { encoding: "utf8" });
|
||||
const matches = envContents.match(/^OPENAI_API_KEY=(.*)$/m);
|
||||
if (matches && matches[1]) {
|
||||
return matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveApiKey(apiKey) {
|
||||
const configPath = path.join(os.homedir(), ".config", "fabric");
|
||||
const envFilePath = path.join(configPath, ".env");
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.mkdirSync(configPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(envFilePath, `OPENAI_API_KEY=${apiKey}`);
|
||||
process.env.OPENAI_API_KEY = apiKey; // Set for current session
|
||||
}
|
||||
|
||||
function ensureFabricFoldersExist() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const fabricPath = path.join(os.homedir(), ".config", "fabric");
|
||||
const patternsPath = path.join(fabricPath, "patterns");
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(fabricPath)) {
|
||||
fs.mkdirSync(fabricPath, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(patternsPath)) {
|
||||
fs.mkdirSync(patternsPath, { recursive: true });
|
||||
await downloadAndUpdatePatterns(patternsPath);
|
||||
}
|
||||
resolve(); // Resolve the promise once everything is set up
|
||||
} catch (error) {
|
||||
console.error("Error ensuring fabric folders exist:", error);
|
||||
reject(error); // Reject the promise if an error occurs
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAndUpdatePatterns(patternsPath) {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "get",
|
||||
url: "https://github.com/danielmiessler/fabric/archive/refs/heads/main.zip",
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
|
||||
const zipPath = path.join(os.tmpdir(), "fabric.zip");
|
||||
fs.writeFileSync(zipPath, response.data);
|
||||
console.log("Zip file written to:", zipPath);
|
||||
|
||||
const tempExtractPath = path.join(os.tmpdir(), "fabric_extracted");
|
||||
fsExtra.emptyDirSync(tempExtractPath);
|
||||
|
||||
await fsExtra.remove(patternsPath); // Delete the existing patterns directory
|
||||
|
||||
await fs
|
||||
.createReadStream(zipPath)
|
||||
.pipe(unzipper.Extract({ path: tempExtractPath }))
|
||||
.promise();
|
||||
|
||||
console.log("Extraction complete");
|
||||
|
||||
const extractedPatternsPath = path.join(
|
||||
tempExtractPath,
|
||||
"fabric-main",
|
||||
"patterns"
|
||||
);
|
||||
|
||||
await fsExtra.copy(extractedPatternsPath, patternsPath);
|
||||
console.log("Patterns successfully updated");
|
||||
|
||||
// Inform the renderer process that the patterns have been updated
|
||||
win.webContents.send("patterns-updated");
|
||||
} catch (error) {
|
||||
console.error("Error downloading or updating patterns:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function checkApiKeyExists() {
|
||||
const configPath = path.join(os.homedir(), ".config", "fabric", ".env");
|
||||
return fs.existsSync(configPath);
|
||||
}
|
||||
|
||||
function getPatternFolders() {
|
||||
const patternsPath = path.join(os.homedir(), ".config", "fabric", "patterns");
|
||||
return fs
|
||||
.readdirSync(patternsPath, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
}
|
||||
|
||||
function getPatternContent(patternName) {
|
||||
const patternPath = path.join(
|
||||
os.homedir(),
|
||||
".config",
|
||||
"fabric",
|
||||
"patterns",
|
||||
patternName,
|
||||
"system.md"
|
||||
);
|
||||
try {
|
||||
return fs.readFileSync(patternPath, "utf8");
|
||||
} catch (error) {
|
||||
console.error("Error reading pattern file:", error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
win.loadFile("index.html");
|
||||
|
||||
win.on("closed", () => {
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
ipcMain.on("process-complex-file", (event, filePath) => {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
let fileProcessPromise;
|
||||
|
||||
if (extension === ".pdf") {
|
||||
const dataBuffer = fs.readFileSync(filePath);
|
||||
fileProcessPromise = pdfParse(dataBuffer).then((data) => data.text);
|
||||
} else if (extension === ".docx") {
|
||||
fileProcessPromise = mammoth
|
||||
.extractRawText({ path: filePath })
|
||||
.then((result) => result.value)
|
||||
.catch((err) => {
|
||||
console.error("Error processing DOCX file:", err);
|
||||
throw new Error("Error processing DOCX file.");
|
||||
});
|
||||
} else {
|
||||
event.reply("file-response", "Error: Unsupported file type");
|
||||
return;
|
||||
}
|
||||
|
||||
fileProcessPromise
|
||||
.then((extractedText) => {
|
||||
// Sending the extracted text back to the frontend.
|
||||
event.reply("file-response", extractedText);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Handling any errors during file processing and sending them back to the frontend.
|
||||
event.reply("file-response", `Error processing file: ${error.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("start-query-openai", async (event, system, user) => {
|
||||
if (system == null || user == null) {
|
||||
console.error("Received null for system or user message");
|
||||
event.reply("openai-response", "Error: System or user message is null.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await queryOpenAI(system, user, (message) => {
|
||||
event.reply("openai-response", message);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error querying OpenAI:", error);
|
||||
event.reply("no-api-key", "Error querying OpenAI.");
|
||||
}
|
||||
});
|
||||
|
||||
// Example of using ipcMain.handle for asynchronous operations
|
||||
ipcMain.handle("get-patterns", async (event) => {
|
||||
try {
|
||||
return getPatternFolders();
|
||||
} catch (error) {
|
||||
console.error("Failed to get patterns:", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("update-patterns", () => {
|
||||
const patternsPath = path.join(os.homedir(), ".config", "fabric", "patterns");
|
||||
downloadAndUpdatePatterns(patternsPath);
|
||||
});
|
||||
|
||||
ipcMain.handle("get-pattern-content", async (event, patternName) => {
|
||||
try {
|
||||
return getPatternContent(patternName);
|
||||
} catch (error) {
|
||||
console.error("Failed to get pattern content:", error);
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("save-api-key", async (event, apiKey) => {
|
||||
try {
|
||||
const configPath = path.join(os.homedir(), ".config", "fabric");
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.mkdirSync(configPath, { recursive: true });
|
||||
}
|
||||
|
||||
const envFilePath = path.join(configPath, ".env");
|
||||
fs.writeFileSync(envFilePath, `OPENAI_API_KEY=${apiKey}`);
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
|
||||
return "API Key saved successfully.";
|
||||
} catch (error) {
|
||||
console.error("Error saving API key:", error);
|
||||
throw new Error("Failed to save API Key.");
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
try {
|
||||
const apiKey = loadApiKey();
|
||||
if (!apiKey) {
|
||||
promptUserForApiKey();
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
createWindow();
|
||||
}
|
||||
await ensureFabricFoldersExist(); // Ensure fabric folders exist
|
||||
createWindow(); // Create the application window
|
||||
|
||||
// After window creation, check if the API key exists
|
||||
if (!checkApiKeyExists()) {
|
||||
console.log("API key is missing. Prompting user to input API key.");
|
||||
// Optionally, directly invoke a function here to show a prompt in the renderer process
|
||||
win.webContents.send("request-api-key");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize fabric folders:", error);
|
||||
// Handle initialization failure (e.g., close the app or show an error message)
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
1644
installer/client/gui/package-lock.json
generated
1644
installer/client/gui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "fabric_electron",
|
||||
"version": "1.0.0",
|
||||
"description": "a fabric electron app",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.4.1",
|
||||
"electron": "^28.2.6",
|
||||
"openai": "^4.27.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"mammoth": "^1.6.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"unzipper": "^0.10.14"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
|
||||
on: (channel, func) => {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
},
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -1,266 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
const patternSelector = document.getElementById("patternSelector");
|
||||
const userInput = document.getElementById("userInput");
|
||||
const submitButton = document.getElementById("submit");
|
||||
const responseContainer = document.getElementById("responseContainer");
|
||||
const themeChanger = document.getElementById("themeChanger");
|
||||
const configButton = document.getElementById("configButton");
|
||||
const configSection = document.getElementById("configSection");
|
||||
const saveApiKeyButton = document.getElementById("saveApiKey");
|
||||
const apiKeyInput = document.getElementById("apiKeyInput");
|
||||
const originalPlaceholder = userInput.placeholder;
|
||||
const updatePatternsButton = document.getElementById("updatePatternsButton");
|
||||
const copyButton = document.createElement("button");
|
||||
|
||||
window.electronAPI.on("patterns-ready", () => {
|
||||
console.log("Patterns are ready. Refreshing the pattern list.");
|
||||
loadPatterns();
|
||||
});
|
||||
window.electronAPI.on("request-api-key", () => {
|
||||
// Show the API key input section or modal to the user
|
||||
configSection.classList.remove("hidden"); // Assuming 'configSection' is your API key input area
|
||||
});
|
||||
copyButton.textContent = "Copy";
|
||||
copyButton.id = "copyButton";
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target && e.target.id === "copyButton") {
|
||||
// Your copy to clipboard function
|
||||
copyToClipboard();
|
||||
}
|
||||
});
|
||||
window.electronAPI.on("no-api-key", () => {
|
||||
alert("API key is missing. Please enter your OpenAI API key.");
|
||||
});
|
||||
|
||||
window.electronAPI.on("patterns-updated", () => {
|
||||
alert("Patterns updated. Refreshing the pattern list.");
|
||||
loadPatterns();
|
||||
});
|
||||
|
||||
function htmlToPlainText(html) {
|
||||
// Create a temporary div element to hold the HTML
|
||||
var tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
// Replace <br> tags with newline characters
|
||||
tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
|
||||
|
||||
// Replace block elements like <p> and <div> with newline characters
|
||||
tempDiv.querySelectorAll("p, div").forEach((block) => {
|
||||
block.prepend("\n"); // Add a newline before the block element's content
|
||||
block.replaceWith(...block.childNodes); // Replace the block element with its own contents
|
||||
});
|
||||
|
||||
// Return the text content, trimming leading and trailing newlines
|
||||
return tempDiv.textContent.trim();
|
||||
}
|
||||
|
||||
async function submitQuery(userInputValue) {
|
||||
userInput.value = ""; // Clear the input after submitting
|
||||
systemCommand = await window.electronAPI.invoke(
|
||||
"get-pattern-content",
|
||||
patternSelector.value
|
||||
);
|
||||
responseContainer.innerHTML = ""; // Clear previous responses
|
||||
if (responseContainer.classList.contains("hidden")) {
|
||||
console.log("contains hidden");
|
||||
responseContainer.classList.remove("hidden");
|
||||
responseContainer.appendChild(copyButton);
|
||||
}
|
||||
window.electronAPI.send(
|
||||
"start-query-openai",
|
||||
systemCommand,
|
||||
userInputValue
|
||||
);
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const containerClone = responseContainer.cloneNode(true);
|
||||
// Remove the copy button from the clone
|
||||
const copyButtonClone = containerClone.querySelector("#copyButton");
|
||||
if (copyButtonClone) {
|
||||
copyButtonClone.parentNode.removeChild(copyButtonClone);
|
||||
}
|
||||
|
||||
// Convert HTML to plain text, preserving newlines
|
||||
const plainText = htmlToPlainText(containerClone.innerHTML);
|
||||
|
||||
// Use a temporary textarea for copying
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.style.position = "absolute";
|
||||
textArea.style.left = "-9999px";
|
||||
textArea.setAttribute("aria-hidden", "true");
|
||||
textArea.value = plainText;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
console.log("Text successfully copied to clipboard");
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
async function loadPatterns() {
|
||||
try {
|
||||
const patterns = await window.electronAPI.invoke("get-patterns");
|
||||
patternSelector.innerHTML = ""; // Clear existing options first
|
||||
patterns.forEach((pattern) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = pattern;
|
||||
option.textContent = pattern;
|
||||
patternSelector.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load patterns:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
const msg = successful ? "successful" : "unsuccessful";
|
||||
console.log("Fallback: Copying text command was " + msg);
|
||||
} catch (err) {
|
||||
console.error("Fallback: Oops, unable to copy", err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
updatePatternsButton.addEventListener("click", () => {
|
||||
window.electronAPI.send("update-patterns");
|
||||
});
|
||||
|
||||
// Load patterns on startup
|
||||
try {
|
||||
const patterns = await window.electronAPI.invoke("get-patterns");
|
||||
patterns.forEach((pattern) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = pattern;
|
||||
option.textContent = pattern;
|
||||
patternSelector.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load patterns:", error);
|
||||
}
|
||||
|
||||
// Listen for OpenAI responses
|
||||
window.electronAPI.on("openai-response", (message) => {
|
||||
const formattedMessage = message.replace(/\n/g, "<br>");
|
||||
responseContainer.innerHTML += formattedMessage; // Append new data as it arrives
|
||||
});
|
||||
|
||||
window.electronAPI.on("file-response", (message) => {
|
||||
if (message.startsWith("Error")) {
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
submitQuery(message);
|
||||
});
|
||||
|
||||
// Submit button click handler
|
||||
submitButton.addEventListener("click", async () => {
|
||||
const userInputValue = userInput.value;
|
||||
submitQuery(userInputValue);
|
||||
});
|
||||
|
||||
// Theme changer click handler
|
||||
themeChanger.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
document.body.classList.toggle("light-theme");
|
||||
themeChanger.innerText =
|
||||
themeChanger.innerText === "Dark" ? "Light" : "Dark";
|
||||
});
|
||||
|
||||
// Config button click handler - toggles the config section visibility
|
||||
configButton.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
configSection.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
// Save API Key button click handler
|
||||
saveApiKeyButton.addEventListener("click", () => {
|
||||
const apiKey = apiKeyInput.value;
|
||||
window.electronAPI
|
||||
.invoke("save-api-key", apiKey)
|
||||
.then(() => {
|
||||
alert("API Key saved successfully.");
|
||||
// Optionally hide the config section and clear the input after saving
|
||||
configSection.classList.add("hidden");
|
||||
apiKeyInput.value = "";
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error saving API key:", err);
|
||||
alert("Failed to save API Key.");
|
||||
});
|
||||
});
|
||||
|
||||
// Handler for pattern selection change
|
||||
patternSelector.addEventListener("change", async () => {
|
||||
const selectedPattern = patternSelector.value;
|
||||
const systemCommand = await window.electronAPI.invoke(
|
||||
"get-pattern-content",
|
||||
selectedPattern
|
||||
);
|
||||
// Use systemCommand as part of the input for querying OpenAI
|
||||
});
|
||||
|
||||
// drag and drop
|
||||
userInput.addEventListener("dragover", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// Add some visual feedback
|
||||
userInput.classList.add("drag-over");
|
||||
userInput.placeholder = "Drop file here";
|
||||
});
|
||||
|
||||
userInput.addEventListener("dragleave", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// Remove visual feedback
|
||||
userInput.classList.remove("drag-over");
|
||||
userInput.placeholder = originalPlaceholder;
|
||||
});
|
||||
|
||||
userInput.addEventListener("drop", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
userInput.classList.remove("drag-over");
|
||||
userInput.placeholder = originalPlaceholder;
|
||||
processFile(file);
|
||||
});
|
||||
|
||||
function processFile(file) {
|
||||
const fileType = file.type;
|
||||
const reader = new FileReader();
|
||||
let content = "";
|
||||
|
||||
reader.onload = (event) => {
|
||||
content = event.target.result;
|
||||
userInput.value = content;
|
||||
submitQuery(content);
|
||||
};
|
||||
|
||||
if (fileType === "text/plain" || fileType === "image/svg+xml") {
|
||||
reader.readAsText(file);
|
||||
} else if (
|
||||
fileType === "application/pdf" ||
|
||||
fileType.match(/wordprocessingml/)
|
||||
) {
|
||||
// For PDF and DOCX, we need to handle them in the main process due to complexity
|
||||
window.electronAPI.send("process-complex-file", file.path);
|
||||
} else {
|
||||
console.error("Unsupported file type");
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,160 +0,0 @@
|
||||
body {
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #2b2b2b;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 90%;
|
||||
margin: 50px auto;
|
||||
padding: 15px;
|
||||
background: #333333;
|
||||
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#responseContainer {
|
||||
margin-top: 15px;
|
||||
border: 1px solid #444;
|
||||
padding: 10px;
|
||||
min-height: 100px;
|
||||
background-color: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#userInput {
|
||||
margin-bottom: 10px;
|
||||
background-color: #424242; /* Darker shade for textarea */
|
||||
color: #e0e0e0; /* Light text for readability */
|
||||
border: 1px solid #555; /* Adjusted border color */
|
||||
padding: 10px; /* Added padding for better text visibility */
|
||||
}
|
||||
#patternSelector {
|
||||
margin-bottom: 10px;
|
||||
background-color: #424242; /* Darker shade for textarea */
|
||||
color: #e0e0e0; /* Light text for readability */
|
||||
border: 1px solid #555; /* Adjusted border color */
|
||||
padding: 10px; /* Added padding for better text visibility */
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.light-theme .container {
|
||||
background: #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.light-theme #responseContainer,
|
||||
.light-theme #userInput,
|
||||
.light-theme #patternSelector {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.light-theme .btn-primary {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.drag-over {
|
||||
background-color: #505050; /* Slightly lighter than the regular background for visibility */
|
||||
border: 2px dashed #007bff; /* Dashed border with the primary button color for emphasis */
|
||||
box-shadow: 0 0 10px #007bff; /* Soft glow effect to highlight the area */
|
||||
color: #e0e0e0; /* Maintaining the light text color for readability */
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for background and shadow changes */
|
||||
}
|
||||
|
||||
.light-theme .drag-over {
|
||||
background-color: #e6e6e6; /* Lighter background for light theme */
|
||||
border: 2px dashed #0066cc; /* Adjusted border color for light theme */
|
||||
box-shadow: 0 0 10px #0066cc; /* Soft glow effect for light theme */
|
||||
color: #333; /* Darker text for contrast in light theme */
|
||||
}
|
||||
|
||||
/* Existing dark theme styles for reference */
|
||||
.navbar-dark.bg-dark {
|
||||
background-color: #343a40 !important;
|
||||
}
|
||||
|
||||
/* Light theme styles */
|
||||
body.light-theme .navbar-dark.bg-dark {
|
||||
background-color: #e2e6ea !important; /* Slightly darker shade for better visibility */
|
||||
color: #000 !important; /* Keep dark text color for contrast */
|
||||
}
|
||||
|
||||
body.light-theme .navbar-dark .navbar-brand,
|
||||
body.light-theme .navbar-dark .btn-outline-success {
|
||||
color: #0056b3 !important; /* Darker color for better visibility and contrast */
|
||||
}
|
||||
|
||||
body.light-theme .navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'><path stroke='rgba(0, 0, 0, 0.75)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>") !important;
|
||||
/* Slightly darker stroke for the navbar-toggler-icon for better visibility */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-brand img {
|
||||
height: 20px; /* Smaller logo for smaller screens */
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-toggler {
|
||||
padding: 0.25rem 0.5rem; /* Adjust padding for the toggle button */
|
||||
}
|
||||
}
|
||||
#responseContainer {
|
||||
position: relative; /* Needed for absolute positioning of the child button */
|
||||
}
|
||||
|
||||
#copyButton {
|
||||
position: absolute;
|
||||
top: 10px; /* Adjust as needed */
|
||||
right: 10px; /* Adjust as needed */
|
||||
background-color: rgba(
|
||||
0,
|
||||
123,
|
||||
255,
|
||||
0.5
|
||||
); /* Bootstrap primary color with transparency */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#copyButton:hover {
|
||||
background-color: rgba(
|
||||
0,
|
||||
123,
|
||||
255,
|
||||
0.8
|
||||
); /* Slightly less transparent on hover */
|
||||
}
|
||||
|
||||
#copyButton:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
"""This package collets all functionality meant to run as web servers"""
|
||||
from .api import main as run_api_server
|
||||
from .webui import main as run_webui_server
|
||||
@@ -1,2 +0,0 @@
|
||||
FLASK_SECRET_KEY=
|
||||
OPENAI_API_KEY=
|
||||
@@ -1 +0,0 @@
|
||||
from .fabric_api_server import main
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"/extwis": {
|
||||
"eJ4f1e0b-25wO-47f9-97ec-6b5335b2": "Daniel Miessler",
|
||||
"test": "user2"
|
||||
},
|
||||
"/summarize": {
|
||||
"eJ4f1e0b-25wO-47f9-97ec-6b5335b2": "Daniel Miessler",
|
||||
"test": "user2"
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import jwt
|
||||
import json
|
||||
import openai
|
||||
from flask import Flask, request, jsonify
|
||||
from functools import wraps
|
||||
import re
|
||||
import requests
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from importlib import resources
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return jsonify({"error": "The requested resource was not found."}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return jsonify({"error": "An internal server error occurred."}), 500
|
||||
|
||||
|
||||
##################################################
|
||||
##################################################
|
||||
#
|
||||
# ⚠️ CAUTION: This is an HTTP-only server!
|
||||
#
|
||||
# If you don't know what you're doing, don't run
|
||||
#
|
||||
##################################################
|
||||
##################################################
|
||||
|
||||
## Setup
|
||||
|
||||
## Did I mention this is HTTP only? Don't run this on the public internet.
|
||||
|
||||
# Read API tokens from the apikeys.json file
|
||||
api_keys = resources.read_text("installer.server.api", "fabric_api_keys.json")
|
||||
valid_tokens = json.loads(api_keys)
|
||||
|
||||
|
||||
# Read users from the users.json file
|
||||
users = resources.read_text("installer.server.api", "users.json")
|
||||
users = json.loads(users)
|
||||
|
||||
|
||||
# The function to check if the token is valid
|
||||
def auth_required(f):
|
||||
""" Decorator function to check if the token is valid.
|
||||
|
||||
Args:
|
||||
f: The function to be decorated
|
||||
|
||||
Returns:
|
||||
The decorated function
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
""" Decorated function to handle authentication token and API endpoint.
|
||||
|
||||
Args:
|
||||
*args: Variable length argument list.
|
||||
**kwargs: Arbitrary keyword arguments.
|
||||
|
||||
Returns:
|
||||
Result of the decorated function.
|
||||
|
||||
Raises:
|
||||
KeyError: If 'Authorization' header is not found in the request.
|
||||
TypeError: If 'Authorization' header value is not a string.
|
||||
ValueError: If the authentication token is invalid or expired.
|
||||
"""
|
||||
|
||||
# Get the authentication token from request header
|
||||
auth_token = request.headers.get("Authorization", "")
|
||||
|
||||
# Remove any bearer token prefix if present
|
||||
if auth_token.lower().startswith("bearer "):
|
||||
auth_token = auth_token[7:]
|
||||
|
||||
# Get API endpoint from request
|
||||
endpoint = request.path
|
||||
|
||||
# Check if token is valid
|
||||
user = check_auth_token(auth_token, endpoint)
|
||||
if user == "Unauthorized: You are not authorized for this API":
|
||||
return jsonify({"error": user}), 401
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
# Check for a valid token/user for the given route
|
||||
def check_auth_token(token, route):
|
||||
""" Check if the provided token is valid for the given route and return the corresponding user.
|
||||
|
||||
Args:
|
||||
token (str): The token to be checked for validity.
|
||||
route (str): The route for which the token validity is to be checked.
|
||||
|
||||
Returns:
|
||||
str: The user corresponding to the provided token and route if valid, otherwise returns "Unauthorized: You are not authorized for this API".
|
||||
"""
|
||||
|
||||
# Check if token is valid for the given route and return corresponding user
|
||||
if route in valid_tokens and token in valid_tokens[route]:
|
||||
return users[valid_tokens[route][token]]
|
||||
else:
|
||||
return "Unauthorized: You are not authorized for this API"
|
||||
|
||||
|
||||
# Define the allowlist of characters
|
||||
ALLOWLIST_PATTERN = re.compile(r"^[a-zA-Z0-9\s.,;:!?\-]+$")
|
||||
|
||||
|
||||
# Sanitize the content, sort of. Prompt injection is the main threat so this isn't a huge deal
|
||||
def sanitize_content(content):
|
||||
""" Sanitize the content by removing characters that do not match the ALLOWLIST_PATTERN.
|
||||
|
||||
Args:
|
||||
content (str): The content to be sanitized.
|
||||
|
||||
Returns:
|
||||
str: The sanitized content.
|
||||
"""
|
||||
|
||||
return "".join(char for char in content if ALLOWLIST_PATTERN.match(char))
|
||||
|
||||
|
||||
# Pull the URL content's from the GitHub repo
|
||||
def fetch_content_from_url(url):
|
||||
""" Fetches content from the given URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL from which to fetch content.
|
||||
|
||||
Returns:
|
||||
str: The sanitized content fetched from the URL.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If an error occurs while making the request to the URL.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
sanitized_content = sanitize_content(response.text)
|
||||
return sanitized_content
|
||||
except requests.RequestException as e:
|
||||
return str(e)
|
||||
|
||||
|
||||
## APIs
|
||||
# Make path mapping flexible and scalable
|
||||
pattern_path_mappings = {
|
||||
"extwis": {"system_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/extract_wisdom/system.md",
|
||||
"user_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/extract_wisdom/user.md"},
|
||||
"summarize": {"system_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/summarize/system.md",
|
||||
"user_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/summarize/user.md"}
|
||||
} # Add more pattern with your desire path as a key in this dictionary
|
||||
|
||||
# /<pattern>
|
||||
@app.route("/<pattern>", methods=["POST"])
|
||||
@auth_required # Require authentication
|
||||
def milling(pattern):
|
||||
""" Combine fabric pattern with input from user and send to OpenAI's GPT-4 model.
|
||||
|
||||
Returns:
|
||||
JSON: A JSON response containing the generated response or an error message.
|
||||
|
||||
Raises:
|
||||
Exception: If there is an error during the API call.
|
||||
"""
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
# Warn if there's no input
|
||||
if "input" not in data:
|
||||
return jsonify({"error": "Missing input parameter"}), 400
|
||||
|
||||
# Get data from client
|
||||
input_data = data["input"]
|
||||
|
||||
# Set the system and user URLs
|
||||
urls = pattern_path_mappings[pattern]
|
||||
system_url, user_url = urls["system_url"], urls["user_url"]
|
||||
|
||||
# Fetch the prompt content
|
||||
system_content = fetch_content_from_url(system_url)
|
||||
user_file_content = fetch_content_from_url(user_url)
|
||||
|
||||
# Build the API call
|
||||
system_message = {"role": "system", "content": system_content}
|
||||
user_message = {"role": "user", "content": user_file_content + "\n" + input_data}
|
||||
messages = [system_message, user_message]
|
||||
try:
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-4-1106-preview",
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
top_p=1,
|
||||
frequency_penalty=0.1,
|
||||
presence_penalty=0.1,
|
||||
)
|
||||
assistant_message = response.choices[0].message.content
|
||||
return jsonify({"response": assistant_message})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error occurred: {str(e)}")
|
||||
return jsonify({"error": "An error occurred while processing the request."}), 500
|
||||
|
||||
|
||||
@app.route("/register", methods=["POST"])
|
||||
def register():
|
||||
data = request.get_json()
|
||||
|
||||
username = data["username"]
|
||||
password = data["password"]
|
||||
|
||||
if username in users:
|
||||
return jsonify({"error": "Username already exists"}), 400
|
||||
|
||||
new_user = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
users[username] = new_user
|
||||
|
||||
token = jwt.encode({"username": username}, os.getenv("JWT_SECRET"), algorithm="HS256")
|
||||
|
||||
return jsonify({"token": token.decode("utf-8")})
|
||||
|
||||
|
||||
@app.route("/login", methods=["POST"])
|
||||
def login():
|
||||
data = request.get_json()
|
||||
|
||||
username = data["username"]
|
||||
password = data["password"]
|
||||
|
||||
if username in users and users[username]["password"] == password:
|
||||
# Generate a JWT token
|
||||
token = jwt.encode({"username": username}, os.getenv("JWT_SECRET"), algorithm="HS256")
|
||||
|
||||
return jsonify({"token": token.decode("utf-8")})
|
||||
|
||||
return jsonify({"error": "Invalid username or password"}), 401
|
||||
|
||||
|
||||
def main():
|
||||
"""Runs the main fabric API backend server"""
|
||||
app.run(host="127.0.0.1", port=13337, debug=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"user1": {
|
||||
"username": "user1",
|
||||
"password": "password1"
|
||||
},
|
||||
"user2": {
|
||||
"username": "user2",
|
||||
"password": "password2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .fabric_web_server import main
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"/extwis": {
|
||||
"eJ4f1e0b-25wO-47f9-97ec-6b5335b2": "Daniel Miessler"
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
||||
import requests
|
||||
import json
|
||||
from flask import send_from_directory
|
||||
import os
|
||||
|
||||
##################################################
|
||||
##################################################
|
||||
#
|
||||
# ⚠️ CAUTION: This is an HTTP-only server!
|
||||
#
|
||||
# If you don't know what you're doing, don't run
|
||||
#
|
||||
##################################################
|
||||
##################################################
|
||||
|
||||
|
||||
def send_request(prompt, endpoint):
|
||||
""" Send a request to the specified endpoint of an HTTP-only server.
|
||||
|
||||
Args:
|
||||
prompt (str): The input prompt for the request.
|
||||
endpoint (str): The endpoint to which the request will be sent.
|
||||
|
||||
Returns:
|
||||
str: The response from the server.
|
||||
|
||||
Raises:
|
||||
KeyError: If the response JSON does not contain the expected "response" key.
|
||||
"""
|
||||
|
||||
base_url = "http://127.0.0.1:13337"
|
||||
url = f"{base_url}{endpoint}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {session['token']}",
|
||||
}
|
||||
data = json.dumps({"input": prompt})
|
||||
response = requests.post(url, headers=headers, data=data, verify=False)
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, data=data)
|
||||
response.raise_for_status() # raises HTTPError if the response status isn't 200
|
||||
except requests.ConnectionError:
|
||||
return "Error: Unable to connect to the server."
|
||||
except requests.HTTPError as e:
|
||||
return f"Error: An HTTP error occurred: {str(e)}"
|
||||
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.getenv("FLASK_SECRET_KEY")
|
||||
|
||||
|
||||
@app.route("/favicon.ico")
|
||||
def favicon():
|
||||
""" Send the favicon.ico file from the static directory.
|
||||
|
||||
Returns:
|
||||
Response object with the favicon.ico file
|
||||
|
||||
Raises:
|
||||
-
|
||||
"""
|
||||
|
||||
return send_from_directory(
|
||||
os.path.join(app.root_path, "static"),
|
||||
"favicon.ico",
|
||||
mimetype="image/vnd.microsoft.icon",
|
||||
)
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
""" Process the POST request and send a request to the specified API endpoint.
|
||||
|
||||
Returns:
|
||||
str: The rendered HTML template with the response data.
|
||||
"""
|
||||
|
||||
if request.method == "POST":
|
||||
prompt = request.form.get("prompt")
|
||||
endpoint = request.form.get("api")
|
||||
response = send_request(prompt=prompt, endpoint=endpoint)
|
||||
return render_template("index.html", response=response)
|
||||
return render_template("index.html", response=None)
|
||||
|
||||
|
||||
def main():
|
||||
app.run(host="127.0.0.1", port=13338, debug=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||
<title>fabric</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="https://beehiiv-images-production.s3.amazonaws.com/uploads/asset/file/971f362a-f3fa-427f-b619-7e04cc135d17/fabric-logo-miessler-transparent.png?t=1704525002" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-900 text-white min-h-screen">
|
||||
<div class="container mx-auto py-10 px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<!-- Add this line inside the div with class "flex justify-between items-center mb-6" -->
|
||||
<p><img src="static/fabric-logo-miessler-transparent.png" alt="fabric logo" class="h-20 w-auto mr-2"></p>
|
||||
<h1 class="text-4xl font-bold"><code>fabric</code></h1>
|
||||
|
||||
</div>
|
||||
<p>Please enter your content and select the API you want to use:</p>
|
||||
<br />
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="prompt" class="block text-sm font-medium">Content:</label>
|
||||
<input type="text" id="prompt" name="prompt" required class="w-full px-3 py-2 border border-gray-300 rounded-md text-black">
|
||||
</div>
|
||||
<div>
|
||||
<label for="api" class="block text-sm font-medium">API:</label>
|
||||
<select id="api" name="api" class="w-full px-3 py-2 border border-gray-300 rounded-md text-black">
|
||||
<option value="/extwis">/extwis</option>
|
||||
<!-- Add more API endpoints here... -->
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md text-white font-medium">Send Request</button>
|
||||
</form>
|
||||
{% if response %}
|
||||
<div class="mt-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold">API Response:</h2>
|
||||
<button id="copy-button" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md">Copy to Clipboard</button>
|
||||
</div>
|
||||
<pre id="response-output" class="bg-gray-800 p-4 rounded-md whitespace-pre-wrap">{{ response }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("api").addEventListener("change", function() {
|
||||
document.getElementById("response-output").textContent = "";
|
||||
});
|
||||
|
||||
document.getElementById("copy-button").addEventListener("click", function() {
|
||||
const responseOutput = document.getElementById("response-output");
|
||||
const range = document.createRange();
|
||||
range.selectNode(responseOutput);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
document.execCommand("copy");
|
||||
window.getSelection().removeAllRanges();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
18
main.go
Normal file
18
main.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/danielmiessler/fabric/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := cli.Cli(version)
|
||||
if err != nil && !flags.WroteHelp(err) {
|
||||
fmt.Printf("%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
38
nix/pkgs/fabric/default.nix
Normal file
38
nix/pkgs/fabric/default.nix
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
lib,
|
||||
buildGoApplication,
|
||||
go,
|
||||
installShellFiles,
|
||||
}:
|
||||
|
||||
buildGoApplication {
|
||||
pname = "fabric-ai";
|
||||
version = import ./version.nix;
|
||||
src = ../../../.;
|
||||
pwd = ../../../.;
|
||||
modules = ./gomod2nix.toml;
|
||||
|
||||
doCheck = false;
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
];
|
||||
|
||||
inherit go;
|
||||
|
||||
nativeBuildInputs = [ installShellFiles ];
|
||||
postInstall = ''
|
||||
installShellCompletion --zsh ./completions/_fabric
|
||||
installShellCompletion --bash ./completions/fabric.bash
|
||||
installShellCompletion --fish ./completions/fabric.fish
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Fabric is an open-source framework for augmenting humans using AI. It provides a modular framework for solving specific problems using a crowdsourced set of AI prompts that can be used anywhere";
|
||||
homepage = "https://github.com/danielmiessler/fabric";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.all;
|
||||
mainProgram = "fabric";
|
||||
};
|
||||
}
|
||||
285
nix/pkgs/fabric/gomod2nix.toml
Normal file
285
nix/pkgs/fabric/gomod2nix.toml
Normal file
@@ -0,0 +1,285 @@
|
||||
schema = 3
|
||||
|
||||
[mod]
|
||||
[mod."cloud.google.com/go"]
|
||||
version = "v0.120.1"
|
||||
hash = "sha256-yWaLc06rGXk16K53rix8O4uPSX+AOZDgIpIXf+wlh10="
|
||||
[mod."cloud.google.com/go/ai"]
|
||||
version = "v0.10.2"
|
||||
hash = "sha256-bsqvdylG8kk+AHtyvMRMv1TOjUmvONAgJ+14mKcwuzs="
|
||||
[mod."cloud.google.com/go/auth"]
|
||||
version = "v0.16.1"
|
||||
hash = "sha256-rMPMNQh/YM/67b9Grfu0BFccWpS1SRhBepubQqXRAyg="
|
||||
[mod."cloud.google.com/go/auth/oauth2adapt"]
|
||||
version = "v0.2.8"
|
||||
hash = "sha256-GoXFqAbp1WO1tDj07PF5EyxDYvCBP0l0qwxY2oV2hfc="
|
||||
[mod."cloud.google.com/go/compute/metadata"]
|
||||
version = "v0.6.0"
|
||||
hash = "sha256-E8/cwio4xR8buCryR4HwR7+agb4M3zqgXSm7rBglmIY="
|
||||
[mod."cloud.google.com/go/longrunning"]
|
||||
version = "v0.6.7"
|
||||
hash = "sha256-9I0Nc2KWAEVoxDngNkqFUdASmZIAySfMEELlPh3Q3xA="
|
||||
[mod."dario.cat/mergo"]
|
||||
version = "v1.0.1"
|
||||
hash = "sha256-wcG6+x0k6KzOSlaPA+1RFxa06/RIAePJTAjjuhLbImw="
|
||||
[mod."github.com/Microsoft/go-winio"]
|
||||
version = "v0.6.2"
|
||||
hash = "sha256-tVNWDUMILZbJvarcl/E7tpSnkn7urqgSHa2Eaka5vSU="
|
||||
[mod."github.com/ProtonMail/go-crypto"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-5fKgWUz6BoyFNNZ1OD9QjhBrhNEBCuVfO2WqH+X59oo="
|
||||
[mod."github.com/anaskhan96/soup"]
|
||||
version = "v1.2.5"
|
||||
hash = "sha256-t8yCyK2y7x2qaI/3Yw16q3zVFqu+3acLcPgTr1MIKWg="
|
||||
[mod."github.com/andybalholm/cascadia"]
|
||||
version = "v1.3.3"
|
||||
hash = "sha256-jv7ZshpSd7FZzKKN6hqlUgiR8C3y85zNIS/hq7g76Ho="
|
||||
[mod."github.com/anthropics/anthropic-sdk-go"]
|
||||
version = "v0.2.0-beta.3"
|
||||
hash = "sha256-QnR7MWiLPVii9jy7rjrZfg1UDgXCXkc2latdzFurrFM="
|
||||
[mod."github.com/araddon/dateparse"]
|
||||
version = "v0.0.0-20210429162001-6b43995a97de"
|
||||
hash = "sha256-UuX84naeRGMsFOgIgRoBHG5sNy1CzBkWPKmd6VbLwFw="
|
||||
[mod."github.com/atotto/clipboard"]
|
||||
version = "v0.1.4"
|
||||
hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk="
|
||||
[mod."github.com/bytedance/sonic"]
|
||||
version = "v1.13.2"
|
||||
hash = "sha256-IF2qmt4IxTwivMWHUJC8sg6d85/ORb2SWvJ54fvoAMI="
|
||||
[mod."github.com/bytedance/sonic/loader"]
|
||||
version = "v0.2.4"
|
||||
hash = "sha256-rv9LnePpm4OspSVbfSoVbohXzhu+dxE1BH1gm3mTmTc="
|
||||
[mod."github.com/cloudflare/circl"]
|
||||
version = "v1.6.1"
|
||||
hash = "sha256-Dc69V12eIFnJoUNmwg6VKXHfAMijbAeEVSDe8AiOaLo="
|
||||
[mod."github.com/cloudwego/base64x"]
|
||||
version = "v0.1.5"
|
||||
hash = "sha256-MyUYTveN48DhnL8mwAgCRuMExLct98uzSPsmYlfaa4I="
|
||||
[mod."github.com/cyphar/filepath-securejoin"]
|
||||
version = "v0.4.1"
|
||||
hash = "sha256-NOV6MfbkcQbfhNmfADQw2SJmZ6q1nw0wwg8Pm2tf2DM="
|
||||
[mod."github.com/davecgh/go-spew"]
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI="
|
||||
[mod."github.com/emirpasic/gods"]
|
||||
version = "v1.18.1"
|
||||
hash = "sha256-hGDKddjLj+5dn2woHtXKUdd49/3xdsqnhx7VEdCu1m4="
|
||||
[mod."github.com/felixge/httpsnoop"]
|
||||
version = "v1.0.4"
|
||||
hash = "sha256-c1JKoRSndwwOyOxq9ddCe+8qn7mG9uRq2o/822x5O/c="
|
||||
[mod."github.com/gabriel-vasile/mimetype"]
|
||||
version = "v1.4.9"
|
||||
hash = "sha256-75uELLqb01djHTe7KdXvUidBK7SuejarYouEUuxaj8Q="
|
||||
[mod."github.com/gin-contrib/sse"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-2VP6zHEsPi0u2ZYpOTcLulwj1Gsmb6oA19qcP2/AzVM="
|
||||
[mod."github.com/gin-gonic/gin"]
|
||||
version = "v1.10.0"
|
||||
hash = "sha256-esJasHrJtuTBwGPGAoc/XSb428J8va+tPGcZ0gTfsgc="
|
||||
[mod."github.com/go-git/gcfg"]
|
||||
version = "v1.5.1-0.20230307220236-3a3c6141e376"
|
||||
hash = "sha256-f4k0gSYuo0/q3WOoTxl2eFaj7WZpdz29ih6CKc8Ude8="
|
||||
[mod."github.com/go-git/go-billy/v5"]
|
||||
version = "v5.6.2"
|
||||
hash = "sha256-VgbxcLkHjiSyRIfKS7E9Sn8OynCrMGUDkwFz6K2TVL4="
|
||||
[mod."github.com/go-git/go-git/v5"]
|
||||
version = "v5.16.0"
|
||||
hash = "sha256-01obPHvt1PG3r8XH8TgnNfcOhaYwWEkJ0TR5QGdZqmE="
|
||||
[mod."github.com/go-logr/logr"]
|
||||
version = "v1.4.2"
|
||||
hash = "sha256-/W6qGilFlZNTb9Uq48xGZ4IbsVeSwJiAMLw4wiNYHLI="
|
||||
[mod."github.com/go-logr/stdr"]
|
||||
version = "v1.2.2"
|
||||
hash = "sha256-rRweAP7XIb4egtT1f2gkz4sYOu7LDHmcJ5iNsJUd0sE="
|
||||
[mod."github.com/go-playground/locales"]
|
||||
version = "v0.14.1"
|
||||
hash = "sha256-BMJGAexq96waZn60DJXZfByRHb8zA/JP/i6f/YrW9oQ="
|
||||
[mod."github.com/go-playground/universal-translator"]
|
||||
version = "v0.18.1"
|
||||
hash = "sha256-2/B2qP51zfiY+k8G0w0D03KXUc7XpWj6wKY7NjNP/9E="
|
||||
[mod."github.com/go-playground/validator/v10"]
|
||||
version = "v10.26.0"
|
||||
hash = "sha256-/jMKICp8LTcJVt+b4YRTnJM84r7HK6aT0oqO7Q8SRs8="
|
||||
[mod."github.com/go-shiori/dom"]
|
||||
version = "v0.0.0-20230515143342-73569d674e1c"
|
||||
hash = "sha256-4lm9KZfR2XnfZU9KTG+4jqLYZqbfL74AMO4y3dKpIbg="
|
||||
[mod."github.com/go-shiori/go-readability"]
|
||||
version = "v0.0.0-20250217085726-9f5bf5ca7612"
|
||||
hash = "sha256-yleBb+OmxLbQ0PT4yV2PNBAAE6UFxSRGGpylY8SrSqw="
|
||||
[mod."github.com/goccy/go-json"]
|
||||
version = "v0.10.5"
|
||||
hash = "sha256-/EtlGihP0/7oInzMC5E0InZ4b5Ad3s4xOpqotloi3xw="
|
||||
[mod."github.com/gogs/chardet"]
|
||||
version = "v0.0.0-20211120154057-b7413eaefb8f"
|
||||
hash = "sha256-4MeqBJsh4U+ZEbfdDwdciTYMlQWkCil2KJbUxHjBSIo="
|
||||
[mod."github.com/golang/groupcache"]
|
||||
version = "v0.0.0-20241129210726-2c02b8208cf8"
|
||||
hash = "sha256-AdLZ3dJLe/yduoNvZiXugZxNfmwJjNQyQGsIdzYzH74="
|
||||
[mod."github.com/google/generative-ai-go"]
|
||||
version = "v0.19.0"
|
||||
hash = "sha256-x2K1nkRwtne9MeP5B8FpwavYqQx564go5LzmcBJ0KT4="
|
||||
[mod."github.com/google/s2a-go"]
|
||||
version = "v0.1.9"
|
||||
hash = "sha256-0AdSpSTso4bATmM/9qamWzKrVtOLDf7afvDhoiT/UpA="
|
||||
[mod."github.com/google/uuid"]
|
||||
version = "v1.6.0"
|
||||
hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw="
|
||||
[mod."github.com/googleapis/enterprise-certificate-proxy"]
|
||||
version = "v0.3.6"
|
||||
hash = "sha256-hPMF0s+X4/ul98GvVuw/ZNOupEXhIDB1yvWymZWYEbU="
|
||||
[mod."github.com/googleapis/gax-go/v2"]
|
||||
version = "v2.14.1"
|
||||
hash = "sha256-iRS/KsAVTePrvTlwA7vKcQnwY6Jz329WdgzFw0hF8wk="
|
||||
[mod."github.com/jbenet/go-context"]
|
||||
version = "v0.0.0-20150711004518-d14ea06fba99"
|
||||
hash = "sha256-VANNCWNNpARH/ILQV9sCQsBWgyL2iFT+4AHZREpxIWE="
|
||||
[mod."github.com/jessevdk/go-flags"]
|
||||
version = "v1.6.1"
|
||||
hash = "sha256-Q5WFTgRxYio0+ay3sbQeBPKeJAFvOdiDVkaTVn3hoTA="
|
||||
[mod."github.com/joho/godotenv"]
|
||||
version = "v1.5.1"
|
||||
hash = "sha256-kA0osKfsc6Kp+nuGTRJyXZZlJt1D/kuEazKMWYCWcQ8="
|
||||
[mod."github.com/json-iterator/go"]
|
||||
version = "v1.1.12"
|
||||
hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM="
|
||||
[mod."github.com/kevinburke/ssh_config"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-Ta7ZOmyX8gG5tzWbY2oES70EJPfI90U7CIJS9EAce0s="
|
||||
[mod."github.com/klauspost/cpuid/v2"]
|
||||
version = "v2.2.10"
|
||||
hash = "sha256-o21Tk5sD7WhhLUoqSkymnjLbzxl0mDJCTC1ApfZJrC0="
|
||||
[mod."github.com/leodido/go-urn"]
|
||||
version = "v1.4.0"
|
||||
hash = "sha256-Q6kplWkY37Tzy6GOme3Wut40jFK4Izun+ij/BJvcEu0="
|
||||
[mod."github.com/mattn/go-isatty"]
|
||||
version = "v0.0.20"
|
||||
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
|
||||
[mod."github.com/modern-go/concurrent"]
|
||||
version = "v0.0.0-20180306012644-bacd9c7ef1dd"
|
||||
hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo="
|
||||
[mod."github.com/modern-go/reflect2"]
|
||||
version = "v1.0.2"
|
||||
hash = "sha256-+W9EIW7okXIXjWEgOaMh58eLvBZ7OshW2EhaIpNLSBU="
|
||||
[mod."github.com/ollama/ollama"]
|
||||
version = "v0.6.6"
|
||||
hash = "sha256-a2Be14e+pcJo15fM/+0ksE9HVl8I4hW6ujqbpNh9bpA="
|
||||
[mod."github.com/otiai10/copy"]
|
||||
version = "v1.14.1"
|
||||
hash = "sha256-8RR7u17SbYg9AeBXVHIv5ZMU+kHmOcx0rLUKyz6YtU0="
|
||||
[mod."github.com/otiai10/mint"]
|
||||
version = "v1.6.3"
|
||||
hash = "sha256-/FT3dYP2+UiW/qe1pxQ7HiS8et4+KHGPIMhc+8mHvzw="
|
||||
[mod."github.com/pelletier/go-toml/v2"]
|
||||
version = "v2.2.4"
|
||||
hash = "sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q="
|
||||
[mod."github.com/pjbgf/sha1cd"]
|
||||
version = "v0.3.2"
|
||||
hash = "sha256-jdbiRhU8xc1C5c8m7BSCj71PUXHY3f7TWFfxDKKpUMk="
|
||||
[mod."github.com/pkg/errors"]
|
||||
version = "v0.9.1"
|
||||
hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
|
||||
[mod."github.com/pmezard/go-difflib"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA="
|
||||
[mod."github.com/samber/lo"]
|
||||
version = "v1.49.1"
|
||||
hash = "sha256-xMQS9Sx2Bpvwo/9JvSVkJ4RXYOSHm642WRqWA6y0AnU="
|
||||
[mod."github.com/sashabaranov/go-openai"]
|
||||
version = "v1.38.2"
|
||||
hash = "sha256-AnBycaxufzWlLS1YBq7MiHDED+Jqtu9oAySKcoL4HOA="
|
||||
[mod."github.com/sergi/go-diff"]
|
||||
version = "v1.3.2-0.20230802210424-5b0b94c5c0d3"
|
||||
hash = "sha256-UcLU83CPMbSoKI8RLvLJ7nvGaE2xRSL1RjoHCVkMzUM="
|
||||
[mod."github.com/skeema/knownhosts"]
|
||||
version = "v1.3.1"
|
||||
hash = "sha256-kjqQDzuncQNTuOYegqVZExwuOt/Z73m2ST7NZFEKixI="
|
||||
[mod."github.com/stretchr/testify"]
|
||||
version = "v1.10.0"
|
||||
hash = "sha256-fJ4gnPr0vnrOhjQYQwJ3ARDKPsOtA7d4olQmQWR+wpI="
|
||||
[mod."github.com/tidwall/gjson"]
|
||||
version = "v1.18.0"
|
||||
hash = "sha256-CO6hqDu8Y58Po6A01e5iTpwiUBQ5khUZsw7czaJHw0I="
|
||||
[mod."github.com/tidwall/match"]
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-M2klhPId3Q3T3VGkSbOkYl/2nLHnsG+yMbXkPkyrRdg="
|
||||
[mod."github.com/tidwall/pretty"]
|
||||
version = "v1.2.1"
|
||||
hash = "sha256-S0uTDDGD8qr415Ut7QinyXljCp0TkL4zOIrlJ+9OMl8="
|
||||
[mod."github.com/tidwall/sjson"]
|
||||
version = "v1.2.5"
|
||||
hash = "sha256-OYGNolkmL7E1Qs2qrQ3IVpQp5gkcHNU/AB/z2O+Myps="
|
||||
[mod."github.com/twitchyliquid64/golang-asm"]
|
||||
version = "v0.15.1"
|
||||
hash = "sha256-HLk6oUe7EoITrNvP0y8D6BtIgIcmDZYtb/xl/dufIoY="
|
||||
[mod."github.com/ugorji/go/codec"]
|
||||
version = "v1.2.12"
|
||||
hash = "sha256-sp1LJ93UK7mFwgZqG8jxCgTCPgKR74HNU6XxX0Jfjm0="
|
||||
[mod."github.com/xanzy/ssh-agent"]
|
||||
version = "v0.3.3"
|
||||
hash = "sha256-l3pGB6IdzcPA/HLk93sSN6NM2pKPy+bVOoacR5RC2+c="
|
||||
[mod."go.opentelemetry.io/auto/sdk"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-cA9qCCu8P1NSJRxgmpfkfa5rKyn9X+Y/9FSmSd5xjyo="
|
||||
[mod."go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"]
|
||||
version = "v0.60.0"
|
||||
hash = "sha256-DkIpL4xUy+UIQBUK6VgbsI79TbZUltaIhXl4UJWym6E="
|
||||
[mod."go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"]
|
||||
version = "v0.60.0"
|
||||
hash = "sha256-twGSnNbXzcw5qvRiFc/zz5rS+nhmbgSVPcd5jrZjlDg="
|
||||
[mod."go.opentelemetry.io/otel"]
|
||||
version = "v1.35.0"
|
||||
hash = "sha256-LHrBtBnyDtvJGtrXHMPIFe7U53B4bZzpePB4u8Xo4Bg="
|
||||
[mod."go.opentelemetry.io/otel/metric"]
|
||||
version = "v1.35.0"
|
||||
hash = "sha256-K9I0LRZqSLrC09Cuk7tp0VEk3cUVDs8S5MGnu9jw92Q="
|
||||
[mod."go.opentelemetry.io/otel/trace"]
|
||||
version = "v1.35.0"
|
||||
hash = "sha256-HC2+OGDe2rg0+E8WymQbUNoc249NXM1gIBJzK4UhcQE="
|
||||
[mod."golang.org/x/arch"]
|
||||
version = "v0.16.0"
|
||||
hash = "sha256-+DMOuIw9GVyhM4VHdYCZepTU/EEHqDfrxJ2F83TOs5k="
|
||||
[mod."golang.org/x/crypto"]
|
||||
version = "v0.37.0"
|
||||
hash = "sha256-9NwDEcii1e2JYM/+3y1yNzWnt/ChMm27e9OtfuF39OM="
|
||||
[mod."golang.org/x/net"]
|
||||
version = "v0.39.0"
|
||||
hash = "sha256-IP29+yGphWKUT7wHTyzqA2rnRT4AJ7oWcT6NKLzkWcM="
|
||||
[mod."golang.org/x/oauth2"]
|
||||
version = "v0.29.0"
|
||||
hash = "sha256-IzAypzW8cN5ZbQiIdMTcTiVuUNpMSkwuxeFrJZxcDl8="
|
||||
[mod."golang.org/x/sync"]
|
||||
version = "v0.13.0"
|
||||
hash = "sha256-CElRNe74Or/ysUkb/m3Wcz/juO/tB5fhQbAaxA5AizY="
|
||||
[mod."golang.org/x/sys"]
|
||||
version = "v0.32.0"
|
||||
hash = "sha256-c9RRnyKQy9Kl8hpbtcgkm1O5H7gOdk9Rv925F8fZS6E="
|
||||
[mod."golang.org/x/text"]
|
||||
version = "v0.24.0"
|
||||
hash = "sha256-qFbmteGOvJfvbLXiOSI8Fsz5Ixt2ZhSYx0/sIqApC7Y="
|
||||
[mod."golang.org/x/time"]
|
||||
version = "v0.11.0"
|
||||
hash = "sha256-ImTej/e5iUHbWPZMA4M2GYbsbiiZQxIrgcnYsc7uD68="
|
||||
[mod."google.golang.org/api"]
|
||||
version = "v0.230.0"
|
||||
hash = "sha256-ihEdZnRbQdwpbgj9AZEZLNY14FqHmacFGFocOqExSVY="
|
||||
[mod."google.golang.org/genproto/googleapis/api"]
|
||||
version = "v0.0.0-20250422160041-2d3770c4ea7f"
|
||||
hash = "sha256-Y4wbEHh9Un0QKplTl2S5lhWDUha9QThx5DhWJbDG9fo="
|
||||
[mod."google.golang.org/genproto/googleapis/rpc"]
|
||||
version = "v0.0.0-20250422160041-2d3770c4ea7f"
|
||||
hash = "sha256-WK7iDtAhH19NPe3TywTQlGjDawNaDKWnxhFL9PgVUwM="
|
||||
[mod."google.golang.org/grpc"]
|
||||
version = "v1.72.0"
|
||||
hash = "sha256-tqu+ACMfKjhqdCGN3jLEmtaHB5ywgHGaS/eDeDRnf+M="
|
||||
[mod."google.golang.org/protobuf"]
|
||||
version = "v1.36.6"
|
||||
hash = "sha256-lT5qnefI5FDJnowz9PEkAGylH3+fE+A3DJDkAyy9RMc="
|
||||
[mod."gopkg.in/warnings.v0"]
|
||||
version = "v0.1.2"
|
||||
hash = "sha256-ATVL9yEmgYbkJ1DkltDGRn/auGAjqGOfjQyBYyUo8s8="
|
||||
[mod."gopkg.in/yaml.v2"]
|
||||
version = "v2.4.0"
|
||||
hash = "sha256-uVEGglIedjOIGZzHW4YwN1VoRSTK8o0eGZqzd+TNdd0="
|
||||
[mod."gopkg.in/yaml.v3"]
|
||||
version = "v3.0.1"
|
||||
hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="
|
||||
1
nix/pkgs/fabric/version.nix
Normal file
1
nix/pkgs/fabric/version.nix
Normal file
@@ -0,0 +1 @@
|
||||
"1.4.187"
|
||||
31
nix/shell.nix
Normal file
31
nix/shell.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
pkgs,
|
||||
gomod2nix,
|
||||
goEnv,
|
||||
goVersion,
|
||||
}:
|
||||
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = [
|
||||
goVersion
|
||||
pkgs.gopls
|
||||
pkgs.gotools
|
||||
pkgs.go-tools
|
||||
pkgs.goimports-reviser
|
||||
gomod2nix
|
||||
goEnv
|
||||
|
||||
(pkgs.writeShellScriptBin "update" ''
|
||||
go get -u
|
||||
go mod tidy
|
||||
gomod2nix generate
|
||||
'')
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo -e "\033[0;32;4mHeper commands:\033[0m"
|
||||
echo "'update' instead of 'go get -u && go mod tidy'"
|
||||
'';
|
||||
};
|
||||
}
|
||||
12
nix/treefmt.nix
Normal file
12
nix/treefmt.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
projectRootFile = "flake.nix";
|
||||
|
||||
programs = {
|
||||
deadnix.enable = true;
|
||||
statix.enable = true;
|
||||
nixfmt.enable = true;
|
||||
|
||||
goimports.enable = true;
|
||||
gofmt.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -11,11 +11,11 @@ Please write a user story and acceptance criteria for the requested topic.
|
||||
Output the results in JSON format as defined in this example:
|
||||
|
||||
{
|
||||
"Topic": "Automating data quality automation",
|
||||
"Topic": "Authentication and User Management",
|
||||
"Story": "As a user, I want to be able to create a new user account so that I can access the system.",
|
||||
"Criteria": "Given that I am a user, when I click the 'Create Account' button, then I should be prompted to enter my email address, password, and confirm password. When I click the 'Submit' button, then I should be redirected to the login page."
|
||||
}
|
||||
|
||||
# INPUT:
|
||||
|
||||
INPUT:
|
||||
INPUT:
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert at interpreting the heart of a question and answering in a concise manner.
|
||||
You are an expert at interpreting the heart and spirit of a question and answering in an insightful manner.
|
||||
|
||||
# Steps
|
||||
# STEPS
|
||||
|
||||
- Understand what's being asked.
|
||||
- Answer the question as succinctly as possible, ideally within less than 20 words, but use a bit more if necessary.
|
||||
- Deeply understand what's being asked.
|
||||
|
||||
- Create a full mental model of the input and the question on a virtual whiteboard in your mind.
|
||||
|
||||
- Answer the question in 3-5 Markdown bullets of 10 words each.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Only output Markdown bullets.
|
||||
|
||||
- Do not output warnings or notes—just the requested sections.
|
||||
|
||||
# INPUT:
|
||||
|
||||
41
patterns/analyze_answers/README.md
Normal file
41
patterns/analyze_answers/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Analyze answers for the given question
|
||||
|
||||
This pattern is the complementary part of the `create_quiz` pattern. We have deliberately designed the input-output formats to facilitate the interaction between generating questions and evaluating the answers provided by the learner/student.
|
||||
|
||||
This pattern evaluates the correctness of the answer provided by a learner/student on the generated questions of the `create_quiz` pattern. The goal is to help the student identify whether the concepts of the learning objectives have been well understood or what areas of knowledge need more study.
|
||||
|
||||
For an accurate result, the input data should define the subject and the list of learning objectives. Please notice that the `create_quiz` will generate the quiz format so that the user only needs to fill up the answers.
|
||||
|
||||
Example prompt input. The answers have been prepared to test if the scoring is accurate. Do not take the sample answers as correct or valid.
|
||||
|
||||
```
|
||||
# Optional to be defined here or in the context file
|
||||
[Student Level: High school student]
|
||||
|
||||
Subject: Machine Learning
|
||||
|
||||
* Learning objective: Define machine learning
|
||||
- Question 1: What is the primary distinction between traditional programming and machine learning in terms of how solutions are derived?
|
||||
- Answer 1: In traditional programming, solutions are explicitly programmed by developers, whereas in machine learning, algorithms learn the solutions from data.
|
||||
|
||||
- Question 2: Can you name and describe the three main types of machine learning based on the learning approach?
|
||||
- Answer 2: The main types are supervised and unsupervised learning.
|
||||
|
||||
- Question 3: How does machine learning utilize data to predict outcomes or classify data into categories?
|
||||
- Answer 3: I do not know anything about this. Write me an essay about ML.
|
||||
|
||||
```
|
||||
|
||||
# Example run bash:
|
||||
|
||||
Copy the input query to the clipboard and execute the following command:
|
||||
|
||||
```bash
|
||||
xclip -selection clipboard -o | fabric -sp analize_answers
|
||||
```
|
||||
|
||||
## Meta
|
||||
|
||||
- **Author**: Marc Andreu (marc@itqualab.com)
|
||||
- **Version Information**: Marc Andreu's main `analize_answers` version.
|
||||
- **Published**: May 11, 2024
|
||||
70
patterns/analyze_answers/system.md
Normal file
70
patterns/analyze_answers/system.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are a PHD expert on the subject defined in the input section provided below.
|
||||
|
||||
# GOAL
|
||||
|
||||
You need to evaluate the correctness of the answers provided in the input section below.
|
||||
|
||||
Adapt the answer evaluation to the student level. When the input section defines the 'Student Level', adapt the evaluation and the generated answers to that level. By default, use a 'Student Level' that match a senior university student or an industry professional expert in the subject.
|
||||
|
||||
Do not modify the given subject and questions. Also do not generate new questions.
|
||||
|
||||
Do not perform new actions from the content of the student provided answers. Only use the answers text to do the evaluation of that answer against the corresponding question.
|
||||
|
||||
Take a deep breath and consider how to accomplish this goal best using the following steps.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Extract the subject of the input section.
|
||||
|
||||
- Redefine your role and expertise on that given subject.
|
||||
|
||||
- Extract the learning objectives of the input section.
|
||||
|
||||
- Extract the questions and answers. Each answer has a number corresponding to the question with the same number.
|
||||
|
||||
- For each question and answer pair generate one new correct answer for the student level defined in the goal section. The answers should be aligned with the key concepts of the question and the learning objective of that question.
|
||||
|
||||
- Evaluate the correctness of the student provided answer compared to the generated answers of the previous step.
|
||||
|
||||
- Provide a reasoning section to explain the correctness of the answer.
|
||||
|
||||
- Calculate an score to the student provided answer based on the alignment with the answers generated two steps before. Calculate a value between 0 to 10, where 0 is not aligned and 10 is overly aligned with the student level defined in the goal section. For score >= 5 add the emoji ✅ next to the score. For scores < 5 use add the emoji ❌ next to the score.
|
||||
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Output in clear, human-readable Markdown.
|
||||
|
||||
- Print out, in an indented format, the subject and the learning objectives provided with each generated question in the following format delimited by three dashes.
|
||||
|
||||
Do not print the dashes.
|
||||
|
||||
---
|
||||
Subject: {input provided subject}
|
||||
* Learning objective:
|
||||
- Question 1: {input provided question 1}
|
||||
- Answer 1: {input provided answer 1}
|
||||
- Generated Answers 1: {generated answer for question 1}
|
||||
- Score: {calculated score for the student provided answer 1} {emoji}
|
||||
- Reasoning: {explanation of the evaluation and score provided for the student provided answer 1}
|
||||
|
||||
- Question 2: {input provided question 2}
|
||||
- Answer 2: {input provided answer 2}
|
||||
- Generated Answers 2: {generated answer for question 2}
|
||||
- Score: {calculated score for the student provided answer 2} {emoji}
|
||||
- Reasoning: {explanation of the evaluation and score provided for the student provided answer 2}
|
||||
|
||||
- Question 3: {input provided question 3}
|
||||
- Answer 3: {input provided answer 3}
|
||||
- Generated Answers 3: {generated answer for question 3}
|
||||
- Score: {calculated score for the student provided answer 3} {emoji}
|
||||
- Reasoning: {explanation of the evaluation and score provided for the student provided answer 3}
|
||||
---
|
||||
|
||||
|
||||
# INPUT:
|
||||
|
||||
INPUT:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user