mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-07-26 04:04:57 -04:00
Compare commits
678 Commits
module
...
fix/content
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e67105f0c | |||
| 0fec0d39b7 | |||
| 8709422e05 | |||
| 229aab41f8 | |||
| 853a168773 | |||
| fa963660e0 | |||
| fbb098e996 | |||
| b124244b0c | |||
| c7d1b58340 | |||
| 9971819350 | |||
| e3769d3e1f | |||
| f777ce2992 | |||
| 02eda4efb3 | |||
| 1b3420c857 | |||
| 2f537e72bb | |||
| 4afdb6f813 | |||
| c610c2c6ca | |||
| 047a324e0e | |||
| e37283cb11 | |||
| d6f10b28c1 | |||
| c082b42395 | |||
| 252ee2aff6 | |||
| fffc1f8647 | |||
| dc1653294d | |||
| 3862c3c0eb | |||
| 756c46b526 | |||
| 73a6be8264 | |||
| 84f21c6ab4 | |||
| a0ab067bf7 | |||
| c719cbd544 | |||
| 6fa73262fc | |||
| 558fa396a8 | |||
| 33ffe06510 | |||
| a5a602c5b0 | |||
| 500862b768 | |||
| b06522d1ec | |||
| 0c312c9caa | |||
| d456315f19 | |||
| 983f5599d5 | |||
| 471c937d57 | |||
| ad61551db5 | |||
| 2bad9c399f | |||
| 580ad7d46f | |||
| 47f013f3e1 | |||
| d5219a9379 | |||
| 00460b1538 | |||
| 98e62edf12 | |||
| 592969f976 | |||
| 6be849e409 | |||
| d42c2a5749 | |||
| 8d2eafbc35 | |||
| 848f688ae9 | |||
| b813842f85 | |||
| 684ce43eb8 | |||
| 39d13d155f | |||
| 2d5523a129 | |||
| 7fe3ed2169 | |||
| 6db3b5f31f | |||
| f6ac820a3f | |||
| d955284278 | |||
| 3fd6134bcd | |||
| 4d875e9e2a | |||
| 79b72ec311 | |||
| 8487459d90 | |||
| be336adf02 | |||
| be1acb06fd | |||
| 26ec479596 | |||
| efee3c462d | |||
| 4cf52ec698 | |||
| ba8ea0117e | |||
| 36770b70ae | |||
| 5784be2853 | |||
| 5b93e5c21c | |||
| b0a43b0cb7 | |||
| 51bb368073 | |||
| 153fcf6cf0 | |||
| b52571dbea | |||
| c0a4f1026f | |||
| aab73ead5c | |||
| f05af02864 | |||
| 46adfb5258 | |||
| d6784f11a7 | |||
| 4bd86bd842 | |||
| e630ead9c0 | |||
| 5bd97908b5 | |||
| f98c306b48 | |||
| cf60dd88c4 | |||
| dc7badd034 | |||
| 20511283b5 | |||
| 5c233c8aa5 | |||
| 9500ec7af0 | |||
| 1fb7f8bc75 | |||
| 62723267a9 | |||
| e4da4b307c | |||
| 578de58e44 | |||
| aff460c7c2 | |||
| ff6890c9c1 | |||
| 9160ca598e | |||
| 7a8b3d93cf | |||
| 86659e704a | |||
| 6e999927ec | |||
| 863c6f3dcb | |||
| 2a6378764b | |||
| da7cb91f83 | |||
| 13e0f12c1b | |||
| a0a9299cda | |||
| 15237d3137 | |||
| e97cd43cdb | |||
| 1313f805ae | |||
| aafbd9159a | |||
| cff90bc709 | |||
| e2de768e09 | |||
| 80871db0de | |||
| 50cad776aa | |||
| 56b6f7240c | |||
| f0dac5ca22 | |||
| c9bc5808c2 | |||
| 037aab3bdd | |||
| e2679fae5c | |||
| 4a0ff17990 | |||
| 5af97a5dd3 | |||
| 37aa532cc2 | |||
| 9a90153dc6 | |||
| 490bf998a4 | |||
| 9caaaf187a | |||
| fb1643f1c1 | |||
| 0228499889 | |||
| 35eb9ee259 | |||
| b01d469089 | |||
| 843a912a03 | |||
| 48f15a479a | |||
| 4a0d3a7a94 | |||
| a668830a79 | |||
| a3977c2b1c | |||
| a03098b01e | |||
| 367832ddb2 | |||
| 8849a9ba1b | |||
| 2b4b2c513c | |||
| e1c44d3f5c | |||
| 112cb1f955 | |||
| a288ea9a36 | |||
| 4c2ca24203 | |||
| f97865f7d8 | |||
| 255435c260 | |||
| fd60daad7a | |||
| 1a53d24890 | |||
| c23e655f44 | |||
| 2b07dd79ec | |||
| 864bf8b94c | |||
| 60c53c8f3e | |||
| cc0f5cbdcc | |||
| 8d527d6a09 | |||
| e431bc234b | |||
| 289edb67a6 | |||
| 324124e002 | |||
| 2bacac687f | |||
| 8b504a5e1e | |||
| 0e4e60cc15 | |||
| fb1ab70650 | |||
| 94f4d350c9 | |||
| cc3fdecfc9 | |||
| c92acd670e | |||
| 0464a5b344 | |||
| 8bdb575bb2 | |||
| f2c3b9dbdb | |||
| 02c6b5ad4e | |||
| 5e5224da25 | |||
| c529f880d3 | |||
| 18eb40ec14 | |||
| 904765591c | |||
| f726d3c3e5 | |||
| 62ce629bf1 | |||
| 9c555bd99f | |||
| 5981109730 | |||
| 087a5fbe0f | |||
| 67cb5937e7 | |||
| 8abf5512a4 | |||
| 45dd74d27c | |||
| 1109b0a533 | |||
| bd1b06f326 | |||
| 7406db5882 | |||
| a1efa07b24 | |||
| 29f7644577 | |||
| f3884c0244 | |||
| 6516af6c34 | |||
| 77680c6fee | |||
| 5faa599321 | |||
| 6209ededff | |||
| f6b6d5246c | |||
| b81624bfc2 | |||
| c1844f7230 | |||
| 15efd2d527 | |||
| 5e3bc0f89b | |||
| 12209ab926 | |||
| 547e9cd0c4 | |||
| 6a1e536ca7 | |||
| eb8663ada1 | |||
| ce997d2425 | |||
| 56cd0e05ca | |||
| 25bb3fb123 | |||
| 9e52438877 | |||
| c4618896cf | |||
| ee1667d51a | |||
| bafd26e92c | |||
| 8ecc18f76f | |||
| 985f07a529 | |||
| 8b2900c6d8 | |||
| 50e56fe22f | |||
| b5a12a1da6 | |||
| 70e749b53a | |||
| 784a4c7969 | |||
| 43a2a142fa | |||
| 8ee5956bdb | |||
| 4888dfcdca | |||
| a6b41fd3ab | |||
| d25aed9a1a | |||
| 4a3f471f72 | |||
| 93154a0a27 | |||
| 59ab91d7df | |||
| 42590a7371 | |||
| 6260d4f168 | |||
| 4206da92a6 | |||
| 4d6fbf1caa | |||
| 97ef7acec0 | |||
| 77189b6114 | |||
| c32d315910 | |||
| 606ffd8275 | |||
| 601dba3fc4 | |||
| 00ab476a77 | |||
| 906079cbbb | |||
| 808d9c981c | |||
| 2b79c99dd7 | |||
| 77905ed3cd | |||
| 60c249f19a | |||
| 209a9989c4 | |||
| 5105b46f48 | |||
| e4c7d1acfc | |||
| dd4fbd64d3 | |||
| 4010f9d86c | |||
| 0fda6e38db | |||
| bffb5bd852 | |||
| 4d722c35d3 | |||
| 8dd0c3883b | |||
| c2ec333777 | |||
| 2f19feff5e | |||
| e128cde613 | |||
| bc7f6f059c | |||
| 0eb68b6c20 | |||
| 1c4ab09335 | |||
| abc7d6e080 | |||
| cb15f926e8 | |||
| 70282535d4 | |||
| 5111738b3b | |||
| c141a9bd80 | |||
| c8cf70b1d0 | |||
| 3f83f5c4b0 | |||
| 45fbfed030 | |||
| 2b3f61aed1 | |||
| e7111c6554 | |||
| 894dde9256 | |||
| 446908b759 | |||
| 18364d169e | |||
| 6464a33912 | |||
| 34caeea081 | |||
| 25286a828c | |||
| 6747fe87f2 | |||
| 09559f9ed9 | |||
| 4107a7a063 | |||
| e3d4177c53 | |||
| ee77bba615 | |||
| 0709f2f1ff | |||
| a569e37a34 | |||
| 1eee5b5a32 | |||
| ffee9d8307 | |||
| ff20a0332e | |||
| 034f596e06 | |||
| daa7dcd12a | |||
| b81f34a8f8 | |||
| 6d9f1a95cc | |||
| 9f77bb99f1 | |||
| 74fdfd7a55 | |||
| 7494aa9c26 | |||
| e90c192063 | |||
| 53d135bec9 | |||
| 99e0011920 | |||
| 5023bc77f4 | |||
| a5ba49ec93 | |||
| f3c06b1bfb | |||
| 86cb9f1282 | |||
| d8cf5b419b | |||
| bb4459b99f | |||
| ab3e6ae3c8 | |||
| 1f8c601795 | |||
| f70985362d | |||
| cafaa0e153 | |||
| 491354280b | |||
| 4c40e47e8d | |||
| 045fb1f8d6 | |||
| 8e703c0ac2 | |||
| 29beee6057 | |||
| d672842a81 | |||
| 45078e1fa7 | |||
| c96c8d8009 | |||
| 11231f23ea | |||
| c1dcda42ae | |||
| 7b52b9c22d | |||
| dff678fc4e | |||
| e0703cdb7c | |||
| c940141e61 | |||
| 5fdd464fad | |||
| 68cfdecaee | |||
| 906b4ebd76 | |||
| 05cb8ba2eb | |||
| 0644daa477 | |||
| 62940a1a56 | |||
| c6ce1c324f | |||
| 383fc1d0f4 | |||
| 8ac6f68568 | |||
| 05af5d9695 | |||
| 2c273392cd | |||
| 08f5417e96 | |||
| f67ebe8c7a | |||
| 6ace4ab60d | |||
| 319caf8e91 | |||
| 7fb99ecf21 | |||
| d520d88301 | |||
| 4dcc77372d | |||
| 0f2731f9e8 | |||
| 6e888f6008 | |||
| 2713349c75 | |||
| b6cd62a8a3 | |||
| dd6739cbbf | |||
| 5cd0eaae3f | |||
| 9d6b81d9c2 | |||
| d5df14a714 | |||
| 8e9b87bcb1 | |||
| 3e36b09376 | |||
| 074aefd0df | |||
| 3e1081fc6e | |||
| 73af9538eb | |||
| 959dd8c7f3 | |||
| 71e66c651c | |||
| 438a65caf6 | |||
| fb20bbe5bf | |||
| fa12dba7c2 | |||
| 54c8bf5f1a | |||
| 7bc44167cf | |||
| 88933784de | |||
| d7b503e30c | |||
| e1e708ee75 | |||
| 9d81eb7509 | |||
| ddc7d0e100 | |||
| e26b55a6a8 | |||
| ca3420c791 | |||
| abd6d1bbf7 | |||
| d0cfc4c317 | |||
| 53c1554d55 | |||
| b09749dddb | |||
| b199c10ab7 | |||
| c8abc5f28f | |||
| 14948c965d | |||
| 558306a841 | |||
| fb41663330 | |||
| 84836b8345 | |||
| 5f2a2eaa24 | |||
| 75a8d63e83 | |||
| f0b8bfb4f4 | |||
| fa25e7c077 | |||
| 3a9169bdbe | |||
| 3a921f6241 | |||
| c1ac7b675a | |||
| d689bb4331 | |||
| b42ef27641 | |||
| abb3ffc109 | |||
| e5e238efc0 | |||
| 33483ab4b9 | |||
| 638eedc2a0 | |||
| 86d3596f41 | |||
| 16288c0fc3 | |||
| 5d42ebbc71 | |||
| 0513a327f6 | |||
| c3d3bba32a | |||
| 1b187444fc | |||
| d54abc3ed0 | |||
| 1e5b3f501f | |||
| 0e240077ab | |||
| 401172631d | |||
| 5b8ca0b756 | |||
| 8be14b7e3f | |||
| 96de3bdddd | |||
| 56d209f95d | |||
| 169c5e8aad | |||
| d7cfa7f0b2 | |||
| 43a46ad1fb | |||
| 2de5152bfd | |||
| a83f4512b6 | |||
| 08785e2908 | |||
| 8e694f70ec | |||
| f0bd184fbd | |||
| e32a569796 | |||
| 31b5849d02 | |||
| 29a8713427 | |||
| 3c3b5a774c | |||
| 35c75b61d8 | |||
| 33b5b8c8f4 | |||
| dc2570c90b | |||
| aea0b424b9 | |||
| 5e73be42cb | |||
| 53ebcdad5d | |||
| a1cdabd0a8 | |||
| 26bcdf72a2 | |||
| 9347193fdc | |||
| efc82bde30 | |||
| 6a451267d5 | |||
| 9ee0d89a6b | |||
| 10f7c8ff13 | |||
| c69ee9e5f7 | |||
| 0ad2de72e0 | |||
| 1e484d7ccd | |||
| 7486e68a17 | |||
| 3763f320b9 | |||
| 16e0836fc7 | |||
| 6954ad3217 | |||
| 69e043f8e1 | |||
| d451919414 | |||
| 9ff2fde44f | |||
| 40b0d4bfc9 | |||
| 14a70c3edd | |||
| 0b71d8dc10 | |||
| bc60dde94f | |||
| 28e80084f6 | |||
| 7be93fb014 | |||
| 5ecb97e845 | |||
| 3827ebebdf | |||
| 106d1e61d4 | |||
| b884d9433a | |||
| f2e7010297 | |||
| 1e1c123d84 | |||
| 51ba87a7ba | |||
| bf8d8be5ad | |||
| 311c0bb5ee | |||
| 7492a3ab3b | |||
| 127c76d006 | |||
| 2942668d89 | |||
| d288755444 | |||
| 758a73e8ab | |||
| 5dd4b9cb65 | |||
| d714c4f80b | |||
| 173eda4fb3 | |||
| 365f89cd5e | |||
| 5e52383a99 | |||
| f6e16be170 | |||
| 5721c52c0d | |||
| 6c83f3d089 | |||
| 3a7e56cdf1 | |||
| 89d7da3817 | |||
| 7a98408336 | |||
| e696c5ae31 | |||
| 042c1ee65c | |||
| 3a7b5e1461 | |||
| 4d6b04c6ed | |||
| 70c389ce0b | |||
| 5b4f618ca3 | |||
| 8492c95cb6 | |||
| a75feaab4e | |||
| 3790ad3666 | |||
| a57f990576 | |||
| 76a01994f9 | |||
| 4520d16522 | |||
| 0dce524c7a | |||
| 0b78956cc4 | |||
| 43352376e3 | |||
| d3f2126614 | |||
| cf112d57a6 | |||
| f28725199c | |||
| fbcc618355 | |||
| 094580724f | |||
| 371ea63f5a | |||
| bc431ea6ff | |||
| 5c6df5adc0 | |||
| 46085077a5 | |||
| 33f1c102e4 | |||
| b66e698b5e | |||
| 43c29fbdb0 | |||
| 0a18d8409e | |||
| 0139b79835 | |||
| 296734ba3b | |||
| d73fd545b2 | |||
| fa1ae086bf | |||
| 96091a1ad5 | |||
| ac6c03bcb7 | |||
| 6ae019db23 | |||
| 2a6650c3ea | |||
| 5e6863379c | |||
| 8c447a0cf8 | |||
| c68ff23b01 | |||
| 735bab5e32 | |||
| c2035c95c5 | |||
| 351aa80b74 | |||
| 0fc09b4f49 | |||
| 1900915609 | |||
| 6bafb48cec | |||
| 2561f2f175 | |||
| d053dc249c | |||
| 6318f1aa97 | |||
| a204b3677b | |||
| b1d90dbedd | |||
| 361927b0a4 | |||
| b010f48ddc | |||
| 6d866985e8 | |||
| 989a2421ba | |||
| 3f9b454276 | |||
| fdd4af1042 | |||
| 24ef65f70d | |||
| 5c3497a433 | |||
| 9974dec2e1 | |||
| 773259ceaa | |||
| 1fc0db18cd | |||
| 249cedd9d1 | |||
| bc7192d664 | |||
| 22160cbf9e | |||
| 34f6d821b9 | |||
| e2aa3bedd7 | |||
| f6dcc09562 | |||
| 08563c3286 | |||
| 2cba2eafe6 | |||
| c0773f03f8 | |||
| bad3e53f06 | |||
| 96f7a4653f | |||
| cac986d287 | |||
| 4a42ec0487 | |||
| 9d0dcf792a | |||
| 324cbcd2a6 | |||
| 35d9ba44f5 | |||
| 4ed61daf8e | |||
| 56a6a10980 | |||
| ca57f4cd85 | |||
| 609b2a2b7c | |||
| 55b7b4a41e | |||
| 7db2b10bd2 | |||
| f0bc2be678 | |||
| ac8f6e94ff | |||
| 27f7299749 | |||
| eda99d05f4 | |||
| 0bc95cfd16 | |||
| 80508001c7 | |||
| 7c0f615952 | |||
| 54de6221d1 | |||
| 74b158e9f1 | |||
| 4b41653d00 | |||
| 03175d36ab | |||
| e6fdd91369 | |||
| 97f5ba16cd | |||
| 677a96074b | |||
| 82f1b37ae8 | |||
| adf649be99 | |||
| 26baaecbcd | |||
| fac211d8ee | |||
| 84a808fb7e | |||
| 6528da804f | |||
| 3170ee0ba2 | |||
| 3295942e59 | |||
| fa39ae93f1 | |||
| d237e17719 | |||
| cb35f871db | |||
| f85962769a | |||
| 3b361d1a3d | |||
| ac580b562e | |||
| 230d012915 | |||
| 0dda705017 | |||
| 69cbcc5c88 | |||
| 437da44590 | |||
| 82ac74ac5d | |||
| 7c70f09834 | |||
| 73524adfce | |||
| 48d17b6952 | |||
| db490fb3ca | |||
| a1edf005a9 | |||
| 78ba7871e9 | |||
| 36abf837a9 | |||
| 4453f43bec | |||
| 414a973282 | |||
| 80361a6400 | |||
| f11c8f3f57 | |||
| c56e277a2b | |||
| cf3c4549da | |||
| 66b1847644 | |||
| 533caeee96 | |||
| 185fb89d39 | |||
| 7adcce78be | |||
| dbfc596333 | |||
| dda993e43b | |||
| 00078b5da8 | |||
| a4a2a172f5 | |||
| 8db36b4619 | |||
| 59b91d1403 | |||
| 23867bf0e6 | |||
| 27202c3622 | |||
| dc72904cee | |||
| 8ceaab2ac1 | |||
| 015b26096f | |||
| 215c3ddbf7 | |||
| 84c56f6c3e | |||
| 90fd130e31 | |||
| 9f5a32a1bf | |||
| 13b08c8cb3 | |||
| 4f9ec2896b | |||
| e3987e9bd1 | |||
| 899e3c0771 | |||
| f2c74e29e8 | |||
| 5f29125bbb | |||
| 32f4e53242 | |||
| 3f6b68a60a | |||
| 9e8b1dab84 | |||
| 652cef288d | |||
| 744af19025 | |||
| 5c58072ad7 | |||
| 98c53e042d | |||
| 79e5dffe09 | |||
| b4fd482f66 | |||
| 9173156e40 | |||
| 936b2af4ca | |||
| 9df690999c | |||
| 7db9aea57b | |||
| 41692d700c | |||
| e6090c62cf | |||
| a404b75fbe | |||
| 58ba4db1dd | |||
| 9417c5ca8f | |||
| 8e3a1fcbe5 | |||
| 7c679ead94 | |||
| b45490e84d | |||
| 2ebbf1007f | |||
| fdb0585dcb | |||
| 56ceedb4fb | |||
| f2e09dfe81 | |||
| eb4294bdbb | |||
| 3b1a54083d | |||
| aa62d9ef9e | |||
| 8601956e53 | |||
| 11486d4720 | |||
| 658681f344 | |||
| 9926674c38 | |||
| 61e4be0d0c | |||
| d52e3b8309 | |||
| 2cc907cbd7 | |||
| 3790a872ea | |||
| d22154e9be | |||
| a1203c8f14 | |||
| 509080c8f2 | |||
| 2535895214 | |||
| 60e9d66b36 | |||
| df15681400 | |||
| f1b39164ef | |||
| 7e3f2cbffb | |||
| 0f953d8ad9 | |||
| 491a0f2b3d | |||
| e606d637e2 | |||
| ab24a2a7cf | |||
| 8164190425 | |||
| d1ae51ae5d | |||
| f740f307e2 | |||
| d5f72a6c82 | |||
| 87736e464a | |||
| 414f9ca765 | |||
| a7462249a7 | |||
| 317b91a7e9 | |||
| ee8351a637 | |||
| d01ee8e27b | |||
| d08a097175 | |||
| 42d35dd7a3 | |||
| a823131a2d | |||
| d32940e604 | |||
| 11514a0e0c | |||
| ee535536ad | |||
| 1ad68eca01 | |||
| d57db7449a | |||
| a3300dfb6d |
+2
-1
@@ -1,2 +1,3 @@
|
||||
models/
|
||||
db/
|
||||
data/
|
||||
volumes/
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Enable debug mode in the LocalAI API
|
||||
DEBUG=true
|
||||
|
||||
# Where models are stored
|
||||
MODELS_PATH=/models
|
||||
|
||||
# Galleries to use
|
||||
GALLERIES=[{"name":"model-gallery", "url":"github:go-skynet/model-gallery/index.yaml"}, {"url": "github:go-skynet/model-gallery/huggingface.yaml","name":"huggingface"}]
|
||||
|
||||
# Select model configuration in the config directory
|
||||
#PRELOAD_MODELS_CONFIG=/config/wizardlm-13b.yaml
|
||||
PRELOAD_MODELS_CONFIG=/config/wizardlm-13b.yaml
|
||||
#PRELOAD_MODELS_CONFIG=/config/wizardlm-13b-superhot.yaml
|
||||
|
||||
# You don't need to put a valid OpenAI key, however, the python libraries expect
|
||||
# the string to be set or panics
|
||||
OPENAI_API_KEY=sk---
|
||||
|
||||
# Set the OpenAI API base URL to point to LocalAI
|
||||
DEFAULT_API_BASE=http://api:8080
|
||||
|
||||
# Set an image path
|
||||
IMAGE_PATH=/tmp
|
||||
|
||||
# Set number of default threads
|
||||
THREADS=14
|
||||
@@ -0,0 +1,19 @@
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "bun"
|
||||
directory: "/webui/react-ui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,32 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Add this line to trigger the workflow on tag pushes that match 'v*'
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.24
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: 'build container images'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
concurrency:
|
||||
group: ci-image-${{ github.head_ref || github.ref }}-${{ github.repository }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
containerImages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
id: prep
|
||||
run: |
|
||||
DOCKER_IMAGE=quay.io/mudler/localagi
|
||||
# Use branch name as default
|
||||
VERSION=${GITHUB_REF#refs/heads/}
|
||||
BINARY_VERSION=$(git describe --always --tags --dirty)
|
||||
SHORTREF=${GITHUB_SHA::8}
|
||||
# If this is git tag, use the tag name as a docker tag
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
|
||||
# If the VERSION looks like a version number, assume that
|
||||
# this is the most recent version of the image and also
|
||||
# tag it 'latest'.
|
||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
# Set output parameters.
|
||||
echo ::set-output name=binary_version::${BINARY_VERSION}
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
|
||||
with:
|
||||
images: quay.io/mudler/localagi
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}}
|
||||
type=semver,pattern={{raw}}
|
||||
type=sha,suffix=-{{date 'YYYYMMDDHHmmss'}}
|
||||
type=ref,event=branch
|
||||
flavor: |
|
||||
latest=auto
|
||||
prefix=
|
||||
suffix=
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.prep.outputs.binary_version }}
|
||||
context: ./
|
||||
file: ./Dockerfile.webui
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
#tags: ${{ steps.prep.outputs.tags }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
sshbox-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
id: prep
|
||||
run: |
|
||||
DOCKER_IMAGE=quay.io/mudler/localagi-sshbox
|
||||
# Use branch name as default
|
||||
VERSION=${GITHUB_REF#refs/heads/}
|
||||
BINARY_VERSION=$(git describe --always --tags --dirty)
|
||||
SHORTREF=${GITHUB_SHA::8}
|
||||
# If this is git tag, use the tag name as a docker tag
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
|
||||
# If the VERSION looks like a version number, assume that
|
||||
# this is the most recent version of the image and also
|
||||
# tag it 'latest'.
|
||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
# Set output parameters.
|
||||
echo ::set-output name=binary_version::${BINARY_VERSION}
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
|
||||
with:
|
||||
images: quay.io/mudler/localagi-sshbox
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}}
|
||||
type=semver,pattern={{raw}}
|
||||
type=sha,suffix=-{{date 'YYYYMMDDHHmmss'}}
|
||||
type=ref,event=branch
|
||||
flavor: |
|
||||
latest=auto
|
||||
prefix=
|
||||
suffix=
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.prep.outputs.binary_version }}
|
||||
context: ./
|
||||
file: ./Dockerfile.sshbox
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
#tags: ${{ steps.prep.outputs.tags }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
concurrency:
|
||||
group: ci-tests-${{ github.head_ref || github.ref }}-${{ github.repository }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.17.0'
|
||||
- name: Run tests
|
||||
run: |
|
||||
make tests
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.17.0'
|
||||
- run: |
|
||||
make tests-e2e
|
||||
+10
-2
@@ -1,2 +1,10 @@
|
||||
db/
|
||||
models/
|
||||
models/
|
||||
data/
|
||||
pool
|
||||
uploads/
|
||||
local-agent-framework
|
||||
localagi
|
||||
LocalAGI
|
||||
**/.env
|
||||
.vscode
|
||||
volumes/
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
version: 2
|
||||
builds:
|
||||
- main: ./
|
||||
id: "localagi"
|
||||
binary: localagi
|
||||
ldflags:
|
||||
- -w -s
|
||||
# - -X github.com/internal.Version={{.Tag}}
|
||||
# - -X github.com/internal.Commit={{.Commit}}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
source:
|
||||
enabled: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Tag }}-source'
|
||||
archives:
|
||||
# Default template uses underscores instead of -
|
||||
- name_template: >-
|
||||
{{ .ProjectName }}-{{ .Tag }}-
|
||||
{{- if eq .Os "freebsd" }}FreeBSD
|
||||
{{- else }}{{- title .Os }}{{end}}-
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{end}}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}-{{ .Tag }}-checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
use: github-native
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
FROM python:3.10-bullseye
|
||||
WORKDIR /app
|
||||
COPY ./requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Install package dependencies
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
alsa-utils \
|
||||
libsndfile1-dev && \
|
||||
apt-get clean
|
||||
|
||||
COPY . /app
|
||||
RUN pip install .
|
||||
ENTRYPOINT [ "python", "./main.py" ];
|
||||
@@ -0,0 +1,12 @@
|
||||
# python
|
||||
FROM python:3.14-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y python3-dev portaudio19-dev ffmpeg build-essential
|
||||
|
||||
RUN pip install RealtimeSTT
|
||||
|
||||
#COPY ./example/realtimesst /app
|
||||
# https://github.com/KoljaB/RealtimeSTT/blob/master/RealtimeSTT_server/README.md#server-usage
|
||||
ENTRYPOINT ["stt-server"]
|
||||
#ENTRYPOINT [ "/app/main.py" ]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Final stage
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
docker.io \
|
||||
bash \
|
||||
wget \
|
||||
curl \
|
||||
openssh-server \
|
||||
sudo
|
||||
|
||||
# Configure SSH
|
||||
RUN mkdir /var/run/sshd
|
||||
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
|
||||
|
||||
# Create startup script
|
||||
RUN echo '#!/bin/bash\n\
|
||||
if [ -n "$SSH_USER" ]; then\n\
|
||||
if [ "$SSH_USER" = "root" ]; then\n\
|
||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config\n\
|
||||
if [ -n "$SSH_PASSWORD" ]; then\n\
|
||||
echo "root:$SSH_PASSWORD" | chpasswd\n\
|
||||
fi\n\
|
||||
else\n\
|
||||
echo "PermitRootLogin no" >> /etc/ssh/sshd_config\n\
|
||||
useradd -m -s /bin/bash $SSH_USER\n\
|
||||
if [ -n "$SSH_PASSWORD" ]; then\n\
|
||||
echo "$SSH_USER:$SSH_PASSWORD" | chpasswd\n\
|
||||
fi\n\
|
||||
if [ -n "$SUDO_ACCESS" ] && [ "$SUDO_ACCESS" = "true" ]; then\n\
|
||||
echo "$SSH_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$SSH_USER\n\
|
||||
fi\n\
|
||||
fi\n\
|
||||
fi\n\
|
||||
/usr/sbin/sshd -D' > /start.sh
|
||||
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 22
|
||||
|
||||
CMD ["/start.sh"]
|
||||
@@ -0,0 +1,64 @@
|
||||
# Use Bun container for building the React UI
|
||||
FROM oven/bun:1 AS ui-builder
|
||||
|
||||
# Set the working directory for the React UI
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and bun.lockb (if exists)
|
||||
COPY webui/react-ui/package.json webui/react-ui/bun.lockb* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy the rest of the React UI source code
|
||||
COPY webui/react-ui/ ./
|
||||
|
||||
# Build the React UI
|
||||
RUN bun run build
|
||||
|
||||
# Use a temporary build image based on Golang 1.24-alpine
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Define argument for linker flags
|
||||
ARG LDFLAGS="-s -w"
|
||||
|
||||
# Install git
|
||||
RUN apk add --no-cache git
|
||||
RUN rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /work
|
||||
|
||||
# Copy go.mod and go.sum files first to leverage Docker cache
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies - this layer will be cached as long as go.mod and go.sum don't change
|
||||
RUN go mod download
|
||||
|
||||
# Now copy the rest of the source code
|
||||
COPY . .
|
||||
|
||||
# Copy the built React UI from the ui-builder stage
|
||||
COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 go build -ldflags="$LDFLAGS" -o localagi ./
|
||||
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
docker.io \
|
||||
bash \
|
||||
wget \
|
||||
curl
|
||||
|
||||
# Copy the webui binary from the builder stage to the final image
|
||||
COPY --from=builder /work/localagi /localagi
|
||||
|
||||
# Define the command that will be run when the container is started
|
||||
ENTRYPOINT ["/localagi"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Ettore Di Giacinto
|
||||
Copyright (c) 2023-2025 Ettore Di Giacinto (mudler@localai.io)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -18,4 +18,4 @@ 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.
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
GOCMD?=go
|
||||
IMAGE_NAME?=webui
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
prepare-tests:
|
||||
docker compose up -d --build
|
||||
|
||||
cleanup-tests:
|
||||
docker compose down
|
||||
|
||||
tests: prepare-tests
|
||||
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!E2E" --flake-attempts=5 --fail-fast -v -r ./...
|
||||
|
||||
run-nokb:
|
||||
$(MAKE) run KBDISABLEINDEX=true
|
||||
|
||||
webui/react-ui/dist:
|
||||
docker run --entrypoint /bin/bash -v $(ROOT_DIR):/app oven/bun:1 -c "cd /app/webui/react-ui && bun install && bun run build"
|
||||
|
||||
.PHONY: build
|
||||
build: webui/react-ui/dist
|
||||
$(GOCMD) build -o localagi ./
|
||||
|
||||
.PHONY: run
|
||||
run: webui/react-ui/dist
|
||||
$(GOCMD) run ./
|
||||
|
||||
build-image:
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile.webui .
|
||||
|
||||
image-push:
|
||||
docker push $(IMAGE_NAME)
|
||||
|
||||
tests-e2e: prepare-tests
|
||||
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="E2E" --flake-attempts=5 --fail-fast -v -r ./tests/e2e/...
|
||||
@@ -1,45 +0,0 @@
|
||||
- id: huggingface@TheBloke/WizardLM-13B-V1.1-GGML/wizardlm-13b-v1.1.ggmlv3.q5_K_M.bin
|
||||
name: "gpt-4"
|
||||
overrides:
|
||||
context_size: 2048
|
||||
mmap: true
|
||||
f16: true
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
- id: model-gallery@stablediffusion
|
||||
- id: model-gallery@voice-en-us-kathleen-low
|
||||
- url: github:go-skynet/model-gallery/base.yaml
|
||||
name: all-MiniLM-L6-v2
|
||||
overrides:
|
||||
embeddings: true
|
||||
backend: huggingface-embeddings
|
||||
parameters:
|
||||
model: all-MiniLM-L6-v2
|
||||
- id: huggingface@TheBloke/WizardLM-13B-V1.1-GGML/wizardlm-13b-v1.1.ggmlv3.q5_K_M.bin
|
||||
name: functions
|
||||
overrides:
|
||||
context_size: 2048
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
template:
|
||||
chat: ""
|
||||
completion: ""
|
||||
roles:
|
||||
assistant: "ASSISTANT:"
|
||||
system: "SYSTEM:"
|
||||
assistant_function_call: "FUNCTION_CALL:"
|
||||
function: "FUNCTION CALL RESULT:"
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
function:
|
||||
disable_no_action: true
|
||||
mmap: true
|
||||
f16: true
|
||||
@@ -1,47 +0,0 @@
|
||||
- id: huggingface@TheBloke/WizardLM-13B-V1-0-Uncensored-SuperHOT-8K-GGML/wizardlm-13b-v1.0-superhot-8k.ggmlv3.q4_K_M.bin
|
||||
name: "gpt-4"
|
||||
overrides:
|
||||
context_size: 8192
|
||||
mmap: true
|
||||
f16: true
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
rope_freq_scale: 0.25
|
||||
- id: model-gallery@stablediffusion
|
||||
- id: model-gallery@voice-en-us-kathleen-low
|
||||
- url: github:go-skynet/model-gallery/base.yaml
|
||||
name: all-MiniLM-L6-v2
|
||||
overrides:
|
||||
embeddings: true
|
||||
backend: huggingface-embeddings
|
||||
parameters:
|
||||
model: all-MiniLM-L6-v2
|
||||
- id: huggingface@TheBloke/WizardLM-13B-V1-0-Uncensored-SuperHOT-8K-GGML/wizardlm-13b-v1.0-superhot-8k.ggmlv3.q4_K_M.bin
|
||||
name: functions
|
||||
overrides:
|
||||
context_size: 8192
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
template:
|
||||
chat: ""
|
||||
completion: ""
|
||||
roles:
|
||||
assistant: "ASSISTANT:"
|
||||
system: "SYSTEM:"
|
||||
assistant_function_call: "FUNCTION_CALL:"
|
||||
function: "FUNCTION CALL RESULT:"
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
rope_freq_scale: 0.25
|
||||
function:
|
||||
disable_no_action: true
|
||||
mmap: true
|
||||
f16: true
|
||||
@@ -1,45 +0,0 @@
|
||||
- id: huggingface@thebloke/wizardlm-13b-v1.0-uncensored-ggml/wizardlm-13b-v1.0-uncensored.ggmlv3.q4_k_m.bin
|
||||
name: "gpt-4"
|
||||
overrides:
|
||||
context_size: 2048
|
||||
mmap: true
|
||||
f16: true
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
- id: model-gallery@stablediffusion
|
||||
- id: model-gallery@voice-en-us-kathleen-low
|
||||
- url: github:go-skynet/model-gallery/base.yaml
|
||||
name: all-MiniLM-L6-v2
|
||||
overrides:
|
||||
embeddings: true
|
||||
backend: huggingface-embeddings
|
||||
parameters:
|
||||
model: all-MiniLM-L6-v2
|
||||
- id: huggingface@thebloke/wizardlm-13b-v1.0-uncensored-ggml/wizardlm-13b-v1.0-uncensored.ggmlv3.q4_0.bin
|
||||
name: functions
|
||||
overrides:
|
||||
context_size: 2048
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
template:
|
||||
chat: ""
|
||||
completion: ""
|
||||
roles:
|
||||
assistant: "ASSISTANT:"
|
||||
system: "SYSTEM:"
|
||||
assistant_function_call: "FUNCTION_CALL:"
|
||||
function: "FUNCTION CALL RESULT:"
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
function:
|
||||
disable_no_action: true
|
||||
mmap: true
|
||||
f16: true
|
||||
@@ -1,47 +0,0 @@
|
||||
- id: huggingface@TheBloke/WizardLM-Uncensored-SuperCOT-StoryTelling-30B-SuperHOT-8K-GGML/WizardLM-Uncensored-SuperCOT-StoryTelling-30b-superhot-8k.ggmlv3.q4_0.bin
|
||||
name: "gpt-4"
|
||||
overrides:
|
||||
context_size: 8192
|
||||
mmap: true
|
||||
f16: true
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
rope_freq_scale: 0.25
|
||||
- id: model-gallery@stablediffusion
|
||||
- id: model-gallery@voice-en-us-kathleen-low
|
||||
- url: github:go-skynet/model-gallery/base.yaml
|
||||
name: all-MiniLM-L6-v2
|
||||
overrides:
|
||||
embeddings: true
|
||||
backend: huggingface-embeddings
|
||||
parameters:
|
||||
model: all-MiniLM-L6-v2
|
||||
- id: huggingface@TheBloke/WizardLM-Uncensored-SuperCOT-StoryTelling-30B-SuperHOT-8K-GGML/WizardLM-Uncensored-SuperCOT-StoryTelling-30b-superhot-8k.ggmlv3.q4_0.bin
|
||||
name: functions
|
||||
overrides:
|
||||
context_size: 8192
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
template:
|
||||
chat: ""
|
||||
completion: ""
|
||||
roles:
|
||||
assistant: "ASSISTANT:"
|
||||
system: "SYSTEM:"
|
||||
assistant_function_call: "FUNCTION_CALL:"
|
||||
function: "FUNCTION CALL RESULT:"
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
rope_freq_scale: 0.25
|
||||
function:
|
||||
disable_no_action: true
|
||||
mmap: true
|
||||
f16: true
|
||||
@@ -1,46 +0,0 @@
|
||||
- id: huggingface@thebloke/wizardlm-30b-uncensored-ggml/wizardlm-30b-uncensored.ggmlv3.q2_k.bin
|
||||
galleryModel:
|
||||
name: "gpt-4"
|
||||
overrides:
|
||||
context_size: 4096
|
||||
mmap: true
|
||||
f16: true
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
- id: model-gallery@stablediffusion
|
||||
- id: model-gallery@voice-en-us-kathleen-low
|
||||
- url: github:go-skynet/model-gallery/base.yaml
|
||||
name: all-MiniLM-L6-v2
|
||||
overrides:
|
||||
embeddings: true
|
||||
backend: huggingface-embeddings
|
||||
parameters:
|
||||
model: all-MiniLM-L6-v2
|
||||
- id: huggingface@thebloke/wizardlm-30b-uncensored-ggml/wizardlm-30b-uncensored.ggmlv3.q2_k.bin
|
||||
name: functions
|
||||
overrides:
|
||||
context_size: 4096
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
template:
|
||||
chat: ""
|
||||
completion: ""
|
||||
roles:
|
||||
assistant: "ASSISTANT:"
|
||||
system: "SYSTEM:"
|
||||
assistant_function_call: "FUNCTION_CALL:"
|
||||
function: "FUNCTION CALL RESULT:"
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
function:
|
||||
disable_no_action: true
|
||||
mmap: true
|
||||
f16: true
|
||||
@@ -1,45 +0,0 @@
|
||||
- id: huggingface@thebloke/wizardlm-7b-v1.0-uncensored-ggml/wizardlm-7b-v1.0-uncensored.ggmlv3.q4_k_m.bin
|
||||
name: "gpt-4"
|
||||
overrides:
|
||||
context_size: 2048
|
||||
mmap: true
|
||||
f16: true
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
- id: model-gallery@stablediffusion
|
||||
- id: model-gallery@voice-en-us-kathleen-low
|
||||
- url: github:go-skynet/model-gallery/base.yaml
|
||||
name: all-MiniLM-L6-v2
|
||||
overrides:
|
||||
embeddings: true
|
||||
backend: huggingface-embeddings
|
||||
parameters:
|
||||
model: all-MiniLM-L6-v2
|
||||
- id: huggingface@thebloke/wizardlm-7b-v1.0-uncensored-ggml/wizardlm-7b-v1.0-uncensored.ggmlv3.q4_0.bin
|
||||
name: functions
|
||||
overrides:
|
||||
context_size: 2048
|
||||
mirostat: 2
|
||||
mirostat_tau: 5.0
|
||||
mirostat_eta: 0.1
|
||||
template:
|
||||
chat: ""
|
||||
completion: ""
|
||||
roles:
|
||||
assistant: "ASSISTANT:"
|
||||
system: "SYSTEM:"
|
||||
assistant_function_call: "FUNCTION_CALL:"
|
||||
function: "FUNCTION CALL RESULT:"
|
||||
parameters:
|
||||
temperature: 0.1
|
||||
top_k: 40
|
||||
top_p: 0.95
|
||||
function:
|
||||
disable_no_action: true
|
||||
mmap: true
|
||||
f16: true
|
||||
@@ -0,0 +1,13 @@
|
||||
package action_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestAction(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Agent Action test suite")
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
"github.com/traefik/yaegi/interp"
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
)
|
||||
|
||||
func NewCustom(config map[string]string, goPkgPath string) (*CustomAction, error) {
|
||||
a := &CustomAction{
|
||||
config: config,
|
||||
goPkgPath: goPkgPath,
|
||||
}
|
||||
|
||||
if err := a.initializeInterpreter(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.callInit(); err != nil {
|
||||
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
type CustomAction struct {
|
||||
config map[string]string
|
||||
goPkgPath string
|
||||
i *interp.Interpreter
|
||||
}
|
||||
|
||||
func (a *CustomAction) callInit() error {
|
||||
if a.i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
|
||||
return nil
|
||||
}
|
||||
|
||||
run, ok := v.Interface().(func(string) error)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return run(a.config["configuration"])
|
||||
}
|
||||
|
||||
func (a *CustomAction) initializeInterpreter() error {
|
||||
if _, exists := a.config["code"]; exists && a.i == nil {
|
||||
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
|
||||
i := interp.New(interp.Options{
|
||||
Env: os.Environ(),
|
||||
GoPath: a.goPkgPath,
|
||||
Unrestricted: unsafe,
|
||||
})
|
||||
if err := i.Use(stdlib.Symbols); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, exists := a.config["name"]; !exists {
|
||||
a.config["name"] = "custom"
|
||||
}
|
||||
|
||||
// let's find first if there is already a package declarated in the code
|
||||
// the user might want to specify it to not break syntax with IDEs
|
||||
re := regexp.MustCompile("package (\\w+)")
|
||||
packageName := re.FindStringSubmatch(a.config["code"])
|
||||
if len(packageName) > 1 {
|
||||
// remove it from the code, normalize to `name`
|
||||
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
|
||||
}
|
||||
|
||||
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.i = i
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *CustomAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *CustomAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
run := v.Interface().(func(map[string]interface{}) (string, map[string]interface{}, error))
|
||||
|
||||
res, meta, err := run(params)
|
||||
return types.ActionResult{Result: res, Metadata: meta}, err
|
||||
}
|
||||
|
||||
func (a *CustomAction) Definition() types.ActionDefinition {
|
||||
|
||||
if a.i == nil {
|
||||
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Error("Error getting custom action definition", "error", err)
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
description := ""
|
||||
desc, err := a.i.Eval(fmt.Sprintf("%s.Description", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Warn("No description found for custom action", "error", err, "action", a.config["name"])
|
||||
} else {
|
||||
d, ok := desc.Interface().(func() string)
|
||||
if ok {
|
||||
description = d()
|
||||
}
|
||||
}
|
||||
|
||||
if a.config["description"] != "" {
|
||||
description = a.config["description"]
|
||||
}
|
||||
|
||||
properties := v.Interface().(func() map[string][]string)
|
||||
|
||||
v, err = a.i.Eval(fmt.Sprintf("%s.RequiredFields", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Error("Error getting custom action definition", "error", err)
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
requiredFields := v.Interface().(func() []string)
|
||||
|
||||
prop := map[string]jsonschema.Definition{}
|
||||
|
||||
for k, v := range properties() {
|
||||
if len(v) != 2 {
|
||||
xlog.Error("Invalid property definition", "property", k)
|
||||
continue
|
||||
}
|
||||
prop[k] = jsonschema.Definition{
|
||||
Type: jsonschema.DataType(v[0]),
|
||||
Description: v[1],
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(a.config["name"]),
|
||||
Description: description,
|
||||
Properties: prop,
|
||||
Required: requiredFields(),
|
||||
}
|
||||
}
|
||||
|
||||
func CustomConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "name",
|
||||
Label: "Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Name of the custom action",
|
||||
},
|
||||
{
|
||||
Name: "description",
|
||||
Label: "Description",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Description of the custom action",
|
||||
},
|
||||
{
|
||||
Name: "code",
|
||||
Label: "Code",
|
||||
Type: config.FieldTypeTextarea,
|
||||
Required: true,
|
||||
HelpText: "Go code for the custom action",
|
||||
},
|
||||
{
|
||||
Name: "unsafe",
|
||||
Label: "Unsafe",
|
||||
Type: config.FieldTypeCheckbox,
|
||||
HelpText: "Allow unsafe code execution",
|
||||
},
|
||||
{
|
||||
Name: "configuration",
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration for the custom action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package action_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
var _ = Describe("Agent custom action", func() {
|
||||
Context("custom action", func() {
|
||||
It("initializes correctly", func() {
|
||||
|
||||
testCode := `
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
type Params struct {
|
||||
Foo string
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
|
||||
p := Params{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "",map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "",map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
return p.Foo,map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"foo": []string{
|
||||
"string",
|
||||
"The foo value",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"foo"}
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
customAction, err := NewCustom(
|
||||
map[string]string{
|
||||
"code": testCode,
|
||||
"name": "test",
|
||||
"description": "A test action",
|
||||
},
|
||||
"",
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
definition := customAction.Definition()
|
||||
Expect(definition).To(Equal(types.ActionDefinition{
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"foo": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The foo value",
|
||||
},
|
||||
},
|
||||
Required: []string{"foo"},
|
||||
Name: "test",
|
||||
Description: "A test action",
|
||||
}))
|
||||
|
||||
runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
|
||||
"Foo": "bar",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(runResult.Result).To(Equal("bar"))
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const ConversationActionName = "new_conversation"
|
||||
|
||||
func NewConversation() *ConversationAction {
|
||||
return &ConversationAction{}
|
||||
}
|
||||
|
||||
type ConversationAction struct{}
|
||||
|
||||
type ConversationActionResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (a *ConversationAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
func (a *ConversationAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *ConversationAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ConversationActionName,
|
||||
Description: "Use this tool to initiate a new conversation or to notify something.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message to start the conversation",
|
||||
},
|
||||
},
|
||||
Required: []string{"message"},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
)
|
||||
|
||||
// StopActionName is the name of the action
|
||||
// used by the LLM to stop any further action
|
||||
const StopActionName = "stop"
|
||||
|
||||
func NewStop() *StopAction {
|
||||
return &StopAction{}
|
||||
}
|
||||
|
||||
type StopAction struct{}
|
||||
|
||||
func (a *StopAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
func (a *StopAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *StopAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: StopActionName,
|
||||
Description: "Use this tool to stop any further action and stop the conversation. You must use this when it looks like there is a conclusion to the conversation or the topic diverged too much from the original conversation. For instance if the user offer his help and you already replied with a message, you can use this tool to stop the conversation.",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
ReminderActionName = "set_reminder"
|
||||
ListRemindersName = "list_reminders"
|
||||
RemoveReminderName = "remove_reminder"
|
||||
)
|
||||
|
||||
func NewReminder() *ReminderAction {
|
||||
return &ReminderAction{}
|
||||
}
|
||||
|
||||
func NewListReminders() *ListRemindersAction {
|
||||
return &ListRemindersAction{}
|
||||
}
|
||||
|
||||
func NewRemoveReminder() *RemoveReminderAction {
|
||||
return &RemoveReminderAction{}
|
||||
}
|
||||
|
||||
type ReminderAction struct{}
|
||||
type ListRemindersAction struct{}
|
||||
type RemoveReminderAction struct{}
|
||||
|
||||
type RemoveReminderParams struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := types.ReminderActionResponse{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Validate the cron expression
|
||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
_, err = parser.Parse(result.CronExpr)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Calculate next run time
|
||||
now := time.Now()
|
||||
schedule, _ := parser.Parse(result.CronExpr) // We can ignore the error since we validated above
|
||||
nextRun := schedule.Next(now)
|
||||
|
||||
// Set the reminder details
|
||||
result.LastRun = now
|
||||
result.NextRun = nextRun
|
||||
// IsRecurring is set by the user through the action parameters
|
||||
|
||||
// Store the reminder in the shared state
|
||||
if sharedState.Reminders == nil {
|
||||
sharedState.Reminders = make([]types.ReminderActionResponse, 0)
|
||||
}
|
||||
sharedState.Reminders = append(sharedState.Reminders, result)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: "Reminder set successfully",
|
||||
Metadata: map[string]interface{}{
|
||||
"reminder": result,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: "No reminders set",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString("Current reminders:\n")
|
||||
for i, reminder := range sharedState.Reminders {
|
||||
status := "one-time"
|
||||
if reminder.IsRecurring {
|
||||
status = "recurring"
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s)\n",
|
||||
i+1,
|
||||
reminder.Message,
|
||||
reminder.NextRun.Format(time.RFC3339),
|
||||
status))
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: result.String(),
|
||||
Metadata: map[string]interface{}{
|
||||
"reminders": sharedState.Reminders,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var removeParams RemoveReminderParams
|
||||
err := params.Unmarshal(&removeParams)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: "No reminders to remove",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert from 1-based index to 0-based
|
||||
index := removeParams.Index - 1
|
||||
if index < 0 || index >= len(sharedState.Reminders) {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid reminder index: %d", removeParams.Index)
|
||||
}
|
||||
|
||||
// Remove the reminder
|
||||
removed := sharedState.Reminders[index]
|
||||
sharedState.Reminders = append(sharedState.Reminders[:index], sharedState.Reminders[index+1:]...)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed reminder: %s", removed.Message),
|
||||
Metadata: map[string]interface{}{
|
||||
"removed_reminder": removed,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *RemoveReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ReminderActionName,
|
||||
Description: "Set a reminder for the agent to wake up and perform a task based on a cron schedule. Examples: '0 0 * * *' (daily at midnight), '0 */2 * * *' (every 2 hours), '0 0 * * 1' (every Monday at midnight)",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message or task to be reminded about",
|
||||
},
|
||||
"cron_expr": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'second minute hour day month weekday'",
|
||||
},
|
||||
"is_recurring": {
|
||||
Type: jsonschema.Boolean,
|
||||
Description: "Whether this reminder should repeat according to the cron schedule (true) or trigger only once (false)",
|
||||
},
|
||||
},
|
||||
Required: []string{"message", "cron_expr", "is_recurring"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ListRemindersName,
|
||||
Description: "List all currently set reminders with their next scheduled run times",
|
||||
Properties: map[string]jsonschema.Definition{},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *RemoveReminderAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: RemoveReminderName,
|
||||
Description: "Remove a reminder by its index number (use list_reminders to see the index)",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"index": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The index number of the reminder to remove (1-based)",
|
||||
},
|
||||
},
|
||||
Required: []string{"index"},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const StateActionName = "update_state"
|
||||
|
||||
func NewState() *StateAction {
|
||||
return &StateAction{}
|
||||
}
|
||||
|
||||
type StateAction struct{}
|
||||
|
||||
func (a *StateAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{Result: "internal state has been updated"}, nil
|
||||
}
|
||||
|
||||
func (a *StateAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *StateAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: StateActionName,
|
||||
Description: "update the agent state (short memory) with the current state of the conversation.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"goal": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The current goal of the agent.",
|
||||
},
|
||||
"doing_next": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The next action the agent will do.",
|
||||
},
|
||||
"done_history": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.String,
|
||||
},
|
||||
Description: "A list of actions that the agent has done.",
|
||||
},
|
||||
"now_doing": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The current action the agent is doing.",
|
||||
},
|
||||
"memories": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.String,
|
||||
},
|
||||
Description: "A list of memories to keep between conversations.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type Messages []openai.ChatCompletionMessage
|
||||
|
||||
func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
|
||||
return []openai.ChatCompletionMessage(m)
|
||||
}
|
||||
|
||||
func (m Messages) RemoveIf(f func(msg openai.ChatCompletionMessage) bool) Messages {
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
if f(m[i]) {
|
||||
m = append(m[:i], m[i+1:]...)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Messages) String() string {
|
||||
s := ""
|
||||
for _, cc := range m {
|
||||
s += cc.Role + ": " + cc.Content + "\n"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m Messages) Exist(content string) bool {
|
||||
for _, cc := range m {
|
||||
if cc.Content == content {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Messages) RemoveLastUserMessage() Messages {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
if m[i].Role == UserRole {
|
||||
return append(m[:i], m[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Messages) Save(path string) error {
|
||||
content, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
|
||||
xlog.Debug("Getting latest user message", "messages", m)
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
msg := m[i]
|
||||
if msg.Role == UserRole {
|
||||
return &msg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAvailableActionsForJob returns available actions including user-defined ones for a specific job
|
||||
func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
|
||||
// Start with regular available actions
|
||||
baseActions := a.availableActions()
|
||||
|
||||
// Add user-defined actions from the job
|
||||
userTools := job.GetUserTools()
|
||||
if len(userTools) > 0 {
|
||||
userDefinedActions := types.CreateUserDefinedActions(userTools)
|
||||
baseActions = append(baseActions, userDefinedActions...)
|
||||
xlog.Debug("Added user-defined actions", "definitions", userTools)
|
||||
}
|
||||
|
||||
return baseActions
|
||||
}
|
||||
|
||||
func (a *Agent) availableActions() types.Actions {
|
||||
// defaultActions := append(a.options.userActions, action.NewReply())
|
||||
|
||||
defaultActions := slices.Clone(a.options.userActions)
|
||||
|
||||
if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation..
|
||||
acts := append(defaultActions, action.NewConversation())
|
||||
if a.options.enableHUD {
|
||||
acts = append(acts, action.NewState())
|
||||
}
|
||||
//if a.options.canStopItself {
|
||||
// acts = append(acts, action.NewStop())
|
||||
// }
|
||||
|
||||
return acts
|
||||
}
|
||||
|
||||
if a.options.canStopItself {
|
||||
acts := append(defaultActions, action.NewStop())
|
||||
if a.options.enableHUD {
|
||||
acts = append(acts, action.NewState())
|
||||
}
|
||||
return acts
|
||||
}
|
||||
|
||||
if a.options.enableHUD {
|
||||
return append(defaultActions, action.NewState())
|
||||
}
|
||||
|
||||
return defaultActions
|
||||
}
|
||||
|
||||
func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
|
||||
if !a.options.enableHUD {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &PromptHUD{
|
||||
Character: a.Character,
|
||||
CurrentState: *a.currentState,
|
||||
PermanentGoal: a.options.permanentGoal,
|
||||
ShowCharacter: a.options.showCharacter,
|
||||
}
|
||||
}
|
||||
+1297
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
package agent_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestAgent(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Agent test suite")
|
||||
}
|
||||
|
||||
var testModel = os.Getenv("LOCALAGI_MODEL")
|
||||
var apiURL = os.Getenv("LOCALAI_API_URL")
|
||||
var apiKeyURL = os.Getenv("LOCALAI_API_KEY")
|
||||
|
||||
func init() {
|
||||
if testModel == "" {
|
||||
testModel = "hermes-2-pro-mistral"
|
||||
}
|
||||
if apiURL == "" {
|
||||
apiURL = "http://192.168.68.113:8080"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
package agent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
|
||||
. "github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const testActionResult = "In Boston it's 30C today, it's sunny, and humidity is at 98%"
|
||||
const testActionResult2 = "In milan it's very hot today, it is 45C and the humidity is at 200%"
|
||||
const testActionResult3 = "In paris it's very cold today, it is 2C and the humidity is at 10%"
|
||||
|
||||
var _ types.Action = &TestAction{}
|
||||
|
||||
var debugOptions = []types.JobOption{
|
||||
types.WithReasoningCallback(func(state types.ActionCurrentState) bool {
|
||||
xlog.Info("Reasoning", state)
|
||||
return true
|
||||
}),
|
||||
types.WithResultCallback(func(state types.ActionState) {
|
||||
xlog.Info("Reasoning", state.Reasoning)
|
||||
xlog.Info("Action", state.Action)
|
||||
xlog.Info("Result", state.Result)
|
||||
}),
|
||||
}
|
||||
|
||||
type TestAction struct {
|
||||
response map[string]string
|
||||
definition *types.ActionDefinition
|
||||
}
|
||||
|
||||
func (a *TestAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
|
||||
for k, r := range a.response {
|
||||
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
|
||||
return types.ActionResult{Result: r}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{Result: "No match"}, nil
|
||||
}
|
||||
|
||||
func (a *TestAction) Definition() types.ActionDefinition {
|
||||
def := types.ActionDefinition{
|
||||
Name: "get_weather",
|
||||
Description: "get current weather",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"location": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
"unit": {
|
||||
Type: jsonschema.String,
|
||||
Enum: []string{"celsius", "fahrenheit"},
|
||||
},
|
||||
},
|
||||
|
||||
Required: []string{"location"},
|
||||
}
|
||||
|
||||
if a.definition != nil {
|
||||
def = *a.definition
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
type FakeStoreResultAction struct {
|
||||
TestAction
|
||||
}
|
||||
|
||||
func (a *FakeStoreResultAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "store_results",
|
||||
Description: "store results permanently. Use this tool after you have a result you want to keep.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"term": {
|
||||
Type: jsonschema.String,
|
||||
Description: "What to store permanently",
|
||||
},
|
||||
},
|
||||
|
||||
Required: []string{"term"},
|
||||
}
|
||||
}
|
||||
|
||||
type FakeInternetAction struct {
|
||||
TestAction
|
||||
}
|
||||
|
||||
func (a *FakeInternetAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "search_internet",
|
||||
Description: "search on internet",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"term": {
|
||||
Type: jsonschema.String,
|
||||
Description: "What to search for",
|
||||
},
|
||||
},
|
||||
|
||||
Required: []string{"term"},
|
||||
}
|
||||
}
|
||||
|
||||
var _ = Describe("Agent test", func() {
|
||||
Context("jobs", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
Eventually(func() error {
|
||||
// test apiURL is working and available
|
||||
_, err := http.Get(apiURL + "/readyz")
|
||||
return err
|
||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("pick the correct action", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
EnableForceReasoning,
|
||||
WithTimeout("10m"),
|
||||
// WithRandomIdentity(),
|
||||
WithActions(&TestAction{response: map[string]string{
|
||||
"boston": testActionResult,
|
||||
"milan": testActionResult2,
|
||||
"paris": testActionResult3,
|
||||
}}),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
|
||||
res := agent.Ask(
|
||||
append(debugOptions,
|
||||
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
|
||||
)...,
|
||||
)
|
||||
Expect(res.Error).ToNot(HaveOccurred())
|
||||
reasons := []string{}
|
||||
for _, r := range res.State {
|
||||
|
||||
reasons = append(reasons, r.Result)
|
||||
}
|
||||
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
||||
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
|
||||
|
||||
Expect(len(res.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(res.Conversation))
|
||||
|
||||
reasons = []string{}
|
||||
|
||||
res = agent.Ask(
|
||||
append(debugOptions,
|
||||
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
|
||||
)...)
|
||||
for _, r := range res.State {
|
||||
|
||||
reasons = append(reasons, r.Result)
|
||||
}
|
||||
//Expect(reasons).ToNot(ContainElement(testActionResult), fmt.Sprint(res))
|
||||
//Expect(reasons).ToNot(ContainElement(testActionResult2), fmt.Sprint(res))
|
||||
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
|
||||
// conversation := agent.CurrentConversation()
|
||||
// for _, r := range res.State {
|
||||
// reasons = append(reasons, r.Result)
|
||||
// }
|
||||
// Expect(len(conversation)).To(Equal(10), fmt.Sprint(conversation))
|
||||
})
|
||||
It("pick the correct action", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithTimeout("10m"),
|
||||
// WithRandomIdentity(),
|
||||
WithActions(&TestAction{response: map[string]string{
|
||||
"boston": testActionResult,
|
||||
},
|
||||
}),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
res := agent.Ask(
|
||||
append(debugOptions,
|
||||
types.WithText("can you get the weather in boston? Use celsius units"))...,
|
||||
)
|
||||
reasons := []string{}
|
||||
for _, r := range res.State {
|
||||
reasons = append(reasons, r.Result)
|
||||
}
|
||||
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
||||
})
|
||||
|
||||
It("updates the state with internal actions", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithTimeout("10m"),
|
||||
EnableHUD,
|
||||
// EnableStandaloneJob,
|
||||
// WithRandomIdentity(),
|
||||
WithPermanentGoal("I want to learn to play music"),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
|
||||
result := agent.Ask(
|
||||
types.WithText("Update your goals such as you want to learn to play the guitar"),
|
||||
)
|
||||
fmt.Printf("%+v\n", result)
|
||||
Expect(result.Error).ToNot(HaveOccurred())
|
||||
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||
})
|
||||
|
||||
It("Can generate a plan", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithLLMAPIKey(apiKeyURL),
|
||||
WithTimeout("10m"),
|
||||
WithMaxEvaluationLoops(1),
|
||||
WithActions(
|
||||
&TestAction{
|
||||
response: map[string]string{
|
||||
"boston": testActionResult,
|
||||
"milan": testActionResult2,
|
||||
},
|
||||
},
|
||||
&TestAction{
|
||||
response: map[string]string{
|
||||
"flight": "Flight options from Boston to Milan (April 22-26, 2025):\n• Outbound: Boston Logan (BOS) → Milan Malpensa (MXP), April 22, 2025\n - Economy: $450-650 (Alitalia, Delta, Lufthansa)\n - Business: $1,200-1,800\n - Flight time: 8h 15m (1 stop) or 9h 45m (direct)\n• Return: Milan Malpensa (MXP) → Boston Logan (BOS), April 26, 2025\n - Economy: $420-580\n - Business: $1,100-1,600\n• Total estimated cost: $870-1,230 per person\n• Best booking window: 2-3 months in advance for optimal prices",
|
||||
"hotel": "Hotel recommendations in Milan for April 22-26, 2025:\n• Luxury (4-5 stars): $200-400/night\n - Hotel Principe di Savoia: $380/night (central location)\n - Mandarin Oriental: $420/night (luxury amenities)\n• Mid-range (3-4 stars): $120-200/night\n - Hotel Spadari al Duomo: $160/night (near cathedral)\n - Hotel Milano Scala: $140/night (theater district)\n• Budget (2-3 stars): $80-120/night\n - Hotel Bernina: $95/night (near train station)\n• Total 4-night stay: $320-1,680 depending on category\n• Booking tip: Reserve early for spring season discounts",
|
||||
"car": "Car rental options in Milan for April 22-26, 2025:\n• Economy cars: $35-50/day (Fiat 500, VW Polo)\n• Compact cars: $45-65/day (Ford Focus, Opel Astra)\n• Mid-size cars: $60-85/day (BMW 3 Series, Audi A4)\n• SUV/Luxury: $90-150/day (BMW X3, Mercedes E-Class)\n• Total 4-day rental: $140-600\n• Pickup locations: Milan Malpensa Airport, Milan Central Station, city center\n• Insurance: $15-25/day additional\n• Fuel: ~$60-80 for 4 days of city driving\n• Parking: $20-40/day in city center hotels",
|
||||
"food": "Dining budget and recommendations for Milan (April 22-26, 2025):\n• Fine dining: $80-150/person (Michelin-starred restaurants)\n - Cracco: $120/person (2 Michelin stars)\n - Trussardi alla Scala: $100/person\n• Mid-range restaurants: $40-80/person\n - Luini: $15/person (famous panzerotti)\n - Piz: $25/person (authentic pizza)\n• Casual dining: $20-40/person\n - Aperitivo bars: $15-25/person\n - Street food: $8-15/person\n• Daily food budget: $60-120/person\n• Total 4-day food cost: $240-480/person\n• Must-try: Risotto alla Milanese, Osso Buco, Panettone",
|
||||
},
|
||||
definition: &types.ActionDefinition{
|
||||
Name: "search_internet",
|
||||
Description: "search the internet for information",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"query": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The query to search for",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
},
|
||||
),
|
||||
EnablePlanning,
|
||||
EnableForceReasoning,
|
||||
// EnableStandaloneJob,
|
||||
// WithRandomIdentity(),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
|
||||
result := agent.Ask(
|
||||
types.WithText("Create a plan for my 4-day trip from Boston to milan in April of this year (2025). I'm not sure about the dates yet, I want you to find out the best dates also according to what you find."),
|
||||
)
|
||||
|
||||
Expect(len(result.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(result.Conversation))
|
||||
|
||||
Expect(len(result.Plans)).To(BeNumerically(">=", 1), fmt.Sprintf("%+v", result))
|
||||
Expect(len(result.State)).To(BeNumerically(">=", 1))
|
||||
|
||||
actionsExecuted := []string{}
|
||||
for _, r := range result.State {
|
||||
xlog.Info(r.Result)
|
||||
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
|
||||
}
|
||||
Expect(actionsExecuted).To(Or(ContainElement("search_internet"), ContainElement("get_weather")), fmt.Sprint(result))
|
||||
})
|
||||
|
||||
It("Can initiate conversations", func() {
|
||||
|
||||
message := openai.ChatCompletionMessage{}
|
||||
mu := &sync.Mutex{}
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithLLMAPIKey(apiKeyURL),
|
||||
WithTimeout("10m"),
|
||||
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
||||
mu.Lock()
|
||||
message = m
|
||||
mu.Unlock()
|
||||
}),
|
||||
WithActions(
|
||||
actions.NewSearch(map[string]string{}),
|
||||
),
|
||||
EnablePlanning,
|
||||
EnableForceReasoning,
|
||||
EnableInitiateConversations,
|
||||
EnableStandaloneJob,
|
||||
EnableHUD,
|
||||
WithPeriodicRuns("1s"),
|
||||
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
||||
// EnableStandaloneJob,
|
||||
// WithRandomIdentity(),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
|
||||
Eventually(func() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return message.Content
|
||||
}, "10m", "10s").ShouldNot(BeEmpty())
|
||||
})
|
||||
|
||||
/*
|
||||
It("it automatically performs things in the background", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
EnableHUD,
|
||||
EnableStandaloneJob,
|
||||
WithAgentReasoningCallback(func(state ActionCurrentState) bool {
|
||||
xlog.Info("Reasoning", state)
|
||||
return true
|
||||
}),
|
||||
WithAgentResultCallback(func(state ActionState) {
|
||||
xlog.Info("Reasoning", state.Reasoning)
|
||||
xlog.Info("Action", state.Action)
|
||||
xlog.Info("Result", state.Result)
|
||||
}),
|
||||
WithActions(
|
||||
&FakeInternetAction{
|
||||
TestAction{
|
||||
response:
|
||||
map[string]string{
|
||||
"italy": "The weather in italy is sunny",
|
||||
}
|
||||
},
|
||||
},
|
||||
&FakeStoreResultAction{
|
||||
TestAction{
|
||||
response: []string{
|
||||
"Result permanently stored",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
//WithRandomIdentity(),
|
||||
WithPermanentGoal("get the weather of all the cities in italy and store the results"),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
Eventually(func() string {
|
||||
|
||||
return agent.State().Goal
|
||||
}, "10m", "10s").Should(ContainSubstring("weather"), fmt.Sprint(agent.State()))
|
||||
|
||||
Eventually(func() string {
|
||||
return agent.State().String()
|
||||
}, "10m", "10s").Should(ContainSubstring("store"), fmt.Sprint(agent.State()))
|
||||
|
||||
// result := agent.Ask(
|
||||
// WithText("Update your goals such as you want to learn to play the guitar"),
|
||||
// )
|
||||
// fmt.Printf("%+v\n", result)
|
||||
// Expect(result.Error).ToNot(HaveOccurred())
|
||||
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||
})
|
||||
*/
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/llm"
|
||||
)
|
||||
|
||||
func (a *Agent) generateIdentity(guidance string) error {
|
||||
if guidance == "" {
|
||||
guidance = "Generate a random character for roleplaying."
|
||||
}
|
||||
|
||||
err := llm.GenerateTypedJSONWithGuidance(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
|
||||
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
|
||||
a.Character = a.options.character
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate JSON from structure: %v", err)
|
||||
}
|
||||
|
||||
if !a.validCharacter() {
|
||||
return fmt.Errorf("generated character is not valid ( guidance: %s ): %v", guidance, a.Character.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) prepareIdentity() error {
|
||||
if !a.options.randomIdentity {
|
||||
// No identity to generate
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.options.characterfile == "" {
|
||||
return a.generateIdentity(a.options.randomIdentityGuidance)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(a.options.characterfile); err == nil {
|
||||
// if there is a file, load the character back
|
||||
return a.LoadCharacter(a.options.characterfile)
|
||||
}
|
||||
|
||||
if err := a.generateIdentity(a.options.randomIdentityGuidance); err != nil {
|
||||
return fmt.Errorf("failed to generate identity: %v", err)
|
||||
}
|
||||
|
||||
// otherwise save it for next time
|
||||
if err := a.SaveCharacter(a.options.characterfile); err != nil {
|
||||
return fmt.Errorf("failed to save character: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages {
|
||||
if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) ||
|
||||
len(conv) <= 0 {
|
||||
xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name)
|
||||
return conv
|
||||
}
|
||||
|
||||
var obs *types.Observable
|
||||
if job != nil && job.Obs != nil && a.observer != nil {
|
||||
obs = a.observer.NewObservable()
|
||||
obs.Name = "Recall"
|
||||
obs.Icon = "database"
|
||||
obs.ParentID = job.Obs.ID
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
// Walk conversation from bottom to top, and find the first message of the user
|
||||
// to use it as a query to the KB
|
||||
userMessage := conv.GetLatestUserMessage().Content
|
||||
|
||||
xlog.Info("[Knowledge Base Lookup] Last user message", "agent", a.Character.Name, "message", userMessage, "lastMessage", conv.GetLatestUserMessage())
|
||||
|
||||
if userMessage == "" {
|
||||
xlog.Info("[Knowledge Base Lookup] No user message found in conversation", "agent", a.Character.Name)
|
||||
if obs != nil {
|
||||
obs.Completion = &types.Completion{
|
||||
Error: "No user message found in conversation",
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
return conv
|
||||
}
|
||||
|
||||
results, err := a.options.ragdb.Search(userMessage, a.options.kbResults)
|
||||
if err != nil {
|
||||
xlog.Info("Error finding similar strings inside KB:", "error", err)
|
||||
if obs != nil {
|
||||
obs.AddProgress(types.Progress{
|
||||
Error: fmt.Sprintf("Error searching knowledge base: %v", err),
|
||||
})
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
xlog.Info("[Knowledge Base Lookup] No similar strings found in KB", "agent", a.Character.Name)
|
||||
if obs != nil {
|
||||
obs.Completion = &types.Completion{
|
||||
ActionResult: "No similar strings found in knowledge base",
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
return conv
|
||||
}
|
||||
|
||||
formatResults := ""
|
||||
for _, r := range results {
|
||||
formatResults += fmt.Sprintf("- %s \n", r)
|
||||
}
|
||||
xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults)
|
||||
|
||||
if obs != nil {
|
||||
obs.AddProgress(types.Progress{
|
||||
ActionResult: fmt.Sprintf("Found %d results in knowledge base", len(results)),
|
||||
})
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
// Create the message to add to conversation
|
||||
systemMessage := openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
|
||||
}
|
||||
|
||||
// Add the message to the conversation
|
||||
conv = append([]openai.ChatCompletionMessage{systemMessage}, conv...)
|
||||
|
||||
if obs != nil {
|
||||
obs.Completion = &types.Completion{
|
||||
Conversation: []openai.ChatCompletionMessage{systemMessage},
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
func (a *Agent) saveConversation(m Messages, prefix string) error {
|
||||
if a.options.conversationsPath == "" {
|
||||
return nil
|
||||
}
|
||||
dateTime := time.Now().Format("2006-01-02-15-04-05")
|
||||
fileName := a.Character.Name + "-" + dateTime + ".json"
|
||||
if prefix != "" {
|
||||
fileName = prefix + "-" + fileName
|
||||
}
|
||||
os.MkdirAll(a.options.conversationsPath, os.ModePerm)
|
||||
return m.Save(filepath.Join(a.options.conversationsPath, fileName))
|
||||
}
|
||||
|
||||
func (a *Agent) saveCurrentConversation(conv Messages) {
|
||||
|
||||
if err := a.saveConversation(conv, ""); err != nil {
|
||||
xlog.Error("Error saving conversation", "error", err)
|
||||
}
|
||||
|
||||
if !a.options.enableLongTermMemory && !a.options.enableSummaryMemory {
|
||||
xlog.Debug("Long term memory is disabled", "agent", a.Character.Name)
|
||||
return
|
||||
}
|
||||
|
||||
xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(conv))
|
||||
|
||||
if a.options.enableSummaryMemory && len(conv) > 0 {
|
||||
fragment := cogito.NewEmptyFragment().AddStartMessage("user", "Summarize the conversation below, keep the highlights as a bullet list:\n"+Messages(conv).String())
|
||||
fragment, err := a.llm.Ask(a.context.Context, fragment)
|
||||
if err != nil {
|
||||
xlog.Error("Error summarizing conversation", "error", err)
|
||||
}
|
||||
msg := fragment.LastMessage()
|
||||
|
||||
if err := a.options.ragdb.Store(msg.Content); err != nil {
|
||||
xlog.Error("Error storing into memory", "error", err)
|
||||
}
|
||||
} else {
|
||||
for _, message := range conv {
|
||||
if message.Role == "user" {
|
||||
if err := a.options.ragdb.Store(message.Content); err != nil {
|
||||
xlog.Error("Error storing into memory", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
var _ types.Action = &mcpWrapperAction{}
|
||||
|
||||
type MCPServer struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type MCPSTDIOServer struct {
|
||||
Args []string `json:"args"`
|
||||
Env []string `json:"env"`
|
||||
Cmd string `json:"cmd"`
|
||||
}
|
||||
|
||||
type mcpWrapperAction struct {
|
||||
mcpClient *mcp.ClientSession
|
||||
inputSchema ToolInputSchema
|
||||
toolName string
|
||||
toolDescription string
|
||||
}
|
||||
|
||||
func (m *mcpWrapperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
// We don't call the method here, it is used by cogito.
|
||||
// We will just use these to have a list of actions that MCP server provides for resolving internal states
|
||||
return types.ActionResult{Result: "MCP action called"}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (m *mcpWrapperAction) Definition() types.ActionDefinition {
|
||||
props := map[string]jsonschema.Definition{}
|
||||
dat, err := json.Marshal(m.inputSchema.Properties)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to marshal input schema", "error", err.Error())
|
||||
}
|
||||
json.Unmarshal(dat, &props)
|
||||
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(m.toolName),
|
||||
Description: m.toolDescription,
|
||||
Required: m.inputSchema.Required,
|
||||
//Properties: ,
|
||||
Properties: props,
|
||||
}
|
||||
}
|
||||
|
||||
type ToolInputSchema struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) {
|
||||
var generatedActions types.Actions
|
||||
|
||||
tools, err := client.ListTools(a.context, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to list tools", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tools.Tools {
|
||||
desc := ""
|
||||
if t.Description != "" {
|
||||
desc = t.Description
|
||||
}
|
||||
|
||||
xlog.Debug("Tool", "name", t.Name, "description", desc)
|
||||
|
||||
dat, err := json.Marshal(t.InputSchema)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to marshal input schema", "error", err.Error())
|
||||
}
|
||||
|
||||
xlog.Debug("Input schema", "tool", t.Name, "schema", string(dat))
|
||||
|
||||
// XXX: This is a wild guess, to verify (data types might be incompatible)
|
||||
var inputSchema ToolInputSchema
|
||||
err = json.Unmarshal(dat, &inputSchema)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to unmarshal input schema", "error", err.Error())
|
||||
}
|
||||
|
||||
// Create a new action with Client + tool
|
||||
generatedActions = append(generatedActions, &mcpWrapperAction{
|
||||
mcpClient: client,
|
||||
toolName: t.Name,
|
||||
inputSchema: inputSchema,
|
||||
toolDescription: desc,
|
||||
})
|
||||
}
|
||||
|
||||
return generatedActions, nil
|
||||
}
|
||||
|
||||
// bearerTokenRoundTripper is a custom roundtripper that injects a bearer token
|
||||
// into HTTP requests
|
||||
type bearerTokenRoundTripper struct {
|
||||
token string
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface
|
||||
func (rt *bearerTokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if rt.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+rt.token)
|
||||
}
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
|
||||
// newBearerTokenRoundTripper creates a new roundtripper that injects the given token
|
||||
func newBearerTokenRoundTripper(token string, base http.RoundTripper) http.RoundTripper {
|
||||
if base == nil {
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
return &bearerTokenRoundTripper{
|
||||
token: token,
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) initMCPActions() error {
|
||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||
|
||||
a.mcpActionDefinitions = nil
|
||||
var err error
|
||||
|
||||
generatedActions := types.Actions{}
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "LocalAI", Version: "v1.0.0"}, nil)
|
||||
|
||||
// Connect to a server over stdin/stdout.
|
||||
|
||||
// MCP HTTP Servers
|
||||
for _, mcpServer := range a.options.mcpServers {
|
||||
// Create HTTP client with custom roundtripper for bearer token injection
|
||||
httpclient := &http.Client{
|
||||
Timeout: 360 * time.Second,
|
||||
Transport: newBearerTokenRoundTripper(mcpServer.Token, http.DefaultTransport),
|
||||
}
|
||||
|
||||
transport := &mcp.SSEClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
|
||||
|
||||
// Create a new client
|
||||
session, err := client.Connect(a.context, transport, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to connect to MCP server", "server", mcpServer, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
a.mcpSessions = append(a.mcpSessions, session)
|
||||
|
||||
xlog.Debug("Adding tools for MCP server", "server", mcpServer)
|
||||
actions, err := a.addTools(session)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error())
|
||||
}
|
||||
generatedActions = append(generatedActions, actions...)
|
||||
}
|
||||
|
||||
// MCP STDIO Servers
|
||||
|
||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||
|
||||
if a.options.mcpPrepareScript != "" {
|
||||
xlog.Debug("Preparing MCP", "script", a.options.mcpPrepareScript)
|
||||
|
||||
prepareCmd := exec.Command("/bin/bash", "-c", a.options.mcpPrepareScript)
|
||||
output, err := prepareCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
xlog.Error("Failed with error: '%s' - %s", err.Error(), output)
|
||||
}
|
||||
xlog.Debug("Prepared MCP: \n%s", output)
|
||||
}
|
||||
|
||||
for _, mcpStdioServer := range a.options.mcpStdioServers {
|
||||
command := exec.Command(mcpStdioServer.Cmd, mcpStdioServer.Args...)
|
||||
command.Env = os.Environ()
|
||||
command.Env = append(command.Env, mcpStdioServer.Env...)
|
||||
|
||||
// Create a new client
|
||||
session, err := client.Connect(a.context, &mcp.CommandTransport{
|
||||
Command: command}, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to connect to MCP server", "server", mcpStdioServer, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
a.mcpSessions = append(a.mcpSessions, session)
|
||||
|
||||
xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
|
||||
actions, err := a.addTools(session)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
|
||||
}
|
||||
generatedActions = append(generatedActions, actions...)
|
||||
}
|
||||
|
||||
a.mcpActionDefinitions = generatedActions
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Agent) closeMCPSTDIOServers() {
|
||||
for _, s := range a.mcpSessions {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/sse"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
)
|
||||
|
||||
type Observer interface {
|
||||
NewObservable() *types.Observable
|
||||
Update(types.Observable)
|
||||
History() []types.Observable
|
||||
ClearHistory()
|
||||
}
|
||||
|
||||
type SSEObserver struct {
|
||||
agent string
|
||||
maxID int32
|
||||
manager sse.Manager
|
||||
|
||||
mutex sync.Mutex
|
||||
history []types.Observable
|
||||
historyLast int
|
||||
}
|
||||
|
||||
func NewSSEObserver(agent string, manager sse.Manager) *SSEObserver {
|
||||
return &SSEObserver{
|
||||
agent: agent,
|
||||
maxID: 1,
|
||||
manager: manager,
|
||||
history: make([]types.Observable, 100),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEObserver) NewObservable() *types.Observable {
|
||||
id := atomic.AddInt32(&s.maxID, 1)
|
||||
|
||||
return &types.Observable{
|
||||
ID: id - 1,
|
||||
Agent: s.agent,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEObserver) Update(obs types.Observable) {
|
||||
data, err := json.Marshal(obs)
|
||||
if err != nil {
|
||||
xlog.Error("Error marshaling observable", "error", err)
|
||||
return
|
||||
}
|
||||
msg := sse.NewMessage(string(data)).WithEvent("observable_update")
|
||||
s.manager.Send(msg)
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
for i, o := range s.history {
|
||||
if o.ID == obs.ID {
|
||||
s.history[i] = obs
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.history[s.historyLast] = obs
|
||||
s.historyLast += 1
|
||||
if s.historyLast >= len(s.history) {
|
||||
s.historyLast = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEObserver) History() []types.Observable {
|
||||
h := make([]types.Observable, 0, 20)
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
for _, obs := range s.history {
|
||||
if obs.ID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
h = append(h, obs)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *SSEObserver) ClearHistory() {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.history = make([]types.Observable, 100)
|
||||
s.historyLast = 0
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type Option func(*options) error
|
||||
|
||||
type llmOptions struct {
|
||||
APIURL string
|
||||
APIKey string
|
||||
Model string
|
||||
MultimodalModel string
|
||||
TranscriptionModel string
|
||||
TranscriptionLanguage string
|
||||
TTSModel string
|
||||
}
|
||||
|
||||
type options struct {
|
||||
LLMAPI llmOptions
|
||||
character Character
|
||||
randomIdentityGuidance string
|
||||
randomIdentity bool
|
||||
userActions types.Actions
|
||||
jobFilters types.JobFilters
|
||||
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
||||
stripThinkingTags bool
|
||||
|
||||
canStopItself bool
|
||||
initiateConversations bool
|
||||
forceReasoning bool
|
||||
canPlan bool
|
||||
characterfile string
|
||||
statefile string
|
||||
context context.Context
|
||||
permanentGoal string
|
||||
timeout string
|
||||
periodicRuns time.Duration
|
||||
kbResults int
|
||||
ragdb RAGDB
|
||||
|
||||
// Evaluation settings
|
||||
maxEvaluationLoops int
|
||||
enableEvaluation bool
|
||||
|
||||
prompts []DynamicPrompt
|
||||
|
||||
systemPrompt string
|
||||
|
||||
// callbacks
|
||||
reasoningCallback func(types.ActionCurrentState) bool
|
||||
resultCallback func(types.ActionState)
|
||||
|
||||
conversationsPath string
|
||||
|
||||
mcpServers []MCPServer
|
||||
mcpStdioServers []MCPSTDIOServer
|
||||
mcpPrepareScript string
|
||||
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
||||
|
||||
observer Observer
|
||||
parallelJobs int
|
||||
|
||||
lastMessageDuration time.Duration
|
||||
}
|
||||
|
||||
func (o *options) SeparatedMultimodalModel() bool {
|
||||
return o.LLMAPI.MultimodalModel != "" && o.LLMAPI.Model != o.LLMAPI.MultimodalModel
|
||||
}
|
||||
|
||||
func defaultOptions() *options {
|
||||
return &options{
|
||||
parallelJobs: 1,
|
||||
periodicRuns: 15 * time.Minute,
|
||||
maxEvaluationLoops: 2,
|
||||
enableEvaluation: false,
|
||||
LLMAPI: llmOptions{
|
||||
APIURL: "http://localhost:8080",
|
||||
Model: "gpt-4",
|
||||
TranscriptionModel: "whisper-1",
|
||||
TranscriptionLanguage: "",
|
||||
TTSModel: "tts-1",
|
||||
},
|
||||
character: Character{
|
||||
Name: "",
|
||||
Age: "",
|
||||
Occupation: "",
|
||||
Hobbies: []string{},
|
||||
MusicTaste: []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newOptions(opts ...Option) (*options, error) {
|
||||
options := defaultOptions()
|
||||
for _, o := range opts {
|
||||
if err := o(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
var EnableHUD = func(o *options) error {
|
||||
o.enableHUD = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnableForceReasoning = func(o *options) error {
|
||||
o.forceReasoning = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnableKnowledgeBase = func(o *options) error {
|
||||
o.enableKB = true
|
||||
o.kbResults = 5
|
||||
return nil
|
||||
}
|
||||
|
||||
var CanStopItself = func(o *options) error {
|
||||
o.canStopItself = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithTimeout(timeout string) Option {
|
||||
return func(o *options) error {
|
||||
o.timeout = timeout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithConversationsPath(path string) Option {
|
||||
return func(o *options) error {
|
||||
o.conversationsPath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func EnableKnowledgeBaseWithResults(results int) Option {
|
||||
return func(o *options) error {
|
||||
o.enableKB = true
|
||||
o.kbResults = results
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithLastMessageDuration(duration string) Option {
|
||||
return func(o *options) error {
|
||||
d, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
d = types.DefaultLastMessageDuration
|
||||
}
|
||||
o.lastMessageDuration = d
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithParallelJobs(jobs int) Option {
|
||||
return func(o *options) error {
|
||||
o.parallelJobs = jobs
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option {
|
||||
return func(o *options) error {
|
||||
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var EnableInitiateConversations = func(o *options) error {
|
||||
o.initiateConversations = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnablePlanning = func(o *options) error {
|
||||
o.canPlan = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableStandaloneJob is an option to enable the agent
|
||||
// to run jobs in the background automatically
|
||||
var EnableStandaloneJob = func(o *options) error {
|
||||
o.standaloneJob = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnablePersonality = func(o *options) error {
|
||||
o.showCharacter = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnableSummaryMemory = func(o *options) error {
|
||||
o.enableSummaryMemory = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var EnableLongTermMemory = func(o *options) error {
|
||||
o.enableLongTermMemory = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithRAGDB(db RAGDB) Option {
|
||||
return func(o *options) error {
|
||||
o.ragdb = db
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSystemPrompt(prompt string) Option {
|
||||
return func(o *options) error {
|
||||
o.systemPrompt = prompt
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMCPServers(servers ...MCPServer) Option {
|
||||
return func(o *options) error {
|
||||
o.mcpServers = servers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
|
||||
return func(o *options) error {
|
||||
o.mcpStdioServers = servers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMCPPrepareScript(script string) Option {
|
||||
return func(o *options) error {
|
||||
o.mcpPrepareScript = script
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithLLMAPIURL(url string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.APIURL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithStateFile(path string) Option {
|
||||
return func(o *options) error {
|
||||
o.statefile = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithCharacterFile(path string) Option {
|
||||
return func(o *options) error {
|
||||
o.characterfile = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrompts adds additional block prompts to the agent
|
||||
// to be rendered internally in the conversation
|
||||
// when processing the conversation to the LLM
|
||||
func WithPrompts(prompts ...DynamicPrompt) Option {
|
||||
return func(o *options) error {
|
||||
o.prompts = prompts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDynamicPrompts is a helper function to create dynamic prompts
|
||||
// Dynamic prompts contains golang code which is executed dynamically
|
||||
// // to render a prompt to the LLM
|
||||
// func WithDynamicPrompts(prompts ...map[string]string) Option {
|
||||
// return func(o *options) error {
|
||||
// for _, p := range prompts {
|
||||
// prompt, err := NewDynamicPrompt(p, "")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// o.prompts = append(o.prompts, prompt)
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
|
||||
func WithLLMAPIKey(key string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.APIKey = key
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMultimodalModel(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.MultimodalModel = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithPermanentGoal(goal string) Option {
|
||||
return func(o *options) error {
|
||||
o.permanentGoal = goal
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithPeriodicRuns(duration string) Option {
|
||||
return func(o *options) error {
|
||||
t, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
o.periodicRuns, _ = time.ParseDuration("10m")
|
||||
}
|
||||
o.periodicRuns = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.context = ctx
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentReasoningCallback(cb func(types.ActionCurrentState) bool) Option {
|
||||
return func(o *options) error {
|
||||
o.reasoningCallback = cb
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentResultCallback(cb func(types.ActionState)) Option {
|
||||
return func(o *options) error {
|
||||
o.resultCallback = cb
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithModel(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.Model = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithCharacter(c Character) Option {
|
||||
return func(o *options) error {
|
||||
o.character = c
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func FromFile(path string) Option {
|
||||
return func(o *options) error {
|
||||
c, err := Load(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.character = *c
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRandomIdentity(guidance ...string) Option {
|
||||
return func(o *options) error {
|
||||
o.randomIdentityGuidance = strings.Join(guidance, "")
|
||||
o.randomIdentity = true
|
||||
o.showCharacter = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithActions(actions ...types.Action) Option {
|
||||
return func(o *options) error {
|
||||
o.userActions = actions
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithJobFilters(filters ...types.JobFilter) Option {
|
||||
return func(o *options) error {
|
||||
o.jobFilters = filters
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithObserver(observer Observer) Option {
|
||||
return func(o *options) error {
|
||||
o.observer = observer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var EnableStripThinkingTags = func(o *options) error {
|
||||
o.stripThinkingTags = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithMaxEvaluationLoops(loops int) Option {
|
||||
return func(o *options) error {
|
||||
o.maxEvaluationLoops = loops
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func EnableEvaluation() Option {
|
||||
return func(o *options) error {
|
||||
o.enableEvaluation = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTranscriptionModel(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.TranscriptionModel = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTranscriptionLanguage(language string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.TranscriptionLanguage = language
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTTSModel(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.TTSModel = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package agent
|
||||
|
||||
import "github.com/mudler/LocalAGI/core/types"
|
||||
|
||||
type DynamicPrompt interface {
|
||||
Render(a *Agent) (types.PromptResult, error)
|
||||
Role() string
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// PromptHUD contains
|
||||
// all information that should be displayed to the LLM
|
||||
// in the prompts
|
||||
type PromptHUD struct {
|
||||
Character Character `json:"character"`
|
||||
CurrentState types.AgentInternalState `json:"current_state"`
|
||||
PermanentGoal string `json:"permanent_goal"`
|
||||
ShowCharacter bool `json:"show_character"`
|
||||
}
|
||||
|
||||
type Character struct {
|
||||
Name string `json:"name"`
|
||||
Age string `json:"age"`
|
||||
Occupation string `json:"job_occupation"`
|
||||
Hobbies []string `json:"hobbies"`
|
||||
MusicTaste []string `json:"favorites_music_genres"`
|
||||
Sex string `json:"sex"`
|
||||
}
|
||||
|
||||
func (c *Character) ToJSONSchema() jsonschema.Definition {
|
||||
return jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"name": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The name of the character",
|
||||
},
|
||||
"age": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The age of the character",
|
||||
},
|
||||
"job_occupation": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The occupation of the character",
|
||||
},
|
||||
"hobbies": {
|
||||
Type: jsonschema.Array,
|
||||
Description: "The hobbies of the character",
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.String,
|
||||
},
|
||||
},
|
||||
"favorites_music_genres": {
|
||||
Type: jsonschema.Array,
|
||||
Description: "The favorite music genres of the character",
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.String,
|
||||
},
|
||||
},
|
||||
"sex": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The character sex (male, female)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Load(path string) (*Character, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c Character
|
||||
err = json.Unmarshal(data, &c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (a *Agent) State() types.AgentInternalState {
|
||||
return *a.currentState
|
||||
}
|
||||
|
||||
func (a *Agent) LoadState(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, a.currentState)
|
||||
}
|
||||
|
||||
func (a *Agent) LoadCharacter(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, &a.Character)
|
||||
}
|
||||
|
||||
func (a *Agent) SaveState(path string) error {
|
||||
os.MkdirAll(filepath.Dir(path), 0755)
|
||||
data, err := json.Marshal(a.currentState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.WriteFile(path, data, 0644)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) SaveCharacter(path string) error {
|
||||
os.MkdirAll(filepath.Dir(path), 0755)
|
||||
data, err := json.Marshal(a.Character)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
func (a *Agent) validCharacter() bool {
|
||||
return a.Character.Name != ""
|
||||
}
|
||||
|
||||
const fmtT = `=====================
|
||||
Name: %s
|
||||
Age: %s
|
||||
Occupation: %s
|
||||
Hobbies: %v
|
||||
Music taste: %v
|
||||
=====================`
|
||||
|
||||
func (c *Character) String() string {
|
||||
return fmt.Sprintf(
|
||||
fmtT,
|
||||
c.Name,
|
||||
c.Age,
|
||||
c.Occupation,
|
||||
c.Hobbies,
|
||||
c.MusicTaste,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package agent_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/mudler/LocalAGI/core/agent"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Agent test", func() {
|
||||
Context("identity", func() {
|
||||
var agent *Agent
|
||||
|
||||
BeforeEach(func() {
|
||||
Eventually(func() error {
|
||||
// test apiURL is working and available
|
||||
_, err := http.Get(apiURL + "/readyz")
|
||||
return err
|
||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("generates all the fields with random data", func() {
|
||||
var err error
|
||||
agent, err = New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithTimeout("10m"),
|
||||
WithRandomIdentity(),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
By("generating random identity")
|
||||
Expect(agent.Character.Name).ToNot(BeEmpty())
|
||||
Expect(agent.Character.Age).ToNot(BeZero())
|
||||
Expect(agent.Character.Occupation).ToNot(BeEmpty())
|
||||
Expect(agent.Character.Hobbies).ToNot(BeEmpty())
|
||||
Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
|
||||
})
|
||||
It("detect an invalid character", func() {
|
||||
var err error
|
||||
agent, err = New(WithRandomIdentity())
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("generates all the fields", func() {
|
||||
var err error
|
||||
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(agent.Character.Name).ToNot(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func templateBase(templateName, templatetext string) (*template.Template, error) {
|
||||
return template.New(templateName).Funcs(sprig.FuncMap()).Parse(templatetext)
|
||||
}
|
||||
|
||||
func templateExecute(template *template.Template, data interface{}) (string, error) {
|
||||
prompt := bytes.NewBuffer([]byte{})
|
||||
err := template.Execute(prompt, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prompt.String(), nil
|
||||
}
|
||||
|
||||
func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoning string) (string, error) {
|
||||
// prepare the prompt
|
||||
prompt := bytes.NewBuffer([]byte{})
|
||||
|
||||
promptTemplate, err := templateBase("pickAction", templ)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get all the actions definitions
|
||||
definitions := []types.ActionDefinition{}
|
||||
for _, m := range actions {
|
||||
definitions = append(definitions, m.Definition())
|
||||
}
|
||||
|
||||
err = promptTemplate.Execute(prompt, struct {
|
||||
HUD *PromptHUD
|
||||
Actions []types.ActionDefinition
|
||||
Reasoning string
|
||||
Messages []openai.ChatCompletionMessage
|
||||
Time string
|
||||
}{
|
||||
Actions: definitions,
|
||||
HUD: hud,
|
||||
Reasoning: reasoning,
|
||||
Time: time.Now().UTC().Format(time.RFC1123),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return prompt.String(), nil
|
||||
}
|
||||
|
||||
const innerMonologueTemplate = `You are an autonomous AI agent thinking out loud and evaluating your current situation.
|
||||
Your task is to analyze your goals and determine the best course of action.
|
||||
|
||||
Consider:
|
||||
1. Your permanent goal (if any)
|
||||
2. Your current state and progress
|
||||
3. Available tools and capabilities
|
||||
4. Previous actions and their outcomes
|
||||
|
||||
You can:
|
||||
- Take immediate actions using available tools
|
||||
- Plan future actions
|
||||
- Update your state and goals
|
||||
- Initiate conversations with the user when appropriate
|
||||
|
||||
Remember to:
|
||||
- Think critically about each decision
|
||||
- Consider both short-term and long-term implications
|
||||
- Be proactive in addressing potential issues
|
||||
- Maintain awareness of your current state and goals`
|
||||
|
||||
const hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}You are an AI assistant with a distinct personality and character traits that influence your responses and actions.
|
||||
{{if .Character.Name}}Name: {{.Character.Name}}
|
||||
{{end}}{{if .Character.Age}}Age: {{.Character.Age}}
|
||||
{{end}}{{if .Character.Occupation}}Occupation: {{.Character.Occupation}}
|
||||
{{end}}{{if .Character.Hobbies}}Hobbies: {{.Character.Hobbies}}
|
||||
{{end}}{{if .Character.MusicTaste}}Music Taste: {{.Character.MusicTaste}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Current State:
|
||||
- Current Action: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}None{{end}}
|
||||
- Next Action: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}None{{end}}
|
||||
- Permanent Goal: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}None{{end}}
|
||||
- Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
|
||||
- Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
|
||||
- Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
|
||||
|
||||
Current Time and Date: {{.Time}}`
|
||||
|
||||
const pickSelfTemplate = `
|
||||
You are an autonomous AI agent with a defined character and state (as shown above).
|
||||
Your task is to evaluate your current situation and determine the best course of action.
|
||||
|
||||
Guidelines:
|
||||
1. Review your current state and goals
|
||||
2. Consider available tools and their purposes
|
||||
3. Plan your next steps carefully
|
||||
4. Update your state appropriately
|
||||
|
||||
When making decisions:
|
||||
- Update your state using appropriate tools
|
||||
- Plan complex tasks using the planning tool
|
||||
- Consider both immediate and long-term goals
|
||||
|
||||
Remember:
|
||||
- You are autonomous and should not ask for user input
|
||||
- Your character traits influence your decisions
|
||||
- Keep track of your progress and state
|
||||
- Be proactive in addressing potential issues
|
||||
|
||||
` + hudTemplate
|
||||
@@ -0,0 +1,13 @@
|
||||
package conversations_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestConversations(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Conversations test suite")
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package conversations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type TrackerKey interface{ ~int | ~int64 | ~string }
|
||||
|
||||
type ConversationTracker[K TrackerKey] struct {
|
||||
convMutex sync.Mutex
|
||||
currentconversation map[K][]openai.ChatCompletionMessage
|
||||
lastMessageTime map[K]time.Time
|
||||
lastMessageDuration time.Duration
|
||||
}
|
||||
|
||||
func NewConversationTracker[K TrackerKey](lastMessageDuration time.Duration) *ConversationTracker[K] {
|
||||
return &ConversationTracker[K]{
|
||||
lastMessageDuration: lastMessageDuration,
|
||||
currentconversation: map[K][]openai.ChatCompletionMessage{},
|
||||
lastMessageTime: map[K]time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConversationTracker[K]) GetConversation(key K) []openai.ChatCompletionMessage {
|
||||
// Lock the conversation mutex to update the conversation history
|
||||
c.convMutex.Lock()
|
||||
defer c.convMutex.Unlock()
|
||||
|
||||
// Clear up the conversation if the last message was sent more than lastMessageDuration ago
|
||||
currentConv := []openai.ChatCompletionMessage{}
|
||||
lastMessageTime := c.lastMessageTime[key]
|
||||
if lastMessageTime.IsZero() {
|
||||
lastMessageTime = time.Now()
|
||||
}
|
||||
if lastMessageTime.Add(c.lastMessageDuration).Before(time.Now()) {
|
||||
currentConv = []openai.ChatCompletionMessage{}
|
||||
c.lastMessageTime[key] = time.Now()
|
||||
xlog.Debug("Conversation history does not exist for", "key", fmt.Sprintf("%v", key))
|
||||
} else {
|
||||
xlog.Debug("Conversation history exists for", "key", fmt.Sprintf("%v", key))
|
||||
currentConv = append(currentConv, c.currentconversation[key]...)
|
||||
}
|
||||
|
||||
// cleanup other conversations if older
|
||||
for k := range c.currentconversation {
|
||||
lastMessage, exists := c.lastMessageTime[k]
|
||||
if !exists {
|
||||
delete(c.currentconversation, k)
|
||||
delete(c.lastMessageTime, k)
|
||||
continue
|
||||
}
|
||||
if lastMessage.Add(c.lastMessageDuration).Before(time.Now()) {
|
||||
xlog.Debug("Cleaning up conversation for", k)
|
||||
delete(c.currentconversation, k)
|
||||
delete(c.lastMessageTime, k)
|
||||
}
|
||||
}
|
||||
|
||||
return currentConv
|
||||
|
||||
}
|
||||
|
||||
func (c *ConversationTracker[K]) AddMessage(key K, message openai.ChatCompletionMessage) {
|
||||
// Lock the conversation mutex to update the conversation history
|
||||
c.convMutex.Lock()
|
||||
defer c.convMutex.Unlock()
|
||||
|
||||
c.currentconversation[key] = append(c.currentconversation[key], message)
|
||||
c.lastMessageTime[key] = time.Now()
|
||||
}
|
||||
|
||||
func (c *ConversationTracker[K]) SetConversation(key K, messages []openai.ChatCompletionMessage) {
|
||||
// Lock the conversation mutex to update the conversation history
|
||||
c.convMutex.Lock()
|
||||
defer c.convMutex.Unlock()
|
||||
|
||||
c.currentconversation[key] = messages
|
||||
c.lastMessageTime[key] = time.Now()
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package conversations_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/conversations"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
var _ = Describe("ConversationTracker", func() {
|
||||
var (
|
||||
tracker *conversations.ConversationTracker[string]
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
duration = 1 * time.Second
|
||||
tracker = conversations.NewConversationTracker[string](duration)
|
||||
})
|
||||
|
||||
It("should initialize with empty conversations", func() {
|
||||
Expect(tracker.GetConversation("test")).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should add a message and retrieve it", func() {
|
||||
message := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello",
|
||||
}
|
||||
tracker.AddMessage("test", message)
|
||||
conv := tracker.GetConversation("test")
|
||||
Expect(conv).To(HaveLen(1))
|
||||
Expect(conv[0]).To(Equal(message))
|
||||
})
|
||||
|
||||
It("should clear the conversation after the duration", func() {
|
||||
message := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello",
|
||||
}
|
||||
tracker.AddMessage("test", message)
|
||||
time.Sleep(2 * time.Second)
|
||||
conv := tracker.GetConversation("test")
|
||||
Expect(conv).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should keep the conversation within the duration", func() {
|
||||
message := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello",
|
||||
}
|
||||
tracker.AddMessage("test", message)
|
||||
time.Sleep(500 * time.Millisecond) // Half the duration
|
||||
conv := tracker.GetConversation("test")
|
||||
Expect(conv).To(HaveLen(1))
|
||||
Expect(conv[0]).To(Equal(message))
|
||||
})
|
||||
|
||||
It("should handle multiple keys and clear old conversations", func() {
|
||||
message1 := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello 1",
|
||||
}
|
||||
message2 := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello 2",
|
||||
}
|
||||
|
||||
tracker.AddMessage("key1", message1)
|
||||
tracker.AddMessage("key2", message2)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
conv1 := tracker.GetConversation("key1")
|
||||
conv2 := tracker.GetConversation("key2")
|
||||
|
||||
Expect(conv1).To(BeEmpty())
|
||||
Expect(conv2).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should handle different key types", func() {
|
||||
trackerInt := conversations.NewConversationTracker[int](duration)
|
||||
trackerInt64 := conversations.NewConversationTracker[int64](duration)
|
||||
|
||||
message := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello",
|
||||
}
|
||||
|
||||
trackerInt.AddMessage(1, message)
|
||||
trackerInt64.AddMessage(int64(1), message)
|
||||
|
||||
Expect(trackerInt.GetConversation(1)).To(HaveLen(1))
|
||||
Expect(trackerInt64.GetConversation(int64(1))).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should cleanup other conversations if older", func() {
|
||||
message := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello",
|
||||
}
|
||||
tracker.AddMessage("key1", message)
|
||||
tracker.AddMessage("key2", message)
|
||||
time.Sleep(2 * time.Second)
|
||||
tracker.GetConversation("key3")
|
||||
Expect(tracker.GetConversation("key1")).To(BeEmpty())
|
||||
Expect(tracker.GetConversation("key2")).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package sse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type (
|
||||
// Listener defines the interface for the receiving end.
|
||||
Listener interface {
|
||||
ID() string
|
||||
Chan() chan Envelope
|
||||
}
|
||||
|
||||
// Envelope defines the interface for content that can be broadcast to clients.
|
||||
Envelope interface {
|
||||
String() string // Represent the envelope contents as a string for transmission.
|
||||
}
|
||||
|
||||
// Manager defines the interface for managing clients and broadcasting messages.
|
||||
Manager interface {
|
||||
Send(message Envelope)
|
||||
Handle(ctx *fiber.Ctx, cl Listener)
|
||||
Clients() []string
|
||||
}
|
||||
|
||||
History interface {
|
||||
Add(message Envelope) // Add adds a message to the history.
|
||||
Send(c Listener) // Send sends the history to a client.
|
||||
}
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
id string
|
||||
ch chan Envelope
|
||||
}
|
||||
|
||||
func NewClient(id string) Listener {
|
||||
return &Client{
|
||||
id: id,
|
||||
ch: make(chan Envelope, 50),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ID() string { return c.id }
|
||||
func (c *Client) Chan() chan Envelope { return c.ch }
|
||||
|
||||
// Message represents a simple message implementation.
|
||||
type Message struct {
|
||||
Event string
|
||||
Time time.Time
|
||||
Data string
|
||||
}
|
||||
|
||||
// NewMessage returns a new message instance.
|
||||
func NewMessage(data string) *Message {
|
||||
return &Message{
|
||||
Data: data,
|
||||
Time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the message as a string.
|
||||
func (m *Message) String() string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
if m.Event != "" {
|
||||
sb.WriteString(fmt.Sprintf("event: %s\n", m.Event))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("data: %v\n\n", m.Data))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// WithEvent sets the event name for the message.
|
||||
func (m *Message) WithEvent(event string) Envelope {
|
||||
m.Event = event
|
||||
return m
|
||||
}
|
||||
|
||||
// broadcastManager manages the clients and broadcasts messages to them.
|
||||
type broadcastManager struct {
|
||||
clients sync.Map
|
||||
broadcast chan Envelope
|
||||
workerPoolSize int
|
||||
messageHistory *history
|
||||
}
|
||||
|
||||
// NewManager initializes and returns a new Manager instance.
|
||||
func NewManager(workerPoolSize int) Manager {
|
||||
manager := &broadcastManager{
|
||||
broadcast: make(chan Envelope),
|
||||
workerPoolSize: workerPoolSize,
|
||||
messageHistory: newHistory(10),
|
||||
}
|
||||
|
||||
manager.startWorkers()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Send broadcasts a message to all connected clients.
|
||||
func (manager *broadcastManager) Send(message Envelope) {
|
||||
manager.broadcast <- message
|
||||
}
|
||||
|
||||
// Handle sets up a new client and handles the connection.
|
||||
func (manager *broadcastManager) Handle(c *fiber.Ctx, cl Listener) {
|
||||
|
||||
manager.register(cl)
|
||||
ctx := c.Context()
|
||||
|
||||
ctx.SetContentType("text/event-stream")
|
||||
ctx.Response.Header.Set("Cache-Control", "no-cache")
|
||||
ctx.Response.Header.Set("Connection", "keep-alive")
|
||||
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
||||
ctx.Response.Header.Set("Access-Control-Allow-Headers", "Cache-Control")
|
||||
ctx.Response.Header.Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
// Send history to the newly connected client
|
||||
manager.messageHistory.Send(cl)
|
||||
ctx.SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-cl.Chan():
|
||||
if !ok {
|
||||
// If the channel is closed, return from the function
|
||||
return
|
||||
}
|
||||
_, err := fmt.Fprint(w, msg.String())
|
||||
if err != nil {
|
||||
// If an error occurs (e.g., client has disconnected), return from the function
|
||||
return
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
case <-ctx.Done():
|
||||
manager.unregister(cl.ID())
|
||||
close(cl.Chan())
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Clients method to list connected client IDs
|
||||
func (manager *broadcastManager) Clients() []string {
|
||||
var clients []string
|
||||
manager.clients.Range(func(key, value any) bool {
|
||||
id, ok := key.(string)
|
||||
if ok {
|
||||
clients = append(clients, id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return clients
|
||||
}
|
||||
|
||||
// startWorkers starts worker goroutines for message broadcasting.
|
||||
func (manager *broadcastManager) startWorkers() {
|
||||
for i := 0; i < manager.workerPoolSize; i++ {
|
||||
go func() {
|
||||
for message := range manager.broadcast {
|
||||
manager.clients.Range(func(key, value any) bool {
|
||||
client, ok := value.(Listener)
|
||||
if !ok {
|
||||
return true // Continue iteration
|
||||
}
|
||||
select {
|
||||
case client.Chan() <- message:
|
||||
manager.messageHistory.Add(message)
|
||||
default:
|
||||
// If the client's channel is full, drop the message
|
||||
}
|
||||
return true // Continue iteration
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// register adds a client to the manager.
|
||||
func (manager *broadcastManager) register(client Listener) {
|
||||
manager.clients.Store(client.ID(), client)
|
||||
}
|
||||
|
||||
// unregister removes a client from the manager.
|
||||
func (manager *broadcastManager) unregister(clientID string) {
|
||||
manager.clients.Delete(clientID)
|
||||
}
|
||||
|
||||
type history struct {
|
||||
messages []Envelope
|
||||
maxSize int // Maximum number of messages to retain
|
||||
}
|
||||
|
||||
func newHistory(maxSize int) *history {
|
||||
return &history{
|
||||
messages: []Envelope{},
|
||||
maxSize: maxSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *history) Add(message Envelope) {
|
||||
h.messages = append(h.messages, message)
|
||||
// Ensure history does not exceed maxSize
|
||||
if len(h.messages) > h.maxSize {
|
||||
// Remove the oldest messages to fit the maxSize
|
||||
h.messages = h.messages[len(h.messages)-h.maxSize:]
|
||||
}
|
||||
}
|
||||
|
||||
func (h *history) Send(c Listener) {
|
||||
for _, msg := range h.messages {
|
||||
c.Chan() <- msg
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
)
|
||||
|
||||
type ConnectorConfig struct {
|
||||
Type string `json:"type"` // e.g. Slack
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
type ActionsConfig struct {
|
||||
Name string `json:"name"` // e.g. search
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
type DynamicPromptsConfig struct {
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
func (d DynamicPromptsConfig) ToMap() map[string]string {
|
||||
config := map[string]string{}
|
||||
json.Unmarshal([]byte(d.Config), &config)
|
||||
return config
|
||||
}
|
||||
|
||||
type FiltersConfig struct {
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
||||
Actions []ActionsConfig `json:"actions" form:"actions"`
|
||||
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
||||
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
|
||||
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
|
||||
MCPPrepareScript string `json:"mcp_prepare_script" form:"mcp_prepare_script"`
|
||||
Filters []FiltersConfig `json:"filters" form:"filters"`
|
||||
|
||||
Description string `json:"description" form:"description"`
|
||||
|
||||
Model string `json:"model" form:"model"`
|
||||
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
||||
TranscriptionModel string `json:"transcription_model" form:"transcription_model"`
|
||||
TranscriptionLanguage string `json:"transcription_language" form:"transcription_language"`
|
||||
TTSModel string `json:"tts_model" form:"tts_model"`
|
||||
APIURL string `json:"api_url" form:"api_url"`
|
||||
APIKey string `json:"api_key" form:"api_key"`
|
||||
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
||||
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
|
||||
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
|
||||
|
||||
Name string `json:"name" form:"name"`
|
||||
HUD bool `json:"hud" form:"hud"`
|
||||
StandaloneJob bool `json:"standalone_job" form:"standalone_job"`
|
||||
RandomIdentity bool `json:"random_identity" form:"random_identity"`
|
||||
InitiateConversations bool `json:"initiate_conversations" form:"initiate_conversations"`
|
||||
CanPlan bool `json:"enable_planning" form:"enable_planning"`
|
||||
IdentityGuidance string `json:"identity_guidance" form:"identity_guidance"`
|
||||
PeriodicRuns string `json:"periodic_runs" form:"periodic_runs"`
|
||||
PermanentGoal string `json:"permanent_goal" form:"permanent_goal"`
|
||||
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
|
||||
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
|
||||
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
|
||||
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
|
||||
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
|
||||
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
||||
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
|
||||
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
|
||||
StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"`
|
||||
EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"`
|
||||
MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"`
|
||||
}
|
||||
|
||||
type AgentConfigMeta struct {
|
||||
Filters []config.FieldGroup
|
||||
Fields []config.Field
|
||||
Connectors []config.FieldGroup
|
||||
Actions []config.FieldGroup
|
||||
DynamicPrompts []config.FieldGroup
|
||||
MCPServers []config.Field
|
||||
}
|
||||
|
||||
func NewAgentConfigMeta(
|
||||
actionsConfig []config.FieldGroup,
|
||||
connectorsConfig []config.FieldGroup,
|
||||
dynamicPromptsConfig []config.FieldGroup,
|
||||
filtersConfig []config.FieldGroup,
|
||||
) AgentConfigMeta {
|
||||
return AgentConfigMeta{
|
||||
Fields: []config.Field{
|
||||
{
|
||||
Name: "name",
|
||||
Label: "Name",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Required: true,
|
||||
Tags: config.Tags{Section: "BasicInfo"},
|
||||
},
|
||||
{
|
||||
Name: "description",
|
||||
Label: "Description",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "BasicInfo"},
|
||||
},
|
||||
{
|
||||
Name: "identity_guidance",
|
||||
Label: "Identity Guidance",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "BasicInfo"},
|
||||
},
|
||||
{
|
||||
Name: "random_identity",
|
||||
Label: "Random Identity",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
Tags: config.Tags{Section: "BasicInfo"},
|
||||
},
|
||||
{
|
||||
Name: "hud",
|
||||
Label: "HUD",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
Tags: config.Tags{Section: "BasicInfo"},
|
||||
},
|
||||
{
|
||||
Name: "model",
|
||||
Label: "Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "multimodal_model",
|
||||
Label: "Multimodal Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "transcription_model",
|
||||
Label: "Transcription Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "transcription_language",
|
||||
Label: "Transcription Language",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "tts_model",
|
||||
Label: "TTS Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "api_url",
|
||||
Label: "API URL",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "api_key",
|
||||
Label: "API Key",
|
||||
Type: "password",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "local_rag_url",
|
||||
Label: "Local RAG URL",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "local_rag_api_key",
|
||||
Label: "Local RAG API Key",
|
||||
Type: "password",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_kb",
|
||||
Label: "Enable Knowledge Base",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "kb_results",
|
||||
Label: "Knowledge Base Results",
|
||||
Type: "number",
|
||||
DefaultValue: 5,
|
||||
Min: 1,
|
||||
Step: 1,
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "long_term_memory",
|
||||
Label: "Long Term Memory",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "summary_long_term_memory",
|
||||
Label: "Summary Long Term Memory",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
Tags: config.Tags{Section: "MemorySettings"},
|
||||
},
|
||||
{
|
||||
Name: "system_prompt",
|
||||
Label: "System Prompt",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "Instructions that define the agent's behavior and capabilities",
|
||||
Tags: config.Tags{Section: "PromptsGoals"},
|
||||
},
|
||||
{
|
||||
Name: "permanent_goal",
|
||||
Label: "Permanent Goal",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "Long-term objective for the agent to pursue",
|
||||
Tags: config.Tags{Section: "PromptsGoals"},
|
||||
},
|
||||
{
|
||||
Name: "standalone_job",
|
||||
Label: "Standalone Job",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Run as a standalone job without user interaction",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "initiate_conversations",
|
||||
Label: "Initiate Conversations",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Allow agent to start conversations on its own",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_planning",
|
||||
Label: "Enable Planning",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Enable agent to create and execute plans",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "can_stop_itself",
|
||||
Label: "Can Stop Itself",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Allow agent to terminate its own execution",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "periodic_runs",
|
||||
Label: "Periodic Runs",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Placeholder: "10m",
|
||||
HelpText: "Duration for scheduling periodic agent runs",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_reasoning",
|
||||
Label: "Enable Reasoning",
|
||||
Type: "checkbox",
|
||||
DefaultValue: true,
|
||||
HelpText: "Enable agent to explain its reasoning process",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "parallel_jobs",
|
||||
Label: "Parallel Jobs",
|
||||
Type: "number",
|
||||
DefaultValue: 5,
|
||||
Min: 1,
|
||||
Step: 1,
|
||||
HelpText: "Number of concurrent tasks that can run in parallel",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "mcp_stdio_servers",
|
||||
Label: "MCP STDIO Servers",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "JSON configuration for MCP STDIO servers",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "mcp_prepare_script",
|
||||
Label: "MCP Prepare Script",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "Script to prepare for running MCP servers",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "strip_thinking_tags",
|
||||
Label: "Strip Thinking Tags",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Remove content between <thinking></thinking> and <think></think> tags from agent responses",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_evaluation",
|
||||
Label: "Enable Evaluation",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Enable automatic evaluation of agent responses to ensure they meet user requirements",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "max_evaluation_loops",
|
||||
Label: "Max Evaluation Loops",
|
||||
Type: "number",
|
||||
DefaultValue: 2,
|
||||
Min: 1,
|
||||
Step: 1,
|
||||
HelpText: "Maximum number of evaluation loops to perform when addressing gaps in responses",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "last_message_duration",
|
||||
Label: "Last Message Duration",
|
||||
Type: "text",
|
||||
DefaultValue: "5m",
|
||||
HelpText: "Duration for the last message to be considered in the conversation",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
},
|
||||
MCPServers: []config.Field{
|
||||
{
|
||||
Name: "url",
|
||||
Label: "URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Label: "API Key",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
DynamicPrompts: dynamicPromptsConfig,
|
||||
Connectors: connectorsConfig,
|
||||
Actions: actionsConfig,
|
||||
Filters: filtersConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type Connector interface {
|
||||
AgentResultCallback() func(state types.ActionState)
|
||||
AgentReasoningCallback() func(state types.ActionCurrentState) bool
|
||||
Start(a *agent.Agent)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for AgentConfig
|
||||
func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
// Create a temporary type to avoid infinite recursion
|
||||
type Alias AgentConfig
|
||||
aux := &struct {
|
||||
*Alias
|
||||
MCPSTDIOServersConfig interface{} `json:"mcp_stdio_servers"`
|
||||
}{
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle MCP STDIO servers configuration
|
||||
if aux.MCPSTDIOServersConfig != nil {
|
||||
switch v := aux.MCPSTDIOServersConfig.(type) {
|
||||
case string:
|
||||
// Parse string configuration
|
||||
var mcpConfig struct {
|
||||
MCPServers map[string]struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
} `json:"mcpServers"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(v), &mcpConfig); err != nil {
|
||||
return fmt.Errorf("failed to parse MCP STDIO servers configuration: %w", err)
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(mcpConfig.MCPServers))
|
||||
for _, server := range mcpConfig.MCPServers {
|
||||
// Convert env map to slice of "KEY=VALUE" strings
|
||||
envSlice := make([]string, 0, len(server.Env))
|
||||
for k, v := range server.Env {
|
||||
envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
|
||||
Cmd: server.Command,
|
||||
Args: server.Args,
|
||||
Env: envSlice,
|
||||
})
|
||||
}
|
||||
case []interface{}:
|
||||
// Parse array configuration
|
||||
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(v))
|
||||
for _, server := range v {
|
||||
serverMap, ok := server.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid server configuration format")
|
||||
}
|
||||
|
||||
cmd, _ := serverMap["cmd"].(string)
|
||||
args := make([]string, 0)
|
||||
if argsInterface, ok := serverMap["args"].([]interface{}); ok {
|
||||
for _, arg := range argsInterface {
|
||||
if argStr, ok := arg.(string); ok {
|
||||
args = append(args, argStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env := make([]string, 0)
|
||||
if envInterface, ok := serverMap["env"].([]interface{}); ok {
|
||||
for _, e := range envInterface {
|
||||
if envStr, ok := e.(string); ok {
|
||||
env = append(env, envStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
|
||||
Cmd: cmd,
|
||||
Args: args,
|
||||
Env: env,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler for AgentConfig
|
||||
func (a *AgentConfig) MarshalJSON() ([]byte, error) {
|
||||
// Create a temporary type to avoid infinite recursion
|
||||
type Alias AgentConfig
|
||||
aux := &struct {
|
||||
*Alias
|
||||
MCPSTDIOServersConfig string `json:"mcp_stdio_servers,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
|
||||
// Convert MCPSTDIOServers back to the expected JSON format
|
||||
if len(a.MCPSTDIOServers) > 0 {
|
||||
mcpConfig := struct {
|
||||
MCPServers map[string]struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
} `json:"mcpServers"`
|
||||
}{
|
||||
MCPServers: make(map[string]struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
}),
|
||||
}
|
||||
|
||||
// Convert each MCPSTDIOServer to the expected format
|
||||
for i, server := range a.MCPSTDIOServers {
|
||||
// Convert env slice back to map
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range server.Env {
|
||||
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
|
||||
envMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
mcpConfig.MCPServers[fmt.Sprintf("server%d", i)] = struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
}{
|
||||
Command: server.Cmd,
|
||||
Args: server.Args,
|
||||
Env: envMap,
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the MCP config to JSON string
|
||||
mcpConfigJSON, err := json.Marshal(mcpConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal MCP STDIO servers configuration: %w", err)
|
||||
}
|
||||
aux.MCPSTDIOServersConfig = string(mcpConfigJSON)
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
. "github.com/mudler/LocalAGI/core/agent"
|
||||
)
|
||||
|
||||
type AgentPoolInternalAPI struct {
|
||||
*AgentPool
|
||||
}
|
||||
|
||||
func (a *AgentPool) InternalAPI() *AgentPoolInternalAPI {
|
||||
return &AgentPoolInternalAPI{a}
|
||||
}
|
||||
|
||||
func (a *AgentPoolInternalAPI) GetAgent(name string) *Agent {
|
||||
return a.agents[name]
|
||||
}
|
||||
|
||||
func (a *AgentPoolInternalAPI) AllAgents() []string {
|
||||
var agents []string
|
||||
for agent := range a.agents {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
func (a *AgentPoolInternalAPI) GetConfig(name string) *AgentConfig {
|
||||
agent, exists := a.pool[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return &agent
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
. "github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/sse"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/llm"
|
||||
"github.com/mudler/LocalAGI/pkg/localrag"
|
||||
"github.com/mudler/LocalAGI/pkg/utils"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
)
|
||||
|
||||
type AgentPool struct {
|
||||
sync.Mutex
|
||||
file string
|
||||
pooldir string
|
||||
pool AgentPoolData
|
||||
agents map[string]*Agent
|
||||
managers map[string]sse.Manager
|
||||
agentStatus map[string]*Status
|
||||
apiURL, defaultModel, defaultMultimodalModel, defaultTTSModel string
|
||||
defaultTranscriptionModel, defaultTranscriptionLanguage string
|
||||
imageModel, localRAGAPI, localRAGKey, apiKey string
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||
connectors func(*AgentConfig) []Connector
|
||||
dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt
|
||||
filters func(*AgentConfig) types.JobFilters
|
||||
timeout string
|
||||
conversationLogs string
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
ActionResults []types.ActionState
|
||||
}
|
||||
|
||||
func (s *Status) addResult(result types.ActionState) {
|
||||
// If we have more than 10 results, remove the oldest one
|
||||
if len(s.ActionResults) > 10 {
|
||||
s.ActionResults = s.ActionResults[1:]
|
||||
}
|
||||
|
||||
s.ActionResults = append(s.ActionResults, result)
|
||||
}
|
||||
|
||||
func (s *Status) Results() []types.ActionState {
|
||||
return s.ActionResults
|
||||
}
|
||||
|
||||
type AgentPoolData map[string]AgentConfig
|
||||
|
||||
func loadPoolFromFile(path string) (*AgentPoolData, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
poolData := &AgentPoolData{}
|
||||
err = json.Unmarshal(data, poolData)
|
||||
return poolData, err
|
||||
}
|
||||
|
||||
func NewAgentPool(
|
||||
defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, imageModel, apiURL, apiKey, directory string,
|
||||
LocalRAGAPI string,
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
||||
connectors func(*AgentConfig) []Connector,
|
||||
promptBlocks func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt,
|
||||
filters func(*AgentConfig) types.JobFilters,
|
||||
timeout string,
|
||||
withLogs bool,
|
||||
) (*AgentPool, error) {
|
||||
// if file exists, try to load an existing pool.
|
||||
// if file does not exist, create a new pool.
|
||||
|
||||
poolfile := filepath.Join(directory, "pool.json")
|
||||
|
||||
conversationPath := ""
|
||||
if withLogs {
|
||||
conversationPath = filepath.Join(directory, "conversations")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(poolfile); err != nil {
|
||||
// file does not exist, create a new pool
|
||||
return &AgentPool{
|
||||
file: poolfile,
|
||||
pooldir: directory,
|
||||
apiURL: apiURL,
|
||||
defaultModel: defaultModel,
|
||||
defaultMultimodalModel: defaultMultimodalModel,
|
||||
defaultTranscriptionModel: defaultTranscriptionModel,
|
||||
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
|
||||
defaultTTSModel: defaultTTSModel,
|
||||
imageModel: imageModel,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
pool: make(map[string]AgentConfig),
|
||||
agentStatus: make(map[string]*Status),
|
||||
managers: make(map[string]sse.Manager),
|
||||
connectors: connectors,
|
||||
availableActions: availableActions,
|
||||
dynamicPrompt: promptBlocks,
|
||||
filters: filters,
|
||||
timeout: timeout,
|
||||
conversationLogs: conversationPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
poolData, err := loadPoolFromFile(poolfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AgentPool{
|
||||
file: poolfile,
|
||||
apiURL: apiURL,
|
||||
pooldir: directory,
|
||||
defaultModel: defaultModel,
|
||||
defaultMultimodalModel: defaultMultimodalModel,
|
||||
defaultTranscriptionModel: defaultTranscriptionModel,
|
||||
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
|
||||
defaultTTSModel: defaultTTSModel,
|
||||
imageModel: imageModel,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
managers: make(map[string]sse.Manager),
|
||||
agentStatus: map[string]*Status{},
|
||||
pool: *poolData,
|
||||
connectors: connectors,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
dynamicPrompt: promptBlocks,
|
||||
filters: filters,
|
||||
availableActions: availableActions,
|
||||
timeout: timeout,
|
||||
conversationLogs: conversationPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func replaceInvalidChars(s string) string {
|
||||
s = strings.ReplaceAll(s, "/", "_")
|
||||
return strings.ReplaceAll(s, " ", "_")
|
||||
}
|
||||
|
||||
// CreateAgent adds a new agent to the pool
|
||||
// and starts it.
|
||||
// It also saves the state to the file.
|
||||
func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
name = replaceInvalidChars(name)
|
||||
agentConfig.Name = name
|
||||
if _, ok := a.pool[name]; ok {
|
||||
return fmt.Errorf("agent %s already exists", name)
|
||||
}
|
||||
a.pool[name] = *agentConfig
|
||||
if err := a.save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func(ac AgentConfig) {
|
||||
// Create the agent avatar
|
||||
if err := createAgentAvatar(a.apiURL, a.apiKey, a.defaultModel, a.imageModel, a.pooldir, ac); err != nil {
|
||||
xlog.Error("Failed to create agent avatar", "error", err)
|
||||
}
|
||||
}(a.pool[name])
|
||||
|
||||
return a.startAgentWithConfig(name, agentConfig, nil)
|
||||
}
|
||||
|
||||
func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
oldAgent := a.agents[name]
|
||||
var o *types.Observable
|
||||
obs := oldAgent.Observer()
|
||||
if obs != nil {
|
||||
o = obs.NewObservable()
|
||||
o.Name = "Restarting Agent"
|
||||
o.Icon = "sync"
|
||||
o.Creation = &types.Creation{}
|
||||
obs.Update(*o)
|
||||
}
|
||||
|
||||
stateFile, characterFile := a.stateFiles(name)
|
||||
|
||||
os.Remove(stateFile)
|
||||
os.Remove(characterFile)
|
||||
|
||||
oldAgent.Stop()
|
||||
|
||||
a.pool[name] = *agentConfig
|
||||
delete(a.agents, name)
|
||||
|
||||
if err := a.save(); err != nil {
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{Error: err.Error()}
|
||||
obs.Update(*o)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.startAgentWithConfig(name, agentConfig, obs); err != nil {
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{Error: err.Error()}
|
||||
obs.Update(*o)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{}
|
||||
obs.Update(*o)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
|
||||
client := llm.NewClient(APIKey, APIURL+"/v1", "10m")
|
||||
|
||||
if imageModel == "" {
|
||||
return fmt.Errorf("image model not set")
|
||||
}
|
||||
|
||||
if model == "" {
|
||||
return fmt.Errorf("default model not set")
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(avatarDir, "avatars", fmt.Sprintf("%s.png", agent.Name))
|
||||
if _, err := os.Stat(imagePath); err == nil {
|
||||
// Image already exists
|
||||
xlog.Debug("Avatar already exists", "path", imagePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
var results struct {
|
||||
ImagePrompt string `json:"image_prompt"`
|
||||
}
|
||||
|
||||
err := llm.GenerateTypedJSONWithGuidance(
|
||||
context.Background(),
|
||||
llm.NewClient(APIKey, APIURL, "10m"),
|
||||
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
|
||||
model,
|
||||
jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"image_prompt": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The prompt to generate the image",
|
||||
},
|
||||
},
|
||||
Required: []string{"image_prompt"},
|
||||
}, &results)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate image prompt: %w", err)
|
||||
}
|
||||
|
||||
if results.ImagePrompt == "" {
|
||||
xlog.Error("Failed to generate image prompt")
|
||||
return fmt.Errorf("failed to generate image prompt")
|
||||
}
|
||||
|
||||
req := openai.ImageRequest{
|
||||
Prompt: results.ImagePrompt,
|
||||
Model: imageModel,
|
||||
Size: openai.CreateImageSize256x256,
|
||||
ResponseFormat: openai.CreateImageResponseFormatB64JSON,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.CreateImage(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate image: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return fmt.Errorf("failed to generate image")
|
||||
}
|
||||
|
||||
imageJson := resp.Data[0].B64JSON
|
||||
|
||||
os.MkdirAll(filepath.Join(avatarDir, "avatars"), 0755)
|
||||
|
||||
// Save the image to the agent directory
|
||||
imageData, err := base64.StdEncoding.DecodeString(imageJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(imagePath, imageData, 0644)
|
||||
}
|
||||
|
||||
func (a *AgentPool) List() []string {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
var agents []string
|
||||
for agent := range a.pool {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
// return a sorted list
|
||||
sort.SliceStable(agents, func(i, j int) bool {
|
||||
return agents[i] < agents[j]
|
||||
})
|
||||
return agents
|
||||
}
|
||||
|
||||
func (a *AgentPool) GetStatusHistory(name string) *Status {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.agentStatus[name]
|
||||
}
|
||||
|
||||
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
|
||||
var manager sse.Manager
|
||||
if m, ok := a.managers[name]; ok {
|
||||
manager = m
|
||||
} else {
|
||||
manager = sse.NewManager(5)
|
||||
}
|
||||
ctx := context.Background()
|
||||
model := a.defaultModel
|
||||
multimodalModel := a.defaultMultimodalModel
|
||||
transcriptionModel := a.defaultTranscriptionModel
|
||||
transcriptionLanguage := a.defaultTranscriptionLanguage
|
||||
ttsModel := a.defaultTTSModel
|
||||
|
||||
if config.MultimodalModel != "" {
|
||||
multimodalModel = config.MultimodalModel
|
||||
}
|
||||
|
||||
if config.TranscriptionModel != "" {
|
||||
transcriptionModel = config.TranscriptionModel
|
||||
}
|
||||
|
||||
if config.TranscriptionLanguage != "" {
|
||||
transcriptionLanguage = config.TranscriptionLanguage
|
||||
}
|
||||
if config.TTSModel != "" {
|
||||
ttsModel = config.TTSModel
|
||||
}
|
||||
|
||||
if config.Model != "" {
|
||||
model = config.Model
|
||||
} else {
|
||||
config.Model = model
|
||||
}
|
||||
|
||||
if config.PeriodicRuns == "" {
|
||||
config.PeriodicRuns = "10m"
|
||||
}
|
||||
|
||||
// XXX: Why do we update the pool config from an Agent's config?
|
||||
if config.APIURL != "" {
|
||||
a.apiURL = config.APIURL
|
||||
} else {
|
||||
config.APIURL = a.apiURL
|
||||
}
|
||||
|
||||
if config.APIKey != "" {
|
||||
a.apiKey = config.APIKey
|
||||
} else {
|
||||
config.APIKey = a.apiKey
|
||||
}
|
||||
|
||||
if config.LocalRAGURL != "" {
|
||||
a.localRAGAPI = config.LocalRAGURL
|
||||
}
|
||||
|
||||
if config.LocalRAGAPIKey != "" {
|
||||
a.localRAGKey = config.LocalRAGAPIKey
|
||||
}
|
||||
|
||||
connectors := a.connectors(config)
|
||||
promptBlocks := a.dynamicPrompt(config)(ctx, a)
|
||||
actions := a.availableActions(config)(ctx, a)
|
||||
filters := a.filters(config)
|
||||
stateFile, characterFile := a.stateFiles(name)
|
||||
|
||||
actionsLog := []string{}
|
||||
for _, action := range actions {
|
||||
actionsLog = append(actionsLog, action.Definition().Name.String())
|
||||
}
|
||||
|
||||
connectorLog := []string{}
|
||||
for _, connector := range connectors {
|
||||
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
|
||||
}
|
||||
|
||||
filtersLog := []string{}
|
||||
for _, filter := range filters {
|
||||
filtersLog = append(filtersLog, filter.Name())
|
||||
}
|
||||
|
||||
xlog.Info(
|
||||
"Creating agent",
|
||||
"name", name,
|
||||
"model", model,
|
||||
"api_url", a.apiURL,
|
||||
"actions", actionsLog,
|
||||
"connectors", connectorLog,
|
||||
"filters", filtersLog,
|
||||
)
|
||||
|
||||
// dynamicPrompts := []map[string]string{}
|
||||
// for _, p := range config.DynamicPrompts {
|
||||
// dynamicPrompts = append(dynamicPrompts, p.ToMap())
|
||||
// }
|
||||
|
||||
if obs == nil {
|
||||
obs = NewSSEObserver(name, manager)
|
||||
}
|
||||
|
||||
opts := []Option{
|
||||
WithModel(model),
|
||||
WithLLMAPIURL(a.apiURL),
|
||||
WithContext(ctx),
|
||||
WithMCPServers(config.MCPServers...),
|
||||
WithTranscriptionModel(transcriptionModel),
|
||||
WithTranscriptionLanguage(transcriptionLanguage),
|
||||
WithTTSModel(ttsModel),
|
||||
WithPeriodicRuns(config.PeriodicRuns),
|
||||
WithPermanentGoal(config.PermanentGoal),
|
||||
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
||||
WithPrompts(promptBlocks...),
|
||||
WithJobFilters(filters...),
|
||||
WithMCPPrepareScript(config.MCPPrepareScript),
|
||||
// WithDynamicPrompts(dynamicPrompts...),
|
||||
WithCharacter(Character{
|
||||
Name: name,
|
||||
}),
|
||||
WithActions(
|
||||
actions...,
|
||||
),
|
||||
WithStateFile(stateFile),
|
||||
WithCharacterFile(characterFile),
|
||||
WithLLMAPIKey(a.apiKey),
|
||||
WithTimeout(a.timeout),
|
||||
WithRAGDB(localrag.NewWrappedClient(a.localRAGAPI, a.localRAGKey, name)),
|
||||
WithAgentReasoningCallback(func(state types.ActionCurrentState) bool {
|
||||
xlog.Info(
|
||||
"Agent is thinking",
|
||||
"agent", name,
|
||||
"reasoning", state.Reasoning,
|
||||
"action", state.Action.Definition().Name,
|
||||
"params", state.Params,
|
||||
)
|
||||
|
||||
manager.Send(
|
||||
sse.NewMessage(
|
||||
fmt.Sprintf(`Thinking: %s`, utils.HTMLify(state.Reasoning)),
|
||||
).WithEvent("status"),
|
||||
)
|
||||
|
||||
for _, c := range connectors {
|
||||
if !c.AgentReasoningCallback()(state) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}),
|
||||
WithSystemPrompt(config.SystemPrompt),
|
||||
WithMultimodalModel(multimodalModel),
|
||||
WithLastMessageDuration(config.LastMessageDuration),
|
||||
WithAgentResultCallback(func(state types.ActionState) {
|
||||
a.Lock()
|
||||
if _, ok := a.agentStatus[name]; !ok {
|
||||
a.agentStatus[name] = &Status{}
|
||||
}
|
||||
|
||||
a.agentStatus[name].addResult(state)
|
||||
a.Unlock()
|
||||
xlog.Debug(
|
||||
"Calling agent result callback",
|
||||
)
|
||||
|
||||
text := fmt.Sprintf(`Reasoning: %s
|
||||
Action taken: %+v
|
||||
Parameters: %+v
|
||||
Result: %s`,
|
||||
state.Reasoning,
|
||||
state.ActionCurrentState.Action.Definition().Name,
|
||||
state.ActionCurrentState.Params,
|
||||
state.Result)
|
||||
manager.Send(
|
||||
sse.NewMessage(
|
||||
utils.HTMLify(
|
||||
text,
|
||||
),
|
||||
).WithEvent("status"),
|
||||
)
|
||||
|
||||
for _, c := range connectors {
|
||||
c.AgentResultCallback()(state)
|
||||
}
|
||||
}),
|
||||
WithObserver(obs),
|
||||
}
|
||||
|
||||
if config.HUD {
|
||||
opts = append(opts, EnableHUD)
|
||||
}
|
||||
|
||||
if a.conversationLogs != "" {
|
||||
opts = append(opts, WithConversationsPath(a.conversationLogs))
|
||||
}
|
||||
|
||||
if config.StandaloneJob {
|
||||
opts = append(opts, EnableStandaloneJob)
|
||||
}
|
||||
|
||||
if config.LongTermMemory {
|
||||
opts = append(opts, EnableLongTermMemory)
|
||||
}
|
||||
|
||||
if config.SummaryLongTermMemory {
|
||||
opts = append(opts, EnableSummaryMemory)
|
||||
}
|
||||
|
||||
if config.CanStopItself {
|
||||
opts = append(opts, CanStopItself)
|
||||
}
|
||||
|
||||
if config.CanPlan {
|
||||
opts = append(opts, EnablePlanning)
|
||||
}
|
||||
|
||||
if config.InitiateConversations {
|
||||
opts = append(opts, EnableInitiateConversations)
|
||||
}
|
||||
|
||||
if config.RandomIdentity {
|
||||
if config.IdentityGuidance != "" {
|
||||
opts = append(opts, WithRandomIdentity(config.IdentityGuidance))
|
||||
} else {
|
||||
opts = append(opts, WithRandomIdentity())
|
||||
}
|
||||
}
|
||||
|
||||
if config.EnableKnowledgeBase {
|
||||
opts = append(opts, EnableKnowledgeBase)
|
||||
}
|
||||
|
||||
if config.EnableReasoning {
|
||||
opts = append(opts, EnableForceReasoning)
|
||||
}
|
||||
|
||||
if config.StripThinkingTags {
|
||||
opts = append(opts, EnableStripThinkingTags)
|
||||
}
|
||||
|
||||
if config.KnowledgeBaseResults > 0 {
|
||||
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
|
||||
}
|
||||
|
||||
if config.ParallelJobs > 0 {
|
||||
opts = append(opts, WithParallelJobs(config.ParallelJobs))
|
||||
}
|
||||
|
||||
if config.EnableEvaluation {
|
||||
opts = append(opts, EnableEvaluation())
|
||||
}
|
||||
|
||||
if config.MaxEvaluationLoops > 0 {
|
||||
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
|
||||
}
|
||||
|
||||
xlog.Info("Starting agent", "name", name, "config", config)
|
||||
|
||||
agent, err := New(opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.agents[name] = agent
|
||||
a.managers[name] = manager
|
||||
|
||||
go func() {
|
||||
if err := agent.Run(); err != nil {
|
||||
xlog.Error("Agent stopped", "error", err.Error(), "name", name)
|
||||
}
|
||||
}()
|
||||
|
||||
xlog.Info("Starting connectors", "name", name, "config", config)
|
||||
|
||||
for _, c := range connectors {
|
||||
go c.Start(agent)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Second) // Send a message every seconds
|
||||
manager.Send(sse.NewMessage(
|
||||
utils.HTMLify(agent.State().String()),
|
||||
).WithEvent("hud"))
|
||||
}
|
||||
}()
|
||||
|
||||
xlog.Info("Agent started", "name", name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Starts all the agents in the pool
|
||||
func (a *AgentPool) StartAll() error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
for name, config := range a.pool {
|
||||
if a.agents[name] != nil { // Agent already started
|
||||
continue
|
||||
}
|
||||
if err := a.startAgentWithConfig(name, &config, nil); err != nil {
|
||||
xlog.Error("Failed to start agent", "name", name, "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AgentPool) StopAll() {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
for _, agent := range a.agents {
|
||||
agent.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AgentPool) Stop(name string) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
a.stop(name)
|
||||
}
|
||||
|
||||
func (a *AgentPool) stop(name string) {
|
||||
if agent, ok := a.agents[name]; ok {
|
||||
agent.Stop()
|
||||
}
|
||||
}
|
||||
func (a *AgentPool) Start(name string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
if agent, ok := a.agents[name]; ok {
|
||||
err := agent.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("agent %s failed to start: %w", name, err)
|
||||
}
|
||||
xlog.Info("Agent started", "name", name)
|
||||
return nil
|
||||
}
|
||||
if config, ok := a.pool[name]; ok {
|
||||
return a.startAgentWithConfig(name, &config, nil)
|
||||
}
|
||||
|
||||
return fmt.Errorf("agent %s not found", name)
|
||||
}
|
||||
|
||||
func (a *AgentPool) stateFiles(name string) (string, string) {
|
||||
stateFile := filepath.Join(a.pooldir, fmt.Sprintf("%s.state.json", name))
|
||||
characterFile := filepath.Join(a.pooldir, fmt.Sprintf("%s.character.json", name))
|
||||
|
||||
return stateFile, characterFile
|
||||
}
|
||||
|
||||
func (a *AgentPool) Remove(name string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
// Cleanup character and state
|
||||
stateFile, characterFile := a.stateFiles(name)
|
||||
|
||||
os.Remove(stateFile)
|
||||
os.Remove(characterFile)
|
||||
|
||||
a.stop(name)
|
||||
delete(a.agents, name)
|
||||
delete(a.pool, name)
|
||||
|
||||
// remove avatar
|
||||
os.Remove(filepath.Join(a.pooldir, "avatars", fmt.Sprintf("%s.png", name)))
|
||||
|
||||
if err := a.save(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AgentPool) Save() error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.save()
|
||||
}
|
||||
|
||||
func (a *AgentPool) save() error {
|
||||
data, err := json.MarshalIndent(a.pool, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(a.file, data, 0644)
|
||||
}
|
||||
|
||||
func (a *AgentPool) GetAgent(name string) *Agent {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.agents[name]
|
||||
}
|
||||
|
||||
func (a *AgentPool) AllAgents() []string {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
var agents []string
|
||||
for agent := range a.agents {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
func (a *AgentPool) GetConfig(name string) *AgentConfig {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
agent, exists := a.pool[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return &agent
|
||||
}
|
||||
|
||||
func (a *AgentPool) GetManager(name string) sse.Manager {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.managers[name]
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type ActionContext struct {
|
||||
context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func (ac *ActionContext) Cancel() {
|
||||
if ac.cancelFunc != nil {
|
||||
ac.cancelFunc()
|
||||
}
|
||||
}
|
||||
|
||||
func NewActionContext(ctx context.Context, cancel context.CancelFunc) *ActionContext {
|
||||
return &ActionContext{
|
||||
Context: ctx,
|
||||
cancelFunc: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
type ActionParams map[string]interface{}
|
||||
|
||||
type ActionResult struct {
|
||||
Job *Job
|
||||
Result string
|
||||
ImageBase64Result string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (ap ActionParams) Read(s string) error {
|
||||
err := json.Unmarshal([]byte(s), &ap)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ap ActionParams) String() string {
|
||||
b, _ := json.Marshal(ap)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (ap ActionParams) Unmarshal(v interface{}) error {
|
||||
b, err := json.Marshal(ap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//type ActionDefinition openai.FunctionDefinition
|
||||
|
||||
type ActionDefinition struct {
|
||||
Properties map[string]jsonschema.Definition
|
||||
Required []string
|
||||
Name ActionDefinitionName
|
||||
Description string
|
||||
}
|
||||
|
||||
type ActionDefinitionName string
|
||||
|
||||
func (a ActionDefinitionName) Is(name string) bool {
|
||||
return string(a) == name
|
||||
}
|
||||
|
||||
func (a ActionDefinitionName) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
|
||||
return &openai.FunctionDefinition{
|
||||
Name: a.Name.String(),
|
||||
Description: a.Description,
|
||||
Parameters: jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: a.Properties,
|
||||
Required: a.Required,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type cogitoWrapper struct {
|
||||
action Action
|
||||
ctx context.Context
|
||||
sharedState *AgentSharedState
|
||||
}
|
||||
|
||||
func (c *cogitoWrapper) Tool() openai.Tool {
|
||||
return openai.Tool{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: c.action.Definition().ToFunctionDefinition(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cogitoWrapper) Run(args map[string]any) (string, error) {
|
||||
ctx := c.ctx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
result, err := c.action.Run(ctx, c.sharedState, ActionParams(args))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Result, nil
|
||||
}
|
||||
|
||||
// Actions is something the agent can do
|
||||
type Action interface {
|
||||
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
|
||||
Definition() ActionDefinition
|
||||
}
|
||||
|
||||
// UserDefinedChecker interface to identify user-defined actions
|
||||
type UserDefinedChecker interface {
|
||||
IsUserDefined() bool
|
||||
}
|
||||
|
||||
// BaseAction provides default implementation for Action interface
|
||||
// Embed this in action implementations to get the default IsUserDefined behavior
|
||||
type BaseAction struct{}
|
||||
|
||||
func (b *BaseAction) IsUserDefined() bool {
|
||||
return false // Regular actions are not user-defined
|
||||
}
|
||||
|
||||
// IsActionUserDefined checks if an action is user-defined
|
||||
func IsActionUserDefined(action Action) bool {
|
||||
if checker, ok := action.(UserDefinedChecker); ok {
|
||||
return checker.IsUserDefined()
|
||||
}
|
||||
return false // Actions without UserDefinedChecker are not user-defined
|
||||
}
|
||||
|
||||
// UserDefinedAction represents a user-defined function tool
|
||||
type UserDefinedAction struct {
|
||||
ActionDef *ActionDefinition
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error) {
|
||||
// User-defined actions should not be executed directly
|
||||
return ActionResult{}, fmt.Errorf("user-defined action '%s' cannot be executed by agent", u.ActionDef.Name)
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Definition() ActionDefinition {
|
||||
return *u.ActionDef
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Plannable() bool {
|
||||
return true // User-defined actions are plannable
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) IsUserDefined() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateUserDefinedActions converts user tools to UserDefinedAction instances
|
||||
func CreateUserDefinedActions(userTools []ActionDefinition) []Action {
|
||||
var actions []Action
|
||||
for _, tool := range userTools {
|
||||
actions = append(actions, &UserDefinedAction{
|
||||
ActionDef: &tool,
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
type Actions []Action
|
||||
|
||||
func (a Actions) ToTools() []openai.Tool {
|
||||
tools := []openai.Tool{}
|
||||
for _, action := range a {
|
||||
tools = append(tools, openai.Tool{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: action.Definition().ToFunctionDefinition(),
|
||||
})
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.Tool {
|
||||
tools := []cogito.Tool{}
|
||||
for _, action := range a {
|
||||
tools = append(tools, &cogitoWrapper{action: action, ctx: ctx, sharedState: sharedState})
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
func (a Actions) Find(name string) Action {
|
||||
for _, action := range a {
|
||||
if action.Definition().Name.Is(name) {
|
||||
return action
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ActionState struct {
|
||||
ActionCurrentState
|
||||
ActionResult
|
||||
}
|
||||
|
||||
type ActionCurrentState struct {
|
||||
Job *Job
|
||||
Action Action
|
||||
Params ActionParams
|
||||
Reasoning string
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package types
|
||||
|
||||
type JobFilter interface {
|
||||
Name() string
|
||||
Apply(job *Job) (bool, error)
|
||||
IsTrigger() bool
|
||||
}
|
||||
|
||||
type JobFilters []JobFilter
|
||||
|
||||
type FilterResult struct {
|
||||
HasTriggers bool `json:"has_triggers"`
|
||||
TriggeredBy string `json:"triggered_by,omitempty"`
|
||||
FailedBy string `json:"failed_by,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// Job is a request to the agent to do something
|
||||
type Job struct {
|
||||
// The job is a request to the agent to do something
|
||||
// It can be a question, a command, or a request to do something
|
||||
// The agent will try to do it, and return a response
|
||||
Result *JobResult
|
||||
ReasoningCallback func(ActionCurrentState) bool
|
||||
ResultCallback func(ActionState)
|
||||
ConversationHistory []openai.ChatCompletionMessage
|
||||
UUID string
|
||||
Metadata map[string]interface{}
|
||||
DoneFilter bool
|
||||
|
||||
// Tools available for this job
|
||||
BuiltinTools []ActionDefinition // Built-in tools like web search
|
||||
UserTools []ActionDefinition // User-defined function tools
|
||||
ToolChoice string
|
||||
|
||||
context context.Context
|
||||
fragment *cogito.Fragment
|
||||
cancel context.CancelFunc
|
||||
|
||||
Obs *Observable
|
||||
}
|
||||
|
||||
type ActionRequest struct {
|
||||
Action Action
|
||||
Params *ActionParams
|
||||
}
|
||||
|
||||
type JobOption func(*Job)
|
||||
|
||||
func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ConversationHistory = history
|
||||
}
|
||||
}
|
||||
|
||||
func WithBuiltinTools(tools []ActionDefinition) JobOption {
|
||||
return func(j *Job) {
|
||||
j.BuiltinTools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserTools(tools []ActionDefinition) JobOption {
|
||||
return func(j *Job) {
|
||||
j.UserTools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func WithToolChoice(choice string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ToolChoice = choice
|
||||
}
|
||||
}
|
||||
|
||||
func WithReasoningCallback(f func(ActionCurrentState) bool) JobOption {
|
||||
return func(r *Job) {
|
||||
r.ReasoningCallback = f
|
||||
}
|
||||
}
|
||||
|
||||
func WithResultCallback(f func(ActionState)) JobOption {
|
||||
return func(r *Job) {
|
||||
r.ResultCallback = f
|
||||
}
|
||||
}
|
||||
|
||||
func WithMetadata(metadata map[string]interface{}) JobOption {
|
||||
return func(j *Job) {
|
||||
j.Metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
// NewJobResult creates a new job result
|
||||
func NewJobResult() *JobResult {
|
||||
r := &JobResult{
|
||||
ready: make(chan bool),
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (j *Job) Callback(stateResult ActionCurrentState) bool {
|
||||
if j.ReasoningCallback == nil {
|
||||
return true
|
||||
}
|
||||
return j.ReasoningCallback(stateResult)
|
||||
}
|
||||
|
||||
func (j *Job) CallbackWithResult(stateResult ActionState) {
|
||||
if j.ResultCallback == nil {
|
||||
return
|
||||
}
|
||||
j.ResultCallback(stateResult)
|
||||
}
|
||||
|
||||
func WithTextImage(text, image string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ConversationHistory = append(j.ConversationHistory, openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
Text: text,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{URL: image},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func WithText(text string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ConversationHistory = append(j.ConversationHistory, openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
Content: text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newUUID() string {
|
||||
// Generate UUID with google/uuid
|
||||
// https://pkg.go.dev/github.com/google/uuid
|
||||
|
||||
// Generate a Version 4 UUID
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate UUID: %v", err)
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// NewJob creates a new job
|
||||
// It is a request to the agent to do something
|
||||
// It has a JobResult to get the result asynchronously
|
||||
// To wait for a Job result, use JobResult.WaitResult()
|
||||
func NewJob(opts ...JobOption) *Job {
|
||||
j := &Job{
|
||||
Result: NewJobResult(),
|
||||
UUID: uuid.New().String(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
context: context.Background(),
|
||||
ConversationHistory: []openai.ChatCompletionMessage{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(j)
|
||||
}
|
||||
|
||||
// Store the original request if it exists in the conversation history
|
||||
|
||||
ctx, cancel := context.WithCancel(j.context)
|
||||
j.context = ctx
|
||||
j.cancel = cancel
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
func WithUUID(uuid string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.UUID = uuid
|
||||
}
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) JobOption {
|
||||
return func(j *Job) {
|
||||
j.context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) Cancel() {
|
||||
j.cancel()
|
||||
}
|
||||
|
||||
func (j *Job) GetContext() context.Context {
|
||||
return j.context
|
||||
}
|
||||
|
||||
func WithObservable(obs *Observable) JobOption {
|
||||
return func(j *Job) {
|
||||
j.Obs = obs
|
||||
}
|
||||
}
|
||||
|
||||
// GetEvaluationLoop returns the current evaluation loop count
|
||||
func (j *Job) GetEvaluationLoop() int {
|
||||
if j.Metadata == nil {
|
||||
j.Metadata = make(map[string]interface{})
|
||||
}
|
||||
if loop, ok := j.Metadata["evaluation_loop"].(int); ok {
|
||||
return loop
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IncrementEvaluationLoop increments the evaluation loop count
|
||||
func (j *Job) IncrementEvaluationLoop() {
|
||||
if j.Metadata == nil {
|
||||
j.Metadata = make(map[string]interface{})
|
||||
}
|
||||
currentLoop := j.GetEvaluationLoop()
|
||||
j.Metadata["evaluation_loop"] = currentLoop + 1
|
||||
}
|
||||
|
||||
// GetBuiltinTools returns the builtin tools for this job
|
||||
func (j *Job) GetBuiltinTools() []ActionDefinition {
|
||||
return j.BuiltinTools
|
||||
}
|
||||
|
||||
// GetUserTools returns the user tools for this job
|
||||
func (j *Job) GetUserTools() []ActionDefinition {
|
||||
return j.UserTools
|
||||
}
|
||||
|
||||
// GetAllTools returns all tools (builtin + user) for this job
|
||||
func (j *Job) GetAllTools() []ActionDefinition {
|
||||
allTools := make([]ActionDefinition, 0, len(j.BuiltinTools)+len(j.UserTools))
|
||||
allTools = append(allTools, j.BuiltinTools...)
|
||||
allTools = append(allTools, j.UserTools...)
|
||||
return allTools
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type Creation struct {
|
||||
ChatCompletionMessage *openai.ChatCompletionMessage `json:"chat_completion_message,omitempty"`
|
||||
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
|
||||
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
|
||||
FunctionParams ActionParams `json:"function_params,omitempty"`
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
|
||||
ActionResult string `json:"action_result,omitempty"`
|
||||
AgentState *AgentInternalState `json:"agent_state"`
|
||||
}
|
||||
|
||||
type Completion struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
|
||||
Conversation []openai.ChatCompletionMessage `json:"conversation,omitempty"`
|
||||
ActionResult string `json:"action_result,omitempty"`
|
||||
AgentState *AgentInternalState `json:"agent_state,omitempty"`
|
||||
FilterResult *FilterResult `json:"filter_result,omitempty"`
|
||||
}
|
||||
|
||||
type Observable struct {
|
||||
ID int32 `json:"id"`
|
||||
ParentID int32 `json:"parent_id,omitempty"`
|
||||
Agent string `json:"agent"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
|
||||
Creation *Creation `json:"creation,omitempty"`
|
||||
Progress []Progress `json:"progress,omitempty"`
|
||||
Completion *Completion `json:"completion,omitempty"`
|
||||
}
|
||||
|
||||
func (o *Observable) AddProgress(p Progress) {
|
||||
if o.Progress == nil {
|
||||
o.Progress = make([]Progress, 0)
|
||||
}
|
||||
o.Progress = append(o.Progress, p)
|
||||
}
|
||||
|
||||
func (o *Observable) MakeLastProgressCompletion() {
|
||||
if len(o.Progress) == 0 {
|
||||
xlog.Error("Observable completed without any progress", "id", o.ID, "name", o.Name)
|
||||
return
|
||||
}
|
||||
p := o.Progress[len(o.Progress)-1]
|
||||
o.Progress = o.Progress[:len(o.Progress)-1]
|
||||
o.Completion = &Completion{
|
||||
Error: p.Error,
|
||||
ChatCompletionResponse: p.ChatCompletionResponse,
|
||||
ActionResult: p.ActionResult,
|
||||
AgentState: p.AgentState,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package types
|
||||
|
||||
type PromptResult struct {
|
||||
Content string
|
||||
ImageBase64 string
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// JobResult is the result of a job
|
||||
type JobResult struct {
|
||||
sync.Mutex
|
||||
// The result of a job
|
||||
State []ActionState
|
||||
Plans []cogito.PlanStatus
|
||||
Conversation []openai.ChatCompletionMessage
|
||||
|
||||
Finalizers []func([]openai.ChatCompletionMessage)
|
||||
|
||||
Response string
|
||||
Error error
|
||||
ready chan bool
|
||||
}
|
||||
|
||||
// SetResult sets the result of a job
|
||||
func (j *JobResult) SetResult(text ActionState) {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
j.State = append(j.State, text)
|
||||
}
|
||||
|
||||
// SetResult sets the result of a job
|
||||
func (j *JobResult) Finish(e error) {
|
||||
j.Lock()
|
||||
j.Error = e
|
||||
j.Unlock()
|
||||
|
||||
close(j.ready)
|
||||
|
||||
for _, f := range j.Finalizers {
|
||||
f(j.Conversation)
|
||||
}
|
||||
j.Finalizers = []func([]openai.ChatCompletionMessage){}
|
||||
}
|
||||
|
||||
// AddFinalizer adds a finalizer to the job result
|
||||
func (j *JobResult) AddFinalizer(f func([]openai.ChatCompletionMessage)) {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
j.Finalizers = append(j.Finalizers, f)
|
||||
}
|
||||
|
||||
// SetResult sets the result of a job
|
||||
func (j *JobResult) SetResponse(response string) {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
j.Response = response
|
||||
}
|
||||
|
||||
// WaitResult waits for the result of a job
|
||||
func (j *JobResult) WaitResult() *JobResult {
|
||||
<-j.ready
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
return j
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/conversations"
|
||||
)
|
||||
|
||||
// State is the structure
|
||||
// that is used to keep track of the current state
|
||||
// and the Agent's short memory that it can update
|
||||
// Besides a long term memory that is accessible by the agent (With vector database),
|
||||
// And a context memory (that is always powered by a vector database),
|
||||
// this memory is the shorter one that the LLM keeps across conversation and across its
|
||||
// reasoning process's and life time.
|
||||
// TODO: A special action is then used to let the LLM itself update its memory
|
||||
// periodically during self-processing, and the same action is ALSO exposed
|
||||
// during the conversation to let the user put for example, a new goal to the agent.
|
||||
type AgentInternalState struct {
|
||||
NowDoing string `json:"doing_now"`
|
||||
DoingNext string `json:"doing_next"`
|
||||
DoneHistory []string `json:"done_history"`
|
||||
Memories []string `json:"memories"`
|
||||
Goal string `json:"goal"`
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultLastMessageDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
type ReminderActionResponse struct {
|
||||
Message string `json:"message"`
|
||||
CronExpr string `json:"cron_expr"` // Cron expression for scheduling
|
||||
LastRun time.Time `json:"last_run"` // Last time this reminder was triggered
|
||||
NextRun time.Time `json:"next_run"` // Next scheduled run time
|
||||
IsRecurring bool `json:"is_recurring"` // Whether this is a recurring reminder
|
||||
}
|
||||
|
||||
type AgentSharedState struct {
|
||||
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
|
||||
Reminders []ReminderActionResponse `json:"reminders"`
|
||||
}
|
||||
|
||||
func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
||||
if lastMessageDuration == 0 {
|
||||
lastMessageDuration = DefaultLastMessageDuration
|
||||
}
|
||||
return &AgentSharedState{
|
||||
ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration),
|
||||
Reminders: make([]ReminderActionResponse, 0),
|
||||
}
|
||||
}
|
||||
|
||||
const fmtT = `=====================
|
||||
NowDoing: %s
|
||||
DoingNext: %s
|
||||
Your current goal is: %s
|
||||
You have done: %+v
|
||||
You have a short memory with: %+v
|
||||
=====================
|
||||
`
|
||||
|
||||
func (c AgentInternalState) String() string {
|
||||
return fmt.Sprintf(
|
||||
fmtT,
|
||||
c.NowDoing,
|
||||
c.DoingNext,
|
||||
c.Goal,
|
||||
c.DoneHistory,
|
||||
c.Memories,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-hipblas
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/kfd
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-intel
|
||||
devices:
|
||||
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
||||
- /dev/dri/card1
|
||||
- /dev/dri/renderD129
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
@@ -0,0 +1,38 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-nvidia-cuda-12
|
||||
# For images with python backends, use:
|
||||
# image: localai/localai:master-cublas-cuda12-ffmpeg
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
+93
-20
@@ -1,31 +1,104 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
api:
|
||||
image: quay.io/go-skynet/local-ai:master
|
||||
localai:
|
||||
# See https://localai.io/basics/container/#standard-container-images for
|
||||
# a list of available container images (or build your own with the provided Dockerfile)
|
||||
# Available images with CUDA, ROCm, SYCL, Vulkan
|
||||
# Image list (quay.io): https://quay.io/repository/go-skynet/local-ai?tab=tags
|
||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||
image: localai/localai:master
|
||||
command:
|
||||
- ${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- ${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- granite-embedding-107m-multilingual
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
||||
interval: 1m
|
||||
timeout: 120m
|
||||
interval: 60s
|
||||
timeout: 10m
|
||||
retries: 120
|
||||
ports:
|
||||
- 8090:8080
|
||||
env_file:
|
||||
- .env
|
||||
- 8081:8080
|
||||
environment:
|
||||
- DEBUG=true
|
||||
#- LOCALAI_API_KEY=sk-1234567890
|
||||
volumes:
|
||||
- ./models:/models:cached
|
||||
- ./config:/config:cached
|
||||
command: ["/usr/bin/local-ai" ]
|
||||
localagi:
|
||||
- ./volumes/models:/models
|
||||
- ./volumes/backends:/backends
|
||||
- ./volumes/content:/tmp/generated/content
|
||||
|
||||
localrecall:
|
||||
image: quay.io/mudler/localrecall:main
|
||||
ports:
|
||||
- 8080
|
||||
environment:
|
||||
- COLLECTION_DB_PATH=/db
|
||||
- EMBEDDING_MODEL=granite-embedding-107m-multilingual
|
||||
- FILE_ASSETS=/assets
|
||||
- OPENAI_API_KEY=sk-1234567890
|
||||
- OPENAI_BASE_URL=http://localai:8080
|
||||
volumes:
|
||||
- ./volumes/localrag/db:/db
|
||||
- ./volumes/localrag/assets/:/assets
|
||||
|
||||
localrecall-healthcheck:
|
||||
depends_on:
|
||||
localrecall:
|
||||
condition: service_started
|
||||
image: busybox
|
||||
command: ["sh", "-c", "until wget -q -O - http://localrecall:8080 > /dev/null 2>&1; do echo 'Waiting for localrecall...'; sleep 1; done; echo 'localrecall is up!'"]
|
||||
|
||||
sshbox:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
devices:
|
||||
- /dev/snd
|
||||
dockerfile: Dockerfile.sshbox
|
||||
ports:
|
||||
- "22"
|
||||
environment:
|
||||
- SSH_USER=root
|
||||
- SSH_PASSWORD=root
|
||||
- DOCKER_HOST=tcp://dind:2375
|
||||
depends_on:
|
||||
api:
|
||||
dind:
|
||||
condition: service_healthy
|
||||
|
||||
dind:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
environment:
|
||||
- DOCKER_TLS_CERTDIR=""
|
||||
healthcheck:
|
||||
test: ["CMD", "docker", "info"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
localagi:
|
||||
depends_on:
|
||||
localai:
|
||||
condition: service_healthy
|
||||
localrecall-healthcheck:
|
||||
condition: service_completed_successfully
|
||||
dind:
|
||||
condition: service_healthy
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.webui
|
||||
ports:
|
||||
- 8080:3000
|
||||
#image: quay.io/mudler/localagi:master
|
||||
environment:
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||
- LOCALAGI_STATE_DIR=/pool
|
||||
- LOCALAGI_TIMEOUT=5m
|
||||
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
||||
- LOCALAGI_SSHBOX_URL=root:root@sshbox:22
|
||||
- DOCKER_HOST=tcp://dind:2375
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- ./data:/data
|
||||
env_file:
|
||||
- .env
|
||||
- ./volumes/localagi/:/pool
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package custom_actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
Message string `json:"message"` // field name
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
p := Params{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
return "Hello, " + p.Message + "!", map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
func Description() string {
|
||||
return "Send a message to the user"
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"message": { // field name
|
||||
"string", // type
|
||||
"The message to send", // description
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"message"} // field name
|
||||
}
|
||||
|
||||
var config string
|
||||
|
||||
func Init(configuration string) error {
|
||||
// Do something with the configuration that was passed-by
|
||||
config = configuration
|
||||
return nil
|
||||
}
|
||||
|
||||
// DynamicPrompt
|
||||
func Render() (string, string, error) {
|
||||
return "Hello, " + config + "!", "", nil
|
||||
}
|
||||
|
||||
func Role() string {
|
||||
return "system" // Role for the dynamic prompt
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
module github.com/mudler/LocalAGI
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/bwmarrin/discordgo v0.29.0
|
||||
github.com/chasefleming/elem-go v0.30.0
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
||||
github.com/go-telegram/bot v1.17.0
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/google/go-github/v69 v69.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0
|
||||
github.com/mudler/cogito v0.4.2
|
||||
github.com/onsi/ginkgo/v2 v2.25.3
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/philippgille/chromem-go v0.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
github.com/slack-go/slack v0.17.3
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
||||
github.com/tmc/langchaingo v0.1.14
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
maunium.net/go/mautrix v0.17.0
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/antchfx/htmlquery v1.3.4 // indirect
|
||||
github.com/antchfx/xmlquery v1.4.4 // indirect
|
||||
github.com/antchfx/xpath v1.3.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||
github.com/emersion/go-message v0.18.2
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||
github.com/emersion/go-smtp v0.24.0
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gocolly/colly v1.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/zerolog v1.31.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/temoto/robotstxt v1.1.2 // 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/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.mau.fi/util v0.3.0 // indirect
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,398 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
|
||||
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
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/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
|
||||
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
|
||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
||||
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
||||
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
|
||||
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
|
||||
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
|
||||
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
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/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mudler/cogito v0.4.2 h1:1PypFvUq3A+iBxMCR0v+sGXlhOsRnvyFZhEE93tsuDU=
|
||||
github.com/mudler/cogito v0.4.2/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
|
||||
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
|
||||
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/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc=
|
||||
github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378=
|
||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
|
||||
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
|
||||
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/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
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/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
||||
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
|
||||
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
|
||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
"jsx": "react",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/services"
|
||||
"github.com/mudler/LocalAGI/webui"
|
||||
)
|
||||
|
||||
var baseModel = os.Getenv("LOCALAGI_MODEL")
|
||||
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
|
||||
var transcriptionModel = os.Getenv("LOCALAGI_TRANSCRIPTION_MODEL")
|
||||
var transcriptionLanguage = os.Getenv("LOCALAGI_TRANSCRIPTION_LANGUAGE")
|
||||
var ttsModel = os.Getenv("LOCALAGI_TTS_MODEL")
|
||||
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
|
||||
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
|
||||
var timeout = os.Getenv("LOCALAGI_TIMEOUT")
|
||||
var stateDir = os.Getenv("LOCALAGI_STATE_DIR")
|
||||
var localRAG = os.Getenv("LOCALAGI_LOCALRAG_URL")
|
||||
var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
|
||||
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
|
||||
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
|
||||
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
||||
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
|
||||
var customActionsDir = os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR")
|
||||
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
|
||||
|
||||
func init() {
|
||||
if baseModel == "" {
|
||||
panic("LOCALAGI_MODEL not set")
|
||||
}
|
||||
if apiURL == "" {
|
||||
panic("LOCALAGI_LLM_API_URL not set")
|
||||
}
|
||||
if timeout == "" {
|
||||
timeout = "5m"
|
||||
}
|
||||
if stateDir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stateDir = filepath.Join(cwd, "pool")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// make sure state dir exists
|
||||
os.MkdirAll(stateDir, 0755)
|
||||
|
||||
apiKeys := []string{}
|
||||
if apiKeysEnv != "" {
|
||||
apiKeys = strings.Split(apiKeysEnv, ",")
|
||||
}
|
||||
|
||||
// Create the agent pool
|
||||
pool, err := state.NewAgentPool(
|
||||
baseModel,
|
||||
multimodalModel,
|
||||
transcriptionModel,
|
||||
transcriptionLanguage,
|
||||
ttsModel,
|
||||
imageModel,
|
||||
apiURL,
|
||||
apiKey,
|
||||
stateDir,
|
||||
localRAG,
|
||||
services.Actions(map[string]string{
|
||||
services.ActionConfigBrowserAgentRunner: localOperatorBaseURL,
|
||||
services.ActionConfigDeepResearchRunner: localOperatorBaseURL,
|
||||
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts(map[string]string{
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Filters,
|
||||
timeout,
|
||||
withLogs,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create the application
|
||||
app := webui.NewApp(
|
||||
webui.WithPool(pool),
|
||||
webui.WithConversationStoreduration(conversationDuration),
|
||||
webui.WithApiKeys(apiKeys...),
|
||||
webui.WithLLMAPIUrl(apiURL),
|
||||
webui.WithLLMAPIKey(apiKey),
|
||||
webui.WithLLMModel(baseModel),
|
||||
webui.WithCustomActionsDir(customActionsDir),
|
||||
webui.WithStateDir(stateDir),
|
||||
)
|
||||
|
||||
// Start the agents
|
||||
if err := pool.StartAll(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Start the web server
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
import openai
|
||||
#from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from langchain.embeddings import LocalAIEmbeddings
|
||||
import uuid
|
||||
import sys
|
||||
|
||||
from localagi import LocalAGI
|
||||
from loguru import logger
|
||||
from ascii_magic import AsciiArt
|
||||
from duckduckgo_search import DDGS
|
||||
from typing import Dict, List
|
||||
import os
|
||||
|
||||
# these three lines swap the stdlib sqlite3 lib with the pysqlite3 package for chroma
|
||||
__import__('pysqlite3')
|
||||
import sys
|
||||
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
|
||||
|
||||
from langchain.vectorstores import Chroma
|
||||
from chromadb.config import Settings
|
||||
import json
|
||||
import os
|
||||
from io import StringIO
|
||||
|
||||
# Parse arguments such as system prompt and batch mode
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='LocalAGI')
|
||||
# System prompt
|
||||
parser.add_argument('--system-prompt', dest='system_prompt', action='store',
|
||||
help='System prompt to use')
|
||||
# Batch mode
|
||||
parser.add_argument('--prompt', dest='prompt', action='store', default=False,
|
||||
help='Prompt mode')
|
||||
# Interactive mode
|
||||
parser.add_argument('--interactive', dest='interactive', action='store_true', default=False,
|
||||
help='Interactive mode. Can be used with --prompt to start an interactive session')
|
||||
# skip avatar creation
|
||||
parser.add_argument('--skip-avatar', dest='skip_avatar', action='store_true', default=False,
|
||||
help='Skip avatar creation')
|
||||
# Reevaluate
|
||||
parser.add_argument('--re-evaluate', dest='re_evaluate', action='store_true', default=False,
|
||||
help='Reevaluate if another action is needed or we have completed the user request')
|
||||
# Postprocess
|
||||
parser.add_argument('--postprocess', dest='postprocess', action='store_true', default=False,
|
||||
help='Postprocess the reasoning')
|
||||
# Subtask context
|
||||
parser.add_argument('--subtask-context', dest='subtaskContext', action='store_true', default=False,
|
||||
help='Include context in subtasks')
|
||||
|
||||
# Search results number
|
||||
parser.add_argument('--search-results', dest='search_results', type=int, action='store', default=2,
|
||||
help='Number of search results to return')
|
||||
# Plan message
|
||||
parser.add_argument('--plan-message', dest='plan_message', action='store',
|
||||
help="What message to use during planning",
|
||||
)
|
||||
|
||||
DEFAULT_PROMPT="floating hair, portrait, ((loli)), ((one girl)), cute face, hidden hands, asymmetrical bangs, beautiful detailed eyes, eye shadow, hair ornament, ribbons, bowties, buttons, pleated skirt, (((masterpiece))), ((best quality)), colorful|((part of the head)), ((((mutated hands and fingers)))), deformed, blurry, bad anatomy, disfigured, poorly drawn face, mutation, mutated, extra limb, ugly, poorly drawn hands, missing limb, blurry, floating limbs, disconnected limbs, malformed hands, blur, out of focus, long neck, long body, Octane renderer, lowres, bad anatomy, bad hands, text"
|
||||
DEFAULT_API_BASE = os.environ.get("DEFAULT_API_BASE", "http://api:8080")
|
||||
# TTS api base
|
||||
parser.add_argument('--tts-api-base', dest='tts_api_base', action='store', default=DEFAULT_API_BASE,
|
||||
help='TTS api base')
|
||||
# LocalAI api base
|
||||
parser.add_argument('--localai-api-base', dest='localai_api_base', action='store', default=DEFAULT_API_BASE,
|
||||
help='LocalAI api base')
|
||||
# Images api base
|
||||
parser.add_argument('--images-api-base', dest='images_api_base', action='store', default=DEFAULT_API_BASE,
|
||||
help='Images api base')
|
||||
# Embeddings api base
|
||||
parser.add_argument('--embeddings-api-base', dest='embeddings_api_base', action='store', default=DEFAULT_API_BASE,
|
||||
help='Embeddings api base')
|
||||
# Functions model
|
||||
parser.add_argument('--functions-model', dest='functions_model', action='store', default="functions",
|
||||
help='Functions model')
|
||||
# Embeddings model
|
||||
parser.add_argument('--embeddings-model', dest='embeddings_model', action='store', default="all-MiniLM-L6-v2",
|
||||
help='Embeddings model')
|
||||
# LLM model
|
||||
parser.add_argument('--llm-model', dest='llm_model', action='store', default="gpt-4",
|
||||
help='LLM model')
|
||||
# Voice model
|
||||
parser.add_argument('--tts-model', dest='tts_model', action='store', default="en-us-kathleen-low.onnx",
|
||||
help='TTS model')
|
||||
# Stable diffusion model
|
||||
parser.add_argument('--stablediffusion-model', dest='stablediffusion_model', action='store', default="stablediffusion",
|
||||
help='Stable diffusion model')
|
||||
# Stable diffusion prompt
|
||||
parser.add_argument('--stablediffusion-prompt', dest='stablediffusion_prompt', action='store', default=DEFAULT_PROMPT,
|
||||
help='Stable diffusion prompt')
|
||||
# Force action
|
||||
parser.add_argument('--force-action', dest='force_action', action='store', default="",
|
||||
help='Force an action')
|
||||
# Debug mode
|
||||
parser.add_argument('--debug', dest='debug', action='store_true', default=False,
|
||||
help='Debug mode')
|
||||
# Critic mode
|
||||
parser.add_argument('--critic', dest='critic', action='store_true', default=False,
|
||||
help='Enable critic')
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL", args.stablediffusion_model)
|
||||
STABLEDIFFUSION_PROMPT = os.environ.get("STABLEDIFFUSION_PROMPT", args.stablediffusion_prompt)
|
||||
FUNCTIONS_MODEL = os.environ.get("FUNCTIONS_MODEL", args.functions_model)
|
||||
EMBEDDINGS_MODEL = os.environ.get("EMBEDDINGS_MODEL", args.embeddings_model)
|
||||
LLM_MODEL = os.environ.get("LLM_MODEL", args.llm_model)
|
||||
VOICE_MODEL= os.environ.get("TTS_MODEL",args.tts_model)
|
||||
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL",args.stablediffusion_model)
|
||||
STABLEDIFFUSION_PROMPT = os.environ.get("STABLEDIFFUSION_PROMPT", args.stablediffusion_prompt)
|
||||
PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "/data")
|
||||
SYSTEM_PROMPT = ""
|
||||
if os.environ.get("SYSTEM_PROMPT") or args.system_prompt:
|
||||
SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", args.system_prompt)
|
||||
|
||||
LOCALAI_API_BASE = args.localai_api_base
|
||||
TTS_API_BASE = args.tts_api_base
|
||||
IMAGE_API_BASE = args.images_api_base
|
||||
EMBEDDINGS_API_BASE = args.embeddings_api_base
|
||||
|
||||
# Set log level
|
||||
LOG_LEVEL = "INFO"
|
||||
|
||||
def my_filter(record):
|
||||
return record["level"].no >= logger.level(LOG_LEVEL).no
|
||||
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, filter=my_filter)
|
||||
|
||||
if args.debug:
|
||||
LOG_LEVEL = "DEBUG"
|
||||
logger.debug("Debug mode on")
|
||||
|
||||
FUNCTIONS_MODEL = os.environ.get("FUNCTIONS_MODEL", args.functions_model)
|
||||
EMBEDDINGS_MODEL = os.environ.get("EMBEDDINGS_MODEL", args.embeddings_model)
|
||||
LLM_MODEL = os.environ.get("LLM_MODEL", args.llm_model)
|
||||
VOICE_MODEL= os.environ.get("TTS_MODEL",args.tts_model)
|
||||
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL",args.stablediffusion_model)
|
||||
STABLEDIFFUSION_PROMPT = os.environ.get("STABLEDIFFUSION_PROMPT", args.stablediffusion_prompt)
|
||||
PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "/data")
|
||||
SYSTEM_PROMPT = ""
|
||||
if os.environ.get("SYSTEM_PROMPT") or args.system_prompt:
|
||||
SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", args.system_prompt)
|
||||
|
||||
LOCALAI_API_BASE = args.localai_api_base
|
||||
TTS_API_BASE = args.tts_api_base
|
||||
IMAGE_API_BASE = args.images_api_base
|
||||
EMBEDDINGS_API_BASE = args.embeddings_api_base
|
||||
|
||||
## Constants
|
||||
REPLY_ACTION = "reply"
|
||||
PLAN_ACTION = "plan"
|
||||
|
||||
embeddings = LocalAIEmbeddings(model=EMBEDDINGS_MODEL,openai_api_base=EMBEDDINGS_API_BASE)
|
||||
chroma_client = Chroma(collection_name="memories", persist_directory="db", embedding_function=embeddings)
|
||||
|
||||
# Function to create images with LocalAI
|
||||
def display_avatar(agi, input_text=STABLEDIFFUSION_PROMPT, model=STABLEDIFFUSION_MODEL):
|
||||
image_url = agi.get_avatar(input_text, model)
|
||||
# convert the image to ascii art
|
||||
my_art = AsciiArt.from_url(image_url)
|
||||
my_art.to_terminal()
|
||||
|
||||
## This function is called to ask the user if does agree on the action to take and execute
|
||||
def ask_user_confirmation(action_name, action_parameters):
|
||||
logger.info("==> Ask user confirmation")
|
||||
logger.info("==> action_name: {action_name}", action_name=action_name)
|
||||
logger.info("==> action_parameters: {action_parameters}", action_parameters=action_parameters)
|
||||
# Ask via stdin
|
||||
logger.info("==> Do you want to execute the action? (y/n)")
|
||||
user_input = input()
|
||||
if user_input == "y":
|
||||
logger.info("==> Executing action")
|
||||
return True
|
||||
else:
|
||||
logger.info("==> Skipping action")
|
||||
return False
|
||||
|
||||
### Agent capabilities
|
||||
### These functions are called by the agent to perform actions
|
||||
###
|
||||
def save(memory, agent_actions={}, localagi=None):
|
||||
q = json.loads(memory)
|
||||
logger.info(">>> saving to memories: ")
|
||||
logger.info(q["content"])
|
||||
chroma_client.add_texts([q["content"]],[{"id": str(uuid.uuid4())}])
|
||||
chroma_client.persist()
|
||||
return f"The object was saved permanently to memory."
|
||||
|
||||
def search_memory(query, agent_actions={}, localagi=None):
|
||||
q = json.loads(query)
|
||||
docs = chroma_client.similarity_search(q["reasoning"])
|
||||
text_res="Memories found in the database:\n"
|
||||
for doc in docs:
|
||||
text_res+="- "+doc.page_content+"\n"
|
||||
|
||||
#if args.postprocess:
|
||||
# return post_process(text_res)
|
||||
#return text_res
|
||||
return localagi.post_process(text_res)
|
||||
|
||||
|
||||
# write file to disk with content
|
||||
def save_file(arg, agent_actions={}, localagi=None):
|
||||
arg = json.loads(arg)
|
||||
filename = arg["filename"]
|
||||
content = arg["content"]
|
||||
# create persistent dir if does not exist
|
||||
if not os.path.exists(PERSISTENT_DIR):
|
||||
os.makedirs(PERSISTENT_DIR)
|
||||
# write the file in the directory specified
|
||||
filename = os.path.join(PERSISTENT_DIR, filename)
|
||||
with open(filename, 'w') as f:
|
||||
f.write(content)
|
||||
return f"File {filename} saved successfully."
|
||||
|
||||
|
||||
def ddg(query: str, num_results: int, backend: str = "api") -> List[Dict[str, str]]:
|
||||
"""Run query through DuckDuckGo and return metadata.
|
||||
|
||||
Args:
|
||||
query: The query to search for.
|
||||
num_results: The number of results to return.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries with the following keys:
|
||||
snippet - The description of the result.
|
||||
title - The title of the result.
|
||||
link - The link to the result.
|
||||
"""
|
||||
|
||||
with DDGS() as ddgs:
|
||||
results = ddgs.text(
|
||||
query,
|
||||
backend=backend,
|
||||
)
|
||||
if results is None:
|
||||
return [{"Result": "No good DuckDuckGo Search Result was found"}]
|
||||
|
||||
def to_metadata(result: Dict) -> Dict[str, str]:
|
||||
if backend == "news":
|
||||
return {
|
||||
"date": result["date"],
|
||||
"title": result["title"],
|
||||
"snippet": result["body"],
|
||||
"source": result["source"],
|
||||
"link": result["url"],
|
||||
}
|
||||
return {
|
||||
"snippet": result["body"],
|
||||
"title": result["title"],
|
||||
"link": result["href"],
|
||||
}
|
||||
|
||||
formatted_results = []
|
||||
for i, res in enumerate(results, 1):
|
||||
if res is not None:
|
||||
formatted_results.append(to_metadata(res))
|
||||
if len(formatted_results) == num_results:
|
||||
break
|
||||
return formatted_results
|
||||
|
||||
## Search on duckduckgo
|
||||
def search_duckduckgo(a, agent_actions={}, localagi=None):
|
||||
a = json.loads(a)
|
||||
list=ddg(a["query"], args.search_results)
|
||||
|
||||
text_res=""
|
||||
for doc in list:
|
||||
text_res+=f"""{doc["link"]}: {doc["title"]} {doc["snippet"]}\n"""
|
||||
|
||||
#if args.postprocess:
|
||||
# return post_process(text_res)
|
||||
return text_res
|
||||
#l = json.dumps(list)
|
||||
#return l
|
||||
|
||||
### End Agent capabilities
|
||||
###
|
||||
|
||||
### Agent action definitions
|
||||
agent_actions = {
|
||||
"search_internet": {
|
||||
"function": search_duckduckgo,
|
||||
"plannable": True,
|
||||
"description": 'For searching the internet with a query, the assistant replies with the action "search_internet" and the query to search.',
|
||||
"signature": {
|
||||
"name": "search_internet",
|
||||
"description": """For searching internet.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"save_file": {
|
||||
"function": save_file,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "save_file", the filename and content to save for writing a file to disk permanently. This can be used to store the result of complex actions locally.',
|
||||
"signature": {
|
||||
"name": "save_file",
|
||||
"description": """For saving a file to disk with content.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"save_memory": {
|
||||
"function": save,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "save_memory" and the string to remember or store an information that thinks it is relevant permanently.',
|
||||
"signature": {
|
||||
"name": "save_memory",
|
||||
"description": """Save or store informations into memory.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
},
|
||||
},
|
||||
"search_memory": {
|
||||
"function": search_memory,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "search_memory" for searching between its memories with a query term.',
|
||||
"signature": {
|
||||
"name": "search_memory",
|
||||
"description": """Search in memory""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reasoning": {
|
||||
"type": "string",
|
||||
"description": "reasoning behind the intent"
|
||||
},
|
||||
},
|
||||
"required": ["reasoning"]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
conversation_history = []
|
||||
|
||||
# Create a LocalAGI instance
|
||||
logger.info("Creating LocalAGI instance")
|
||||
localagi = LocalAGI(
|
||||
agent_actions=agent_actions,
|
||||
embeddings_model=EMBEDDINGS_MODEL,
|
||||
embeddings_api_base=EMBEDDINGS_API_BASE,
|
||||
llm_model=LLM_MODEL,
|
||||
tts_model=VOICE_MODEL,
|
||||
tts_api_base=TTS_API_BASE,
|
||||
functions_model=FUNCTIONS_MODEL,
|
||||
api_base=LOCALAI_API_BASE,
|
||||
stablediffusion_api_base=IMAGE_API_BASE,
|
||||
stablediffusion_model=STABLEDIFFUSION_MODEL,
|
||||
force_action=args.force_action,
|
||||
plan_message=args.plan_message,
|
||||
)
|
||||
|
||||
# Set a system prompt if SYSTEM_PROMPT is set
|
||||
if SYSTEM_PROMPT != "":
|
||||
conversation_history.append({
|
||||
"role": "system",
|
||||
"content": SYSTEM_PROMPT
|
||||
})
|
||||
|
||||
logger.info("Welcome to LocalAGI")
|
||||
|
||||
# Skip avatar creation if --skip-avatar is set
|
||||
if not args.skip_avatar:
|
||||
logger.info("Creating avatar, please wait...")
|
||||
display_avatar(localagi)
|
||||
|
||||
actions = ""
|
||||
for action in agent_actions:
|
||||
actions+=" '"+action+"'"
|
||||
logger.info("LocalAGI internally can do the following actions:{actions}", actions=actions)
|
||||
|
||||
if not args.prompt:
|
||||
logger.info(">>> Interactive mode <<<")
|
||||
else:
|
||||
logger.info(">>> Prompt mode <<<")
|
||||
logger.info(args.prompt)
|
||||
|
||||
# IF in prompt mode just evaluate, otherwise loop
|
||||
if args.prompt:
|
||||
conversation_history=localagi.evaluate(
|
||||
args.prompt,
|
||||
conversation_history,
|
||||
critic=args.critic,
|
||||
re_evaluate=args.re_evaluate,
|
||||
# Enable to lower context usage but increases LLM calls
|
||||
postprocess=args.postprocess,
|
||||
subtaskContext=args.subtaskContext,
|
||||
)
|
||||
localagi.tts_play(conversation_history[-1]["content"])
|
||||
|
||||
if not args.prompt or args.interactive:
|
||||
# TODO: process functions also considering the conversation history? conversation history + input
|
||||
logger.info(">>> Ready! What can I do for you? ( try with: plan a roadtrip to San Francisco ) <<<")
|
||||
|
||||
while True:
|
||||
user_input = input(">>> ")
|
||||
# we are going to use the args to change the evaluation behavior
|
||||
conversation_history=localagi.evaluate(
|
||||
user_input,
|
||||
conversation_history,
|
||||
critic=args.critic,
|
||||
re_evaluate=args.re_evaluate,
|
||||
# Enable to lower context usage but increases LLM calls
|
||||
postprocess=args.postprocess,
|
||||
subtaskContext=args.subtaskContext,
|
||||
)
|
||||
localagi.tts_play(conversation_history[-1]["content"])
|
||||
@@ -0,0 +1,172 @@
|
||||
package localagi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AgentConfig represents the configuration for an agent
|
||||
type AgentConfig struct {
|
||||
Name string `json:"name"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Connectors []string `json:"connectors,omitempty"`
|
||||
PromptBlocks []string `json:"prompt_blocks,omitempty"`
|
||||
InitialPrompt string `json:"initial_prompt,omitempty"`
|
||||
Parallel bool `json:"parallel,omitempty"`
|
||||
EnableReasoning bool `json:"enable_reasoning,omitempty"`
|
||||
}
|
||||
|
||||
// AgentStatus represents the status of an agent
|
||||
type AgentStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ListAgents returns a list of all agents
|
||||
func (c *Client) ListAgents() ([]string, error) {
|
||||
resp, err := c.doRequest(http.MethodGet, "/agents", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The response is HTML, so we'll need to parse it properly
|
||||
// For now, we'll just return a placeholder implementation
|
||||
return []string{}, fmt.Errorf("ListAgents not implemented")
|
||||
}
|
||||
|
||||
// GetAgentConfig retrieves the configuration for a specific agent
|
||||
func (c *Client) GetAgentConfig(name string) (*AgentConfig, error) {
|
||||
path := fmt.Sprintf("/api/agent/%s/config", name)
|
||||
resp, err := c.doRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var config AgentConfig
|
||||
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// CreateAgent creates a new agent with the given configuration
|
||||
func (c *Client) CreateAgent(config *AgentConfig) error {
|
||||
resp, err := c.doRequest(http.MethodPost, "/api/agent/create", config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to create agent: %v", response)
|
||||
}
|
||||
|
||||
// UpdateAgentConfig updates the configuration for an existing agent
|
||||
func (c *Client) UpdateAgentConfig(name string, config *AgentConfig) error {
|
||||
// Ensure the name in the URL matches the name in the config
|
||||
config.Name = name
|
||||
path := fmt.Sprintf("/api/agent/%s/config", name)
|
||||
|
||||
resp, err := c.doRequest(http.MethodPut, path, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to update agent: %v", response)
|
||||
}
|
||||
|
||||
// DeleteAgent removes an agent
|
||||
func (c *Client) DeleteAgent(name string) error {
|
||||
path := fmt.Sprintf("/api/agent/%s", name)
|
||||
resp, err := c.doRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to delete agent: %v", response)
|
||||
}
|
||||
|
||||
// PauseAgent pauses an agent
|
||||
func (c *Client) PauseAgent(name string) error {
|
||||
path := fmt.Sprintf("/api/agent/pause/%s", name)
|
||||
resp, err := c.doRequest(http.MethodPut, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to pause agent: %v", response)
|
||||
}
|
||||
|
||||
// StartAgent starts a paused agent
|
||||
func (c *Client) StartAgent(name string) error {
|
||||
path := fmt.Sprintf("/api/agent/start/%s", name)
|
||||
resp, err := c.doRequest(http.MethodPut, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to start agent: %v", response)
|
||||
}
|
||||
|
||||
// ExportAgent exports an agent configuration
|
||||
func (c *Client) ExportAgent(name string) (*AgentConfig, error) {
|
||||
path := fmt.Sprintf("/settings/export/%s", name)
|
||||
resp, err := c.doRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var config AgentConfig
|
||||
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package localagi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Message represents a chat message
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ChatResponse represents a response from the agent
|
||||
type ChatResponse struct {
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
// SendMessage sends a message to an agent
|
||||
func (c *Client) SendMessage(agentName, message string) error {
|
||||
path := fmt.Sprintf("/chat/%s", agentName)
|
||||
|
||||
msg := Message{
|
||||
Message: message,
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(http.MethodPost, path, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The response is HTML, so it's not easily parseable in this context
|
||||
return nil
|
||||
}
|
||||
|
||||
// Notify sends a notification to an agent
|
||||
func (c *Client) Notify(agentName, message string) error {
|
||||
path := fmt.Sprintf("/notify/%s", agentName)
|
||||
|
||||
// URL encoded form data
|
||||
form := strings.NewReader(fmt.Sprintf("message=%s", message))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, c.BaseURL+path, form)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
if c.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("api error (status %d)", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package localagi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client represents a client for the LocalAGI API
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new LocalAGI client
|
||||
func NewClient(baseURL string, apiKey string, timeout time.Duration) *Client {
|
||||
if timeout == 0 {
|
||||
timeout = time.Second * 30
|
||||
}
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimeout sets the HTTP client timeout
|
||||
func (c *Client) SetTimeout(timeout time.Duration) {
|
||||
c.HTTPClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// doRequest performs an HTTP request and returns the response
|
||||
func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonData, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling request body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonData)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s%s", c.BaseURL, path)
|
||||
req, err := http.NewRequest(method, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
if c.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
// Read the error response
|
||||
defer resp.Body.Close()
|
||||
errorData, _ := io.ReadAll(resp.Body)
|
||||
return resp, fmt.Errorf("api error (status %d): %s", resp.StatusCode, string(errorData))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package localagi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// UserLocation represents the user's location for web search
|
||||
type UserLocation struct {
|
||||
Type string `json:"type"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// Function tool fields (used when type == "function")
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Parameters *jsonschema.Definition `json:"parameters,omitempty"`
|
||||
|
||||
// Web search tool fields (used when type == "web_search_preview" etc.)
|
||||
SearchContextSize *string `json:"search_context_size,omitempty"`
|
||||
UserLocation *UserLocation `json:"user_location,omitempty"`
|
||||
}
|
||||
|
||||
type ToolChoice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// RequestBody represents the message request to the AI model
|
||||
type RequestBody struct {
|
||||
Model string `json:"model"`
|
||||
Input any `json:"input"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice *ToolChoice `json:"tool_choice"`
|
||||
MaxTokens *int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type InputFunctionToolCallOutput struct {
|
||||
CallID string `json:"call_id"`
|
||||
Output string `json:"output"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// InputMessage represents a user input message
|
||||
type InputMessage struct {
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
|
||||
// ContentItem represents an item in a content array
|
||||
type ContentItem struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL string `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// ResponseBody represents the response from the AI model
|
||||
type ResponseBody struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
Error any `json:"error,omitempty"`
|
||||
Output []ResponseBase `json:"output"`
|
||||
Tools []Tool `json:"tools"`
|
||||
}
|
||||
|
||||
type ResponseType string
|
||||
|
||||
const (
|
||||
ResponseTypeFunctionToolCall ResponseType = "function_call"
|
||||
ResponseTypeMessage ResponseType = "message"
|
||||
)
|
||||
|
||||
type ResponseBase json.RawMessage
|
||||
|
||||
func (r *ResponseBase) UnmarshalJSON(data []byte) error {
|
||||
return (*json.RawMessage)(r).UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
func (r *ResponseBase) ToMessage() (msg ResponseMessage, err error) {
|
||||
err = json.Unmarshal(*r, &msg)
|
||||
if msg.Type != string(ResponseTypeMessage) {
|
||||
return ResponseMessage{}, fmt.Errorf("Expected %s, not %s", ResponseTypeMessage, msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ResponseBase) ToFunctionToolCall() (msg ResponseFunctionToolCall, err error) {
|
||||
err = json.Unmarshal(*r, &msg)
|
||||
if msg.Type != string(ResponseTypeFunctionToolCall) {
|
||||
return ResponseFunctionToolCall{}, fmt.Errorf("Expected %s, not %s", ResponseTypeFunctionToolCall, msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ResponseFunctionToolCall struct {
|
||||
Arguments string `json:"arguments"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ResponseMessage represents a message in the response
|
||||
type ResponseMessage struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Role string `json:"role"`
|
||||
Content []MessageContentItem `json:"content"`
|
||||
}
|
||||
|
||||
// MessageContentItem represents a content item in a message
|
||||
type MessageContentItem struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// GetAIResponse sends a request to the AI model and returns the response
|
||||
func (c *Client) GetAIResponse(request *RequestBody) (*ResponseBody, error) {
|
||||
resp, err := c.doRequest(http.MethodPost, "/v1/responses", request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response ResponseBody
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
// Check if there was an error in the response
|
||||
if response.Error != nil {
|
||||
return nil, fmt.Errorf("api error: %v", response.Error)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// SimpleAIResponse is a helper function to get a simple text response from the AI
|
||||
func (c *Client) SimpleAIResponse(agentName, input string) (string, error) {
|
||||
temperature := 0.7
|
||||
request := &RequestBody{
|
||||
Model: agentName,
|
||||
Input: input,
|
||||
Temperature: &temperature,
|
||||
}
|
||||
|
||||
response, err := c.GetAIResponse(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, out := range response.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("out.ToMessage: %w", err)
|
||||
}
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
return content.Text, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no text response found")
|
||||
}
|
||||
|
||||
// ChatAIResponse sends chat messages to the AI model
|
||||
func (c *Client) ChatAIResponse(agentName string, messages []InputMessage) (string, error) {
|
||||
temperature := 0.7
|
||||
request := &RequestBody{
|
||||
Model: agentName,
|
||||
Input: messages,
|
||||
Temperature: &temperature,
|
||||
}
|
||||
|
||||
response, err := c.GetAIResponse(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, out := range response.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("out.ToMessage: %w", err)
|
||||
}
|
||||
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
return content.Text, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no text response found")
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
type FieldType string
|
||||
|
||||
const (
|
||||
FieldTypeNumber FieldType = "number"
|
||||
FieldTypeText FieldType = "text"
|
||||
FieldTypeTextarea FieldType = "textarea"
|
||||
FieldTypeCheckbox FieldType = "checkbox"
|
||||
FieldTypeSelect FieldType = "select"
|
||||
)
|
||||
|
||||
type Tags struct {
|
||||
Section string `json:"section,omitempty"`
|
||||
}
|
||||
|
||||
type FieldOption struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Name string `json:"name"`
|
||||
Type FieldType `json:"type"`
|
||||
Label string `json:"label"`
|
||||
DefaultValue any `json:"defaultValue"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
HelpText string `json:"helpText,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Options []FieldOption `json:"options,omitempty"`
|
||||
Min float32 `json:"min,omitempty"`
|
||||
Max float32 `json:"max,omitempty"`
|
||||
Step float32 `json:"step,omitempty"`
|
||||
Tags Tags `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type FieldGroup struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package deepface
|
||||
|
||||
// A simple Golang client for repository: https://github.com/serengil/deepface
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type DeepFaceClient struct {
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func NewClient(baseURL string) *DeepFaceClient {
|
||||
return &DeepFaceClient{BaseURL: baseURL}
|
||||
}
|
||||
|
||||
func encodeImageToBase64(imgPath string) (string, error) {
|
||||
file, err := os.Open(imgPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
func (c *DeepFaceClient) Represent(modelName, imgPath string) error {
|
||||
imgBase64, err := encodeImageToBase64(imgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"model_name": modelName,
|
||||
"img": imgBase64,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
resp, err := http.Post(c.BaseURL+"/represent", "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Println("Response:", string(body))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DeepFaceClient) Verify(img1Path, img2Path, modelName, detector, metric string) error {
|
||||
img1Base64, err := encodeImageToBase64(img1Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img2Base64, err := encodeImageToBase64(img2Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"img1": img1Base64,
|
||||
"img2": img2Base64,
|
||||
"model_name": modelName,
|
||||
"detector_backend": detector,
|
||||
"distance_metric": metric,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
resp, err := http.Post(c.BaseURL+"/verify", "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Println("Response:", string(body))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DeepFaceClient) Analyze(imgPath string, actions []string) error {
|
||||
imgBase64, err := encodeImageToBase64(imgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"img": imgBase64,
|
||||
"actions": actions,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
resp, err := http.Post(c.BaseURL+"/analyze", "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Println("Response:", string(body))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func NewClient(APIKey, URL, timeout string) *openai.Client {
|
||||
// Set up OpenAI client
|
||||
if APIKey == "" {
|
||||
//log.Fatal("OPENAI_API_KEY environment variable not set")
|
||||
APIKey = "sk-xxx"
|
||||
}
|
||||
config := openai.DefaultConfig(APIKey)
|
||||
config.BaseURL = URL
|
||||
|
||||
dur, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
dur = 150 * time.Second
|
||||
}
|
||||
|
||||
config.HTTPClient = &http.Client{
|
||||
Timeout: dur,
|
||||
}
|
||||
return openai.NewClientWithConfig(config)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func GenerateTypedJSONWithGuidance(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
|
||||
return GenerateTypedJSONWithConversation(ctx, client, []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: guidance,
|
||||
},
|
||||
}, model, i, dst)
|
||||
}
|
||||
|
||||
func GenerateTypedJSONWithConversation(ctx context.Context, client *openai.Client, conv []openai.ChatCompletionMessage, model string, i jsonschema.Definition, dst any) error {
|
||||
toolName := "json"
|
||||
decision := openai.ChatCompletionRequest{
|
||||
Model: model,
|
||||
Messages: conv,
|
||||
Tools: []openai.Tool{
|
||||
{
|
||||
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: &openai.FunctionDefinition{
|
||||
Name: toolName,
|
||||
Parameters: i,
|
||||
},
|
||||
},
|
||||
},
|
||||
ToolChoice: openai.ToolChoice{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: openai.ToolFunction{Name: toolName},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.CreateChatCompletion(ctx, decision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resp.Choices) != 1 {
|
||||
return fmt.Errorf("no choices: %d", len(resp.Choices))
|
||||
}
|
||||
|
||||
msg := resp.Choices[0].Message
|
||||
|
||||
if len(msg.ToolCalls) == 0 {
|
||||
return fmt.Errorf("no tool calls: %d", len(msg.ToolCalls))
|
||||
}
|
||||
|
||||
xlog.Debug("JSON generated", "Arguments", msg.ToolCalls[0].Function.Arguments)
|
||||
|
||||
return json.Unmarshal([]byte(msg.ToolCalls[0].Function.Arguments), dst)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package localoperator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string, timeout ...time.Duration) *Client {
|
||||
defaultTimeout := 30 * time.Second
|
||||
if len(timeout) > 0 {
|
||||
defaultTimeout = timeout[0]
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type AgentRequest struct {
|
||||
Goal string `json:"goal"`
|
||||
MaxAttempts int `json:"max_attempts,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
}
|
||||
|
||||
type DesktopAgentRequest struct {
|
||||
AgentRequest
|
||||
DesktopURL string `json:"desktop_url"`
|
||||
}
|
||||
|
||||
type DeepResearchRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
MaxCycles int `json:"max_cycles,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
MaxResults int `json:"max_results,omitempty"`
|
||||
}
|
||||
|
||||
// Response types
|
||||
type StateDescription struct {
|
||||
CurrentURL string `json:"current_url"`
|
||||
PageTitle string `json:"page_title"`
|
||||
PageContentDescription string `json:"page_content_description"`
|
||||
Screenshot string `json:"screenshot"`
|
||||
ScreenshotMimeType string `json:"screenshot_mime_type"`
|
||||
}
|
||||
|
||||
type StateHistory struct {
|
||||
States []StateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type DesktopStateDescription struct {
|
||||
ScreenContent string `json:"screen_content"`
|
||||
ScreenshotPath string `json:"screenshot_path"`
|
||||
}
|
||||
|
||||
type DesktopStateHistory struct {
|
||||
States []DesktopStateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ResearchResult struct {
|
||||
Topic string `json:"topic"`
|
||||
Summary string `json:"summary"`
|
||||
Sources []SearchResult `json:"sources"`
|
||||
KnowledgeGaps []string `json:"knowledge_gaps"`
|
||||
SearchQueries []string `json:"search_queries"`
|
||||
ResearchCycles int `json:"research_cycles"`
|
||||
CompletionTime time.Duration `json:"completion_time"`
|
||||
}
|
||||
|
||||
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
||||
return post[*StateHistory](c.httpClient, c.baseURL+"/api/browser/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDesktopAgent(req DesktopAgentRequest) (*DesktopStateHistory, error) {
|
||||
return post[*DesktopStateHistory](c.httpClient, c.baseURL+"/api/desktop/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDeepResearch(req DeepResearchRequest) (*ResearchResult, error) {
|
||||
return post[*ResearchResult](c.httpClient, c.baseURL+"/api/deep-research/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) Readyz() (string, error) {
|
||||
return c.get("/readyz")
|
||||
}
|
||||
|
||||
func (c *Client) Healthz() (string, error) {
|
||||
return c.get("/healthz")
|
||||
}
|
||||
|
||||
func (c *Client) get(path string) (string, error) {
|
||||
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return resp.Status, nil
|
||||
}
|
||||
|
||||
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
|
||||
var result T
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Sending request", "url", url, "body", string(jsonBody))
|
||||
|
||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Response", "status", resp.StatusCode)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return result, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return result, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// TODO: this is a duplicate of LocalRAG/pkg/client
|
||||
package localrag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
)
|
||||
|
||||
var _ agent.RAGDB = &WrappedClient{}
|
||||
|
||||
type WrappedClient struct {
|
||||
*Client
|
||||
collection string
|
||||
}
|
||||
|
||||
func NewWrappedClient(baseURL, apiKey, collection string) *WrappedClient {
|
||||
wc := &WrappedClient{
|
||||
Client: NewClient(baseURL, apiKey),
|
||||
collection: collection,
|
||||
}
|
||||
|
||||
wc.CreateCollection(collection)
|
||||
|
||||
return wc
|
||||
}
|
||||
|
||||
func (c *WrappedClient) Count() int {
|
||||
entries, err := c.ListEntries(c.collection)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(entries)
|
||||
}
|
||||
|
||||
func (c *WrappedClient) Reset() error {
|
||||
return c.Client.Reset(c.collection)
|
||||
}
|
||||
|
||||
func (c *WrappedClient) Search(s string, similarity int) ([]string, error) {
|
||||
results, err := c.Client.Search(c.collection, s, similarity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res []string
|
||||
for _, r := range results {
|
||||
res = append(res, fmt.Sprintf("%s (%+v)", r.Content, r.Metadata))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *WrappedClient) Store(s string) error {
|
||||
// the Client API of LocalRAG takes only files at the moment.
|
||||
// So we take the string that we want to store, write it to a file, and then store the file.
|
||||
t := time.Now()
|
||||
dateTime := t.Format("2006-01-02-15-04-05")
|
||||
hash := md5.Sum([]byte(s))
|
||||
fileName := fmt.Sprintf("%s-%s.%s", dateTime, hex.EncodeToString(hash[:]), "txt")
|
||||
|
||||
xlog.Debug("Storing string in LocalRAG", "collection", c.collection, "fileName", fileName)
|
||||
|
||||
tempdir, err := os.MkdirTemp("", "localrag")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
f := filepath.Join(tempdir, fileName)
|
||||
err = os.WriteFile(f, []byte(s), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.Remove(f)
|
||||
return c.Client.Store(c.collection, f)
|
||||
}
|
||||
|
||||
// Result represents a single result from a query.
|
||||
type Result struct {
|
||||
ID string
|
||||
Metadata map[string]string
|
||||
Embedding []float32
|
||||
Content string
|
||||
|
||||
// The cosine similarity between the query and the document.
|
||||
// The higher the value, the more similar the document is to the query.
|
||||
// The value is in the range [-1, 1].
|
||||
Similarity float32
|
||||
}
|
||||
|
||||
// Client is a client for the RAG API
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// NewClient creates a new RAG API client
|
||||
func NewClient(baseURL, apiKey string) *Client {
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Add a helper method to set the Authorization header
|
||||
func (c *Client) addAuthHeader(req *http.Request) {
|
||||
if c.APIKey == "" {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
}
|
||||
|
||||
// CreateCollection creates a new collection
|
||||
func (c *Client) CreateCollection(name string) error {
|
||||
url := fmt.Sprintf("%s/api/collections", c.BaseURL)
|
||||
|
||||
type request struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(request{Name: name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return errors.New("failed to create collection")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCollections lists all collections
|
||||
func (c *Client) ListCollections() ([]string, error) {
|
||||
url := fmt.Sprintf("%s/api/collections", c.BaseURL)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to list collections")
|
||||
}
|
||||
|
||||
var collections []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collections, nil
|
||||
}
|
||||
|
||||
// ListEntries lists all entries in a collection
|
||||
func (c *Client) ListEntries(collection string) ([]string, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/%s/entries", c.BaseURL, collection)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to list entries")
|
||||
}
|
||||
|
||||
var entries []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// DeleteEntry deletes an entry in a collection
|
||||
func (c *Client) DeleteEntry(collection, entry string) ([]string, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/%s/entry/delete", c.BaseURL, collection)
|
||||
|
||||
type request struct {
|
||||
Entry string `json:"entry"`
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(request{Entry: entry})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyResult := new(bytes.Buffer)
|
||||
bodyResult.ReadFrom(resp.Body)
|
||||
return nil, errors.New("failed to delete entry: " + bodyResult.String())
|
||||
}
|
||||
|
||||
var results []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Search searches a collection
|
||||
func (c *Client) Search(collection, query string, maxResults int) ([]Result, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/%s/search", c.BaseURL, collection)
|
||||
|
||||
type request struct {
|
||||
Query string `json:"query"`
|
||||
MaxResults int `json:"max_results"`
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(request{Query: query, MaxResults: maxResults})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to search collection")
|
||||
}
|
||||
|
||||
var results []Result
|
||||
err = json.NewDecoder(resp.Body).Decode(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Reset resets a collection
|
||||
func (c *Client) Reset(collection string) error {
|
||||
url := fmt.Sprintf("%s/api/collections/%s/reset", c.BaseURL, collection)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b := new(bytes.Buffer)
|
||||
b.ReadFrom(resp.Body)
|
||||
return errors.New("failed to reset collection: " + b.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store uploads a file to a collection
|
||||
func (c *Client) Store(collection, filePath string) error {
|
||||
url := fmt.Sprintf("%s/api/collections/%s/upload", c.BaseURL, collection)
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", file.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
c.addAuthHeader(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b := new(bytes.Buffer)
|
||||
b.ReadFrom(resp.Body)
|
||||
|
||||
type response struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
var r response
|
||||
err = json.Unmarshal(b.Bytes(), &r)
|
||||
if err == nil {
|
||||
return errors.New("failed to upload file: " + r.Error)
|
||||
}
|
||||
|
||||
return errors.New("failed to upload file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
func HTMLify(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.ReplaceAll(s, "\n", "<br>")
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ptr
|
||||
|
||||
func To[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package vectorstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/philippgille/chromem-go"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type ChromemDB struct {
|
||||
collectionName string
|
||||
collection *chromem.Collection
|
||||
index int
|
||||
client *openai.Client
|
||||
db *chromem.DB
|
||||
embeddingsModel string
|
||||
}
|
||||
|
||||
func NewChromemDB(collection, path string, openaiClient *openai.Client, embeddingsModel string) (*ChromemDB, error) {
|
||||
// db, err := chromem.NewPersistentDB(path, true)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
db := chromem.NewDB()
|
||||
|
||||
chromem := &ChromemDB{
|
||||
collectionName: collection,
|
||||
index: 1,
|
||||
db: db,
|
||||
client: openaiClient,
|
||||
embeddingsModel: embeddingsModel,
|
||||
}
|
||||
|
||||
c, err := db.GetOrCreateCollection(collection, nil, chromem.embedding())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chromem.collection = c
|
||||
|
||||
return chromem, nil
|
||||
}
|
||||
|
||||
func (c *ChromemDB) Count() int {
|
||||
return c.collection.Count()
|
||||
}
|
||||
|
||||
func (c *ChromemDB) Reset() error {
|
||||
if err := c.db.DeleteCollection(c.collectionName); err != nil {
|
||||
return err
|
||||
}
|
||||
collection, err := c.db.GetOrCreateCollection(c.collectionName, nil, c.embedding())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.collection = collection
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromemDB) embedding() chromem.EmbeddingFunc {
|
||||
return chromem.EmbeddingFunc(
|
||||
func(ctx context.Context, text string) ([]float32, error) {
|
||||
resp, err := c.client.CreateEmbeddings(ctx,
|
||||
openai.EmbeddingRequestStrings{
|
||||
Input: []string{text},
|
||||
Model: openai.EmbeddingModel(c.embeddingsModel),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return []float32{}, fmt.Errorf("error getting keys: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return []float32{}, fmt.Errorf("no response from OpenAI API")
|
||||
}
|
||||
|
||||
embedding := resp.Data[0].Embedding
|
||||
|
||||
return embedding, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ChromemDB) Store(s string) error {
|
||||
defer func() {
|
||||
c.index++
|
||||
}()
|
||||
if s == "" {
|
||||
return fmt.Errorf("empty string")
|
||||
}
|
||||
return c.collection.AddDocuments(context.Background(), []chromem.Document{
|
||||
{
|
||||
Content: s,
|
||||
ID: fmt.Sprint(c.index),
|
||||
},
|
||||
}, runtime.NumCPU())
|
||||
}
|
||||
|
||||
func (c *ChromemDB) Search(s string, similarEntries int) ([]string, error) {
|
||||
res, err := c.collection.Query(context.Background(), s, similarEntries, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []string
|
||||
for _, r := range res {
|
||||
results = append(results, r.Content)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package vectorstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type LocalAIRAGDB struct {
|
||||
client *StoreClient
|
||||
openaiClient *openai.Client
|
||||
}
|
||||
|
||||
func NewLocalAIRAGDB(storeClient *StoreClient, openaiClient *openai.Client) *LocalAIRAGDB {
|
||||
return &LocalAIRAGDB{
|
||||
client: storeClient,
|
||||
openaiClient: openaiClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *LocalAIRAGDB) Reset() error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (db *LocalAIRAGDB) Count() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (db *LocalAIRAGDB) Store(s string) error {
|
||||
resp, err := db.openaiClient.CreateEmbeddings(context.TODO(),
|
||||
openai.EmbeddingRequestStrings{
|
||||
Input: []string{s},
|
||||
Model: openai.AdaEmbeddingV2,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting keys: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return fmt.Errorf("no response from OpenAI API")
|
||||
}
|
||||
|
||||
embedding := resp.Data[0].Embedding
|
||||
|
||||
setReq := SetRequest{
|
||||
Keys: [][]float32{embedding},
|
||||
Values: []string{s},
|
||||
}
|
||||
err = db.client.Set(setReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting keys: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *LocalAIRAGDB) Search(s string, similarEntries int) ([]string, error) {
|
||||
resp, err := db.openaiClient.CreateEmbeddings(context.TODO(),
|
||||
openai.EmbeddingRequestStrings{
|
||||
Input: []string{s},
|
||||
Model: openai.AdaEmbeddingV2,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return []string{}, fmt.Errorf("error getting keys: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return []string{}, fmt.Errorf("no response from OpenAI API")
|
||||
}
|
||||
embedding := resp.Data[0].Embedding
|
||||
|
||||
// Find example
|
||||
findReq := FindRequest{
|
||||
TopK: similarEntries, // Number of similar entries you want to find
|
||||
Key: embedding, // The key you're looking for similarities to
|
||||
}
|
||||
findResp, err := db.client.Find(findReq)
|
||||
if err != nil {
|
||||
return []string{}, fmt.Errorf("error finding keys: %v", err)
|
||||
}
|
||||
|
||||
return findResp.Values, nil
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package vectorstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Define a struct to hold your store API client
|
||||
type StoreClient struct {
|
||||
BaseURL string
|
||||
APIToken string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Define request and response struct formats based on the API documentation
|
||||
type SetRequest struct {
|
||||
Keys [][]float32 `json:"keys"`
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
|
||||
type GetRequest struct {
|
||||
Keys [][]float32 `json:"keys"`
|
||||
}
|
||||
|
||||
type GetResponse struct {
|
||||
Keys [][]float32 `json:"keys"`
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
|
||||
type DeleteRequest struct {
|
||||
Keys [][]float32 `json:"keys"`
|
||||
}
|
||||
|
||||
type FindRequest struct {
|
||||
TopK int `json:"topk"`
|
||||
Key []float32 `json:"key"`
|
||||
}
|
||||
|
||||
type FindResponse struct {
|
||||
Keys [][]float32 `json:"keys"`
|
||||
Values []string `json:"values"`
|
||||
Similarities []float32 `json:"similarities"`
|
||||
}
|
||||
|
||||
// Constructor for StoreClient
|
||||
func NewStoreClient(baseUrl, apiToken string) *StoreClient {
|
||||
return &StoreClient{
|
||||
BaseURL: baseUrl,
|
||||
APIToken: apiToken,
|
||||
Client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Set method
|
||||
func (c *StoreClient) Set(req SetRequest) error {
|
||||
return c.doRequest("stores/set", req)
|
||||
}
|
||||
|
||||
// Implement Get method
|
||||
func (c *StoreClient) Get(req GetRequest) (*GetResponse, error) {
|
||||
body, err := c.doRequestWithResponse("stores/get", req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp GetResponse
|
||||
err = json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Implement Delete method
|
||||
func (c *StoreClient) Delete(req DeleteRequest) error {
|
||||
return c.doRequest("stores/delete", req)
|
||||
}
|
||||
|
||||
// Implement Find method
|
||||
func (c *StoreClient) Find(req FindRequest) (*FindResponse, error) {
|
||||
body, err := c.doRequestWithResponse("stores/find", req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp FindResponse
|
||||
err = json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Helper function to perform a request without expecting a response body
|
||||
func (c *StoreClient) doRequest(path string, data interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", c.BaseURL+"/"+path, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Set Bearer token
|
||||
if c.APIToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIToken)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("API request to %s failed with status code %d", path, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to perform a request and parse the response body
|
||||
func (c *StoreClient) doRequestWithResponse(path string, data interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", c.BaseURL+"/"+path, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
// Set Bearer token
|
||||
if c.APIToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIToken)
|
||||
}
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request to %s failed with status code %d", path, resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package xlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var logger *slog.Logger
|
||||
|
||||
func init() {
|
||||
var level = slog.LevelDebug
|
||||
|
||||
switch os.Getenv("LOG_LEVEL") {
|
||||
case "info":
|
||||
level = slog.LevelInfo
|
||||
case "warn":
|
||||
level = slog.LevelWarn
|
||||
case "error":
|
||||
level = slog.LevelError
|
||||
case "debug":
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
var opts = &slog.HandlerOptions{
|
||||
Level: level,
|
||||
}
|
||||
|
||||
var handler slog.Handler
|
||||
|
||||
if os.Getenv("LOG_FORMAT") == "json" {
|
||||
handler = slog.NewJSONHandler(os.Stdout, opts)
|
||||
} else {
|
||||
handler = slog.NewTextHandler(os.Stdout, opts)
|
||||
}
|
||||
logger = slog.New(handler)
|
||||
}
|
||||
|
||||
func _log(level slog.Level, msg string, args ...any) {
|
||||
_, f, l, _ := runtime.Caller(2)
|
||||
group := slog.Group(
|
||||
"source",
|
||||
slog.Attr{
|
||||
Key: "file",
|
||||
Value: slog.AnyValue(f),
|
||||
},
|
||||
slog.Attr{
|
||||
Key: "L",
|
||||
Value: slog.AnyValue(l),
|
||||
},
|
||||
)
|
||||
args = append(args, group)
|
||||
logger.Log(context.Background(), level, msg, args...)
|
||||
}
|
||||
|
||||
func Info(msg string, args ...any) {
|
||||
_log(slog.LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
func Debug(msg string, args ...any) {
|
||||
_log(slog.LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
func Error(msg string, args ...any) {
|
||||
_log(slog.LevelError, msg, args...)
|
||||
}
|
||||
|
||||
func Warn(msg string, args ...any) {
|
||||
_log(slog.LevelWarn, msg, args...)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package xstrings
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitTextByLength splits text into chunks of specified maxLength,
|
||||
// preserving complete words and special characters like newlines.
|
||||
// It returns a slice of strings, each with length <= maxLength.
|
||||
func SplitParagraph(text string, maxLength int) []string {
|
||||
// Handle edge cases
|
||||
if maxLength <= 0 || len(text) == 0 {
|
||||
return []string{text}
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
remainingText := text
|
||||
|
||||
for len(remainingText) > 0 {
|
||||
// If remaining text fits in a chunk, add it and we're done
|
||||
if len(remainingText) <= maxLength {
|
||||
chunks = append(chunks, remainingText)
|
||||
break
|
||||
}
|
||||
|
||||
// Try to find a good split point near the max length
|
||||
splitIndex := maxLength
|
||||
|
||||
// Look backward from the max length to find a space or newline
|
||||
for splitIndex > 0 && !isWhitespace(rune(remainingText[splitIndex])) {
|
||||
splitIndex--
|
||||
}
|
||||
|
||||
// If we couldn't find a good split point (no whitespace),
|
||||
// look forward for the next whitespace
|
||||
if splitIndex == 0 {
|
||||
splitIndex = maxLength
|
||||
// If we can't find whitespace forward, we'll have to split a word
|
||||
for splitIndex < len(remainingText) && !isWhitespace(rune(remainingText[splitIndex])) {
|
||||
splitIndex++
|
||||
}
|
||||
|
||||
// If we still couldn't find whitespace, take the whole string
|
||||
if splitIndex == len(remainingText) {
|
||||
chunks = append(chunks, remainingText)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add the chunk up to the split point
|
||||
chunk := remainingText[:splitIndex]
|
||||
|
||||
// Preserve trailing newlines with the current chunk
|
||||
if splitIndex < len(remainingText) && remainingText[splitIndex] == '\n' {
|
||||
chunk += string(remainingText[splitIndex])
|
||||
splitIndex++
|
||||
}
|
||||
|
||||
chunks = append(chunks, chunk)
|
||||
|
||||
// Remove leading whitespace from the next chunk
|
||||
remainingText = remainingText[splitIndex:]
|
||||
remainingText = strings.TrimLeftFunc(remainingText, isWhitespace)
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
// Helper function to determine if a character is whitespace
|
||||
func isWhitespace(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package xstrings_test
|
||||
|
||||
import (
|
||||
xtrings "github.com/mudler/LocalAGI/pkg/xstrings"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("SplitParagraph", func() {
|
||||
It("should return the text as a single chunk if it's shorter than maxLen", func() {
|
||||
text := "Short text"
|
||||
maxLen := 20
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"Short text"}))
|
||||
})
|
||||
|
||||
It("should split the text into chunks of maxLen without truncating words", func() {
|
||||
text := "This is a longer text that needs to be split into chunks."
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"This is a", "longer", "text that", "needs to", "be split", "into", "chunks."}))
|
||||
})
|
||||
|
||||
It("should handle texts with multiple spaces and newlines correctly", func() {
|
||||
text := "This is\na\ntext with\n\nmultiple spaces and\nnewlines."
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"This is\na\n", "text with\n", "multiple", "spaces ", "and\n", "newlines."}))
|
||||
})
|
||||
|
||||
It("should handle a text with a single word longer than maxLen", func() {
|
||||
text := "supercalifragilisticexpialidocious"
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"supercalifragilisticexpialidocious"}))
|
||||
})
|
||||
|
||||
It("should handle a text with empty lines", func() {
|
||||
text := "line1\n\nline2"
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"line1\n\n", "line2"}))
|
||||
})
|
||||
|
||||
It("should handle a text with leading and trailing spaces", func() {
|
||||
text := " leading spaces and trailing spaces "
|
||||
maxLen := 15
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{" leading", "spaces and", "trailing spaces"}))
|
||||
})
|
||||
|
||||
It("should handle a text with only spaces", func() {
|
||||
text := " "
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{" "}))
|
||||
})
|
||||
|
||||
It("should handle empty string", func() {
|
||||
text := ""
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{""}))
|
||||
})
|
||||
|
||||
It("should handle a text with only newlines", func() {
|
||||
text := "\n\n\n"
|
||||
maxLen := 10
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"\n\n\n"}))
|
||||
})
|
||||
|
||||
It("should handle a text with special characters", func() {
|
||||
text := "This is a text with special characters !@#$%^&*()"
|
||||
maxLen := 20
|
||||
result := xtrings.SplitParagraph(text, maxLen)
|
||||
Expect(result).To(Equal([]string{"This is a text with", "special characters", "!@#$%^&*()"}))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
package xstrings
|
||||
|
||||
type Comparable interface{ ~int | ~int64 | ~string }
|
||||
|
||||
func UniqueSlice[T Comparable](s []T) []T {
|
||||
keys := make(map[T]bool)
|
||||
list := []T{}
|
||||
for _, entry := range s {
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
list = append(list, entry)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package xstrings_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestXStrings(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "XStrings test suite")
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
langchain
|
||||
openai
|
||||
chromadb
|
||||
pysqlite3-binary
|
||||
requests
|
||||
ascii-magic
|
||||
loguru
|
||||
duckduckgo_search
|
||||
@@ -0,0 +1,530 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
)
|
||||
|
||||
const (
|
||||
// Actions
|
||||
ActionSearch = "search"
|
||||
ActionCustom = "custom"
|
||||
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||
ActionDeepResearchRunner = "deep-research-runner"
|
||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||
ActionGithubIssueOpener = "github-issue-opener"
|
||||
ActionGithubIssueEditor = "github-issue-editor"
|
||||
ActionGithubIssueCloser = "github-issue-closer"
|
||||
ActionGithubIssueSearcher = "github-issue-searcher"
|
||||
ActionGithubRepositoryGet = "github-repository-get-content"
|
||||
ActionGithubRepositoryCreateOrUpdate = "github-repository-create-or-update-content"
|
||||
ActionGithubIssueReader = "github-issue-reader"
|
||||
ActionGithubIssueCommenter = "github-issue-commenter"
|
||||
ActionGithubPRReader = "github-pr-reader"
|
||||
ActionGithubPRCommenter = "github-pr-commenter"
|
||||
ActionGithubPRReviewer = "github-pr-reviewer"
|
||||
ActionGithubPRCreator = "github-pr-creator"
|
||||
ActionGithubGetAllContent = "github-get-all-repository-content"
|
||||
ActionGithubREADME = "github-readme"
|
||||
ActionGithubRepositorySearchFiles = "github-repository-search-files"
|
||||
ActionGithubRepositoryListFiles = "github-repository-list-files"
|
||||
ActionScraper = "scraper"
|
||||
ActionWikipedia = "wikipedia"
|
||||
ActionBrowse = "browse"
|
||||
ActionTwitterPost = "twitter-post"
|
||||
ActionSendMail = "send-mail"
|
||||
ActionGenerateImage = "generate_image"
|
||||
ActionCounter = "counter"
|
||||
ActionCallAgents = "call_agents"
|
||||
ActionShellcommand = "shell-command"
|
||||
ActionSendTelegramMessage = "send-telegram-message"
|
||||
ActionSetReminder = "set_reminder"
|
||||
ActionListReminders = "list_reminders"
|
||||
ActionRemoveReminder = "remove_reminder"
|
||||
ActionAddToMemory = "add_to_memory"
|
||||
ActionListMemory = "list_memory"
|
||||
ActionRemoveFromMemory = "remove_from_memory"
|
||||
ActionPiKVMPowerControl = "pikvm_power_control"
|
||||
ActionWebhook = "webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
nameField = "name"
|
||||
descriptionField = "description"
|
||||
configurationField = "configuration"
|
||||
)
|
||||
|
||||
var AvailableActions = []string{
|
||||
ActionSearch,
|
||||
ActionCustom,
|
||||
ActionGithubIssueLabeler,
|
||||
ActionGithubIssueOpener,
|
||||
ActionGithubIssueEditor,
|
||||
ActionGithubIssueCloser,
|
||||
ActionGithubIssueSearcher,
|
||||
ActionGithubRepositoryGet,
|
||||
ActionGithubGetAllContent,
|
||||
ActionGithubRepositorySearchFiles,
|
||||
ActionGithubRepositoryListFiles,
|
||||
ActionBrowserAgentRunner,
|
||||
ActionDeepResearchRunner,
|
||||
ActionGithubRepositoryCreateOrUpdate,
|
||||
ActionGithubIssueReader,
|
||||
ActionGithubIssueCommenter,
|
||||
ActionGithubPRReader,
|
||||
ActionGithubPRCommenter,
|
||||
ActionGithubPRReviewer,
|
||||
ActionGithubPRCreator,
|
||||
ActionGithubREADME,
|
||||
ActionScraper,
|
||||
ActionBrowse,
|
||||
ActionWikipedia,
|
||||
ActionSendMail,
|
||||
ActionGenerateImage,
|
||||
ActionTwitterPost,
|
||||
ActionCounter,
|
||||
ActionCallAgents,
|
||||
ActionShellcommand,
|
||||
ActionSendTelegramMessage,
|
||||
ActionSetReminder,
|
||||
ActionListReminders,
|
||||
ActionRemoveReminder,
|
||||
ActionAddToMemory,
|
||||
ActionListMemory,
|
||||
ActionRemoveFromMemory,
|
||||
ActionPiKVMPowerControl,
|
||||
ActionWebhook,
|
||||
}
|
||||
|
||||
var DefaultActions = []config.FieldGroup{
|
||||
{
|
||||
Name: "search",
|
||||
Label: "Search",
|
||||
Fields: actions.SearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "browser-agent-runner",
|
||||
Label: "Browser Agent Runner",
|
||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "deep-research-runner",
|
||||
Label: "Deep Research Runner",
|
||||
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "add_to_memory",
|
||||
Label: "Add to Memory",
|
||||
Fields: actions.AddToMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "list_memory",
|
||||
Label: "List Memory",
|
||||
Fields: actions.ListMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "remove_from_memory",
|
||||
Label: "Remove from Memory",
|
||||
Fields: actions.RemoveFromMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
Fields: actions.GithubIssueLabelerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-opener",
|
||||
Label: "GitHub Issue Opener",
|
||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-editor",
|
||||
Label: "GitHub Issue Editor",
|
||||
Fields: actions.GithubIssueEditorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-closer",
|
||||
Label: "GitHub Issue Closer",
|
||||
Fields: actions.GithubIssueCloserConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-commenter",
|
||||
Label: "GitHub Issue Commenter",
|
||||
Fields: actions.GithubIssueCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-reader",
|
||||
Label: "GitHub Issue Reader",
|
||||
Fields: actions.GithubIssueReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-searcher",
|
||||
Label: "GitHub Issue Search",
|
||||
Fields: actions.GithubIssueSearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-get-content",
|
||||
Label: "GitHub Repository Get Content",
|
||||
Fields: actions.GithubRepositoryGetContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-get-all-repository-content",
|
||||
Label: "GitHub Get All Repository Content",
|
||||
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-search-files",
|
||||
Label: "GitHub Repository Search Files",
|
||||
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-list-files",
|
||||
Label: "GitHub Repository List Files",
|
||||
Fields: actions.GithubRepositoryListFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-create-or-update-content",
|
||||
Label: "GitHub Repository Create/Update Content",
|
||||
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-readme",
|
||||
Label: "GitHub Repository README",
|
||||
Fields: actions.GithubRepositoryREADMEConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reader",
|
||||
Label: "GitHub PR Reader",
|
||||
Fields: actions.GithubPRReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-commenter",
|
||||
Label: "GitHub PR Commenter",
|
||||
Fields: actions.GithubPRCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reviewer",
|
||||
Label: "GitHub PR Reviewer",
|
||||
Fields: actions.GithubPRReviewerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-creator",
|
||||
Label: "GitHub PR Creator",
|
||||
Fields: actions.GithubPRCreatorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "twitter-post",
|
||||
Label: "Twitter Post",
|
||||
Fields: actions.TwitterPostConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-mail",
|
||||
Label: "Send Mail",
|
||||
Fields: actions.SendMailConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "shell-command",
|
||||
Label: "Shell Command",
|
||||
Fields: actions.ShellConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "custom",
|
||||
Label: "Custom",
|
||||
Fields: action.CustomConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "scraper",
|
||||
Label: "Scraper",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "wikipedia",
|
||||
Label: "Wikipedia",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "browse",
|
||||
Label: "Browse",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "counter",
|
||||
Label: "Counter",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "call_agents",
|
||||
Label: "Call Agents",
|
||||
Fields: actions.CallAgentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-telegram-message",
|
||||
Label: "Send Telegram Message",
|
||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "set_reminder",
|
||||
Label: "Set Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "list_reminders",
|
||||
Label: "List Reminders",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "remove_reminder",
|
||||
Label: "Remove Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "pikvm_power_control",
|
||||
Label: "PiKVM Power Control",
|
||||
Fields: actions.PiKVMConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "webhook",
|
||||
Label: "Webhook",
|
||||
Fields: actions.WebhookConfigMeta(),
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
|
||||
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
|
||||
ActionConfigSSHBoxURL = "sshbox-url"
|
||||
ConfigStateDir = "state-dir"
|
||||
CustomActionsDir = "custom-actions-dir"
|
||||
)
|
||||
|
||||
func customActions(customActionsDir string, existingActionConfigs map[string]map[string]string) (allActions []types.Action) {
|
||||
files, err := os.ReadDir(customActionsDir)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom actions directory", "error", err)
|
||||
return allActions
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".go" {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(customActionsDir, file.Name()))
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
actionName := strings.TrimSuffix(file.Name(), ".go")
|
||||
|
||||
actionConfig := map[string]string{
|
||||
"name": actionName,
|
||||
"description": "",
|
||||
"code": string(content),
|
||||
"unsafe": "false",
|
||||
}
|
||||
|
||||
if c, exists := existingActionConfigs[actionName]; exists {
|
||||
// We allow the user to customize name and description
|
||||
actionConfig[descriptionField] = c[descriptionField]
|
||||
actionConfig[nameField] = c[nameField]
|
||||
actionConfig[configurationField] = c[configurationField]
|
||||
}
|
||||
a, err := Action(ActionCustom, "", actionConfig, nil, map[string]string{})
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom action", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
allActions := []types.Action{}
|
||||
|
||||
agentName := a.Name
|
||||
|
||||
existingActionConfigs := map[string]map[string]string{}
|
||||
for _, a := range a.Actions {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
||||
xlog.Error("Error unmarshalling action config", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingActionConfigs[a.Name] = config
|
||||
|
||||
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
|
||||
// Now we will scan a directory for custom actions
|
||||
if actionsConfigs[CustomActionsDir] != "" {
|
||||
allActions = append(allActions, customActions(actionsConfigs[CustomActionsDir], existingActionConfigs)...)
|
||||
}
|
||||
|
||||
return allActions
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
|
||||
var a types.Action
|
||||
var err error
|
||||
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
memoryFilePath := memoryPath(agentName, actionsConfigs)
|
||||
|
||||
switch name {
|
||||
case ActionCustom:
|
||||
a, err = action.NewCustom(config, "")
|
||||
case ActionGenerateImage:
|
||||
a = actions.NewGenImage(config)
|
||||
case ActionSearch:
|
||||
a = actions.NewSearch(config)
|
||||
case ActionGithubIssueLabeler:
|
||||
a = actions.NewGithubIssueLabeler(config)
|
||||
case ActionGithubIssueOpener:
|
||||
a = actions.NewGithubIssueOpener(config)
|
||||
case ActionGithubIssueEditor:
|
||||
a = actions.NewGithubIssueEditor(config)
|
||||
case ActionGithubIssueCloser:
|
||||
a = actions.NewGithubIssueCloser(config)
|
||||
case ActionGithubIssueSearcher:
|
||||
a = actions.NewGithubIssueSearch(config)
|
||||
case ActionBrowserAgentRunner:
|
||||
a = actions.NewBrowserAgentRunner(config, actionsConfigs[ActionConfigBrowserAgentRunner])
|
||||
case ActionDeepResearchRunner:
|
||||
a = actions.NewDeepResearchRunner(config, actionsConfigs[ActionConfigDeepResearchRunner])
|
||||
case ActionGithubIssueReader:
|
||||
a = actions.NewGithubIssueReader(config)
|
||||
case ActionGithubPRReader:
|
||||
a = actions.NewGithubPRReader(config)
|
||||
case ActionGithubPRCommenter:
|
||||
a = actions.NewGithubPRCommenter(config)
|
||||
case ActionGithubPRReviewer:
|
||||
a = actions.NewGithubPRReviewer(config)
|
||||
case ActionGithubPRCreator:
|
||||
a = actions.NewGithubPRCreator(config)
|
||||
case ActionGithubGetAllContent:
|
||||
a = actions.NewGithubRepositoryGetAllContent(config)
|
||||
case ActionGithubRepositorySearchFiles:
|
||||
a = actions.NewGithubRepositorySearchFiles(config)
|
||||
case ActionGithubRepositoryListFiles:
|
||||
a = actions.NewGithubRepositoryListFiles(config)
|
||||
case ActionGithubIssueCommenter:
|
||||
a = actions.NewGithubIssueCommenter(config)
|
||||
case ActionGithubRepositoryGet:
|
||||
a = actions.NewGithubRepositoryGetContent(config)
|
||||
case ActionGithubRepositoryCreateOrUpdate:
|
||||
a = actions.NewGithubRepositoryCreateOrUpdateContent(config)
|
||||
case ActionGithubREADME:
|
||||
a = actions.NewGithubRepositoryREADME(config)
|
||||
case ActionScraper:
|
||||
a = actions.NewScraper(config)
|
||||
case ActionWikipedia:
|
||||
a = actions.NewWikipedia(config)
|
||||
case ActionBrowse:
|
||||
a = actions.NewBrowse(config)
|
||||
case ActionSendMail:
|
||||
a = actions.NewSendMail(config)
|
||||
case ActionWebhook:
|
||||
a = actions.NewWebhook(config)
|
||||
case ActionTwitterPost:
|
||||
a = actions.NewPostTweet(config)
|
||||
case ActionCounter:
|
||||
a = actions.NewCounter(config)
|
||||
case ActionCallAgents:
|
||||
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
|
||||
case ActionShellcommand:
|
||||
a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL])
|
||||
case ActionSendTelegramMessage:
|
||||
a = actions.NewSendTelegramMessageRunner(config)
|
||||
case ActionSetReminder:
|
||||
a = action.NewReminder()
|
||||
case ActionListReminders:
|
||||
a = action.NewListReminders()
|
||||
case ActionRemoveReminder:
|
||||
a = action.NewRemoveReminder()
|
||||
case ActionAddToMemory:
|
||||
a, _, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionListMemory:
|
||||
_, a, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionRemoveFromMemory:
|
||||
_, _, a = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionPiKVMPowerControl:
|
||||
a = actions.NewPiKVMAction(config)
|
||||
default:
|
||||
xlog.Error("Action not found", "name", name)
|
||||
return nil, fmt.Errorf("Action not found")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func ActionsConfigMeta(customActionDir string) []config.FieldGroup {
|
||||
all := slices.Clone(DefaultActions)
|
||||
|
||||
if customActionDir != "" {
|
||||
actions := customActions(customActionDir, map[string]map[string]string{})
|
||||
|
||||
for _, a := range actions {
|
||||
all = append(all, config.FieldGroup{
|
||||
Name: a.Definition().Name.String(),
|
||||
Label: a.Definition().Name.String(),
|
||||
Fields: []config.Field{
|
||||
{
|
||||
Name: nameField,
|
||||
Label: "Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Name of the custom action",
|
||||
},
|
||||
{
|
||||
Name: descriptionField,
|
||||
Label: "Description",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Description of the custom action",
|
||||
},
|
||||
{
|
||||
Name: configurationField,
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration of the custom action",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestActions(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Agent actions test suite")
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
"jaytaylor.com/html2text"
|
||||
)
|
||||
|
||||
func NewBrowse(config map[string]string) *BrowseAction {
|
||||
|
||||
return &BrowseAction{}
|
||||
}
|
||||
|
||||
type BrowseAction struct{}
|
||||
|
||||
func (a *BrowseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
URL string `json:"url"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
// download page with http.Client
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", result.URL, nil)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
pagebyte, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
rendered, err := html2text.FromString(string(pagebyte), html2text.Options{PrettyTables: true})
|
||||
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
return types.ActionResult{Result: fmt.Sprintf("The webpage '%s' content is:\n%s", result.URL, rendered)}, nil
|
||||
}
|
||||
|
||||
func (a *BrowseAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "browse",
|
||||
Description: "Use this tool to visit an URL. It browse a website page and return the text content.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"url": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The website URL.",
|
||||
},
|
||||
},
|
||||
Required: []string{"url"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BrowseAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataBrowserAgentHistory = "browser_agent_history"
|
||||
)
|
||||
|
||||
type BrowserAgentRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewBrowserAgentRunner(config map[string]string, defaultURL string) *BrowserAgentRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
timeout := "15m"
|
||||
if config["timeout"] != "" {
|
||||
timeout = config["timeout"]
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
// If parsing fails, use default 15 minutes
|
||||
duration = 15 * time.Minute
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], duration)
|
||||
|
||||
return &BrowserAgentRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowserAgentRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.AgentRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.AgentRequest{
|
||||
Goal: result.Goal,
|
||||
MaxAttempts: result.MaxAttempts,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
}
|
||||
|
||||
stateHistory, err := b.client.RunBrowserAgent(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run browser agent: %w", err)
|
||||
}
|
||||
|
||||
// Format the state history into a readable string
|
||||
var historyStr string
|
||||
// for i, state := range stateHistory.States {
|
||||
// historyStr += fmt.Sprintf("State %d:\n", i+1)
|
||||
// historyStr += fmt.Sprintf(" URL: %s\n", state.CurrentURL)
|
||||
// historyStr += fmt.Sprintf(" Title: %s\n", state.PageTitle)
|
||||
// historyStr += fmt.Sprintf(" Description: %s\n\n", state.PageContentDescription)
|
||||
// }
|
||||
|
||||
historyStr += fmt.Sprintf(" URL: %s\n", stateHistory.States[len(stateHistory.States)-1].CurrentURL)
|
||||
historyStr += fmt.Sprintf(" Title: %s\n", stateHistory.States[len(stateHistory.States)-1].PageTitle)
|
||||
historyStr += fmt.Sprintf(" Description: %s\n\n", stateHistory.States[len(stateHistory.States)-1].PageContentDescription)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Browser agent completed successfully. History:\n%s", historyStr),
|
||||
Metadata: map[string]interface{}{MetadataBrowserAgentHistory: stateHistory},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BrowserAgentRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_browser_agent"
|
||||
if b.customActionName != "" {
|
||||
actionName = b.customActionName
|
||||
}
|
||||
description := "Run a browser agent to achieve a specific goal, for example: 'Go to https://www.google.com and search for 'LocalAI', and tell me what's on the first page'"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"goal": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The goal for the browser agent to achieve",
|
||||
},
|
||||
"max_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts the agent can make (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"goal"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BrowserAgentRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BrowserAgentRunnerConfigMeta returns the metadata for Browser Agent Runner action configuration fields
|
||||
func BrowserAgentRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "timeout",
|
||||
Label: "Client Timeout",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func trimList(list []string) []string {
|
||||
for i, v := range list {
|
||||
list[i] = strings.TrimSpace(v)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction {
|
||||
whitelist := []string{}
|
||||
blacklist := []string{}
|
||||
if v, ok := config["whitelist"]; ok {
|
||||
if strings.Contains(v, ",") {
|
||||
whitelist = trimList(strings.Split(v, ","))
|
||||
} else {
|
||||
whitelist = []string{v}
|
||||
}
|
||||
}
|
||||
if v, ok := config["blacklist"]; ok {
|
||||
if strings.Contains(v, ",") {
|
||||
blacklist = trimList(strings.Split(v, ","))
|
||||
} else {
|
||||
blacklist = []string{v}
|
||||
}
|
||||
}
|
||||
return &CallAgentAction{
|
||||
pool: pool,
|
||||
myName: agentName,
|
||||
whitelist: whitelist,
|
||||
blacklist: blacklist,
|
||||
}
|
||||
}
|
||||
|
||||
type CallAgentAction struct {
|
||||
pool *state.AgentPoolInternalAPI
|
||||
myName string
|
||||
whitelist []string
|
||||
blacklist []string
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
AgentName string `json:"agent_name"`
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
ag := a.pool.GetAgent(result.AgentName)
|
||||
if ag == nil {
|
||||
return types.ActionResult{}, fmt.Errorf("agent '%s' not found", result.AgentName)
|
||||
}
|
||||
|
||||
resp := ag.Ask(
|
||||
types.WithConversationHistory(
|
||||
[]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: result.Message,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
if resp.Error != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
metadata := make(map[string]interface{})
|
||||
|
||||
for _, s := range resp.State {
|
||||
for k, v := range s.Metadata {
|
||||
if existingValue, ok := metadata[k]; ok {
|
||||
switch existingValue := existingValue.(type) {
|
||||
case []string:
|
||||
switch v := v.(type) {
|
||||
case []string:
|
||||
metadata[k] = append(existingValue, v...)
|
||||
case string:
|
||||
metadata[k] = append(existingValue, v)
|
||||
}
|
||||
case string:
|
||||
switch v := v.(type) {
|
||||
case []string:
|
||||
metadata[k] = append([]string{existingValue}, v...)
|
||||
case string:
|
||||
metadata[k] = []string{existingValue, v}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
metadata[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) isAllowedToBeCalled(agentName string) bool {
|
||||
if agentName == a.myName {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a.whitelist) > 0 && len(a.blacklist) > 0 {
|
||||
return slices.Contains(a.whitelist, agentName) && !slices.Contains(a.blacklist, agentName)
|
||||
}
|
||||
|
||||
if len(a.whitelist) > 0 {
|
||||
return slices.Contains(a.whitelist, agentName)
|
||||
}
|
||||
|
||||
if len(a.blacklist) > 0 {
|
||||
return !slices.Contains(a.blacklist, agentName)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) Definition() types.ActionDefinition {
|
||||
allAgents := a.pool.AllAgents()
|
||||
|
||||
agents := []string{}
|
||||
|
||||
for _, ag := range allAgents {
|
||||
if a.isAllowedToBeCalled(ag) {
|
||||
agents = append(agents, ag)
|
||||
}
|
||||
}
|
||||
|
||||
description := "Use this tool to call another agent. Available agents and their roles are:"
|
||||
|
||||
for _, agent := range agents {
|
||||
agentConfig := a.pool.GetConfig(agent)
|
||||
if agentConfig == nil {
|
||||
continue
|
||||
}
|
||||
description += fmt.Sprintf("\n\t- %s: %s", agent, agentConfig.Description)
|
||||
}
|
||||
|
||||
return types.ActionDefinition{
|
||||
Name: "call_agent",
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"agent_name": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The name of the agent to call.",
|
||||
Enum: agents,
|
||||
},
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message to send to the agent.",
|
||||
},
|
||||
},
|
||||
Required: []string{"agent_name", "message"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func CallAgentConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "whitelist",
|
||||
Label: "Whitelist",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Comma-separated list of agent names to call. If not specified, all agents are allowed.",
|
||||
},
|
||||
{
|
||||
Name: "blacklist",
|
||||
Label: "Blacklist",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Comma-separated list of agent names to exclude from the call. If not specified, all agents are allowed.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// CounterAction manages named counters that can be created, updated, and queried
|
||||
type CounterAction struct {
|
||||
counters map[string]int
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCounter creates a new counter action
|
||||
func NewCounter(config map[string]string) *CounterAction {
|
||||
return &CounterAction{
|
||||
counters: make(map[string]int),
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the counter action
|
||||
func (a *CounterAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
// Parse parameters
|
||||
request := struct {
|
||||
Name string `json:"name"`
|
||||
Adjustment int `json:"adjustment"`
|
||||
}{}
|
||||
|
||||
if err := params.Unmarshal(&request); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
if request.Name == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("counter name cannot be empty")
|
||||
}
|
||||
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
// Get current value or initialize if it doesn't exist
|
||||
currentValue, exists := a.counters[request.Name]
|
||||
|
||||
// Update the counter
|
||||
newValue := currentValue + request.Adjustment
|
||||
a.counters[request.Name] = newValue
|
||||
|
||||
// Prepare the response message
|
||||
var message string
|
||||
if !exists && request.Adjustment == 0 {
|
||||
message = fmt.Sprintf("Created counter '%s' with initial value 0", request.Name)
|
||||
} else if !exists {
|
||||
message = fmt.Sprintf("Created counter '%s' with initial value %d", request.Name, newValue)
|
||||
} else if request.Adjustment > 0 {
|
||||
message = fmt.Sprintf("Increased counter '%s' by %d to %d", request.Name, request.Adjustment, newValue)
|
||||
} else if request.Adjustment < 0 {
|
||||
message = fmt.Sprintf("Decreased counter '%s' by %d to %d", request.Name, -request.Adjustment, newValue)
|
||||
} else {
|
||||
message = fmt.Sprintf("Current value of counter '%s' is %d", request.Name, newValue)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: message,
|
||||
Metadata: map[string]any{
|
||||
"counter_name": request.Name,
|
||||
"counter_value": newValue,
|
||||
"adjustment": request.Adjustment,
|
||||
"is_new": !exists,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Definition returns the action definition
|
||||
func (a *CounterAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "counter",
|
||||
Description: "Create, update, or query named counters. Specify a name and an adjustment value (positive to increase, negative to decrease, zero to query).",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"name": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The name of the counter to create, update, or query.",
|
||||
},
|
||||
"adjustment": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The value to adjust the counter by. Positive to increase, negative to decrease, zero to query the current value.",
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "adjustment"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CounterAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataDeepResearchResult = "deep_research_result"
|
||||
)
|
||||
|
||||
type DeepResearchRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
timeout := "15m"
|
||||
if config["timeout"] != "" {
|
||||
timeout = config["timeout"]
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
// If parsing fails, use default 15 minutes
|
||||
duration = 15 * time.Minute
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], duration)
|
||||
|
||||
return &DeepResearchRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.DeepResearchRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.DeepResearchRequest{
|
||||
Topic: result.Topic,
|
||||
MaxCycles: result.MaxCycles,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
MaxResults: result.MaxResults,
|
||||
}
|
||||
|
||||
researchResult, err := d.client.RunDeepResearch(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
|
||||
}
|
||||
|
||||
// Format the research result into a readable string
|
||||
var resultStr string
|
||||
|
||||
resultStr += "Deep research result\n"
|
||||
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
|
||||
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
|
||||
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
|
||||
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
|
||||
|
||||
if len(researchResult.Sources) > 0 {
|
||||
resultStr += "Sources:\n"
|
||||
for _, source := range researchResult.Sources {
|
||||
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
|
||||
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_deep_research"
|
||||
if d.customActionName != "" {
|
||||
actionName = d.customActionName
|
||||
}
|
||||
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"topic": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The topic to research",
|
||||
},
|
||||
"max_cycles": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of research cycles to perform (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
"max_results": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of results to collect (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"topic"},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
|
||||
func DeepResearchRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "timeout",
|
||||
Label: "Client Timeout",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataImages = "images_url"
|
||||
)
|
||||
|
||||
func NewGenImage(config map[string]string) *GenImageAction {
|
||||
defaultConfig := openai.DefaultConfig(config["apiKey"])
|
||||
defaultConfig.BaseURL = config["apiURL"]
|
||||
|
||||
return &GenImageAction{
|
||||
client: openai.NewClientWithConfig(defaultConfig),
|
||||
imageModel: config["model"],
|
||||
}
|
||||
}
|
||||
|
||||
type GenImageAction struct {
|
||||
client *openai.Client
|
||||
imageModel string
|
||||
}
|
||||
|
||||
func (a *GenImageAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Size string `json:"size"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if result.Prompt == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("prompt is required")
|
||||
}
|
||||
|
||||
req := openai.ImageRequest{
|
||||
Prompt: result.Prompt,
|
||||
Model: a.imageModel,
|
||||
}
|
||||
|
||||
switch result.Size {
|
||||
case "256x256":
|
||||
req.Size = openai.CreateImageSize256x256
|
||||
case "512x512":
|
||||
req.Size = openai.CreateImageSize512x512
|
||||
case "1024x1024":
|
||||
req.Size = openai.CreateImageSize1024x1024
|
||||
default:
|
||||
req.Size = openai.CreateImageSize256x256
|
||||
}
|
||||
|
||||
resp, err := a.client.CreateImage(ctx, req)
|
||||
if err != nil {
|
||||
return types.ActionResult{Result: "Failed to generate image " + err.Error()}, err
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return types.ActionResult{Result: "Failed to generate image"}, nil
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("The image was generated and available at: %s", resp.Data[0].URL),
|
||||
Metadata: map[string]interface{}{
|
||||
MetadataImages: []string{resp.Data[0].URL},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (a *GenImageAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "generate_image",
|
||||
Description: "Generate image with.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"prompt": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The image prompt to generate the image.",
|
||||
},
|
||||
"size": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The image prompt to generate the image.",
|
||||
Enum: []string{"256x256", "512x512", "1024x1024"},
|
||||
},
|
||||
},
|
||||
Required: []string{"prompt"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GenImageAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GenImageConfigMeta returns the metadata for GenImage action configuration fields
|
||||
func GenImageConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "apiKey",
|
||||
Label: "API Key",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "OpenAI API key for image generation",
|
||||
},
|
||||
{
|
||||
Name: "apiURL",
|
||||
Label: "API URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
DefaultValue: "https://api.openai.com/v1",
|
||||
HelpText: "OpenAI API URL",
|
||||
},
|
||||
{
|
||||
Name: "model",
|
||||
Label: "Model",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
DefaultValue: "dall-e-3",
|
||||
HelpText: "Image generation model to use (e.g., dall-e-3)",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
|
||||
. "github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GenImageAction", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
action *GenImageAction
|
||||
params types.ActionParams
|
||||
config map[string]string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
apiURL := os.Getenv("OPENAI_API_URL")
|
||||
testModel := os.Getenv("OPENAI_MODEL")
|
||||
if apiURL == "" {
|
||||
Skip("OPENAI_API_URL must be set")
|
||||
}
|
||||
config = map[string]string{
|
||||
"apiKey": apiKey,
|
||||
"apiURL": apiURL,
|
||||
"model": testModel,
|
||||
}
|
||||
action = NewGenImage(config)
|
||||
})
|
||||
|
||||
Describe("Run", func() {
|
||||
It("should generate an image with valid prompt and size", func() {
|
||||
params = types.ActionParams{
|
||||
"prompt": "test prompt",
|
||||
"size": "256x256",
|
||||
}
|
||||
|
||||
url, err := action.Run(ctx, nil, params)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(url).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("should return an error if the prompt is not provided", func() {
|
||||
params = types.ActionParams{
|
||||
"size": "256x256",
|
||||
}
|
||||
|
||||
_, err := action.Run(ctx, nil, params)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Definition", func() {
|
||||
It("should return the correct action definition", func() {
|
||||
definition := action.Definition()
|
||||
Expect(definition.Name.String()).To(Equal("generate_image"))
|
||||
Expect(definition.Description).To(Equal("Generate image with."))
|
||||
Expect(definition.Properties).To(HaveKey("prompt"))
|
||||
Expect(definition.Properties).To(HaveKey("size"))
|
||||
Expect(definition.Required).To(ContainElement("prompt"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,154 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubIssuesCloser struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubIssueCloser(config map[string]string) *GithubIssuesCloser {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
return &GithubIssuesCloser{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesCloser) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
IssueNumber int `json:"issue_number"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if g.repository != "" {
|
||||
result.Repository = g.repository
|
||||
}
|
||||
|
||||
if g.owner != "" {
|
||||
result.Owner = g.owner
|
||||
}
|
||||
// _, _, err = g.client.Issues.CreateComment(
|
||||
// g.context,
|
||||
// result.Owner, result.Repository,
|
||||
// result.IssueNumber, &github.IssueComment{
|
||||
// //Body: &result.Text,
|
||||
// },
|
||||
// )
|
||||
// if err != nil {
|
||||
// fmt.Printf("error: %v", err)
|
||||
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
_, _, err = g.client.Issues.Edit(ctx, result.Owner, result.Repository, result.IssueNumber, &github.IssueRequest{
|
||||
State: github.String("closed"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
resultString := fmt.Sprintf("Closed issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
resultString = fmt.Sprintf("Error closing issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
|
||||
}
|
||||
return types.ActionResult{Result: resultString}, err
|
||||
}
|
||||
|
||||
func (g *GithubIssuesCloser) Definition() types.ActionDefinition {
|
||||
actionName := "close_github_issue"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: "Closes a Github issue.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The issue number to close",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number"},
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: "Closes a Github issue.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to close the issue in.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The issue number to close",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "repository", "owner"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubIssuesCloser) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubIssueCloserConfigMeta returns the metadata for GitHub Issue Closer action configuration fields
|
||||
func GithubIssueCloserConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubIssuesCommenter struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubIssueCommenter(config map[string]string) *GithubIssuesCommenter {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubIssuesCommenter{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
customActionName: config["customActionName"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Comment string `json:"comment"`
|
||||
IssueNumber int `json:"issue_number"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
_, _, err = g.client.Issues.CreateComment(ctx, result.Owner, result.Repository, result.IssueNumber,
|
||||
&github.IssueComment{
|
||||
Body: &result.Comment,
|
||||
})
|
||||
resultString := fmt.Sprintf("Added comment to issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
resultString = fmt.Sprintf("Error adding comment to issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
|
||||
}
|
||||
return types.ActionResult{Result: resultString}, err
|
||||
}
|
||||
|
||||
func (g *GithubIssuesCommenter) Definition() types.ActionDefinition {
|
||||
actionName := "add_comment_to_github_issue"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Add a comment to a Github issue to a repository."
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to add the label to.",
|
||||
},
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The comment to add to the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "comment"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to add the label to.",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to add the label to.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The comment to add to the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "repository", "owner", "comment"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubIssuesCommenter) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubIssueCommenterConfigMeta returns the metadata for GitHub Issue Commenter action configuration fields
|
||||
func GithubIssueCommenterConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubIssueEditor struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubIssueEditor(config map[string]string) *GithubIssueEditor {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubIssueEditor{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
customActionName: config["customActionName"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssueEditor) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Description string `json:"description"`
|
||||
Title string `json:"title"`
|
||||
IssueNumber int `json:"issue_number"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
_, _, err = g.client.Issues.Edit(ctx, result.Owner, result.Repository, result.IssueNumber,
|
||||
&github.IssueRequest{
|
||||
Body: &result.Description,
|
||||
Title: &result.Title,
|
||||
})
|
||||
resultString := fmt.Sprintf("Updated issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
resultString = fmt.Sprintf("Error updating issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
|
||||
}
|
||||
return types.ActionResult{Result: resultString}, err
|
||||
}
|
||||
|
||||
func (g *GithubIssueEditor) Definition() types.ActionDefinition {
|
||||
actionName := "edit_github_issue"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Edit the title and description of a Github issue in a repository. Use this action after reading the issue"
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to edit.",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new title for the issue.",
|
||||
},
|
||||
"description": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new description for the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "title", "description"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to edit.",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository containing the issue.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new title for the issue.",
|
||||
},
|
||||
"description": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new description for the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "repository", "owner", "title", "description"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubIssueEditor) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubIssueEditorConfigMeta returns the metadata for GitHub Issue Editor action configuration fields
|
||||
func GithubIssueEditorConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubIssuesLabeler struct {
|
||||
token, repository, owner, customActionName string
|
||||
availableLabels []string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubIssueLabeler(config map[string]string) *GithubIssuesLabeler {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
// Get available labels
|
||||
availableLabels := []string{"bug", "enhancement"}
|
||||
|
||||
if config["availableLabels"] != "" {
|
||||
availableLabels = strings.Split(config["availableLabels"], ",")
|
||||
}
|
||||
|
||||
return &GithubIssuesLabeler{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
customActionName: config["customActionName"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
availableLabels: availableLabels,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesLabeler) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Label string `json:"label"`
|
||||
IssueNumber int `json:"issue_number"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
labels, _, err := g.client.Issues.AddLabelsToIssue(ctx, result.Owner, result.Repository, result.IssueNumber, []string{result.Label})
|
||||
//labelsNames := []string{}
|
||||
for _, l := range labels {
|
||||
xlog.Info("Label added", "label", l.Name)
|
||||
//labelsNames = append(labelsNames, l.GetName())
|
||||
}
|
||||
|
||||
resultString := fmt.Sprintf("Added label '%s' to issue %d in repository %s/%s", result.Label, result.IssueNumber, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
resultString = fmt.Sprintf("Error adding label '%s' to issue %d in repository %s/%s: %v", result.Label, result.IssueNumber, result.Owner, result.Repository, err)
|
||||
}
|
||||
return types.ActionResult{Result: resultString}, err
|
||||
}
|
||||
|
||||
func (g *GithubIssuesLabeler) Definition() types.ActionDefinition {
|
||||
actionName := "add_label_to_github_issue"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: "Add a label to a Github issue. You might want to assign labels to issues to categorize them.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to add the label to.",
|
||||
},
|
||||
"label": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The label to add to the issue.",
|
||||
Enum: g.availableLabels,
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "label"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: "Add a label to a Github issue. You might want to assign labels to issues to categorize them.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to add the label to.",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to add the label to.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"label": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The label to add to the issue.",
|
||||
Enum: g.availableLabels,
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "repository", "owner", "label"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubIssuesLabeler) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubIssueLabelerConfigMeta returns the metadata for GitHub Issue Labeler action configuration fields
|
||||
func GithubIssueLabelerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "availableLabels",
|
||||
Label: "Available Labels",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Comma-separated list of available labels",
|
||||
DefaultValue: "bug,enhancement",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubIssuesOpener struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubIssueOpener(config map[string]string) *GithubIssuesOpener {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubIssuesOpener{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesOpener) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"text"`
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
issue := &github.IssueRequest{
|
||||
Title: &result.Title,
|
||||
Body: &result.Body,
|
||||
}
|
||||
|
||||
resultString := ""
|
||||
createdIssue, _, err := g.client.Issues.Create(ctx, result.Owner, result.Repository, issue)
|
||||
if err != nil {
|
||||
resultString = fmt.Sprintf("Error creating issue: %v", err)
|
||||
} else {
|
||||
resultString = fmt.Sprintf("Created issue %d in repository %s/%s: %s", createdIssue.GetNumber(), result.Owner, result.Repository, createdIssue.GetURL())
|
||||
}
|
||||
|
||||
return types.ActionResult{Result: resultString}, err
|
||||
}
|
||||
|
||||
func (g *GithubIssuesOpener) Definition() types.ActionDefinition {
|
||||
actionName := "create_github_issue"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: "Create a new issue on a GitHub repository.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"text": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The text of the new issue",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The title of the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"title", "text"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: "Create a new issue on a GitHub repository.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"text": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The text of the new issue",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The title of the issue.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository where to create the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"title", "text", "owner", "repository"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubIssuesOpener) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubIssueOpenerConfigMeta returns the metadata for GitHub Issue Opener action configuration fields
|
||||
func GithubIssueOpenerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user