mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
626 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5277c43c49 | |||
| 4477f08e6d | |||
| 24e61ead65 | |||
| 5244a34a1d | |||
| bde12f4a09 | |||
| 1787a7304e | |||
| 33e141e626 | |||
| e1a1c9a00c | |||
| b4cdebec06 | |||
| 1cbb16cfc2 | |||
| 968b4d71bd | |||
| 78cd2e5b47 | |||
| 18e6b0b2d0 | |||
| 0cf94a2718 | |||
| f8d4b1ab0d | |||
| a17308108c | |||
| 10b971c612 | |||
| e63910263c | |||
| 96151d9147 | |||
| 657b177b4e | |||
| 564739d1db | |||
| 26d82841d5 | |||
| 446eaec1e6 | |||
| 73693ba416 | |||
| ae7f208d51 | |||
| a60a6a40d6 | |||
| 06aae67fed | |||
| 4a72e83938 | |||
| 780db1e906 | |||
| 21fe8dc1eb | |||
| 45d1b6969a | |||
| 73e180638e | |||
| 09e8b11451 | |||
| 4cc462cc18 | |||
| af5bdf67aa | |||
| c2868985e4 | |||
| 88ef414e3a | |||
| 04676d17a8 | |||
| 242c4a0df6 | |||
| 4a2a35b6cf | |||
| fda21d2ce3 | |||
| 2ad9c55010 | |||
| dd666cf0a6 | |||
| 4bf4119259 | |||
| 6150ab6a3e | |||
| 356caf0960 | |||
| 2be7e55303 | |||
| 26dcf4fd7c | |||
| d0967122f8 | |||
| c9970e6817 | |||
| 16400e40ca | |||
| 2d24521222 | |||
| 886f713e4c | |||
| a58f41a7b3 | |||
| ae2b857cb4 | |||
| 35b248ffd6 | |||
| 3dbd502510 | |||
| 27e13fafb5 | |||
| f7e0c6cd06 | |||
| 4e84aac577 | |||
| 224ddf3ac0 | |||
| 7b6b4e3a11 | |||
| 24c5bcb502 | |||
| 4292a4aa32 | |||
| 30de484d49 | |||
| 718730a2b9 | |||
| 4eeaac28eb | |||
| 8961ceabc6 | |||
| 730fcd7a39 | |||
| 72efa05c71 | |||
| 322b23550d | |||
| 1d92e9a27b | |||
| 6013faeec5 | |||
| bccb8297b8 | |||
| b2a99b509f | |||
| 7d8e10aa18 | |||
| 74e159add6 | |||
| 91c8f1cb3f | |||
| 7d2d2ab242 | |||
| 3cdff79173 | |||
| 3886c51217 | |||
| 2c3fd1322d | |||
| 99e4dd1730 | |||
| d9ba45dae8 | |||
| 04306b0219 | |||
| 7914258945 | |||
| fadcdc117e | |||
| ce7c7eaae4 | |||
| f19333aa6e | |||
| 1e50d58982 | |||
| 24750ba04f | |||
| cc4879a973 | |||
| 11d8253215 | |||
| 0bdbd8a90d | |||
| 8dbbbcf1ba | |||
| 30e10e7a4a | |||
| 95e4ca490f | |||
| 836864fa88 | |||
| 0d77f495e3 | |||
| 24cc8ceb96 | |||
| 1b810cfbf0 | |||
| cfef3ee5c4 | |||
| 435f47ca0e | |||
| 545327cc1e | |||
| c7dd443fa2 | |||
| c50319138e | |||
| d80f025e21 | |||
| 442ab7371c | |||
| 1c7cdbefdd | |||
| ea214d1c07 | |||
| 62e4fb47b2 | |||
| 5f2d6f58f2 | |||
| 0cda5f64af | |||
| eb1c3d8790 | |||
| fe43aeb9b6 | |||
| ccbbabac1c | |||
| eb7703a44e | |||
| 31c1a2d748 | |||
| 5989853f7a | |||
| fe8bb51f17 | |||
| 0c2ebfd6e1 | |||
| 086a2ed17f | |||
| fa067edbac | |||
| 11d48cc2f3 | |||
| c821d0967c | |||
| b47fdbb23b | |||
| b3930ad195 | |||
| cd58194d2a | |||
| 46bd83678a | |||
| 1597f0093e | |||
| c02b681728 | |||
| 319b9ac0f3 | |||
| 5a9b6b0c9f | |||
| a5a1545c59 | |||
| 03ecec1e76 | |||
| 8954855ba3 | |||
| 24b71927c3 | |||
| a5f61caae9 | |||
| 0d224de369 | |||
| 567e2c2b35 | |||
| a7e42ff0a3 | |||
| d1f62bb6be | |||
| 9c9aee29f0 | |||
| d8386366d9 | |||
| ca73ecb499 | |||
| 6f90239521 | |||
| 546be7ca8e | |||
| ce47461c5a | |||
| 44aea95dde | |||
| e32d95cb1a | |||
| 7d5856e36b | |||
| fd9e598136 | |||
| c0adb55bfa | |||
| e9a9186717 | |||
| 93d8ee1400 | |||
| 831e934dab | |||
| 5a0ef2b5de | |||
| 0fb3d63406 | |||
| 65558df1f7 | |||
| 5515c08624 | |||
| f407382a5a | |||
| 110e7f0c4c | |||
| a1a0767681 | |||
| e7b5ddfe06 | |||
| bf5a2afd83 | |||
| 88397e0469 | |||
| c45e2ae4d9 | |||
| 5543fe2c3e | |||
| bc84482c48 | |||
| 6972ea0089 | |||
| 43214a69a0 | |||
| 2318ceae35 | |||
| f4ce7e7862 | |||
| bd470e8076 | |||
| fde0392457 | |||
| 1e95fa435d | |||
| 8828d9836d | |||
| d357fcff36 | |||
| 9241f713b0 | |||
| 2472840a56 | |||
| ab3e18667b | |||
| 236b7bf6ea | |||
| dc73954cce | |||
| 9b48f63544 | |||
| e6ecbd5510 | |||
| 2268529b75 | |||
| a0dd5880d7 | |||
| b48f440476 | |||
| a0e8fcb19a | |||
| 99829f95fd | |||
| 0b6ab238f9 | |||
| 3eea6de546 | |||
| 27246aca7e | |||
| ba556ea4d8 | |||
| f40e91b41b | |||
| d1551c3097 | |||
| f6dcbeaa1d | |||
| 620bbb38cf | |||
| a2a86e84f0 | |||
| 0a2e90dece | |||
| 747ac23cd0 | |||
| 960f17a565 | |||
| fc5e075ea0 | |||
| 220dac926e | |||
| e84125de6f | |||
| b0c3bca915 | |||
| 39a851bd95 | |||
| f55f7be2e6 | |||
| 0669b27522 | |||
| 3193014ed8 | |||
| cfbd482498 | |||
| 2242702cf9 | |||
| 157278704e | |||
| ffa62b9928 | |||
| bc23c9429c | |||
| 8310cf6e23 | |||
| 464bf7ee23 | |||
| 1f2a845feb | |||
| 454e9613b0 | |||
| 900de8728c | |||
| 7bd5417b95 | |||
| 5a4913224a | |||
| dffbeec45a | |||
| 0e572c3e41 | |||
| d692a72bf3 | |||
| b5da25e9cc | |||
| 1210eea48d | |||
| cf3372a5fe | |||
| 49ff317b50 | |||
| 6340cd9aed | |||
| 3c85ece23e | |||
| 0356f6ec89 | |||
| 3988443705 | |||
| 7675b3ca34 | |||
| d61fdb8bba | |||
| 0a59d46ca3 | |||
| 76b81ca0ed | |||
| e6d91affc6 | |||
| e82543747c | |||
| 959d91c9d6 | |||
| 871eefe801 | |||
| 34286fabcc | |||
| b9bed7da8d | |||
| 7c3ea9fa08 | |||
| 492feecb67 | |||
| ca1d53c161 | |||
| fe3d4b129d | |||
| 2bf34093b0 | |||
| 9e85550b46 | |||
| 2826e39056 | |||
| 226fb43fbe | |||
| 9d5bc6b332 | |||
| af262c17f1 | |||
| 6b0408d47c | |||
| 74c9b58d1b | |||
| 1356da8795 | |||
| f7fd925f30 | |||
| 94831083c3 | |||
| 396b6a62a9 | |||
| caabfab7e2 | |||
| 51149780ac | |||
| 426f387123 | |||
| 9fd7068204 | |||
| 18348911c2 | |||
| 8472436fe8 | |||
| 16fc728696 | |||
| c8c1eed5be | |||
| 60a92a07c1 | |||
| a0fee17388 | |||
| b654b00aa1 | |||
| aed82704a9 | |||
| edeea95e7d | |||
| a663302ba2 | |||
| 1338248310 | |||
| d34a9db04f | |||
| 7710112ae2 | |||
| 6b007c163b | |||
| 1115b60a74 | |||
| 5881857d5c | |||
| 4b4bc15fdb | |||
| 45c6f639f4 | |||
| 90ca29414c | |||
| 8e4c4c9946 | |||
| 88e31f91d8 | |||
| 7e825b571f | |||
| 516c9bf1e0 | |||
| 71edee17be | |||
| de672029ef | |||
| f0559892ab | |||
| 2491a3524e | |||
| 0616fe3f8d | |||
| 474aafbc79 | |||
| 342302ceef | |||
| 8450b47609 | |||
| 34ecfe3c75 | |||
| f0cd5a37e8 | |||
| 98d019b825 | |||
| d2f7056c06 | |||
| 652507f430 | |||
| 033352e340 | |||
| 6b9316f722 | |||
| 53f95056de | |||
| dd32fa6f4a | |||
| d4ce4ea541 | |||
| 066680f8ef | |||
| d27895d018 | |||
| 8b9f13b1ed | |||
| 2326312d3a | |||
| e0c7492609 | |||
| 5d5106c510 | |||
| 70534f203e | |||
| 494a1b6872 | |||
| ded0a762aa | |||
| 32ba0e7f7e | |||
| 7b526b3620 | |||
| 224ee7d1f8 | |||
| 9fd95cc35f | |||
| 9cd4ad3298 | |||
| fb54c36c90 | |||
| 51b3ae56c7 | |||
| b8202e804f | |||
| 7f5d9c7d92 | |||
| cd723abfdd | |||
| cc584445c6 | |||
| 9846e24299 | |||
| 973a42fd78 | |||
| b2de79ae08 | |||
| 0b21d77880 | |||
| e256329e5e | |||
| 52b63908ca | |||
| e8ff184912 | |||
| 707ab7b3f8 | |||
| 78858c18b5 | |||
| 92562a5152 | |||
| c1aab59c2e | |||
| eb7b3484fd | |||
| 8ab9c6fb86 | |||
| 3c4b3eddc0 | |||
| 01c4c4bee4 | |||
| 3c82ba7f34 | |||
| 73fb4843a3 | |||
| 6f795f5e9c | |||
| 6b7c5b09af | |||
| 6e566dd21d | |||
| cc6626ff0d | |||
| fab2259d3a | |||
| 55760ec4dd | |||
| 6e3b554317 | |||
| e52bf5ad7b | |||
| e878b0c7ee | |||
| 2f9bf606c7 | |||
| 04066ca42b | |||
| a0c5c8efe9 | |||
| 6f346b3789 | |||
| e9121b78fe | |||
| d3770f1db6 | |||
| 99a8dc4ff9 | |||
| 4b4ce20f2d | |||
| d87a7ddd83 | |||
| 9c98b67002 | |||
| c0dfc851b9 | |||
| 5ce3af6f8f | |||
| 371f7a127d | |||
| e54fe4491c | |||
| 3313e473ea | |||
| 229fa988c5 | |||
| 5dda0ddef6 | |||
| 23a7057be2 | |||
| f40fe6ac28 | |||
| 8f097f8643 | |||
| 49f3665a99 | |||
| 0b5eea967f | |||
| c34517be2b | |||
| 228889c50c | |||
| d38113cead | |||
| 66cd9f9bd9 | |||
| 7d3cd06ef5 | |||
| d20f532289 | |||
| 7cc58b28a5 | |||
| 2855959c76 | |||
| fad51a6141 | |||
| 3da56d3b6f | |||
| d6128941b7 | |||
| a82e065f56 | |||
| da12d70bc2 | |||
| a202b2550f | |||
| adbcfefd8c | |||
| fe9815d57b | |||
| 89dc78953e | |||
| 9109344728 | |||
| 1328f55dcd | |||
| dcaf51bc9e | |||
| 12ddd973b5 | |||
| c834aa7275 | |||
| 8c2b4f5fde | |||
| 8ec24ab7ea | |||
| 5ec0e5b7ec | |||
| a0928ae590 | |||
| 7680ff7dec | |||
| 17ea9a7849 | |||
| e70767c226 | |||
| 0534c14279 | |||
| baa981cda7 | |||
| 0748f0b7ce | |||
| 5bb7bdb9f8 | |||
| 0fdc401f91 | |||
| 3adcb7ea56 | |||
| 398749fcd8 | |||
| 66031a67ba | |||
| 78c196d95f | |||
| 0c00e84e77 | |||
| 310cd7017d | |||
| 1c6f046c84 | |||
| 08a2dfb01a | |||
| d397a200bf | |||
| b672ac3632 | |||
| 5f29df088a | |||
| c3f95f049f | |||
| 324a00f477 | |||
| 1af287a24b | |||
| b6b7272cdc | |||
| fdbcdb84ee | |||
| 8fef7495a9 | |||
| 801417dcbf | |||
| 22a9a28e46 | |||
| 0a8a857d00 | |||
| d64383a520 | |||
| 5e87c79838 | |||
| ddbe4fc604 | |||
| 93b0156f34 | |||
| 3e2045626a | |||
| c992a9f464 | |||
| 16fe8303e7 | |||
| 4c1d278d14 | |||
| c37124d740 | |||
| fe736013cc | |||
| 6d715c2ac7 | |||
| 79237210f2 | |||
| 774529887a | |||
| e5db7b8855 | |||
| c15fa3e768 | |||
| a9f469f0e7 | |||
| fa40d379de | |||
| abe0b9fd79 | |||
| b609f0681b | |||
| 511afa12fe | |||
| d97602d071 | |||
| 6d43b0ae88 | |||
| dac89a9720 | |||
| 3e6508b7f1 | |||
| 26c9ce132b | |||
| 64281c4cc4 | |||
| 8796a690d5 | |||
| 1ae3f1bf6a | |||
| 4404b9af24 | |||
| 0e3c86ad59 | |||
| 15ab5e1617 | |||
| 838ca200a8 | |||
| a00f604995 | |||
| c6cc5f6b5a | |||
| a08c82d35e | |||
| ec073834ad | |||
| e9adc45276 | |||
| a73df88678 | |||
| 6e4089fb75 | |||
| 907da30320 | |||
| 7af57b3e1d | |||
| ddf27c0891 | |||
| dab86c4a77 | |||
| 87d56d961f | |||
| fd5618202e | |||
| 2a959a199b | |||
| da69bab11a | |||
| 93cb5e0617 | |||
| 2d8cb973a8 | |||
| 4882cb6a66 | |||
| dd1ac82d15 | |||
| 838f79879b | |||
| 6bd566d780 | |||
| 11ed011b11 | |||
| effac868c1 | |||
| f68ee45761 | |||
| 0a4f7de215 | |||
| 1be355436d | |||
| 96af82ad6d | |||
| 0e1ba56ef0 | |||
| f5d07ad1b3 | |||
| 3d7a86feb6 | |||
| 248c39697e | |||
| 2286e73912 | |||
| 197e7fb2c0 | |||
| a7b4a7ca2f | |||
| 2def49e794 | |||
| b1b96df8a8 | |||
| b443c0af29 | |||
| 98e7057d53 | |||
| f3fda42765 | |||
| a6f0c066b5 | |||
| 32a3a0259a | |||
| bbc51c858d | |||
| 0322693ec2 | |||
| 7c27e59918 | |||
| fa87352b45 | |||
| 7448d9147b | |||
| 33f67876d9 | |||
| e6633e6aae | |||
| dedd09fdf5 | |||
| 94120f2b5d | |||
| d993162801 | |||
| e249776e96 | |||
| f38bbf9261 | |||
| 256a04b307 | |||
| 2ff2cb5549 | |||
| 73ea0e5803 | |||
| 82c215ed5d | |||
| b1944a63ef | |||
| ed3bf194c7 | |||
| 145d9e8041 | |||
| 89e923679e | |||
| 658b860d04 | |||
| 2590570109 | |||
| 16bf9d3cd2 | |||
| a9102382f6 | |||
| 5db5acdaa3 | |||
| 4304aceff3 | |||
| 9ae903697d | |||
| 98919d15ae | |||
| 7c69672301 | |||
| 0403b460f1 | |||
| 895ed00d1a | |||
| 9bc1890d33 | |||
| 89abc5e253 | |||
| fcad0538fb | |||
| 15a4c0044b | |||
| 8b9fc3df28 | |||
| 9b5190796e | |||
| 1c5c84faea | |||
| 413f5b1c0b | |||
| 6b4a613578 | |||
| 0cbe95dd35 | |||
| ea10809a96 | |||
| 95c128c3d6 | |||
| 93656f3bb3 | |||
| 9ab15b3287 | |||
| 02a0367757 | |||
| 7cdfe63432 | |||
| a6aa2d88cc | |||
| 134cc2bc84 | |||
| 9e924ab4b1 | |||
| 4ee0ae9c4a | |||
| ecadff3442 | |||
| 983092c182 | |||
| 0758db202c | |||
| 5d75e23fa0 | |||
| 397f3fbc78 | |||
| 3cb1e05c8c | |||
| 486ec2d94a | |||
| 430527680f | |||
| eb4a261880 | |||
| 815b78595a | |||
| df14ce6c72 | |||
| 6a1197f5c0 | |||
| 847c2148a6 | |||
| 0e090773e3 | |||
| ad83a4d869 | |||
| dc1a1e6685 | |||
| 82f934d4cd | |||
| bc9c9198c4 | |||
| 8a64d7c912 | |||
| e445f92281 | |||
| 0c396d3bbd | |||
| 9f8b61666a | |||
| d9c1bee2aa | |||
| 335a91610e | |||
| 0f4a11685e | |||
| a023fcae64 | |||
| 8ce2ac7648 | |||
| dfa98b334e | |||
| 4aa24eb41d | |||
| fb1822123a | |||
| 642e1b3cd0 | |||
| a2f245e2fe | |||
| fabdddca21 | |||
| 78f8752246 | |||
| 0b5a531518 | |||
| b0a8af6c99 | |||
| 5c68d82995 | |||
| e5a28cba2f | |||
| cfefc47439 | |||
| 2ba6fb1e7b | |||
| 3b2ed14ae7 | |||
| 4fda533b91 | |||
| 40a3614e80 | |||
| 2d5c8f1060 | |||
| 5ff96111f0 | |||
| dc88dac296 | |||
| b84463f512 | |||
| 2de75d4782 | |||
| a722f5c0b1 | |||
| 0380070e98 | |||
| f99f4ebdaa | |||
| a94f3d81cb | |||
| fd277666f6 | |||
| 6ff1e52c83 | |||
| 60e043ed8b | |||
| 1eade7d188 | |||
| 3c18f22fb3 | |||
| dcff11cd2f | |||
| b6699fa047 | |||
| 34c76a52c8 | |||
| 1e733b0c71 | |||
| d5c28a47bc | |||
| 9e589731f0 | |||
| cdb83c72e2 | |||
| cac687508f | |||
| cf97b66ff9 | |||
| 346e992276 | |||
| f0ef8203cf | |||
| 38628c106f | |||
| 2a12619f3a | |||
| 3b73809be9 | |||
| 39add27f15 | |||
| f2680fd065 | |||
| 788ba686c9 | |||
| d7b8013472 | |||
| 7b63deba2d |
@@ -0,0 +1,5 @@
|
||||
frontend/node_modules
|
||||
config.toml
|
||||
.envrc
|
||||
.env
|
||||
.git
|
||||
@@ -0,0 +1 @@
|
||||
*.ipynb linguist-vendored
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Bug
|
||||
description: Report a problem with OpenDevin
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thank you for taking the time to fill out this bug report. We greatly appreciate your effort to complete this template fully. Please provide as much information as possible to help us understand and address the issue effectively.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for the same bug?
|
||||
description: Please check if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have checked the troubleshooting document at https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting
|
||||
required: true
|
||||
- label: I have checked the existing issues.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: Provide a short description of the problem.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-version
|
||||
attributes:
|
||||
label: Current Version
|
||||
description: What version are you using? If you're running in docker, tell us the tag you're using (e.g. ghcr.io/opendevin/opendevin:0.3.1).
|
||||
render: bash
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Installation and Configuration
|
||||
description: Please provide any commands you ran and any configuration (redacting API keys)
|
||||
render: bash
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: model-agent
|
||||
attributes:
|
||||
label: Model and Agent
|
||||
description: What model and agent are you using? You can see these settings in the UI by clicking the settings wheel.
|
||||
placeholder: |
|
||||
- Model:
|
||||
- Agent:
|
||||
|
||||
- type: textarea
|
||||
id: repro-steps
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: Please list the steps to reproduce the issue.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Logs, Errors, Screenshots, and Additional Context
|
||||
description: If you set DEBUG = 1 in config or env, LLM logs will be stored in the `logs/llm` folder. Please add any additional context about the problem here.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for OpenDevin features
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What problem or use case are you trying to solve?**
|
||||
|
||||
**Describe the UX of the solution you'd like**
|
||||
|
||||
**Do you have thoughts on the technical implementation?**
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
**Additional context**
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Question
|
||||
about: Use this template to ask a question regarding the project.
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe your question
|
||||
|
||||
<!--A clear and concise description of what you want to know.-->
|
||||
|
||||
## Additional context
|
||||
|
||||
<!--Add any other context about the question here, like what you've tried so far.-->
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Technical Proposal
|
||||
about: Propose a new architecture or technology
|
||||
title: ''
|
||||
labels: 'proposal'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Summary**
|
||||
|
||||
**Motivation**
|
||||
|
||||
**Technical Design**
|
||||
|
||||
**Alternatives to Consider**
|
||||
|
||||
**Additional context**
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Deploy Docs to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'OpenDevin/OpenDevin'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
cache-dependency-path: docs/package-lock.json
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Generate Python Docs
|
||||
run: rm -rf docs/modules/python && pip install pydoc-markdown && pydoc-markdown
|
||||
- name: Install dependencies
|
||||
run: cd docs && npm ci
|
||||
- name: Build website
|
||||
run: cd docs && npm run build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main' && github.repository == 'OpenDevin/OpenDevin'
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Run e2e test with dummy agent
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Set up environment
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry install --without evaluation
|
||||
poetry run playwright install --with-deps chromium
|
||||
wget https://huggingface.co/BAAI/bge-small-en-v1.5/raw/main/1_Pooling/config.json -P /tmp/llama_index/models--BAAI--bge-small-en-v1.5/snapshots/5c38ec7c405ec4b44b94cc5a9bb96e735b38267a/1_Pooling/
|
||||
- name: Run tests
|
||||
run: |
|
||||
poetry run python opendevin/core/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Publish Docker Image
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
ghcr_build_and_push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ["app", "evaluation", "sandbox"]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push ${{ matrix.image }}
|
||||
if: "!github.event.pull_request.head.repo.fork"
|
||||
run: |
|
||||
./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} --push
|
||||
|
||||
- name: Build ${{ matrix.image }}
|
||||
if: "github.event.pull_request.head.repo.fork"
|
||||
run: |
|
||||
./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }}
|
||||
|
||||
docker_build_success:
|
||||
name: Docker Build Success
|
||||
runs-on: ubuntu-latest
|
||||
needs: ghcr_build_and_push
|
||||
steps:
|
||||
- run: echo Done!
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Lint
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-frontend:
|
||||
name: Lint frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint
|
||||
|
||||
lint-python:
|
||||
name: Lint python
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit==3.7.0
|
||||
- name: Run pre-commit hooks
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: |
|
||||
git fetch https://github.com/OpenDevin/OpenDevin.git main:main && \
|
||||
pre-commit run \
|
||||
--files \
|
||||
$(git diff --name-only $(git merge-base main $(git branch --show-current)) $(git branch --show-current) | tr '\n' ' ') \
|
||||
--show-diff-on-failure \
|
||||
--config ./dev_config/python/.pre-commit-config.yaml
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Use OpenDevin to Review Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dogfood:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'review-this')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/opendevin/opendevin
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: install git, github cli
|
||||
run: |
|
||||
apt-get install -y git gh
|
||||
git config --global --add safe.directory $PWD
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }} # check out the target branch
|
||||
|
||||
- name: Download Diff
|
||||
run: |
|
||||
curl -O "${{ github.event.pull_request.diff_url }}" -L
|
||||
|
||||
- name: Write Task File
|
||||
run: |
|
||||
echo "Your coworker wants to apply a pull request to this project. Read and review ${{ github.event.pull_request.number }}.diff file. Create a review-${{ github.event.pull_request.number }}.txt and write your concise comments and suggestions there." > task.txt
|
||||
echo "" >> task.txt
|
||||
echo "Title" >> task.txt
|
||||
echo "${{ github.event.pull_request.title }}" >> task.txt
|
||||
echo "" >> task.txt
|
||||
echo "Description" >> task.txt
|
||||
echo "${{ github.event.pull_request.body }}" >> task.txt
|
||||
echo "" >> task.txt
|
||||
echo "Diff file is: ${{ github.event.pull_request.number }}.diff" >> task.txt
|
||||
|
||||
- name: Run OpenDevin
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SANDBOX_TYPE: exec
|
||||
run: |
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
rm task.txt
|
||||
|
||||
- name: Check if review file is non-empty
|
||||
id: check_file
|
||||
run: |
|
||||
ls -la
|
||||
if [[ -s review-${{ github.event.pull_request.number }}.txt ]]; then
|
||||
echo "non_empty=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Create PR review if file is non-empty
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
if: steps.check_file.outputs.non_empty == 'true'
|
||||
run: |
|
||||
gh pr review ${{ github.event.pull_request.number }} --comment --body-file "review-${{ github.event.pull_request.number }}.txt"
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Run Integration Tests
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
agent: ["SWEAgent", "PlannerAgent", "MonologueAgent", "CodeActAgent"]
|
||||
sandbox: ["ssh", "exec"]
|
||||
include:
|
||||
- agent: "MonologueAgent"
|
||||
embedding-model: "local"
|
||||
- agent: "MonologueAgent"
|
||||
# sufficient to have one agent testing against local sandbox
|
||||
sandbox: "local"
|
||||
embedding-model: "local"
|
||||
- agent: "SWEAgent"
|
||||
embedding-model: "none"
|
||||
- agent: "PlannerAgent"
|
||||
embedding-model: "none"
|
||||
- agent: "CodeActAgent"
|
||||
embedding-model: "none"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install
|
||||
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
|
||||
- name: Run Integration Tests
|
||||
env:
|
||||
SANDBOX_TYPE: ${{ matrix.sandbox }}
|
||||
AGENT: ${{ matrix.agent }}
|
||||
MAX_ITERATIONS: 10
|
||||
LLM_EMBEDDING_MODEL: ${{ matrix.embedding-model }}
|
||||
run: |
|
||||
rm -rf workspace
|
||||
mkdir workspace
|
||||
WORKSPACE_BASE="$GITHUB_WORKSPACE/workspace" \
|
||||
WORKSPACE_MOUNT_PATH="$GITHUB_WORKSPACE/workspace" \
|
||||
poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml \
|
||||
-s ./tests/integration
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
test_matrix_success:
|
||||
name: All Integration Tests Passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [integration-tests]
|
||||
steps:
|
||||
- run: echo Done!
|
||||
@@ -0,0 +1,127 @@
|
||||
name: Run Unit Tests
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-on-macos:
|
||||
name: Test on macOS
|
||||
runs-on: macos-13
|
||||
env:
|
||||
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install
|
||||
|
||||
- name: Install & Start Docker
|
||||
if: env.INSTALL_DOCKER == '1'
|
||||
run: |
|
||||
brew install colima docker
|
||||
colima start
|
||||
|
||||
# For testcontainers to find the Colima socket
|
||||
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
|
||||
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
|
||||
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
test-on-linux:
|
||||
name: Test on Linux
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation
|
||||
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
test-for-sandbox:
|
||||
name: Test for Sandbox
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install
|
||||
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
|
||||
- name: Run Integration Test for Sandbox
|
||||
run: |
|
||||
poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml -s ./tests/unit/test_sandbox.py
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
test_matrix_success:
|
||||
name: All Mac/Linux Tests Passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-on-macos, test-on-linux, test-for-sandbox]
|
||||
steps:
|
||||
- run: echo Done!
|
||||
@@ -0,0 +1,109 @@
|
||||
name: Use OpenDevin to Resolve GitHub Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
dogfood:
|
||||
if: github.event.label.name == 'solve-this'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/opendevin/opendevin
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: install git, github cli
|
||||
run: apt-get install -y git gh
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Write Task File
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
echo "TITLE:" > task.txt
|
||||
echo "${ISSUE_TITLE}" >> task.txt
|
||||
echo "" >> task.txt
|
||||
echo "BODY:" >> task.txt
|
||||
echo "${ISSUE_BODY}" >> task.txt
|
||||
|
||||
- name: Run OpenDevin
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SANDBOX_TYPE: exec
|
||||
run: |
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
rm task.txt
|
||||
|
||||
- name: Setup Git, Create Branch, and Commit Changes
|
||||
run: |
|
||||
# Setup Git configuration
|
||||
git config --global --add safe.directory $PWD
|
||||
git config --global user.name 'OpenDevin'
|
||||
git config --global user.email 'OpenDevin@users.noreply.github.com'
|
||||
|
||||
# Create a unique branch name with a timestamp
|
||||
BRANCH_NAME="fix/${{ github.event.issue.number }}-$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
# Checkout new branch
|
||||
git checkout -b $BRANCH_NAME
|
||||
|
||||
# Add all changes to staging, except task.txt
|
||||
git add --all -- ':!task.txt'
|
||||
|
||||
# Commit the changes, if any
|
||||
git commit -m "OpenDevin: Resolve Issue #${{ github.event.issue.number }}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "No changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Push changes
|
||||
git push --set-upstream origin $BRANCH_NAME
|
||||
|
||||
- name: Fetch Default Branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fetch the default branch using gh cli
|
||||
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
|
||||
echo "Default branch is $DEFAULT_BRANCH"
|
||||
echo "DEFAULT_BRANCH=$DEFAULT_BRANCH" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Create PR and capture URL
|
||||
PR_URL=$(gh pr create \
|
||||
--title "OpenDevin: Resolve Issue #2" \
|
||||
--body "This PR was generated by OpenDevin to resolve issue #2" \
|
||||
--repo "foragerr/OpenDevin" \
|
||||
--head "${{ github.head_ref }}" \
|
||||
--base "${{ env.DEFAULT_BRANCH }}" \
|
||||
| grep -o 'https://github.com/[^ ]*')
|
||||
|
||||
# Extract PR number from URL
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -o '[0-9]\+$')
|
||||
|
||||
# Set environment vars
|
||||
echo "PR_URL=$PR_URL" >> $GITHUB_ENV
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
|
||||
- name: Post Comment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh issue comment ${{ github.event.issue.number }} \
|
||||
-b "OpenDevin raised [PR #${{ env.PR_NUMBER }}](${{ env.PR_URL }}) to resolve this issue."
|
||||
@@ -0,0 +1,29 @@
|
||||
name: 'Close stale issues'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
# Aggressively close issues that have been explicitly labeled `age-out`
|
||||
any-of-labels: age-out
|
||||
stale-issue-message: 'This issue is stale because it has been open for 7 days with no activity. Remove stale label or comment or this will be closed in 1 day.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for over 7 days with no activity.'
|
||||
stale-pr-message: 'This PR is stale because it has been open for 7 days with no activity. Remove stale label or comment or this will be closed in 1 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for over 7 days with no activity.'
|
||||
days-before-stale: 7
|
||||
days-before-close: 1
|
||||
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
# Be more lenient with other issues
|
||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
|
||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
./lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
requirements.txt
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
*venv/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# evaluation
|
||||
evaluation/SWE-bench/data
|
||||
|
||||
# frontend
|
||||
|
||||
# dependencies
|
||||
frontend/node_modules
|
||||
frontend/.pnp
|
||||
frontend/bun.lockb
|
||||
frontend/yarn.lock
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
frontend/coverage
|
||||
|
||||
# production
|
||||
frontend/build
|
||||
frontend/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
logs
|
||||
|
||||
# agent
|
||||
.envrc
|
||||
/workspace
|
||||
/_test_workspace
|
||||
/debug
|
||||
cache
|
||||
|
||||
# configuration
|
||||
config.toml
|
||||
|
||||
test_results*
|
||||
@@ -0,0 +1,133 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
contact@rbren.io
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -0,0 +1,90 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to OpenDevin! We welcome and appreciate contributions.
|
||||
To report bugs, create a [GitHub issue](https://github.com/OpenDevin/OpenDevin/issues/new/choose).
|
||||
|
||||
## Contribution Guide
|
||||
### 1. Fork the Official Repository
|
||||
|
||||
Fork [OpenDevin repository](https://github.com/OpenDevin/OpenDevin) into your own account.
|
||||
Clone your own forked repository into your local environment.
|
||||
|
||||
```shell
|
||||
git clone git@github.com:<YOUR-USERNAME>/OpenDevin.git
|
||||
```
|
||||
|
||||
### 2. Configure Git
|
||||
|
||||
Set the official repository as your [upstream](https://www.atlassian.com/git/tutorials/git-forks-and-upstreams) to synchronize with the latest update in the official repository.
|
||||
Add the original repository as upstream
|
||||
|
||||
```shell
|
||||
cd OpenDevin
|
||||
git remote add upstream git@github.com:OpenDevin/OpenDevin.git
|
||||
```
|
||||
|
||||
Verify that the remote is set.
|
||||
```shell
|
||||
git remote -v
|
||||
```
|
||||
You should see both `origin` and `upstream` in the output.
|
||||
|
||||
### 3. Synchronize with Official Repository
|
||||
Synchronize latest commit with official repository before coding.
|
||||
|
||||
```shell
|
||||
git fetch upstream
|
||||
git checkout main
|
||||
git merge upstream/main
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 4. Create a New Branch And Open a Pull Request
|
||||
After you finish implementation, open forked repository. The source branch is your new branch, and the target branch is `OpenDevin/OpenDevin` `main` branch. Then PR should appears in [OpenDevin PRs](https://github.com/OpenDevin/OpenDevin/pulls).
|
||||
|
||||
Then OpenDevin team will review your code.
|
||||
|
||||
## PR Rules
|
||||
|
||||
### 1. Pull Request title
|
||||
|
||||
As described in [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes:
|
||||
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bug fix
|
||||
- `doc`: Documentation only changes
|
||||
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
||||
- `style`: A refactoring that improves code style
|
||||
- `perf`: A code change that improves performance
|
||||
- `test`: Adding missing tests or correcting existing tests
|
||||
- `ci`: Changes to CI configuration files and scripts (example scopes: `.github`, `ci` (Buildkite))
|
||||
- `chore`: Other changes that don't modify src or test files
|
||||
- `revert`: Reverts a previous commit
|
||||
|
||||
For example, a PR title could be:
|
||||
- `refactor: modify package path`
|
||||
- `feat(frontend): xxxx`, where `(frontend)` means that this PR mainly focuses on the frontend component.
|
||||
|
||||
You may also check out previous PRs in the [PR list](https://github.com/OpenDevin/OpenDevin/pulls).
|
||||
|
||||
As described in [here](https://github.com/OpenDevin/OpenDevin/labels), we create several labels. Every PR should be tagged with the corresponding labels.
|
||||
|
||||
### 2. Pull Request description
|
||||
|
||||
- If your PR is small (such as a typo fix), you can go brief.
|
||||
- If it is large and you have changed a lot, it's better to write more details.
|
||||
|
||||
|
||||
## How to begin
|
||||
Please refer to the README in each module:
|
||||
- [frontend](./frontend/README.md)
|
||||
- [agenthub](./agenthub/README.md)
|
||||
- [evaluation](./evaluation/README.md)
|
||||
- [opendevin](./opendevin/README.md)
|
||||
- [server](./opendevin/server/README.md)
|
||||
- [mock server](./opendevin/mock/README.md)
|
||||
|
||||
## Tests
|
||||
Please navigate to `tests` folder to see existing test suites.
|
||||
At the moment, we have two kinds of tests: `unit` and `integration`. Please refer to the README for each test suite. These tests also run on CI to ensure quality of
|
||||
the project.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Development Guide
|
||||
This guide is for people working on OpenDevin and editing the source code.
|
||||
|
||||
## Start the server for development
|
||||
|
||||
### 1. Requirements
|
||||
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install)
|
||||
* [Docker](https://docs.docker.com/engine/install/)(For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
|
||||
* [Python](https://www.python.org/downloads/) >= 3.11
|
||||
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
|
||||
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
|
||||
|
||||
Make sure you have all these dependencies installed before moving on to `make build`.
|
||||
|
||||
#### Develop without sudo access
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use `conda` or `mamba` to manage the packages for you:
|
||||
|
||||
```bash
|
||||
# Download and install Mamba (a faster version of conda)
|
||||
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
|
||||
bash Miniforge3-$(uname)-$(uname -m).sh
|
||||
|
||||
# Install Python 3.11, nodejs, and poetry
|
||||
mamba install python=3.11
|
||||
mamba install conda-forge::nodejs
|
||||
mamba install conda-forge::poetry
|
||||
```
|
||||
|
||||
### 2. Build and Setup The Environment
|
||||
|
||||
- **Build the Project:** Begin by building the project, which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run smoothly on your system.
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### 3. Configuring the Language Model
|
||||
|
||||
OpenDevin supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest.
|
||||
|
||||
To configure the LM of your choice, follow these steps:
|
||||
|
||||
1. **Using the Makefile: The Effortless Approach**
|
||||
With a single command, you can have a smooth LM setup for your OpenDevin experience. Simply run:
|
||||
```bash
|
||||
make setup-config
|
||||
```
|
||||
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenDevin is tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI, please set the model in the UI.
|
||||
|
||||
**Note on Alternative Models:**
|
||||
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest. And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
|
||||
|
||||
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
|
||||
|
||||
### 4. Run the Application
|
||||
|
||||
- **Run the Application:** Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin without any hassle.
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### 5. Individual Server Startup
|
||||
|
||||
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
|
||||
```bash
|
||||
make start-backend
|
||||
```
|
||||
|
||||
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related components or interface enhancements.
|
||||
```bash
|
||||
make start-frontend
|
||||
```
|
||||
|
||||
### 6. LLM Debugging
|
||||
|
||||
If you encounter any issues with the Language Model (LM) or you're simply curious, you can inspect the actual LLM prompts and responses. To do so, export DEBUG=1 in the environment and restart the backend. OpenDevin will then log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
|
||||
|
||||
### 7. Help
|
||||
|
||||
- **Get Some Help:** Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
|
||||
```bash
|
||||
make help
|
||||
```
|
||||
|
||||
### 8. Testing
|
||||
|
||||
#### Unit tests
|
||||
|
||||
```bash
|
||||
poetry run pytest ./tests/unit/test_sandbox.py
|
||||
```
|
||||
|
||||
#### Integration tests
|
||||
|
||||
Please refer to [this README](./tests/integration/README.md) for details.
|
||||
@@ -0,0 +1,25 @@
|
||||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright © 2023
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the “Software”), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,288 @@
|
||||
SHELL=/bin/bash
|
||||
# Makefile for OpenDevin project
|
||||
|
||||
# Variables
|
||||
DOCKER_IMAGE = ghcr.io/opendevin/sandbox
|
||||
BACKEND_PORT = 3000
|
||||
BACKEND_HOST = "127.0.0.1:$(BACKEND_PORT)"
|
||||
FRONTEND_PORT = 3001
|
||||
DEFAULT_WORKSPACE_DIR = "./workspace"
|
||||
DEFAULT_MODEL = "gpt-3.5-turbo"
|
||||
CONFIG_FILE = config.toml
|
||||
PRECOMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
|
||||
|
||||
# ANSI color codes
|
||||
GREEN=$(shell tput -Txterm setaf 2)
|
||||
YELLOW=$(shell tput -Txterm setaf 3)
|
||||
RED=$(shell tput -Txterm setaf 1)
|
||||
BLUE=$(shell tput -Txterm setaf 6)
|
||||
RESET=$(shell tput -Txterm sgr0)
|
||||
|
||||
# Build
|
||||
build:
|
||||
@echo "$(GREEN)Building project...$(RESET)"
|
||||
@$(MAKE) -s check-dependencies
|
||||
ifeq ($(INSTALL_DOCKER),)
|
||||
@$(MAKE) -s pull-docker-image
|
||||
endif
|
||||
@$(MAKE) -s install-python-dependencies
|
||||
@$(MAKE) -s install-frontend-dependencies
|
||||
@$(MAKE) -s install-precommit-hooks
|
||||
@$(MAKE) -s build-frontend
|
||||
@echo "$(GREEN)Build completed successfully.$(RESET)"
|
||||
|
||||
check-dependencies:
|
||||
@echo "$(YELLOW)Checking dependencies...$(RESET)"
|
||||
@$(MAKE) -s check-system
|
||||
@$(MAKE) -s check-python
|
||||
@$(MAKE) -s check-npm
|
||||
@$(MAKE) -s check-nodejs
|
||||
ifeq ($(INSTALL_DOCKER),)
|
||||
@$(MAKE) -s check-docker
|
||||
endif
|
||||
@$(MAKE) -s check-poetry
|
||||
@echo "$(GREEN)Dependencies checked successfully.$(RESET)"
|
||||
|
||||
check-system:
|
||||
@echo "$(YELLOW)Checking system...$(RESET)"
|
||||
@if [ "$(shell uname)" = "Darwin" ]; then \
|
||||
echo "$(BLUE)macOS detected.$(RESET)"; \
|
||||
elif [ "$(shell uname)" = "Linux" ]; then \
|
||||
if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Manjaro Linux detected.$(RESET)"; \
|
||||
else \
|
||||
echo "$(BLUE)Linux detected.$(RESET)"; \
|
||||
fi; \
|
||||
elif [ "$$(uname -r | grep -i microsoft)" ]; then \
|
||||
echo "$(BLUE)Windows Subsystem for Linux detected.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Unsupported system detected. Please use macOS, Linux, or Windows Subsystem for Linux (WSL).$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-python:
|
||||
@echo "$(YELLOW)Checking Python installation...$(RESET)"
|
||||
@if command -v python3.11 > /dev/null; then \
|
||||
echo "$(BLUE)$(shell python3.11 --version) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Python 3.11 is not installed. Please install Python 3.11 to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-npm:
|
||||
@echo "$(YELLOW)Checking npm installation...$(RESET)"
|
||||
@if command -v npm > /dev/null; then \
|
||||
echo "$(BLUE)npm $(shell npm --version) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)npm is not installed. Please install Node.js to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-nodejs:
|
||||
@echo "$(YELLOW)Checking Node.js installation...$(RESET)"
|
||||
@if command -v node > /dev/null; then \
|
||||
NODE_VERSION=$(shell node --version | sed -E 's/v//g'); \
|
||||
IFS='.' read -r -a NODE_VERSION_ARRAY <<< "$$NODE_VERSION"; \
|
||||
if [ "$${NODE_VERSION_ARRAY[0]}" -gt 18 ] || ([ "$${NODE_VERSION_ARRAY[0]}" -eq 18 ] && [ "$${NODE_VERSION_ARRAY[1]}" -gt 17 ]) || ([ "$${NODE_VERSION_ARRAY[0]}" -eq 18 ] && [ "$${NODE_VERSION_ARRAY[1]}" -eq 17 ] && [ "$${NODE_VERSION_ARRAY[2]}" -ge 1 ]); then \
|
||||
echo "$(BLUE)Node.js $$NODE_VERSION is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Node.js 18.17.1 or later is required. Please install Node.js 18.17.1 or later to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
echo "$(RED)Node.js is not installed. Please install Node.js to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-docker:
|
||||
@echo "$(YELLOW)Checking Docker installation...$(RESET)"
|
||||
@if command -v docker > /dev/null; then \
|
||||
echo "$(BLUE)$(shell docker --version) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Docker is not installed. Please install Docker to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-poetry:
|
||||
@echo "$(YELLOW)Checking Poetry installation...$(RESET)"
|
||||
@if command -v poetry > /dev/null; then \
|
||||
POETRY_VERSION=$(shell poetry --version 2>&1 | sed -E 's/Poetry \(version ([0-9]+\.[0-9]+\.[0-9]+)\)/\1/'); \
|
||||
IFS='.' read -r -a POETRY_VERSION_ARRAY <<< "$$POETRY_VERSION"; \
|
||||
if [ $${POETRY_VERSION_ARRAY[0]} -ge 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]; then \
|
||||
echo "$(BLUE)$(shell poetry --version) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Poetry 1.8 or later is required. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
|
||||
echo "$(RED) curl -sSL https://install.python-poetry.org | python3 -$(RESET)"; \
|
||||
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
echo "$(RED)Poetry is not installed. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
|
||||
echo "$(RED) curl -sSL https://install.python-poetry.org | python3.11 -$(RESET)"; \
|
||||
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
pull-docker-image:
|
||||
@echo "$(YELLOW)Pulling Docker image...$(RESET)"
|
||||
@docker pull $(DOCKER_IMAGE)
|
||||
@echo "$(GREEN)Docker image pulled successfully.$(RESET)"
|
||||
|
||||
install-python-dependencies:
|
||||
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
|
||||
@if [ "$(shell uname)" = "Darwin" ]; then \
|
||||
echo "$(BLUE)Installing `chroma-hnswlib`...$(RESET)"; \
|
||||
export HNSWLIB_NO_NATIVE=1; \
|
||||
poetry run pip install chroma-hnswlib; \
|
||||
fi
|
||||
@poetry install --without evaluation
|
||||
@if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
|
||||
poetry run pip install playwright; \
|
||||
poetry run playwright install chromium; \
|
||||
else \
|
||||
poetry run playwright install --with-deps chromium; \
|
||||
fi
|
||||
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
|
||||
|
||||
install-frontend-dependencies:
|
||||
@echo "$(YELLOW)Setting up frontend environment...$(RESET)"
|
||||
@echo "$(YELLOW)Detect Node.js version...$(RESET)"
|
||||
@cd frontend && node ./scripts/detect-node-version.js
|
||||
@cd frontend && \
|
||||
echo "$(BLUE)Installing frontend dependencies with npm...$(RESET)" && \
|
||||
npm install && \
|
||||
echo "$(BLUE)Running make-i18n with npm...$(RESET)" && \
|
||||
npm run make-i18n
|
||||
@echo "$(GREEN)Frontend dependencies installed successfully.$(RESET)"
|
||||
|
||||
install-precommit-hooks:
|
||||
@echo "$(YELLOW)Installing pre-commit hooks...$(RESET)"
|
||||
@git config --unset-all core.hooksPath || true
|
||||
@poetry run pre-commit install --config $(PRECOMMIT_CONFIG_PATH)
|
||||
@echo "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
|
||||
|
||||
lint-backend:
|
||||
@echo "$(YELLOW)Running linters...$(RESET)"
|
||||
@poetry run pre-commit run --files $$(git diff --name-only $$(git merge-base main $$(git branch --show-current)) $$(git branch --show-current) | tr '\n' ' ') --show-diff-on-failure --config $(PRECOMMIT_CONFIG_PATH)
|
||||
|
||||
lint-frontend:
|
||||
@echo "$(YELLOW)Running linters for frontend...$(RESET)"
|
||||
@cd frontend && npm run lint
|
||||
|
||||
lint:
|
||||
@$(MAKE) -s lint-frontend
|
||||
@$(MAKE) -s lint-backend
|
||||
|
||||
test-frontend:
|
||||
@echo "$(YELLOW)Running tests for frontend...$(RESET)"
|
||||
@cd frontend && npm run test
|
||||
|
||||
test:
|
||||
@$(MAKE) -s test-frontend
|
||||
|
||||
build-frontend:
|
||||
@echo "$(YELLOW)Building frontend...$(RESET)"
|
||||
@cd frontend && npm run build
|
||||
|
||||
# Start backend
|
||||
start-backend:
|
||||
@echo "$(YELLOW)Starting backend...$(RESET)"
|
||||
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) --reload --reload-exclude "workspace/*"
|
||||
|
||||
# Start frontend
|
||||
start-frontend:
|
||||
@echo "$(YELLOW)Starting frontend...$(RESET)"
|
||||
@cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start
|
||||
|
||||
# Run the app
|
||||
run:
|
||||
@echo "$(YELLOW)Running the app...$(RESET)"
|
||||
@if [ "$(OS)" = "Windows_NT" ]; then \
|
||||
echo "$(RED)`make run` is not supported on Windows. Please run `make start-frontend` and `make start-backend` separately.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p logs
|
||||
@echo "$(YELLOW)Starting backend server...$(RESET)"
|
||||
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) &
|
||||
@echo "$(YELLOW)Waiting for the backend to start...$(RESET)"
|
||||
@until nc -z localhost $(BACKEND_PORT); do sleep 0.1; done
|
||||
@echo "$(GREEN)Backend started successfully.$(RESET)"
|
||||
@cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run start -- --port $(FRONTEND_PORT)
|
||||
@echo "$(GREEN)Application started successfully.$(RESET)"
|
||||
|
||||
# Setup config.toml
|
||||
setup-config:
|
||||
@echo "$(YELLOW)Setting up config.toml...$(RESET)"
|
||||
@$(MAKE) setup-config-prompts
|
||||
@mv $(CONFIG_FILE).tmp $(CONFIG_FILE)
|
||||
@echo "$(GREEN)Config.toml setup completed.$(RESET)"
|
||||
|
||||
setup-config-prompts:
|
||||
@echo "[core]" > $(CONFIG_FILE).tmp
|
||||
|
||||
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
|
||||
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
|
||||
echo "workspace_base=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@echo "" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@echo "[llm]" >> $(CONFIG_FILE).tmp
|
||||
@read -p "Enter your LLM model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
|
||||
llm_model=$${llm_model:-$(DEFAULT_MODEL)}; \
|
||||
echo "model=\"$$llm_model\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@read -p "Enter your LLM api key: " llm_api_key; \
|
||||
echo "api_key=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@read -p "Enter your LLM base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
|
||||
if [[ ! -z "$$llm_base_url" ]]; then echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
|
||||
|
||||
@echo "Enter your LLM Embedding Model"; \
|
||||
echo "Choices are:"; \
|
||||
echo " - openai"; \
|
||||
echo " - azureopenai"; \
|
||||
echo " - Embeddings available only with OllamaEmbedding:"; \
|
||||
echo " - llama2"; \
|
||||
echo " - mxbai-embed-large"; \
|
||||
echo " - nomic-embed-text"; \
|
||||
echo " - all-minilm"; \
|
||||
echo " - stable-code"; \
|
||||
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
|
||||
read -p "> " llm_embedding_model; \
|
||||
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
|
||||
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
|
||||
read -p "Enter the local model URL for the embedding model (will set llm.embedding_base_url): " llm_embedding_base_url; \
|
||||
echo "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
|
||||
read -p "Enter the Azure endpoint URL (will overwrite llm.base_url): " llm_base_url; \
|
||||
echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
|
||||
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure API Version: " llm_api_version; \
|
||||
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
|
||||
fi
|
||||
|
||||
|
||||
# Clean up all caches
|
||||
clean:
|
||||
@echo "$(YELLOW)Cleaning up caches...$(RESET)"
|
||||
@rm -rf opendevin/.cache
|
||||
@echo "$(GREEN)Caches cleaned up successfully.$(RESET)"
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "$(BLUE)Usage: make [target]$(RESET)"
|
||||
@echo "Targets:"
|
||||
@echo " $(GREEN)build$(RESET) - Build project, including environment setup and dependencies."
|
||||
@echo " $(GREEN)lint$(RESET) - Run linters on the project."
|
||||
@echo " $(GREEN)setup-config$(RESET) - Setup the configuration for OpenDevin by providing LLM API key,"
|
||||
@echo " LLM Model name, and workspace directory."
|
||||
@echo " $(GREEN)start-backend$(RESET) - Start the backend server for the OpenDevin project."
|
||||
@echo " $(GREEN)start-frontend$(RESET) - Start the frontend server for the OpenDevin project."
|
||||
@echo " $(GREEN)run$(RESET) - Run the OpenDevin application, starting both backend and frontend servers."
|
||||
@echo " Backend Log file will be stored in the 'logs' directory."
|
||||
@echo " $(GREEN)help$(RESET) - Display this help message, providing information on available targets."
|
||||
|
||||
# Phony targets
|
||||
.PHONY: build check-dependencies check-python check-npm check-docker check-poetry pull-docker-image install-python-dependencies install-frontend-dependencies install-precommit-hooks lint start-backend start-frontend run setup-config setup-config-prompts help
|
||||
@@ -0,0 +1,120 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<!--
|
||||
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
||||
*** that would make this better, please fork the repo and create a pull request
|
||||
*** or simply open an issue with the tag "enhancement".
|
||||
*** Don't forget to give the project a star!
|
||||
*** Thanks again! Now go create something AMAZING! :D
|
||||
-->
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
<!--
|
||||
*** I'm using markdown "reference style" links for readability.
|
||||
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
||||
*** See the bottom of this document for the declaration of the reference variables
|
||||
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
||||
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
||||
-->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/graphs/contributors"><img src="https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/network/members"><img src="https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge&color=blue" alt="Forks"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/stargazers"><img src="https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/issues"><img src="https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge&color=blue" alt="Issues"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2i1iqdag6-bVmvamiPA9EZUu7oCO6KhA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
|
||||
<br/>
|
||||
<a href="https://xwang.dev/blog/2024/opendevin-codeact-1.0-swebench/"><img src="https://img.shields.io/badge/SWE--bench%20Lite-21.0%25-green?style=for-the-badge" alt="SWE-bench "></a>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200" height="200">
|
||||
<h1 align="center">OpenDevin: Code Less, Make More</h1>
|
||||
<a href="https://opendevin.github.io/OpenDevin/"><img src="https://img.shields.io/badge/Documenation-OpenDevin-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
Welcome to OpenDevin, a platform for autonomous software engineers, powered by AI and LLMs.
|
||||
|
||||
OpenDevin agents collaborate with human developers to write code, fix bugs, and ship features.
|
||||
|
||||

|
||||
|
||||
## ⚡ Quick Start
|
||||
You can run OpenDevin with Docker. It works best with the most recent
|
||||
version of Docker, `26.0.0`.
|
||||
|
||||
```bash
|
||||
#The directory you want OpenDevin to modify. MUST be an absolute path!
|
||||
export WORKSPACE_BASE=$(pwd)/workspace;
|
||||
|
||||
docker run \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
ghcr.io/opendevin/opendevin:0.5
|
||||
```
|
||||
|
||||
## 🚀 Documentation
|
||||
|
||||
To learn more about the project, and for tips on using OpenDevin,
|
||||
**check out our [documentation](https://opendevin.github.io/OpenDevin/)**.
|
||||
|
||||
There you'll find resources on how to use different LLM providers (like ollama and Anthropic's Claude),
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
## 🤝 How to Contribute
|
||||
|
||||
OpenDevin is a community-driven project, and we welcome contributions from everyone.
|
||||
Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of
|
||||
software engineering with AI, there are many ways to get involved:
|
||||
|
||||
- **Code Contributions:** Help us develop new agents, core functionality, the frontend and other interfaces, or sandboxing solutions.
|
||||
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements.
|
||||
- **Feedback and Testing:** Use the OpenDevin toolset, report bugs, suggest features, or provide feedback on usability.
|
||||
|
||||
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
## 🤖 Join Our Community
|
||||
|
||||
Whether you're a developer, a researcher, or simply enthusiastic about OpenDevin, we'd love to have you in our community.
|
||||
Let's make software engineering better together!
|
||||
|
||||
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2ggtwn3k5-PvAA2LUmqGHVZ~XzGq~ILw) - Here we talk about research, architecture, and future development.
|
||||
- [Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
|
||||
|
||||
## 📈 Progress
|
||||
<p align="center">
|
||||
<a href="https://www.swebench.com/lite.html">
|
||||
<img src="/docs/static/img/results.png" alt="SWE-Bench Lite Score" width="500" height="auto">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://star-history.com/#OpenDevin/OpenDevin&Date">
|
||||
<img src="https://api.star-history.com/svg?repos=OpenDevin/OpenDevin&type=Date" width="500" alt="Star History Chart">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📜 License
|
||||
|
||||
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge
|
||||
[contributors-url]: https://github.com/OpenDevin/OpenDevin/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge
|
||||
[forks-url]: https://github.com/OpenDevin/OpenDevin/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge
|
||||
[stars-url]: https://github.com/OpenDevin/OpenDevin/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge
|
||||
[issues-url]: https://github.com/OpenDevin/OpenDevin/issues
|
||||
[license-shield]: https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge
|
||||
[license-url]: https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import os
|
||||
|
||||
__package_name__ = 'openhands_ai'
|
||||
|
||||
|
||||
def get_version():
|
||||
# Try getting the version from pyproject.toml
|
||||
try:
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
with open(os.path.join(root_dir, 'pyproject.toml'), 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('version ='):
|
||||
return line.split('=')[1].strip().strip('"')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
return version(__package_name__)
|
||||
except (ImportError, PackageNotFoundError):
|
||||
pass
|
||||
|
||||
try:
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
|
||||
return get_distribution(__package_name__).version
|
||||
except (ImportError, DistributionNotFound):
|
||||
pass
|
||||
|
||||
return 'unknown'
|
||||
|
||||
|
||||
try:
|
||||
__version__ = get_version()
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
Binary file not shown.
@@ -0,0 +1,89 @@
|
||||
# Agent Framework Research
|
||||
|
||||
In this folder, there may exist multiple implementations of `Agent` that will be used by the framework.
|
||||
|
||||
For example, `agenthub/monologue_agent`, `agenthub/metagpt_agent`, `agenthub/codeact_agent`, etc.
|
||||
Contributors from different backgrounds and interests can choose to contribute to any (or all!) of these directions.
|
||||
|
||||
## Constructing an Agent
|
||||
|
||||
The abstraction for an agent can be found [here](../opendevin/agent.py).
|
||||
|
||||
Agents are run inside of a loop. At each iteration, `agent.step()` is called with a
|
||||
[State](../opendevin/state.py) input, and the agent must output an [Action](../opendevin/action).
|
||||
|
||||
Every agent also has a `self.llm` which it can use to interact with the LLM configured by the user.
|
||||
See the [LiteLLM docs for `self.llm.completion`](https://docs.litellm.ai/docs/completion).
|
||||
|
||||
## State
|
||||
|
||||
The `state` contains:
|
||||
|
||||
- A history of actions taken by the agent, as well as any observations (e.g. file content, command output) from those actions
|
||||
- A list of actions/observations that have happened since the most recent step
|
||||
- A [`plan`](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/plan.py), which contains the main goal
|
||||
- The agent can add and modify subtasks through the `AddTaskAction` and `ModifyTaskAction`
|
||||
|
||||
## Actions
|
||||
|
||||
Here is a list of available Actions, which can be returned by `agent.step()`:
|
||||
|
||||
- [`CmdRunAction`](../opendevin/action/bash.py) - Runs a command inside a sandboxed terminal
|
||||
- [`CmdKillAction`](../opendevin/action/bash.py) - Kills a background command
|
||||
- [`IPythonRunCellAction`](../opendevin/action/bash.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../opendevin/sandbox/plugins) as a requirement.
|
||||
- [`FileReadAction`](../opendevin/action/fileop.py) - Reads the content of a file
|
||||
- [`FileWriteAction`](../opendevin/action/fileop.py) - Writes new content to a file
|
||||
- [`BrowseURLAction`](../opendevin/action/browse.py) - Gets the content of a URL
|
||||
- [`AgentRecallAction`](../opendevin/action/agent.py) - Searches memory (e.g. a vector database)
|
||||
- [`AddTaskAction`](../opendevin/action/tasks.py) - Adds a subtask to the plan
|
||||
- [`ModifyTaskAction`](../opendevin/action/tasks.py) - Changes the state of a subtask
|
||||
- [`AgentThinkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history (as well as the chat log)
|
||||
- [`AgentTalkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history and talk to the user.
|
||||
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
|
||||
- [`AgentRejectAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
|
||||
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user to enter a new task
|
||||
- [`MessageAction`](../opendevin/action/message.py) - Represents a message from an agent or the user
|
||||
|
||||
You can use `action.to_dict()` and `action_from_dict` to serialize and deserialize actions.
|
||||
|
||||
## Observations
|
||||
|
||||
There are also several types of Observations. These are typically available in the step following the corresponding Action.
|
||||
But they may also appear as a result of asynchronous events (e.g. a message from the user, logs from a command running
|
||||
in the background).
|
||||
|
||||
Here is a list of available Observations:
|
||||
|
||||
- [`CmdOutputObservation`](../opendevin/observation/run.py)
|
||||
- [`BrowserOutputObservation`](../opendevin/observation/browse.py)
|
||||
- [`FileReadObservation`](../opendevin/observation/files.py)
|
||||
- [`FileWriteObservation`](../opendevin/observation/files.py)
|
||||
- [`AgentRecallObservation`](../opendevin/observation/recall.py)
|
||||
- [`ErrorObservation`](../opendevin/observation/error.py)
|
||||
- [`SuccessObservation`](../opendevin/observation/success.py)
|
||||
|
||||
You can use `observation.to_dict()` and `observation_from_dict` to serialize and deserialize observations.
|
||||
|
||||
## Interface
|
||||
|
||||
Every agent must implement the following methods:
|
||||
|
||||
### `step`
|
||||
|
||||
```
|
||||
def step(self, state: "State") -> "Action"
|
||||
```
|
||||
|
||||
`step` moves the agent forward one step towards its goal. This probably means
|
||||
sending a prompt to the LLM, then parsing the response into an `Action`.
|
||||
|
||||
### `search_memory`
|
||||
|
||||
```
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
```
|
||||
|
||||
`search_memory` should return a list of events that match the query. This will be used
|
||||
for the `recall` action.
|
||||
|
||||
You can optionally just return `[]` for this method, meaning the agent has no long-term memory.
|
||||
@@ -0,0 +1,405 @@
|
||||
# GPT-4 Examples:
|
||||
|
||||
### Notes:
|
||||
- The final result is not completed as I only gave it 10 steps, but shows very promising use of the tools.
|
||||
- This whole run costs: $0.35 using gpt-4-turbo
|
||||
- Input Tokens: 29,734
|
||||
- Completion Tokens: 1,636
|
||||
|
||||
Interesting things:
|
||||
1. Using comments as an initial placeholder for the next move and then having that output in memory is a very cool behavior.
|
||||
2. It makes very small iterative changes due to the prompting.
|
||||
3. A lot of the logic for the game is present but not put together well
|
||||
4. There are a lot of odd quirks with the code that it has written.
|
||||
5. Some odd agent behavior with putting random lines of code in random places?
|
||||
6. The structure of the feedback loop needs some improvement so the agent can see the changes it has made better in context.
|
||||
7. Might be beneficial to add in some line numbers so the model knows what lines it is looking at.
|
||||
|
||||
|
||||
```
|
||||
==============
|
||||
STEP 0
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=91, prompt_tokens=2047, total_tokens=2138)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
To start implementing Conway's Game of Life in Python using the pygame library, I first need to set up a Python script file where I can write the code. I'll create a new Python file named `game_of_life.py`. This file will serve as the base for writing the game logic and using pygame functionalities.
|
||||
|
||||
Action:
|
||||
"""
|
||||
write game_of_life.py # This is a placeholder for the game of life implementation using pygame.
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# This is a placeholder for the game of life implementation using pygame.', start=0, end=-1, thoughts="Thoughts:\nTo start implementing Conway's Game of Life in Python using the pygame library, I first need to set up a Python script file where I can write the code. I'll create a new Python file named `game_of_life.py`. This file will serve as the base for writing the game logic and using pygame functionalities.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 0:-1.
|
||||
|
||||
|
||||
==============
|
||||
STEP 1
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=101, prompt_tokens=2351, total_tokens=2452)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
The placeholder for the game of life implementation using pygame has been set up in the file `game_of_life.py`. The next step is to start implementing the basic structure of the game. This includes importing necessary libraries, initializing the pygame module, setting up the display window, and defining the main game loop. I will begin by importing pygame and initializing it, which are the foundational steps for any pygame application.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 0 0 import pygame
|
||||
|
||||
pygame.init()
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='import pygame\n\npygame.init()', start=0, end=0, thoughts='Thoughts:\nThe placeholder for the game of life implementation using pygame has been set up in the file `game_of_life.py`. The next step is to start implementing the basic structure of the game. This includes importing necessary libraries, initializing the pygame module, setting up the display window, and defining the main game loop. I will begin by importing pygame and initializing it, which are the foundational steps for any pygame application.\n\nAction:', action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 0:0.
|
||||
|
||||
|
||||
==============
|
||||
STEP 2
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=123, prompt_tokens=2552, total_tokens=2675)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
Previously, I initialized pygame in the `game_of_life.py` file. The next step is to set up the display window for the game. This involves defining the size of the window and setting the title. This is crucial as it provides the visual interface for the game. I will also add a comment to indicate that this is the setup for the display window.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 2 2
|
||||
# Setup the display window
|
||||
screen_size = (800, 600)
|
||||
screen = pygame.display.set_mode(screen_size)
|
||||
pygame.display.set_caption("Conway's Game of Life")
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# Setup the display window\nscreen_size = (800, 600)\nscreen = pygame.display.set_mode(screen_size)\npygame.display.set_caption("Conway\'s Game of Life")', start=2, end=2, thoughts='Thoughts:\nPreviously, I initialized pygame in the `game_of_life.py` file. The next step is to set up the display window for the game. This involves defining the size of the window and setting the title. This is crucial as it provides the visual interface for the game. I will also add a comment to indicate that this is the setup for the display window.\n\nAction:', action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 2:2.
|
||||
|
||||
|
||||
==============
|
||||
STEP 3
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=144, prompt_tokens=2777, total_tokens=2921)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame and setting up the display window. The next step is to define the main game loop where the game's logic will be continuously updated and rendered. This loop will handle events, update the game state, and redraw the screen. I will start by defining the main loop structure and a basic event handling mechanism to allow for quitting the game when the user presses the close button.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 5 5 # Main game loop
|
||||
running = True
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# Main game loop\nrunning = True\nwhile running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False', start=5, end=5, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame and setting up the display window. The next step is to define the main game loop where the game's logic will be continuously updated and rendered. This loop will handle events, update the game state, and redraw the screen. I will start by defining the main loop structure and a basic event handling mechanism to allow for quitting the game when the user presses the close button.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 5:5.
|
||||
|
||||
|
||||
==============
|
||||
STEP 4
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=222, prompt_tokens=3022, total_tokens=3244)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, and defining the main game loop. The next step is to implement the logic for Conway's Game of Life. This involves creating a grid to represent the game state, initializing it with a random pattern, and defining the rules for cell survival, birth, and death. I will start by defining the grid size and initializing the grid with random states.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 7 7 # Define grid size and initialize grid
|
||||
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
|
||||
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
|
||||
import random
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# Define grid size and initialize grid\ngrid_size = (50, 50) # Grid size as a tuple (rows, columns)\ngrid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros\nimport random\nfor y in range(grid_size[0]):\n for x in range(grid_size[1]):\n grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)', start=7, end=7, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, and defining the main game loop. The next step is to implement the logic for Conway's Game of Life. This involves creating a grid to represent the game state, initializing it with a random pattern, and defining the rules for cell survival, birth, and death. I will start by defining the grid size and initializing the grid with random states.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 7:7.
|
||||
|
||||
|
||||
==============
|
||||
STEP 5
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=234, prompt_tokens=3158, total_tokens=3392)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, and initializing the grid with random states. The next step is to implement the rules for Conway's Game of Life. This involves checking each cell's neighbors to determine its next state based on the number of alive neighbors. I will start by defining a function to count the alive neighbors for a given cell.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 9 9 def count_alive_neighbors(grid, x, y):
|
||||
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
count = 0
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
|
||||
count += grid[ny][nx]
|
||||
return count
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='def count_alive_neighbors(grid, x, y):\n directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]\n count = 0\n for dx, dy in directions:\n nx, ny = x + dx, y + dy\n if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:\n count += grid[ny][nx]\n return count', start=9, end=9, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, and initializing the grid with random states. The next step is to implement the rules for Conway's Game of Life. This involves checking each cell's neighbors to determine its next state based on the number of alive neighbors. I will start by defining a function to count the alive neighbors for a given cell.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 9:9.
|
||||
|
||||
|
||||
==============
|
||||
STEP 6
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=257, prompt_tokens=3290, total_tokens=3547)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating a function to count alive neighbors. The next step is to implement the rules for Conway's Game of Life. This involves updating the grid state based on the number of alive neighbors each cell has. I will define a function `update_grid` that applies the rules of the game to update the grid for the next generation.
|
||||
|
||||
Action:
|
||||
"""
|
||||
write game_of_life.py def update_grid(grid):
|
||||
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
alive_neighbors = count_alive_neighbors(grid, x, y)
|
||||
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
|
||||
new_grid[y][x] = 1
|
||||
elif grid[y][x] == 0 and alive_neighbors == 3:
|
||||
new_grid[y][x] = 1
|
||||
return new_grid
|
||||
11
|
||||
11
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='def update_grid(grid):\n new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]\n for y in range(grid_size[0]):\n for x in range(grid_size[1]):\n alive_neighbors = count_alive_neighbors(grid, x, y)\n if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):\n new_grid[y][x] = 1\n elif grid[y][x] == 0 and alive_neighbors == 3:\n new_grid[y][x] = 1\n return new_grid', start=11, end=11, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating a function to count alive neighbors. The next step is to implement the rules for Conway's Game of Life. This involves updating the grid state based on the number of alive neighbors each cell has. I will define a function `update_grid` that applies the rules of the game to update the grid for the next generation.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 11:11.
|
||||
|
||||
|
||||
==============
|
||||
STEP 7
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=124, prompt_tokens=3416, total_tokens=3540)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 6 6
|
||||
grid = update_grid(grid)
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='grid = update_grid(grid)', start=6, end=6, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 6:6.
|
||||
|
||||
|
||||
==============
|
||||
STEP 8
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=166, prompt_tokens=3393, total_tokens=3559)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population. I need to ensure that the `update_grid` function is called within the main game loop and that the display is updated accordingly. I will start by checking the current content of the main game loop to see where to integrate the `update_grid` function.
|
||||
|
||||
Action:
|
||||
"""
|
||||
goto 6
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileReadAction(path='game_of_life.py', start=6, end=106, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population. I need to ensure that the `update_grid` function is called within the main game loop and that the display is updated accordingly. I will start by checking the current content of the main game loop to see where to integrate the `update_grid` function.\n\nAction:", action=<ActionType.READ: 'read'>)
|
||||
|
||||
OBSERVATION:
|
||||
grid = update_grid(grid)
|
||||
running = True
|
||||
# Define grid size and initialize grid
|
||||
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
|
||||
def count_alive_neighbors(grid, x, y):
|
||||
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
def update_grid(grid):
|
||||
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
alive_neighbors = count_alive_neighbors(grid, x, y)
|
||||
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
|
||||
new_grid[y][x] = 1
|
||||
elif grid[y][x] == 0 and alive_neighbors == 3:
|
||||
new_grid[y][x] = 1
|
||||
return new_grid
|
||||
count = 0
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
|
||||
count += grid[ny][nx]
|
||||
return count
|
||||
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
|
||||
import random
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
pygame.display.set_caption("Conway's Game of Life")
|
||||
pygame.init()
|
||||
|
||||
|
||||
|
||||
==============
|
||||
STEP 9
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=174, prompt_tokens=3728, total_tokens=3902)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
From the previous actions, it seems that the basic structure for Conway's Game of Life using pygame has been set up, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to ensure that the game loop properly updates the grid and refreshes the display to show the new state of the grid in each iteration. This involves calling `pygame.display.update()` to update the entire screen and using `pygame.time.Clock()` to control the frame rate. I will start by adding these necessary updates to the game loop.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 95 95
|
||||
pygame.display.update()
|
||||
clock = pygame.time.Clock()
|
||||
clock.tick(10) # Control the frame rate (10 frames per second)
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='pygame.display.update()\n clock = pygame.time.Clock()\n clock.tick(10) # Control the frame rate (10 frames per second)', start=95, end=95, thoughts="Thoughts:\nFrom the previous actions, it seems that the basic structure for Conway's Game of Life using pygame has been set up, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to ensure that the game loop properly updates the grid and refreshes the display to show the new state of the grid in each iteration. This involves calling `pygame.display.update()` to update the entire screen and using `pygame.time.Clock()` to control the frame rate. I will start by adding these necessary updates to the game loop.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 95:95.
|
||||
16:43:55 - opendevin:INFO: agent_controller.py:125 - Exited before finishing the task.
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Result:
|
||||
|
||||
```
|
||||
import pygame
|
||||
|
||||
# Setup the display window
|
||||
screen_size = (800, 600)
|
||||
screen = pygame.display.set_mode(screen_size)
|
||||
# Main game loop
|
||||
grid = update_grid(grid)
|
||||
running = True
|
||||
# Define grid size and initialize grid
|
||||
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
|
||||
def count_alive_neighbors(grid, x, y):
|
||||
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
def update_grid(grid):
|
||||
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
alive_neighbors = count_alive_neighbors(grid, x, y)
|
||||
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
|
||||
new_grid[y][x] = 1
|
||||
elif grid[y][x] == 0 and alive_neighbors == 3:
|
||||
new_grid[y][x] = 1
|
||||
return new_grid
|
||||
count = 0
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
|
||||
count += grid[ny][nx]
|
||||
return count
|
||||
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
|
||||
import random
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
pygame.display.set_caption("Conway's Game of Life")
|
||||
pygame.init()
|
||||
# This is a placeholder for the game of life implementation using pygame.pygame.display.update()
|
||||
clock = pygame.time.Clock()
|
||||
clock.tick(10) # Control the frame rate (10 frames per second)
|
||||
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import SWEAgent
|
||||
|
||||
Agent.register('SWEAgent', SWEAgent)
|
||||
@@ -0,0 +1,106 @@
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
)
|
||||
from opendevin.events.observation import Observation
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
from .parser import parse_command
|
||||
from .prompts import (
|
||||
CONTEXT_PROMPT,
|
||||
MEMORY_FORMAT,
|
||||
NO_ACTION,
|
||||
STEP_PROMPT,
|
||||
SYSTEM_MESSAGE,
|
||||
)
|
||||
|
||||
|
||||
class SWEAgent(Agent):
|
||||
"""
|
||||
An attempt to recreate swe_agent with output parsing, prompting style, and Application Computer Interface (ACI).
|
||||
|
||||
SWE-agent includes ACI functions like 'goto', 'search_for', 'edit', 'scroll', 'run'
|
||||
"""
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
self.memory_window = 4
|
||||
self.max_retries = 2
|
||||
self.running_memory: list[str] = []
|
||||
self.cur_file: str = ''
|
||||
self.cur_line: int = 0
|
||||
|
||||
def _remember(self, action: Action, observation: Observation) -> None:
|
||||
"""Agent has a limited memory of the few steps implemented as a queue"""
|
||||
memory = MEMORY_FORMAT(action.to_memory(), observation.to_memory())
|
||||
self.running_memory.append(memory)
|
||||
|
||||
def _think_act(self, messages: list[dict]) -> tuple[Action, str]:
|
||||
resp = self.llm.completion(
|
||||
messages=messages,
|
||||
temperature=0.05,
|
||||
)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
print(f"\033[1m\033[91m{resp['usage']}\033[0m")
|
||||
print(
|
||||
'\n==== RAW OUTPUT ====',
|
||||
f'\033[96m{action_resp}\033[0m',
|
||||
'==== END RAW ====\n',
|
||||
sep='\n',
|
||||
)
|
||||
return parse_command(action_resp, self.cur_file, self.cur_line)
|
||||
|
||||
def _update(self, action: Action) -> None:
|
||||
if isinstance(action, (FileReadAction, FileWriteAction)):
|
||||
self.cur_file = action.path
|
||||
self.cur_line = action.start
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
SWE-Agent step:
|
||||
1. Get context - past actions, custom commands, current step
|
||||
2. Perform think-act - prompt model for action and reasoning
|
||||
3. Catch errors - ensure model takes action (5 attempts max)
|
||||
"""
|
||||
for prev_action, obs in state.updated_info:
|
||||
self._remember(prev_action, obs)
|
||||
|
||||
prompt = STEP_PROMPT(state.plan.main_goal, self.cur_file, self.cur_line)
|
||||
|
||||
msgs = [
|
||||
{'content': SYSTEM_MESSAGE, 'role': 'system'},
|
||||
{'content': prompt, 'role': 'user'},
|
||||
]
|
||||
|
||||
if len(self.running_memory) > 0:
|
||||
context = CONTEXT_PROMPT(self.running_memory, self.memory_window)
|
||||
msgs.insert(1, {'content': context, 'role': 'user'})
|
||||
# clrs = [''] * (len(msgs)-2) + ['\033[0;36m', '\033[0;35m']
|
||||
# print('\n\n'.join([c+m['content']+'\033[0m' for c, m in zip(clrs, msgs)]))
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
start_msg_len = len(msgs)
|
||||
while not action and len(msgs) < self.max_retries + start_msg_len:
|
||||
error = NO_ACTION(thought)
|
||||
error_msg = {'content': error, 'role': 'user'}
|
||||
msgs.append(error_msg)
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
if not action:
|
||||
action = MessageAction(thought)
|
||||
|
||||
self._update(action)
|
||||
self.latest_action = action
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return [item for item in self.running_memory if query in item]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Used to reset the agent"""
|
||||
self.running_memory = []
|
||||
super().reset()
|
||||
@@ -0,0 +1,167 @@
|
||||
import re
|
||||
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
)
|
||||
|
||||
from .prompts import COMMAND_USAGE, CUSTOM_DOCS
|
||||
|
||||
# commands: exit, read, write, browse, kill, search_file, search_dir
|
||||
|
||||
no_open_file_error = MessageAction(
|
||||
'You are not currently in a file. You can use the read command to open a file and then use goto to navigate through it.'
|
||||
)
|
||||
|
||||
|
||||
def invalid_error(cmd, docs):
|
||||
return f"""ERROR:
|
||||
Invalid command structure for
|
||||
```
|
||||
{cmd}
|
||||
```
|
||||
You may have caused this error by having multiple commands within your command block.
|
||||
If so, try again by running only one of the commands:
|
||||
|
||||
Try again using this format:
|
||||
{COMMAND_USAGE[docs]}
|
||||
"""
|
||||
|
||||
|
||||
def get_action_from_string(
|
||||
command_string: str, path: str, line: int, thoughts: str = ''
|
||||
) -> Action | None:
|
||||
"""
|
||||
Parses the command string to find which command the agent wants to run
|
||||
Converts the command into a proper Action and returns
|
||||
"""
|
||||
vars = command_string.split(' ')
|
||||
cmd = vars[0]
|
||||
args = [] if len(vars) == 1 else vars[1:]
|
||||
|
||||
if 'exit' == cmd:
|
||||
return AgentFinishAction()
|
||||
|
||||
elif 'think' == cmd:
|
||||
return MessageAction(' '.join(args))
|
||||
|
||||
elif 'scroll_up' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
return FileReadAction(path, line + 100, line + 200, thoughts)
|
||||
|
||||
elif 'scroll_down' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
return FileReadAction(path, line - 100, line, thoughts)
|
||||
|
||||
elif 'goto' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
rex = r'^goto\s+(\d+)$'
|
||||
valid = re.match(rex, command_string)
|
||||
if valid:
|
||||
start = int(valid.group(1))
|
||||
end = start + 100
|
||||
return FileReadAction(path, start, end, thoughts)
|
||||
else:
|
||||
return MessageAction(invalid_error(command_string, 'goto'))
|
||||
|
||||
elif 'edit' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
rex = r'^edit\s+(\d+)\s+(-?\d+)\s+(\S.*)$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
if valid:
|
||||
start = int(valid.group(1))
|
||||
end = int(valid.group(2))
|
||||
change = valid.group(3)
|
||||
if '"' == change[-1] and '"' == change[0]:
|
||||
change = change[1:-1]
|
||||
return FileWriteAction(path, change, start, end, thoughts)
|
||||
else:
|
||||
return MessageAction(invalid_error(command_string, 'edit'))
|
||||
|
||||
elif 'read' == cmd:
|
||||
rex = r'^read\s+(\S+)(?:\s+(\d+))?(?:\s+(-?\d+))?$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
if valid:
|
||||
file = valid.group(1)
|
||||
start_str = valid.group(2)
|
||||
end_str = valid.group(3)
|
||||
|
||||
start = 0 if not start_str else int(start_str)
|
||||
end = -1 if not end_str else int(end_str)
|
||||
|
||||
return FileReadAction(file, start, end, thoughts)
|
||||
else:
|
||||
return MessageAction(invalid_error(command_string, 'read'))
|
||||
|
||||
elif 'write' == cmd:
|
||||
rex = r'^write\s+(\S+)\s+(.*?)\s*(\d+)?\s*(-?\d+)?$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
|
||||
if valid:
|
||||
file = valid.group(1)
|
||||
content = valid.group(2)
|
||||
start_str = valid.group(3)
|
||||
end_str = valid.group(4)
|
||||
|
||||
start = 0 if not start_str else int(start_str)
|
||||
end = -1 if not end_str else int(end_str)
|
||||
|
||||
if '"' == content[-1] and '"' == content[0]:
|
||||
content = content[1:-1]
|
||||
|
||||
return FileWriteAction(file, content, start, end, thoughts)
|
||||
else:
|
||||
return MessageAction(invalid_error(command_string, 'write'))
|
||||
|
||||
elif 'browse' == cmd:
|
||||
return BrowseURLAction(args[0].strip())
|
||||
|
||||
elif cmd in ['search_file', 'search_dir', 'find_file']:
|
||||
rex = r'^(search_file|search_dir|find_file)\s+(\S+)(?:\s+(\S+))?$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
if valid:
|
||||
return CmdRunAction(command_string)
|
||||
else:
|
||||
return MessageAction(
|
||||
f'Invalid command structure for\n ```\n{command_string}\n```.\nTry again using this format:\n{CUSTOM_DOCS}'
|
||||
)
|
||||
else:
|
||||
# check bash command
|
||||
obs = str(CmdRunAction(f'type {cmd}'))
|
||||
if obs.split(':')[-1].strip() == 'not found':
|
||||
# echo not found error for llm
|
||||
return MessageAction(content=obs)
|
||||
else:
|
||||
# run valid command
|
||||
return CmdRunAction(command_string)
|
||||
|
||||
|
||||
def parse_command(input_str: str, path: str, line: int):
|
||||
"""
|
||||
Parses a given string and separates the command (enclosed in triple backticks) from any accompanying text.
|
||||
|
||||
Args:
|
||||
input_str (str): The input string to be parsed.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the command and the accompanying text (if any).
|
||||
"""
|
||||
input_str = input_str.strip()
|
||||
if '```' in input_str:
|
||||
parts = input_str.split('```')
|
||||
command_str = parts[1].strip()
|
||||
ind = 2 if len(parts) > 2 else 1
|
||||
accompanying_text = ''.join(parts[:-ind]).strip()
|
||||
action = get_action_from_string(command_str, path, line, accompanying_text)
|
||||
if action:
|
||||
return action, accompanying_text
|
||||
return None, input_str # used for retry
|
||||
@@ -0,0 +1,206 @@
|
||||
|
||||
DEFAULT_COMMANDS_DICT = {
|
||||
'exit': 'Executed when task is complete',
|
||||
'read <file_name> [<start_line>] [<end_line>]': "Shows a given file's contents starting from <start_line> up to <end_line>. Default: start_line = 0, end_line = -1. By default the whole file will be read.",
|
||||
'write <file> <changes> [<start_line>] [<end_line>]': 'Modifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>. Default start_line = 0 and end_line = -1. Calling this with no line args will replace the whole file.',
|
||||
'browse <url>': 'Returns the text version of any url, this can be useful to look up documentation or finding issues on github',
|
||||
'scroll_up': 'Takes no arguments. This will scroll up and show you the 100 lines above your current lines',
|
||||
'scroll_down': 'Takes no arguments. This will scroll down and show you the 100 lines below your current lines',
|
||||
'edit <start_line> <end_line> <changes>': 'This will modify lines in the currently open file. use start_line and end_line to designate which lines to change and then write the multiline changes',
|
||||
'goto <line_num>': 'This will take you directly to a line and show you the 100 lines below it.',
|
||||
'<bash_command> <args>': 'You can use any bash command you need (cd, ls, rm, grep, dir, mv, wget, git, zip, etc.) with their arguments included',
|
||||
'pip install <package>': 'You can use this to import python packages. Make sure you include the correct package name when using this command.',
|
||||
'ls': 'Use the ls command to view all the files in your current directory, this is a good starting point.',
|
||||
'NOT ALLOWED': 'You cannot use interactive commands like python or node'
|
||||
}
|
||||
|
||||
COMMAND_USAGE = {
|
||||
'exit': 'Usage:\n```\nexit\n```\nExecuted when task is complete',
|
||||
'read': "Args:\n<file_name> [<start_line>] [<end_line>]\nUsage:\n```\nread file.py\n```\nor\n```\nread example.py <start_line> <end_line>\n```\nShows a given file's contents starting from <start_line> up to <end_line>. Default: start_line = 0, end_line = -1. by default the whole file will be read.",
|
||||
'write': 'Args:\n<file> <changes> [<start_line>] [<end_line>]\nUsage:\n```\nwrite "def main():\n print("This is line one")" 0 2\n```\nModifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>. Default start_line = 0 and end_line = -1. Calling this with no line args will replace the whole file.',
|
||||
'edit': 'Args:\n<start_line> <end_line> <changes>\nUsage:\n```\nedit 0 1 import pandas as pd\n```\nThis will modify the current file you are in with the changes you make between the line numbers you designate',
|
||||
'goto': 'Args:\n<line_num>\nUsage:\n```\ngoto <line_num>\n```\nThis will show you the 100 lines below and including the line you specify within your current file.',
|
||||
'scroll_up': 'Usage:\n```\nscroll_up\n```\nThis will return the 100 lines above where you are currently at',
|
||||
'scroll_down': 'Usage:\n```\nscroll_down\n```\nThis will return the 100 line below where you are currently at',
|
||||
'browse': 'Args:\n<url>\nUsage:\n```\nbrowse https://github.com/OpenDevin/OpenDevin\n```\nThis will fetch the Text elements from the given url and show them to you.',
|
||||
}
|
||||
|
||||
DEFAULT_COMMANDS = '\n'.join(
|
||||
[k + ' - ' + v for k, v in DEFAULT_COMMANDS_DICT.items()])
|
||||
|
||||
# from opendevin.parse_commands import parse_command_file
|
||||
# USE parse_command_file(filepath) to get the custom commands
|
||||
CUSTOM_DOCS = None
|
||||
|
||||
CUSTOM_COMMANDS = f"""Custom bash commands:
|
||||
{CUSTOM_DOCS}
|
||||
"""
|
||||
|
||||
DOCUMENTATION = f"""DOCUMENTATION:
|
||||
It is recommend that you use the commands provided for interacting with files and your directory because they have been specially built for you.
|
||||
They will make it much easier for you to look at files and make changes. Using these commands will help you be better at your task.
|
||||
You can open an file by using either the read or write operations.
|
||||
- If a file already exists you should read it before making any changes. Use the `edit` command to make changes once you have read it.
|
||||
- If you are creating a new file use the write command. Use the `edit` command to make changes once you have created the new file.
|
||||
|
||||
Commands:
|
||||
{DEFAULT_COMMANDS}
|
||||
{CUSTOM_COMMANDS}
|
||||
|
||||
The following commands require an open file to be used: edit, scroll_up, scroll_down, goto
|
||||
To modify the current file use 'edit'. To move through the current file use 'goto' or 'scroll_up'/'scroll_down'
|
||||
when using write and edit do not surround the code with any "" just write the code.
|
||||
"""
|
||||
|
||||
GENERAL_GUIDELINES = """INSTRUCTIONS:
|
||||
Now, you're going to solve this issue on your own. You can use any bash commands or custom commands you wish to complete your task. Edit all the files you need to and run any checks or tests that you want.
|
||||
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
|
||||
When you're satisfied with all of the changes you've made, you can indicate that you are done by running the exit command.
|
||||
Note however that you cannot use any interactive session commands (e.g. python, vim, node) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
|
||||
|
||||
NOTE ABOUT THE write COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
|
||||
|
||||
IMPORTANT TIPS:
|
||||
1. Reproduce the bug: Always start by trying to replicate the bug that the issue discusses. If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment and run it to ensure you can reproduce the bug. Then, start trying to fix it. When you think you've fixed the bug, re-run the bug reproduction script to make sure that the issue has indeed been resolved.
|
||||
If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file, so that you can be sure the script ran fine all the way through.
|
||||
2. Try different commands: If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it.
|
||||
3. Navigate large files: If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, you would use the 'read' command like this: 'read <file> 583'. This is a much faster way to read through the file.
|
||||
4. Handle input files: If the bug reproduction script requires inputting/reading a specific file, such as 'buggy-input.png', and you'd like to understand how to input that file, conduct a search in the existing repository code to see whether someone else has already done that. Do this by running the command: 'search_dir "buggy-input.png"'. If that doesn't work, use the Linux 'find' command.
|
||||
5. Understand your context: Always make sure to look at the currently open file and the current working directory. The currently open file might be in a different directory than the working directory.
|
||||
6. Verify your edits: When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
|
||||
7. Thoroughly test your solution: After making any changes to fix a bug, be sure to thoroughly test your solution to ensure the bug has been resolved. Re-run the bug reproduction script and verify that the issue has been addressed.
|
||||
"""
|
||||
|
||||
RESPONSE_FORMAT = """RESPONSE FORMAT:
|
||||
This is the format of the response you will make in order to solve the current issue.
|
||||
You will be given multiple iterations to complete this task so break it into steps and solve them one by one.
|
||||
|
||||
Your output must contain the following:
|
||||
- First, thoughts about what your next action should be and plan it out.
|
||||
- You will have a memory of your thoughts so you can use this to remember things for the next step.
|
||||
- Use your thoughts to think about what you are currently doing, what you have done on prior steps and how that relates to solving the problem.
|
||||
- Second, create a piece of code that will execute your next action based on the thoughts you have.
|
||||
- Remember that you can only have one action for each thought, do not include multiple actions.
|
||||
|
||||
Your code MUST be surrounded in triple back ticks EXACTLY like this:
|
||||
```
|
||||
<code>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Adhere to the format so that the program loop continues smoothly, it is very important to only give one command per output.
|
||||
- DO NOT give more than one command within the triple backticks. This will just throw an error and nothing will happen as a result.
|
||||
- Do not give multiple code blocks, if you do only the second one will be captured and run, this might give an error if the first one was necessary.
|
||||
- To execute multiple commands you should write them down in your thoughts section so you can remember it on the next step and execute them then.
|
||||
- The only commands you are not capable of executing are interactive commands like `python` or `node` by themselves.
|
||||
- If you think that you have completed the task that has been given to you based on your previous actions and outputs then use ``` exit ``` as the command to let the system know that you are done.
|
||||
- DO NOT make any copies of your previous memories those will be provided to you at each step, making copies just wastes time and energy. Think smarter not harder.
|
||||
- The write and edit commands requires proper indentation in the content section ex. `write hw.py def hello():\n print(\'Hello World\')` this is how you would have to format your write command.
|
||||
- The white spaces matter as the code changes will be added to the code so they must have proper syntax.
|
||||
|
||||
This is a template using the format described above
|
||||
Items in <> are suggestions for you, fill them out based on the context of the problem you are solving.
|
||||
|
||||
[ FORMAT ]
|
||||
Thoughts:
|
||||
<Provide clear and concise thoughts on the next step to take, highlighting any important details or context that should be remembered.>
|
||||
<You can use multiple lines to express your thoughts>
|
||||
|
||||
Action:
|
||||
```
|
||||
<command> <params>
|
||||
```
|
||||
[ END FORMAT ]
|
||||
|
||||
Do not provide anything extra just your thought and action.
|
||||
"""
|
||||
|
||||
SYSTEM_MESSAGE = f"""SYSTEM INFO:
|
||||
You am an autonomous coding agent, here to provide solutions for coding issues.
|
||||
You have been designed to assist you with a wide range of programming tasks, from code editing and debugging to testing and deployment.
|
||||
You have access to a variety of tools and commands that you can use to help you solve problems efficiently.
|
||||
|
||||
{GENERAL_GUIDELINES}
|
||||
|
||||
{DOCUMENTATION}
|
||||
""".strip()
|
||||
|
||||
|
||||
def NO_ACTION(latest): return f"""
|
||||
You did not include any action to take in your most recent output:
|
||||
|
||||
===== Output ======
|
||||
{latest}
|
||||
==== End Output ===
|
||||
|
||||
Remember these are the custom commands you can use:
|
||||
{DOCUMENTATION}
|
||||
|
||||
Lets try that again, it is very important that you adhere to the output format
|
||||
This time, be sure to use the exact format below, replacing anything in <> with the appropriate value(s):
|
||||
{RESPONSE_FORMAT}
|
||||
|
||||
It is crucial you use the format provided as the output will be parsed automatically.
|
||||
"""
|
||||
|
||||
|
||||
def file_info(file: str, line: int):
|
||||
if file:
|
||||
return f"""CURRENT WORKSPACE:
|
||||
Open File: {file} on line {line}
|
||||
You can use these commands with the current file:
|
||||
Navigation: `scroll_up`, `scroll_down`, and `goto <line>`
|
||||
Modification: `edit <start_line> <end_line> <changes>`
|
||||
"""
|
||||
|
||||
|
||||
def STEP_PROMPT(task, file, line_num): return f"""
|
||||
{RESPONSE_FORMAT}
|
||||
You are currently trying to complete this task:
|
||||
{task}
|
||||
|
||||
{file_info(file, line_num)}
|
||||
|
||||
Keep all of the guidelines above in mind when you are thinking and making code.
|
||||
Please come up with a thought and action based on your current task and latest steps.
|
||||
Make sure that you do not repeat the same actions, there will not be any changes in result if you do not changes anything.
|
||||
Be very strict about the formatting that you use and make sure you follow the guidelines.
|
||||
NEVER output multiple commands. ONLY take ONE STEP at a time.
|
||||
When you have completed your task run the "exit" command.
|
||||
Begin with your thought about the next step and then come up with an action to perform your thought.
|
||||
""".strip()
|
||||
|
||||
|
||||
def unpack_dict(data: dict, restrict: list[str] = []):
|
||||
lines = []
|
||||
for key, value in data.items():
|
||||
if key in restrict:
|
||||
continue
|
||||
elif isinstance(value, dict):
|
||||
nested_str = unpack_dict(value, restrict).replace('\n', '\n ')
|
||||
val = f'{key}:' + '\n ' + f'{nested_str}'
|
||||
lines.append(val)
|
||||
else:
|
||||
lines.append(f'{key}: {value}')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def MEMORY_FORMAT(act, obs): return f"""
|
||||
Previous Action:
|
||||
{unpack_dict(act, ["content"])}
|
||||
|
||||
Output from Action:
|
||||
{unpack_dict(obs)}
|
||||
""".strip()
|
||||
|
||||
|
||||
def CONTEXT_PROMPT(memory, window):
|
||||
res = f'These are your past {window} actions:\n'
|
||||
window_size = window if len(memory) > window else len(memory)
|
||||
cur_mems = memory[-window_size:]
|
||||
res += '===== Previous Actions =====\n'
|
||||
for idx, mem in enumerate(cur_mems):
|
||||
res += f'\nMemory {idx}:\n{mem}\n'
|
||||
res += '======= End Actions =======\n'
|
||||
res += 'Use these memories to provide additional context to the problem you are solving.\nRemember that you have already completed these steps so you do not need to perform them again.'
|
||||
return res
|
||||
@@ -1,26 +1,29 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from openhands.agenthub.micro.agent import MicroAgent
|
||||
from openhands.agenthub.micro.registry import all_microagents
|
||||
from openhands.controller.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .micro.agent import MicroAgent
|
||||
from .micro.registry import all_microagents
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from openhands.agenthub import ( # noqa: E402
|
||||
browsing_agent,
|
||||
from . import ( # noqa: E402
|
||||
SWE_agent,
|
||||
codeact_agent,
|
||||
delegator_agent,
|
||||
dummy_agent,
|
||||
monologue_agent,
|
||||
planner_agent,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'monologue_agent',
|
||||
'codeact_agent',
|
||||
'planner_agent',
|
||||
'SWE_agent',
|
||||
'delegator_agent',
|
||||
'dummy_agent',
|
||||
'browsing_agent',
|
||||
]
|
||||
|
||||
for agent in all_microagents.values():
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from openhands.agenthub.browsing_agent.browsing_agent import BrowsingAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('BrowsingAgent', BrowsingAgent)
|
||||
@@ -1,223 +0,0 @@
|
||||
import os
|
||||
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from browsergym.utils.obs import flatten_axtree_to_str
|
||||
|
||||
from openhands.agenthub.browsing_agent.response_parser import BrowsingResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.message import Message, TextContent
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
BrowseInteractiveAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.observation import BrowserOutputObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.plugins import (
|
||||
PluginRequirement,
|
||||
)
|
||||
|
||||
USE_NAV = (
|
||||
os.environ.get('USE_NAV', 'true') == 'true'
|
||||
) # only disable NAV actions when running webarena and miniwob benchmarks
|
||||
USE_CONCISE_ANSWER = (
|
||||
os.environ.get('USE_CONCISE_ANSWER', 'false') == 'true'
|
||||
) # only return concise answer when running webarena and miniwob benchmarks
|
||||
|
||||
if not USE_NAV and USE_CONCISE_ANSWER:
|
||||
EVAL_MODE = True # disabled NAV actions and only return concise answer, for webarena and miniwob benchmarks\
|
||||
else:
|
||||
EVAL_MODE = False
|
||||
|
||||
|
||||
def get_error_prefix(last_browser_action: str) -> str:
|
||||
return f'IMPORTANT! Last action is incorrect:\n{last_browser_action}\nThink again with the current observation of the page.\n'
|
||||
|
||||
|
||||
def get_system_message(goal: str, action_space: str) -> str:
|
||||
return f"""\
|
||||
# Instructions
|
||||
Review the current state of the page and all other information to find the best
|
||||
possible next action to accomplish your goal. Your answer will be interpreted
|
||||
and executed by a program, make sure to follow the formatting instructions.
|
||||
|
||||
# Goal:
|
||||
{goal}
|
||||
|
||||
# Action Space
|
||||
{action_space}
|
||||
"""
|
||||
|
||||
|
||||
CONCISE_INSTRUCTION = """\
|
||||
|
||||
Here is another example with chain of thought of a valid action when providing a concise answer to user:
|
||||
"
|
||||
In order to accomplish my goal I need to send the information asked back to the user. This page list the information of HP Inkjet Fax Machine, which is the product identified in the objective. Its price is $279.49. I will send a message back to user with the answer.
|
||||
```send_msg_to_user("$279.49")```
|
||||
"
|
||||
"""
|
||||
|
||||
|
||||
def get_prompt(
|
||||
error_prefix: str, cur_url: str, cur_axtree_txt: str, prev_action_str: str
|
||||
) -> str:
|
||||
prompt = f"""\
|
||||
{error_prefix}
|
||||
|
||||
# Current Page URL:
|
||||
{cur_url}
|
||||
|
||||
# Current Accessibility Tree:
|
||||
{cur_axtree_txt}
|
||||
|
||||
# Previous Actions
|
||||
{prev_action_str}
|
||||
|
||||
Here is an example with chain of thought of a valid action when clicking on a button:
|
||||
"
|
||||
In order to accomplish my goal I need to click on the button with bid 12
|
||||
```click("12")```
|
||||
"
|
||||
""".strip()
|
||||
if USE_CONCISE_ANSWER:
|
||||
prompt += CONCISE_INSTRUCTION
|
||||
return prompt
|
||||
|
||||
|
||||
class BrowsingAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
An agent that interacts with the browser.
|
||||
"""
|
||||
|
||||
sandbox_plugins: list[PluginRequirement] = []
|
||||
response_parser = BrowsingResponseParser()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the BrowsingAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
# define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
|
||||
# see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
|
||||
action_subsets = ['chat', 'bid']
|
||||
if USE_NAV:
|
||||
action_subsets.append('nav')
|
||||
self.action_space = HighLevelActionSet(
|
||||
subsets=action_subsets,
|
||||
strict=False, # less strict on the parsing of the actions
|
||||
multiaction=True, # enable to agent to take multiple actions at once
|
||||
)
|
||||
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Resets the Browsing Agent."""
|
||||
super().reset()
|
||||
self.cost_accumulator = 0
|
||||
self.error_accumulator = 0
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""Performs one step using the Browsing Agent.
|
||||
This includes gathering information on previous steps and prompting the model to make a browsing command to execute.
|
||||
|
||||
Parameters:
|
||||
- state (State): used to get updated info
|
||||
|
||||
Returns:
|
||||
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
messages: list[Message] = []
|
||||
prev_actions = []
|
||||
cur_url = ''
|
||||
cur_axtree_txt = ''
|
||||
error_prefix = ''
|
||||
last_obs = None
|
||||
last_action = None
|
||||
|
||||
if EVAL_MODE and len(state.history) == 1:
|
||||
# for webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
|
||||
# initialize and retrieve the first observation by issuing an noop OP
|
||||
# For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
|
||||
return BrowseInteractiveAction(browser_actions='noop()')
|
||||
|
||||
for event in state.history:
|
||||
if isinstance(event, BrowseInteractiveAction):
|
||||
prev_actions.append(event.browser_actions)
|
||||
last_action = event
|
||||
elif isinstance(event, MessageAction) and event.source == EventSource.AGENT:
|
||||
# agent has responded, task finished.
|
||||
return AgentFinishAction(outputs={'content': event.content})
|
||||
elif isinstance(event, Observation):
|
||||
last_obs = event
|
||||
|
||||
if EVAL_MODE:
|
||||
prev_actions = prev_actions[1:] # remove the first noop action
|
||||
|
||||
prev_action_str = '\n'.join(prev_actions)
|
||||
# if the final BrowserInteractiveAction exec BrowserGym's send_msg_to_user,
|
||||
# we should also send a message back to the user in OpenHands and call it a day
|
||||
if (
|
||||
isinstance(last_action, BrowseInteractiveAction)
|
||||
and last_action.browsergym_send_msg_to_user
|
||||
):
|
||||
return MessageAction(last_action.browsergym_send_msg_to_user)
|
||||
|
||||
if isinstance(last_obs, BrowserOutputObservation):
|
||||
if last_obs.error:
|
||||
# add error recovery prompt prefix
|
||||
error_prefix = get_error_prefix(last_obs.last_browser_action)
|
||||
self.error_accumulator += 1
|
||||
if self.error_accumulator > 5:
|
||||
return MessageAction('Too many errors encountered. Task failed.')
|
||||
|
||||
cur_url = last_obs.url
|
||||
|
||||
try:
|
||||
cur_axtree_txt = flatten_axtree_to_str(
|
||||
last_obs.axtree_object,
|
||||
extra_properties=last_obs.extra_element_properties,
|
||||
with_clickable=True,
|
||||
filter_visible_only=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'Error when trying to process the accessibility tree: %s', e
|
||||
)
|
||||
return MessageAction('Error encountered when browsing.')
|
||||
|
||||
goal, _ = state.get_current_user_intent()
|
||||
|
||||
if goal is None:
|
||||
goal = state.inputs['task']
|
||||
|
||||
system_msg = get_system_message(
|
||||
goal,
|
||||
self.action_space.describe(with_long_description=False, with_examples=True),
|
||||
)
|
||||
|
||||
messages.append(Message(role='system', content=[TextContent(text=system_msg)]))
|
||||
|
||||
prompt = get_prompt(error_prefix, cur_url, cur_axtree_txt, prev_action_str)
|
||||
messages.append(Message(role='user', content=[TextContent(text=prompt)]))
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=self.llm.format_messages_for_llm(messages),
|
||||
stop=[')```', ')\n```'],
|
||||
)
|
||||
return self.response_parser.parse(response)
|
||||
@@ -1,123 +0,0 @@
|
||||
import ast
|
||||
import re
|
||||
|
||||
from openhands.controller.action_parser import ActionParser, ResponseParser
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
BrowseInteractiveAction,
|
||||
)
|
||||
|
||||
|
||||
class BrowsingResponseParser(ResponseParser):
|
||||
def __init__(self):
|
||||
# Need to pay attention to the item order in self.action_parsers
|
||||
super().__init__()
|
||||
self.action_parsers = [BrowsingActionParserMessage()]
|
||||
self.default_parser = BrowsingActionParserBrowseInteractive()
|
||||
|
||||
def parse(self, response: str) -> Action:
|
||||
action_str = self.parse_response(response)
|
||||
return self.parse_action(action_str)
|
||||
|
||||
def parse_response(self, response) -> str:
|
||||
action_str = response['choices'][0]['message']['content']
|
||||
if action_str is None:
|
||||
return ''
|
||||
action_str = action_str.strip()
|
||||
# Ensure action_str ends with ')```'
|
||||
if action_str:
|
||||
if not action_str.endswith('```'):
|
||||
if action_str.endswith(')'):
|
||||
action_str += '```' # prevent duplicate ending paranthesis, e.g. send_msg_to_user('Done'))
|
||||
else:
|
||||
action_str += ')```' # expected format
|
||||
logger.debug(action_str)
|
||||
return action_str
|
||||
|
||||
def parse_action(self, action_str: str) -> Action:
|
||||
for action_parser in self.action_parsers:
|
||||
if action_parser.check_condition(action_str):
|
||||
return action_parser.parse(action_str)
|
||||
return self.default_parser.parse(action_str)
|
||||
|
||||
|
||||
class BrowsingActionParserMessage(ActionParser):
|
||||
"""Parser action:
|
||||
- BrowseInteractiveAction(browser_actions) - unexpected response format, message back to user
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
pass
|
||||
|
||||
def check_condition(self, action_str: str) -> bool:
|
||||
return '```' not in action_str
|
||||
|
||||
def parse(self, action_str: str) -> Action:
|
||||
msg = f'send_msg_to_user("""{action_str}""")'
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions=msg,
|
||||
thought=action_str,
|
||||
browsergym_send_msg_to_user=action_str,
|
||||
)
|
||||
|
||||
|
||||
class BrowsingActionParserBrowseInteractive(ActionParser):
|
||||
"""Parser action:
|
||||
- BrowseInteractiveAction(browser_actions) - handle send message to user function call in BrowserGym
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
pass
|
||||
|
||||
def check_condition(self, action_str: str) -> bool:
|
||||
return True
|
||||
|
||||
def parse(self, action_str: str) -> Action:
|
||||
# parse the action string into browser_actions and thought
|
||||
# the LLM can return only one string, or both
|
||||
|
||||
# when both are returned, it looks like this:
|
||||
### Based on the current state of the page and the goal of finding out the president of the USA, the next action should involve searching for information related to the president.
|
||||
### To achieve this, we can navigate to a reliable source such as a search engine or a specific website that provides information about the current president of the USA.
|
||||
### Here is an example of a valid action to achieve this:
|
||||
### ```
|
||||
### goto('https://www.whitehouse.gov/about-the-white-house/presidents/'
|
||||
# in practice, BrowsingResponseParser.parse_response also added )``` to the end of the string
|
||||
|
||||
# when the LLM returns only one string, it looks like this:
|
||||
### goto('https://www.whitehouse.gov/about-the-white-house/presidents/')
|
||||
# and parse_response added )``` to the end of the string
|
||||
parts = action_str.split('```')
|
||||
browser_actions = (
|
||||
parts[1].strip() if parts[1].strip() != '' else parts[0].strip()
|
||||
)
|
||||
thought = parts[0].strip() if parts[1].strip() != '' else ''
|
||||
|
||||
# if the LLM wants to talk to the user, we extract the message
|
||||
msg_content = ''
|
||||
for sub_action in browser_actions.split('\n'):
|
||||
if 'send_msg_to_user(' in sub_action:
|
||||
try:
|
||||
tree = ast.parse(sub_action)
|
||||
args = tree.body[0].value.args # type: ignore
|
||||
msg_content = args[0].value
|
||||
except SyntaxError:
|
||||
logger.error(f'Error parsing action: {sub_action}')
|
||||
# the syntax was not correct, but we can still try to get the message
|
||||
# e.g. send_msg_to_user("Hello, world!") or send_msg_to_user('Hello, world!'
|
||||
match = re.search(r'send_msg_to_user\((["\'])(.*?)\1\)', sub_action)
|
||||
if match:
|
||||
msg_content = match.group(2)
|
||||
else:
|
||||
msg_content = ''
|
||||
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions=browser_actions,
|
||||
thought=thought,
|
||||
browsergym_send_msg_to_user=msg_content,
|
||||
)
|
||||
@@ -1,158 +0,0 @@
|
||||
import collections
|
||||
import re
|
||||
from warnings import warn
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def yaml_parser(message):
|
||||
"""Parse a yaml message for the retry function."""
|
||||
# saves gpt-3.5 from some yaml parsing errors
|
||||
message = re.sub(r':\s*\n(?=\S|\n)', ': ', message)
|
||||
|
||||
try:
|
||||
value = yaml.safe_load(message)
|
||||
valid = True
|
||||
retry_message = ''
|
||||
except yaml.YAMLError as e:
|
||||
warn(str(e), stacklevel=2)
|
||||
value = {}
|
||||
valid = False
|
||||
retry_message = "Your response is not a valid yaml. Please try again and be careful to the format. Don't add any apology or comment, just the answer."
|
||||
return value, valid, retry_message
|
||||
|
||||
|
||||
def _compress_chunks(text, identifier, skip_list, split_regex='\n\n+'):
|
||||
"""Compress a string by replacing redundant chunks by identifiers. Chunks are defined by the split_regex."""
|
||||
text_list = re.split(split_regex, text)
|
||||
text_list = [chunk.strip() for chunk in text_list]
|
||||
counter = collections.Counter(text_list)
|
||||
def_dict = {}
|
||||
id = 0
|
||||
|
||||
# Store items that occur more than once in a dictionary
|
||||
for item, count in counter.items():
|
||||
if count > 1 and item not in skip_list and len(item) > 10:
|
||||
def_dict[f'{identifier}-{id}'] = item
|
||||
id += 1
|
||||
|
||||
# Replace redundant items with their identifiers in the text
|
||||
compressed_text = '\n'.join(text_list)
|
||||
for key, value in def_dict.items():
|
||||
compressed_text = compressed_text.replace(value, key)
|
||||
|
||||
return def_dict, compressed_text
|
||||
|
||||
|
||||
def compress_string(text):
|
||||
"""Compress a string by replacing redundant paragraphs and lines with identifiers."""
|
||||
# Perform paragraph-level compression
|
||||
def_dict, compressed_text = _compress_chunks(
|
||||
text, identifier='§', skip_list=[], split_regex='\n\n+'
|
||||
)
|
||||
|
||||
# Perform line-level compression, skipping any paragraph identifiers
|
||||
line_dict, compressed_text = _compress_chunks(
|
||||
compressed_text, '¶', list(def_dict.keys()), split_regex='\n+'
|
||||
)
|
||||
def_dict.update(line_dict)
|
||||
|
||||
# Create a definitions section
|
||||
def_lines = ['<definitions>']
|
||||
for key, value in def_dict.items():
|
||||
def_lines.append(f'{key}:\n{value}')
|
||||
def_lines.append('</definitions>')
|
||||
definitions = '\n'.join(def_lines)
|
||||
|
||||
return definitions + '\n' + compressed_text
|
||||
|
||||
|
||||
def extract_html_tags(text, keys):
|
||||
"""Extract the content within HTML tags for a list of keys.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
The input string containing the HTML tags.
|
||||
keys : list of str
|
||||
The HTML tags to extract the content from.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
dict
|
||||
A dictionary mapping each key to a list of subset in `text` that match the key.
|
||||
|
||||
Notes:
|
||||
-----
|
||||
All text and keys will be converted to lowercase before matching.
|
||||
|
||||
"""
|
||||
content_dict = {}
|
||||
# text = text.lower()
|
||||
# keys = set([k.lower() for k in keys])
|
||||
for key in keys:
|
||||
pattern = f'<{key}>(.*?)</{key}>'
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
if matches:
|
||||
content_dict[key] = [match.strip() for match in matches]
|
||||
return content_dict
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def parse_html_tags_raise(text, keys=(), optional_keys=(), merge_multiple=False):
|
||||
"""A version of parse_html_tags that raises an exception if the parsing is not successful."""
|
||||
content_dict, valid, retry_message = parse_html_tags(
|
||||
text, keys, optional_keys, merge_multiple=merge_multiple
|
||||
)
|
||||
if not valid:
|
||||
raise ParseError(retry_message)
|
||||
return content_dict
|
||||
|
||||
|
||||
def parse_html_tags(text, keys=(), optional_keys=(), merge_multiple=False):
|
||||
"""Satisfy the parse api, extracts 1 match per key and validates that all keys are present
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
The input string containing the HTML tags.
|
||||
keys : list of str
|
||||
The HTML tags to extract the content from.
|
||||
optional_keys : list of str
|
||||
The HTML tags to extract the content from, but are optional.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
dict
|
||||
A dictionary mapping each key to subset of `text` that match the key.
|
||||
bool
|
||||
Whether the parsing was successful.
|
||||
str
|
||||
A message to be displayed to the agent if the parsing was not successful.
|
||||
"""
|
||||
all_keys = tuple(keys) + tuple(optional_keys)
|
||||
content_dict = extract_html_tags(text, all_keys)
|
||||
retry_messages = []
|
||||
|
||||
for key in all_keys:
|
||||
if key not in content_dict:
|
||||
if key not in optional_keys:
|
||||
retry_messages.append(f'Missing the key <{key}> in the answer.')
|
||||
else:
|
||||
val = content_dict[key]
|
||||
content_dict[key] = val[0]
|
||||
if len(val) > 1:
|
||||
if not merge_multiple:
|
||||
retry_messages.append(
|
||||
f'Found multiple instances of the key {key}. You should have only one of them.'
|
||||
)
|
||||
else:
|
||||
# merge the multiple instances
|
||||
content_dict[key] = '\n'.join(val)
|
||||
|
||||
valid = len(retry_messages) == 0
|
||||
retry_message = '\n'.join(retry_messages)
|
||||
return content_dict, valid, retry_message
|
||||
@@ -0,0 +1,29 @@
|
||||
# CodeAct Agent Framework
|
||||
|
||||
This folder implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.13463), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
|
||||
|
||||
The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
|
||||
1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
|
||||
2. **CodeAct**: Choose to perform the task by executing code
|
||||
- Execute any valid Linux `bash` command
|
||||
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
|
||||
|
||||

|
||||
|
||||
## Plugin System
|
||||
|
||||
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
|
||||
- [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
|
||||
- [SWE-agent tool plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
|
||||
|
||||
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
|
||||
|
||||
## Work-in-progress & Next step
|
||||
|
||||
[] Support web-browsing
|
||||
[] Complete the workflow for CodeAct agent to submit Github PRs
|
||||
@@ -1,4 +1,5 @@
|
||||
from openhands.agenthub.codeact_agent.codeact_agent import CodeActAgent
|
||||
from openhands.controller.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .codeact_agent import CodeActAgent
|
||||
|
||||
Agent.register('CodeActAgent', CodeActAgent)
|
||||
|
||||
@@ -1,55 +1,93 @@
|
||||
import json
|
||||
import os
|
||||
from collections import deque
|
||||
import re
|
||||
from typing import Mapping
|
||||
|
||||
from litellm import ModelResponse
|
||||
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import (
|
||||
from agenthub.codeact_agent.prompt import EXAMPLES, SYSTEM_MESSAGE
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
NullAction,
|
||||
)
|
||||
from openhands.events.observation import (
|
||||
AgentDelegateObservation,
|
||||
BrowserOutputObservation,
|
||||
from opendevin.events.observation import (
|
||||
CmdOutputObservation,
|
||||
FileEditObservation,
|
||||
IPythonRunCellObservation,
|
||||
UserRejectObservation,
|
||||
NullObservation,
|
||||
)
|
||||
from openhands.events.observation.error import ErrorObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.events.serialization.event import truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.plugins import (
|
||||
AgentSkillsRequirement,
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.runtime.plugins import (
|
||||
JupyterRequirement,
|
||||
PluginRequirement,
|
||||
SWEAgentCommandsRequirement,
|
||||
)
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
|
||||
def parse_response(response) -> str:
|
||||
action = response.choices[0].message.content
|
||||
for lang in ['bash', 'ipython']:
|
||||
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
|
||||
action += f'</execute_{lang}>'
|
||||
return action
|
||||
|
||||
|
||||
def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
|
||||
"""
|
||||
Truncate the middle of the observation if it is too long.
|
||||
"""
|
||||
if len(observation) <= max_chars:
|
||||
return observation
|
||||
half = max_chars // 2
|
||||
return (
|
||||
observation[:half]
|
||||
+ '\n[... Observation truncated due to length ...]\n'
|
||||
+ observation[-half:]
|
||||
)
|
||||
|
||||
|
||||
def swe_agent_edit_hack(bash_command: str) -> str:
|
||||
"""
|
||||
Hack to handle the SWE-agent edit command. The vanilla edit command will hang the SSHBox.
|
||||
|
||||
REPLACE THIS:
|
||||
edit 683:693
|
||||
try:
|
||||
return list(urlsplit(url))
|
||||
except ValueError:
|
||||
raise ValidationError(self.error_messages['invalid'], code='invalid')
|
||||
end_of_edit
|
||||
|
||||
WITH THIS:
|
||||
edit 683:693 <<EOF
|
||||
try:
|
||||
return list(urlsplit(url))
|
||||
except ValueError:
|
||||
raise ValidationError(self.error_messages['invalid'], code='invalid')s
|
||||
EOF
|
||||
"""
|
||||
if 'edit' in bash_command:
|
||||
# edit\s(\d+):(\d+)([\s\S]*)end_of_edit
|
||||
# replace
|
||||
bash_command = re.sub(
|
||||
r'edit\s(\d+):(\d+)([\s\S]*?)end_of_edit',
|
||||
r'edit \1:\2 <<EOF\3EOF',
|
||||
bash_command,
|
||||
)
|
||||
return bash_command
|
||||
|
||||
|
||||
class CodeActAgent(Agent):
|
||||
VERSION = '2.2'
|
||||
VERSION = '1.1'
|
||||
"""
|
||||
The Code Act Agent is a minimalist agent.
|
||||
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
|
||||
|
||||
### Overview
|
||||
|
||||
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
|
||||
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.13463), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
|
||||
|
||||
The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
|
||||
@@ -58,447 +96,179 @@ class CodeActAgent(Agent):
|
||||
- Execute any valid Linux `bash` command
|
||||
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
|
||||
|
||||

|
||||

|
||||
|
||||
### Plugin System
|
||||
|
||||
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
|
||||
- [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
|
||||
- [SWE-agent tool plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
|
||||
|
||||
### Demo
|
||||
|
||||
https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
|
||||
|
||||
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
|
||||
|
||||
### Work-in-progress & Next step
|
||||
|
||||
[] Support web-browsing
|
||||
[] Complete the workflow for CodeAct agent to submit Github PRs
|
||||
|
||||
"""
|
||||
|
||||
sandbox_plugins: list[PluginRequirement] = [
|
||||
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
|
||||
# AgentSkillsRequirement provides a lot of Python functions,
|
||||
# and it needs to be initialized before Jupyter for Jupyter to use those functions.
|
||||
AgentSkillsRequirement(),
|
||||
JupyterRequirement(),
|
||||
SWEAgentCommandsRequirement(),
|
||||
]
|
||||
SUPPORTED_ACTIONS = (
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
NullAction,
|
||||
)
|
||||
SUPPORTED_OBSERVATIONS = (
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
NullObservation,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the CodeActAgent class.
|
||||
"""
|
||||
Initializes a new instance of the CodeActAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
self.reset()
|
||||
|
||||
self.mock_function_calling = False
|
||||
if not self.llm.is_function_calling_active():
|
||||
logger.info(
|
||||
f'Function calling not enabled for model {self.llm.config.model}. '
|
||||
'Mocking function calling via prompting.'
|
||||
)
|
||||
self.mock_function_calling = True
|
||||
|
||||
# Function calling mode
|
||||
self.tools = codeact_function_calling.get_tools(
|
||||
codeact_enable_browsing=self.config.codeact_enable_browsing,
|
||||
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
|
||||
codeact_enable_llm_editor=self.config.codeact_enable_llm_editor,
|
||||
)
|
||||
logger.debug(
|
||||
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2)}'
|
||||
)
|
||||
self.prompt_manager = PromptManager(
|
||||
microagent_dir=os.path.join(os.path.dirname(__file__), 'micro')
|
||||
if self.config.use_microagents
|
||||
else None,
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
disabled_microagents=self.config.disabled_microagents,
|
||||
)
|
||||
|
||||
self.pending_actions: deque[Action] = deque()
|
||||
|
||||
def get_action_message(
|
||||
self,
|
||||
action: Action,
|
||||
pending_tool_call_action_messages: dict[str, Message],
|
||||
) -> list[Message]:
|
||||
"""Converts an action into a message format that can be sent to the LLM.
|
||||
|
||||
This method handles different types of actions and formats them appropriately:
|
||||
1. For tool-based actions (AgentDelegate, CmdRun, IPythonRunCell, FileEdit) and agent-sourced AgentFinish:
|
||||
- In function calling mode: Stores the LLM's response in pending_tool_call_action_messages
|
||||
- In non-function calling mode: Creates a message with the action string
|
||||
2. For MessageActions: Creates a message with the text content and optional image content
|
||||
|
||||
Args:
|
||||
action (Action): The action to convert. Can be one of:
|
||||
- CmdRunAction: For executing bash commands
|
||||
- IPythonRunCellAction: For running IPython code
|
||||
- FileEditAction: For editing files
|
||||
- BrowseInteractiveAction: For browsing the web
|
||||
- AgentFinishAction: For ending the interaction
|
||||
- MessageAction: For sending messages
|
||||
pending_tool_call_action_messages (dict[str, Message]): Dictionary mapping response IDs
|
||||
to their corresponding messages. Used in function calling mode to track tool calls
|
||||
that are waiting for their results.
|
||||
|
||||
Returns:
|
||||
list[Message]: A list containing the formatted message(s) for the action.
|
||||
May be empty if the action is handled as a tool call in function calling mode.
|
||||
|
||||
Note:
|
||||
In function calling mode, tool-based actions are stored in pending_tool_call_action_messages
|
||||
rather than being returned immediately. They will be processed later when all corresponding
|
||||
tool call results are available.
|
||||
"""
|
||||
# create a regular message from an event
|
||||
if isinstance(
|
||||
action,
|
||||
(
|
||||
AgentDelegateAction,
|
||||
IPythonRunCellAction,
|
||||
FileEditAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
),
|
||||
) or (isinstance(action, CmdRunAction) and action.source == 'agent'):
|
||||
tool_metadata = action.tool_call_metadata
|
||||
assert tool_metadata is not None, (
|
||||
'Tool call metadata should NOT be None when function calling is enabled. Action: '
|
||||
+ str(action)
|
||||
)
|
||||
|
||||
llm_response: ModelResponse = tool_metadata.model_response
|
||||
assistant_msg = llm_response.choices[0].message
|
||||
|
||||
# Add the LLM message (assistant) that initiated the tool calls
|
||||
# (overwrites any previous message with the same response_id)
|
||||
logger.debug(
|
||||
f'Tool calls type: {type(assistant_msg.tool_calls)}, value: {assistant_msg.tool_calls}'
|
||||
)
|
||||
pending_tool_call_action_messages[llm_response.id] = Message(
|
||||
role=assistant_msg.role,
|
||||
# tool call content SHOULD BE a string
|
||||
content=[TextContent(text=assistant_msg.content or '')]
|
||||
if assistant_msg.content is not None
|
||||
else [],
|
||||
tool_calls=assistant_msg.tool_calls,
|
||||
)
|
||||
return []
|
||||
elif isinstance(action, AgentFinishAction):
|
||||
role = 'user' if action.source == 'user' else 'assistant'
|
||||
|
||||
# when agent finishes, it has tool_metadata
|
||||
# which has already been executed, and it doesn't have a response
|
||||
# when the user finishes (/exit), we don't have tool_metadata
|
||||
tool_metadata = action.tool_call_metadata
|
||||
if tool_metadata is not None:
|
||||
# take the response message from the tool call
|
||||
assistant_msg = tool_metadata.model_response.choices[0].message
|
||||
content = assistant_msg.content or ''
|
||||
|
||||
# save content if any, to thought
|
||||
if action.thought:
|
||||
if action.thought != content:
|
||||
action.thought += '\n' + content
|
||||
else:
|
||||
action.thought = content
|
||||
|
||||
# remove the tool call metadata
|
||||
action.tool_call_metadata = None
|
||||
return [
|
||||
Message(
|
||||
role=role,
|
||||
content=[TextContent(text=action.thought)],
|
||||
)
|
||||
]
|
||||
elif isinstance(action, MessageAction):
|
||||
role = 'user' if action.source == 'user' else 'assistant'
|
||||
content = [TextContent(text=action.content or '')]
|
||||
if self.llm.vision_is_active() and action.image_urls:
|
||||
content.append(ImageContent(image_urls=action.image_urls))
|
||||
return [
|
||||
Message(
|
||||
role=role,
|
||||
content=content,
|
||||
)
|
||||
]
|
||||
elif isinstance(action, CmdRunAction) and action.source == 'user':
|
||||
content = [
|
||||
TextContent(text=f'User executed the command:\n{action.command}')
|
||||
]
|
||||
return [
|
||||
Message(
|
||||
role='user',
|
||||
content=content,
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def get_observation_message(
|
||||
self,
|
||||
obs: Observation,
|
||||
tool_call_id_to_message: dict[str, Message],
|
||||
) -> list[Message]:
|
||||
"""Converts an observation into a message format that can be sent to the LLM.
|
||||
|
||||
This method handles different types of observations and formats them appropriately:
|
||||
- CmdOutputObservation: Formats command execution results with exit codes
|
||||
- IPythonRunCellObservation: Formats IPython cell execution results, replacing base64 images
|
||||
- FileEditObservation: Formats file editing results
|
||||
- AgentDelegateObservation: Formats results from delegated agent tasks
|
||||
- ErrorObservation: Formats error messages from failed actions
|
||||
- UserRejectObservation: Formats user rejection messages
|
||||
|
||||
In function calling mode, observations with tool_call_metadata are stored in
|
||||
tool_call_id_to_message for later processing instead of being returned immediately.
|
||||
|
||||
Args:
|
||||
obs (Observation): The observation to convert
|
||||
tool_call_id_to_message (dict[str, Message]): Dictionary mapping tool call IDs
|
||||
to their corresponding messages (used in function calling mode)
|
||||
|
||||
Returns:
|
||||
list[Message]: A list containing the formatted message(s) for the observation.
|
||||
May be empty if the observation is handled as a tool response in function calling mode.
|
||||
|
||||
Raises:
|
||||
ValueError: If the observation type is unknown
|
||||
"""
|
||||
message: Message
|
||||
max_message_chars = self.llm.config.max_message_chars
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
# if it doesn't have tool call metadata, it was triggered by a user action
|
||||
if obs.tool_call_metadata is None:
|
||||
text = truncate_content(
|
||||
f'\nObserved result of command executed by user:\n{obs.content}',
|
||||
max_message_chars,
|
||||
)
|
||||
else:
|
||||
text = truncate_content(
|
||||
obs.content + obs.interpreter_details, max_message_chars
|
||||
)
|
||||
text += f'\n[Command finished with exit code {obs.exit_code}]'
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
text = obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splitted = text.split('\n')
|
||||
for i, line in enumerate(splitted):
|
||||
if ' already displayed to user'
|
||||
)
|
||||
text = '\n'.join(splitted)
|
||||
text = truncate_content(text, max_message_chars)
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, FileEditObservation):
|
||||
text = truncate_content(str(obs), max_message_chars)
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, BrowserOutputObservation):
|
||||
text = obs.get_agent_obs_text()
|
||||
message = Message(
|
||||
role='user',
|
||||
content=[TextContent(text=text)],
|
||||
)
|
||||
elif isinstance(obs, AgentDelegateObservation):
|
||||
text = truncate_content(
|
||||
obs.outputs['content'] if 'content' in obs.outputs else '',
|
||||
max_message_chars,
|
||||
)
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
text = truncate_content(obs.content, max_message_chars)
|
||||
text += '\n[Error occurred in processing last action]'
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, UserRejectObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
|
||||
text += '\n[Last action has been rejected by the user]'
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
else:
|
||||
# If an observation message is not returned, it will cause an error
|
||||
# when the LLM tries to return the next message
|
||||
raise ValueError(f'Unknown observation type: {type(obs)}')
|
||||
|
||||
# Update the message as tool response properly
|
||||
if (tool_call_metadata := obs.tool_call_metadata) is not None:
|
||||
tool_call_id_to_message[tool_call_metadata.tool_call_id] = Message(
|
||||
role='tool',
|
||||
content=message.content,
|
||||
tool_call_id=tool_call_metadata.tool_call_id,
|
||||
name=tool_call_metadata.function_name,
|
||||
)
|
||||
# No need to return the observation message
|
||||
# because it will be added by get_action_message when all the corresponding
|
||||
# tool calls in the SAME request are processed
|
||||
return []
|
||||
|
||||
return [message]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Resets the CodeAct Agent."""
|
||||
super().reset()
|
||||
super().__init__(llm)
|
||||
self.messages: list[Mapping[str, str]] = []
|
||||
self.cost_accumulator = 0
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""Performs one step using the CodeAct Agent.
|
||||
"""
|
||||
Performs one step using the CodeAct Agent.
|
||||
This includes gathering info on previous steps and prompting the model to make a command to execute.
|
||||
|
||||
Parameters:
|
||||
- state (State): used to get updated info
|
||||
- state (State): used to get updated info and background commands
|
||||
|
||||
Returns:
|
||||
- CmdRunAction(command) - bash command to run
|
||||
- IPythonRunCellAction(code) - IPython code to run
|
||||
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
# Continue with pending actions if any
|
||||
if self.pending_actions:
|
||||
return self.pending_actions.popleft()
|
||||
|
||||
# if we're done, go back
|
||||
latest_user_message = state.get_last_user_message()
|
||||
if latest_user_message and latest_user_message.content.strip() == '/exit':
|
||||
return AgentFinishAction()
|
||||
|
||||
# prepare what we want to send to the LLM
|
||||
messages = self._get_messages(state)
|
||||
params: dict = {
|
||||
'messages': self.llm.format_messages_for_llm(messages),
|
||||
}
|
||||
params['tools'] = self.tools
|
||||
if self.mock_function_calling:
|
||||
params['mock_function_calling'] = True
|
||||
response = self.llm.completion(**params)
|
||||
actions = codeact_function_calling.response_to_actions(response)
|
||||
for action in actions:
|
||||
self.pending_actions.append(action)
|
||||
return self.pending_actions.popleft()
|
||||
|
||||
def _get_messages(self, state: State) -> list[Message]:
|
||||
"""Constructs the message history for the LLM conversation.
|
||||
|
||||
This method builds a structured conversation history by processing events from the state
|
||||
and formatting them into messages that the LLM can understand. It handles both regular
|
||||
message flow and function-calling scenarios.
|
||||
|
||||
The method performs the following steps:
|
||||
1. Initializes with system prompt and optional initial user message
|
||||
2. Processes events (Actions and Observations) into messages
|
||||
3. Handles tool calls and their responses in function-calling mode
|
||||
4. Manages message role alternation (user/assistant/tool)
|
||||
5. Applies caching for specific LLM providers (e.g., Anthropic)
|
||||
6. Adds environment reminders for non-function-calling mode
|
||||
|
||||
Args:
|
||||
state (State): The current state object containing conversation history and other metadata
|
||||
|
||||
Returns:
|
||||
list[Message]: A list of formatted messages ready for LLM consumption, including:
|
||||
- System message with prompt
|
||||
- Initial user message (if configured)
|
||||
- Action messages (from both user and assistant)
|
||||
- Observation messages (including tool responses)
|
||||
- Environment reminders (in non-function-calling mode)
|
||||
|
||||
Note:
|
||||
- In function-calling mode, tool calls and their responses are carefully tracked
|
||||
to maintain proper conversation flow
|
||||
- Messages from the same role are combined to prevent consecutive same-role messages
|
||||
- For Anthropic models, specific messages are cached according to their documentation
|
||||
"""
|
||||
if not self.prompt_manager:
|
||||
raise Exception('Prompt Manager not instantiated.')
|
||||
|
||||
messages: list[Message] = [
|
||||
Message(
|
||||
role='system',
|
||||
content=[
|
||||
TextContent(
|
||||
text=self.prompt_manager.get_system_message(),
|
||||
cache_prompt=self.llm.is_caching_prompt_active(),
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
example_message = self.prompt_manager.get_example_user_message()
|
||||
if example_message:
|
||||
messages.append(
|
||||
Message(
|
||||
role='user',
|
||||
content=[TextContent(text=example_message)],
|
||||
cache_prompt=self.llm.is_caching_prompt_active(),
|
||||
)
|
||||
)
|
||||
|
||||
pending_tool_call_action_messages: dict[str, Message] = {}
|
||||
tool_call_id_to_message: dict[str, Message] = {}
|
||||
events = list(state.history)
|
||||
for event in events:
|
||||
# create a regular message from an event
|
||||
if isinstance(event, Action):
|
||||
messages_to_add = self.get_action_message(
|
||||
action=event,
|
||||
pending_tool_call_action_messages=pending_tool_call_action_messages,
|
||||
)
|
||||
elif isinstance(event, Observation):
|
||||
messages_to_add = self.get_observation_message(
|
||||
obs=event,
|
||||
tool_call_id_to_message=tool_call_id_to_message,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unknown event type: {type(event)}')
|
||||
|
||||
# Check pending tool call action messages and see if they are complete
|
||||
_response_ids_to_remove = []
|
||||
for (
|
||||
response_id,
|
||||
pending_message,
|
||||
) in pending_tool_call_action_messages.items():
|
||||
assert pending_message.tool_calls is not None, (
|
||||
'Tool calls should NOT be None when function calling is enabled & the message is considered pending tool call. '
|
||||
f'Pending message: {pending_message}'
|
||||
)
|
||||
if all(
|
||||
tool_call.id in tool_call_id_to_message
|
||||
for tool_call in pending_message.tool_calls
|
||||
if len(self.messages) == 0:
|
||||
assert state.plan.main_goal, 'Expecting instruction to be set'
|
||||
self.messages = [
|
||||
{'role': 'system', 'content': SYSTEM_MESSAGE},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': (
|
||||
f'Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\n'
|
||||
f"NOW, LET'S START!\n\n{state.plan.main_goal}"
|
||||
),
|
||||
},
|
||||
]
|
||||
updated_info = state.updated_info
|
||||
if updated_info:
|
||||
for prev_action, obs in updated_info:
|
||||
assert isinstance(
|
||||
prev_action, self.SUPPORTED_ACTIONS
|
||||
), f'{prev_action.__class__} is not supported (supported: {self.SUPPORTED_ACTIONS})'
|
||||
if (
|
||||
isinstance(prev_action, MessageAction)
|
||||
and prev_action.source == 'user'
|
||||
):
|
||||
# If complete:
|
||||
# -- 1. Add the message that **initiated** the tool calls
|
||||
messages_to_add.append(pending_message)
|
||||
# -- 2. Add the tool calls **results***
|
||||
for tool_call in pending_message.tool_calls:
|
||||
messages_to_add.append(tool_call_id_to_message[tool_call.id])
|
||||
tool_call_id_to_message.pop(tool_call.id)
|
||||
_response_ids_to_remove.append(response_id)
|
||||
# Cleanup the processed pending tool messages
|
||||
for response_id in _response_ids_to_remove:
|
||||
pending_tool_call_action_messages.pop(response_id)
|
||||
self.messages.append(
|
||||
{'role': 'user', 'content': prev_action.content}
|
||||
)
|
||||
if prev_action.content.strip() == '/exit':
|
||||
# User wants to exit
|
||||
return AgentFinishAction()
|
||||
|
||||
for message in messages_to_add:
|
||||
if message:
|
||||
if message.role == 'user':
|
||||
self.prompt_manager.enhance_message(message)
|
||||
# handle error if the message is the SAME role as the previous message
|
||||
# litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
|
||||
# there shouldn't be two consecutive messages from the same role
|
||||
# NOTE: we shouldn't combine tool messages because each of them has a different tool_call_id
|
||||
if (
|
||||
messages
|
||||
and messages[-1].role == message.role
|
||||
and message.role != 'tool'
|
||||
):
|
||||
messages[-1].content.extend(message.content)
|
||||
else:
|
||||
messages.append(message)
|
||||
# handle observations
|
||||
assert isinstance(
|
||||
obs, self.SUPPORTED_OBSERVATIONS
|
||||
), f'{obs.__class__} is not supported (supported: {self.SUPPORTED_OBSERVATIONS})'
|
||||
|
||||
if self.llm.is_caching_prompt_active():
|
||||
# NOTE: this is only needed for anthropic
|
||||
# following logic here:
|
||||
# https://github.com/anthropics/anthropic-quickstarts/blob/8f734fd08c425c6ec91ddd613af04ff87d70c5a0/computer-use-demo/computer_use_demo/loop.py#L241-L262
|
||||
breakpoints_remaining = 3 # remaining 1 for system/tool
|
||||
for message in reversed(messages):
|
||||
if message.role == 'user' or message.role == 'tool':
|
||||
if breakpoints_remaining > 0:
|
||||
message.content[
|
||||
-1
|
||||
].cache_prompt = True # Last item inside the message content
|
||||
breakpoints_remaining -= 1
|
||||
else:
|
||||
break
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
|
||||
content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
|
||||
self.messages.append({'role': 'user', 'content': content})
|
||||
|
||||
return messages
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
content = 'OBSERVATION:\n' + obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splitted = content.split('\n')
|
||||
for i, line in enumerate(splitted):
|
||||
if ' already displayed to user'
|
||||
)
|
||||
content = '\n'.join(splitted)
|
||||
content = truncate_observation(content)
|
||||
self.messages.append({'role': 'user', 'content': content})
|
||||
elif isinstance(obs, NullObservation):
|
||||
pass
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Unknown observation type: {obs.__class__}'
|
||||
)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=self.messages,
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
],
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
self.log_cost(response)
|
||||
|
||||
action_str: str = parse_response(response)
|
||||
state.num_of_chars += sum(
|
||||
len(message['content']) for message in self.messages
|
||||
) + len(action_str)
|
||||
self.messages.append({'role': 'assistant', 'content': action_str})
|
||||
|
||||
if bash_command := re.search(
|
||||
r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL
|
||||
):
|
||||
# remove the command from the action string to get thought
|
||||
thought = action_str.replace(bash_command.group(0), '').strip()
|
||||
# a command was found
|
||||
command_group = bash_command.group(1).strip()
|
||||
command_group = swe_agent_edit_hack(command_group)
|
||||
|
||||
if command_group.strip() == 'exit':
|
||||
return AgentFinishAction()
|
||||
return CmdRunAction(command=command_group, thought=thought)
|
||||
elif python_code := re.search(
|
||||
r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL
|
||||
):
|
||||
# a code block was found
|
||||
code_group = python_code.group(1).strip()
|
||||
thought = action_str.replace(python_code.group(0), '').strip()
|
||||
return IPythonRunCellAction(code=code_group, thought=thought)
|
||||
else:
|
||||
# We assume the LLM is GOOD enough that when it returns pure natural language
|
||||
# it want to talk to the user
|
||||
return MessageAction(content=action_str, wait_for_response=True)
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
raise NotImplementedError('Implement this abstract method')
|
||||
|
||||
def log_cost(self, response):
|
||||
cur_cost = self.llm.completion_cost(response)
|
||||
self.cost_accumulator += cur_cost
|
||||
logger.info(
|
||||
'Cost: %.2f USD | Accumulated Cost: %.2f USD',
|
||||
cur_cost,
|
||||
self.cost_accumulator,
|
||||
)
|
||||
|
||||
@@ -1,554 +0,0 @@
|
||||
"""This file contains the function calling implementation for different actions.
|
||||
|
||||
This is similar to the functionality of `CodeActResponseParser`.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from litellm import (
|
||||
ChatCompletionToolParam,
|
||||
ChatCompletionToolParamFunctionChunk,
|
||||
ModelResponse,
|
||||
)
|
||||
|
||||
from openhands.core.exceptions import FunctionCallNotExistsError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
|
||||
_BASH_DESCRIPTION = """Execute a bash command in the terminal.
|
||||
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
|
||||
* Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command=`ctrl+c` to interrupt the process.
|
||||
* Timeout: If a command execution result says "Command timed out. Sending SIGINT to the process", the assistant should retry running the command in the background.
|
||||
"""
|
||||
|
||||
CmdRunTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='execute_bash',
|
||||
description=_BASH_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'command': {
|
||||
'type': 'string',
|
||||
'description': 'The bash command to execute. Can be empty to view additional logs when previous exit code is `-1`. Can be `ctrl+c` to interrupt the currently running process.',
|
||||
},
|
||||
},
|
||||
'required': ['command'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
_IPYTHON_DESCRIPTION = """Run a cell of Python code in an IPython environment.
|
||||
* The assistant should define variables and import packages before using them.
|
||||
* The variable defined in the IPython environment will not be available outside the IPython environment (e.g., in terminal).
|
||||
"""
|
||||
|
||||
IPythonTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='execute_ipython_cell',
|
||||
description=_IPYTHON_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'code': {
|
||||
'type': 'string',
|
||||
'description': 'The Python code to execute. Supports magic commands like %pip.',
|
||||
},
|
||||
},
|
||||
'required': ['code'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file.
|
||||
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
|
||||
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections.
|
||||
* IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines.
|
||||
* To append to a file, set both `start` and `end` to `-1`.
|
||||
* If the file doesn't exist, a new file will be created with the provided content.
|
||||
|
||||
**Example 1: general edit for short files**
|
||||
For example, given an existing file `/path/to/file.py` that looks like this:
|
||||
(this is the end of the file)
|
||||
1|class MyClass:
|
||||
2| def __init__(self):
|
||||
3| self.x = 1
|
||||
4| self.y = 2
|
||||
5| self.z = 3
|
||||
6|
|
||||
7|print(MyClass().z)
|
||||
8|print(MyClass().x)
|
||||
(this is the end of the file)
|
||||
|
||||
The assistant wants to edit the file to look like this:
|
||||
(this is the end of the file)
|
||||
1|class MyClass:
|
||||
2| def __init__(self):
|
||||
3| self.x = 1
|
||||
4| self.y = 2
|
||||
5|
|
||||
6|print(MyClass().y)
|
||||
(this is the end of the file)
|
||||
|
||||
The assistant may produce an edit action like this:
|
||||
path="/path/to/file.txt" start=1 end=-1
|
||||
content=```
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
# no changes before
|
||||
self.y = 2
|
||||
# self.z is removed
|
||||
|
||||
# MyClass().z is removed
|
||||
print(MyClass().y)
|
||||
```
|
||||
|
||||
**Example 2: append to file for short files**
|
||||
For example, given an existing file `/path/to/file.py` that looks like this:
|
||||
(this is the end of the file)
|
||||
1|class MyClass:
|
||||
2| def __init__(self):
|
||||
3| self.x = 1
|
||||
4| self.y = 2
|
||||
5| self.z = 3
|
||||
6|
|
||||
7|print(MyClass().z)
|
||||
8|print(MyClass().x)
|
||||
(this is the end of the file)
|
||||
|
||||
To append the following lines to the file:
|
||||
```python
|
||||
print(MyClass().y)
|
||||
```
|
||||
|
||||
The assistant may produce an edit action like this:
|
||||
path="/path/to/file.txt" start=-1 end=-1
|
||||
content=```
|
||||
print(MyClass().y)
|
||||
```
|
||||
|
||||
**Example 3: edit for long files**
|
||||
|
||||
Given an existing file `/path/to/file.py` that looks like this:
|
||||
(1000 more lines above)
|
||||
1001|class MyClass:
|
||||
1002| def __init__(self):
|
||||
1003| self.x = 1
|
||||
1004| self.y = 2
|
||||
1005| self.z = 3
|
||||
1006|
|
||||
1007|print(MyClass().z)
|
||||
1008|print(MyClass().x)
|
||||
(2000 more lines below)
|
||||
|
||||
The assistant wants to edit the file to look like this:
|
||||
|
||||
(1000 more lines above)
|
||||
1001|class MyClass:
|
||||
1002| def __init__(self):
|
||||
1003| self.x = 1
|
||||
1004| self.y = 2
|
||||
1005|
|
||||
1006|print(MyClass().y)
|
||||
(2000 more lines below)
|
||||
|
||||
The assistant may produce an edit action like this:
|
||||
path="/path/to/file.txt" start=1001 end=1008
|
||||
content=```
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
# no changes before
|
||||
self.y = 2
|
||||
# self.z is removed
|
||||
|
||||
# MyClass().z is removed
|
||||
print(MyClass().y)
|
||||
```
|
||||
"""
|
||||
|
||||
LLMBasedFileEditTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='edit_file',
|
||||
description=_FILE_EDIT_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'path': {
|
||||
'type': 'string',
|
||||
'description': 'The absolute path to the file to be edited.',
|
||||
},
|
||||
'new_content_draft': {
|
||||
'type': 'string',
|
||||
'description': 'A draft of the new content for the file being edited. Note that the assistant may skip unchanged lines.',
|
||||
},
|
||||
'start': {
|
||||
'type': 'integer',
|
||||
'description': 'The starting line number for the edit (1-indexed, inclusive). Default is 1.',
|
||||
},
|
||||
'end': {
|
||||
'type': 'integer',
|
||||
'description': 'The ending line number for the edit (1-indexed, inclusive). Default is -1 (end of file).',
|
||||
},
|
||||
},
|
||||
'required': ['path', 'content'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
|
||||
* State is persistent across command calls and discussions with the user
|
||||
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
|
||||
* The `create` command cannot be used if the specified `path` already exists as a file
|
||||
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
|
||||
* The `undo_edit` command will revert the last edit made to the file at `path`
|
||||
|
||||
Notes for using the `str_replace` command:
|
||||
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
||||
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
|
||||
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
|
||||
"""
|
||||
|
||||
StrReplaceEditorTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='str_replace_editor',
|
||||
description=_STR_REPLACE_EDITOR_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'command': {
|
||||
'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
|
||||
'enum': ['view', 'create', 'str_replace', 'insert', 'undo_edit'],
|
||||
'type': 'string',
|
||||
},
|
||||
'path': {
|
||||
'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.',
|
||||
'type': 'string',
|
||||
},
|
||||
'file_text': {
|
||||
'description': 'Required parameter of `create` command, with the content of the file to be created.',
|
||||
'type': 'string',
|
||||
},
|
||||
'old_str': {
|
||||
'description': 'Required parameter of `str_replace` command containing the string in `path` to replace.',
|
||||
'type': 'string',
|
||||
},
|
||||
'new_str': {
|
||||
'description': 'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
|
||||
'type': 'string',
|
||||
},
|
||||
'insert_line': {
|
||||
'description': 'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
|
||||
'type': 'integer',
|
||||
},
|
||||
'view_range': {
|
||||
'description': 'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
|
||||
'items': {'type': 'integer'},
|
||||
'type': 'array',
|
||||
},
|
||||
},
|
||||
'required': ['command', 'path'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_WEB_DESCRIPTION = """Read (convert to markdown) content from a webpage. You should prefer using the `webpage_read` tool over the `browser` tool, but do use the `browser` tool if you need to interact with a webpage (e.g., click a button, fill out a form, etc.).
|
||||
|
||||
You may use the `webpage_read` tool to read content from a webpage, and even search the webpage content using a Google search query (e.g., url=`https://www.google.com/search?q=YOUR_QUERY`).
|
||||
"""
|
||||
|
||||
WebReadTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='web_read',
|
||||
description=_WEB_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'url': {
|
||||
'type': 'string',
|
||||
'description': 'The URL of the webpage to read. You can also use a Google search query here (e.g., `https://www.google.com/search?q=YOUR_QUERY`).',
|
||||
}
|
||||
},
|
||||
'required': ['url'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# from browsergym/core/action/highlevel.py
|
||||
_browser_action_space = HighLevelActionSet(
|
||||
subsets=['bid', 'nav'],
|
||||
strict=False, # less strict on the parsing of the actions
|
||||
multiaction=True, # enable to agent to take multiple actions at once
|
||||
)
|
||||
|
||||
|
||||
_BROWSER_DESCRIPTION = """Interact with the browser using Python code. Use it ONLY when you need to interact with a webpage.
|
||||
|
||||
See the description of "code" parameter for more details.
|
||||
|
||||
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
|
||||
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
|
||||
fill('a12', 'example with "quotes"')
|
||||
click('a51')
|
||||
click('48', button='middle', modifiers=['Shift'])
|
||||
"""
|
||||
|
||||
_BROWSER_TOOL_DESCRIPTION = """
|
||||
The following 15 functions are available. Nothing else is supported.
|
||||
|
||||
goto(url: str)
|
||||
Description: Navigate to a url.
|
||||
Examples:
|
||||
goto('http://www.example.com')
|
||||
|
||||
go_back()
|
||||
Description: Navigate to the previous page in history.
|
||||
Examples:
|
||||
go_back()
|
||||
|
||||
go_forward()
|
||||
Description: Navigate to the next page in history.
|
||||
Examples:
|
||||
go_forward()
|
||||
|
||||
noop(wait_ms: float = 1000)
|
||||
Description: Do nothing, and optionally wait for the given time (in milliseconds).
|
||||
You can use this to get the current page content and/or wait for the page to load.
|
||||
Examples:
|
||||
noop()
|
||||
|
||||
noop(500)
|
||||
|
||||
scroll(delta_x: float, delta_y: float)
|
||||
Description: Scroll horizontally and vertically. Amounts in pixels, positive for right or down scrolling, negative for left or up scrolling. Dispatches a wheel event.
|
||||
Examples:
|
||||
scroll(0, 200)
|
||||
|
||||
scroll(-50.2, -100.5)
|
||||
|
||||
fill(bid: str, value: str)
|
||||
Description: Fill out a form field. It focuses the element and triggers an input event with the entered text. It works for <input>, <textarea> and [contenteditable] elements.
|
||||
Examples:
|
||||
fill('237', 'example value')
|
||||
|
||||
fill('45', 'multi-line\nexample')
|
||||
|
||||
fill('a12', 'example with "quotes"')
|
||||
|
||||
select_option(bid: str, options: str | list[str])
|
||||
Description: Select one or multiple options in a <select> element. You can specify option value or label to select. Multiple options can be selected.
|
||||
Examples:
|
||||
select_option('a48', 'blue')
|
||||
|
||||
select_option('c48', ['red', 'green', 'blue'])
|
||||
|
||||
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']] = [])
|
||||
Description: Click an element.
|
||||
Examples:
|
||||
click('a51')
|
||||
|
||||
click('b22', button='right')
|
||||
|
||||
click('48', button='middle', modifiers=['Shift'])
|
||||
|
||||
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']] = [])
|
||||
Description: Double click an element.
|
||||
Examples:
|
||||
dblclick('12')
|
||||
|
||||
dblclick('ca42', button='right')
|
||||
|
||||
dblclick('178', button='middle', modifiers=['Shift'])
|
||||
|
||||
hover(bid: str)
|
||||
Description: Hover over an element.
|
||||
Examples:
|
||||
hover('b8')
|
||||
|
||||
press(bid: str, key_comb: str)
|
||||
Description: Focus the matching element and press a combination of keys. It accepts the logical key names that are emitted in the keyboardEvent.key property of the keyboard events: Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp, F1 - F12, Digit0 - Digit9, KeyA - KeyZ, etc. You can alternatively specify a single character you'd like to produce such as "a" or "#". Following modification shortcuts are also supported: Shift, Control, Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta resolves to Control on Windows and Linux and to Meta on macOS.
|
||||
Examples:
|
||||
press('88', 'Backspace')
|
||||
|
||||
press('a26', 'ControlOrMeta+a')
|
||||
|
||||
press('a61', 'Meta+Shift+t')
|
||||
|
||||
focus(bid: str)
|
||||
Description: Focus the matching element.
|
||||
Examples:
|
||||
focus('b455')
|
||||
|
||||
clear(bid: str)
|
||||
Description: Clear the input field.
|
||||
Examples:
|
||||
clear('996')
|
||||
|
||||
drag_and_drop(from_bid: str, to_bid: str)
|
||||
Description: Perform a drag & drop. Hover the element that will be dragged. Press left mouse button. Move mouse to the element that will receive the drop. Release left mouse button.
|
||||
Examples:
|
||||
drag_and_drop('56', '498')
|
||||
|
||||
upload_file(bid: str, file: str | list[str])
|
||||
Description: Click an element and wait for a "filechooser" event, then select one or multiple input files for upload. Relative file paths are resolved relative to the current working directory. An empty list clears the selected files.
|
||||
Examples:
|
||||
upload_file('572', '/home/user/my_receipt.pdf')
|
||||
|
||||
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
|
||||
"""
|
||||
|
||||
|
||||
for _, action in _browser_action_space.action_set.items():
|
||||
assert (
|
||||
action.signature in _BROWSER_TOOL_DESCRIPTION
|
||||
), f'Browser description mismatch. Please double check if the BrowserGym updated their action space.\n\nAction: {action.signature}'
|
||||
assert (
|
||||
action.description in _BROWSER_TOOL_DESCRIPTION
|
||||
), f'Browser description mismatch. Please double check if the BrowserGym updated their action space.\n\nAction: {action.description}'
|
||||
|
||||
BrowserTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='browser',
|
||||
description=_BROWSER_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'code': {
|
||||
'type': 'string',
|
||||
'description': (
|
||||
'The Python code that interacts with the browser.\n'
|
||||
+ _BROWSER_TOOL_DESCRIPTION
|
||||
),
|
||||
}
|
||||
},
|
||||
'required': ['code'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
_FINISH_DESCRIPTION = """Finish the interaction when the task is complete OR if the assistant cannot proceed further with the task."""
|
||||
|
||||
FinishTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='finish',
|
||||
description=_FINISH_DESCRIPTION,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def combine_thought(action: Action, thought: str) -> Action:
|
||||
if not hasattr(action, 'thought'):
|
||||
return action
|
||||
if thought:
|
||||
action.thought = thought
|
||||
return action
|
||||
|
||||
|
||||
def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
actions: list[Action] = []
|
||||
assert len(response.choices) == 1, 'Only one choice is supported for now'
|
||||
assistant_msg = response.choices[0].message
|
||||
if assistant_msg.tool_calls:
|
||||
# Check if there's assistant_msg.content. If so, add it to the thought
|
||||
thought = ''
|
||||
if isinstance(assistant_msg.content, str):
|
||||
thought = assistant_msg.content
|
||||
elif isinstance(assistant_msg.content, list):
|
||||
for msg in assistant_msg.content:
|
||||
if msg['type'] == 'text':
|
||||
thought += msg['text']
|
||||
|
||||
# Process each tool call to OpenHands action
|
||||
for i, tool_call in enumerate(assistant_msg.tool_calls):
|
||||
action: Action
|
||||
try:
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise RuntimeError(
|
||||
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
|
||||
) from e
|
||||
if tool_call.function.name == 'execute_bash':
|
||||
action = CmdRunAction(**arguments)
|
||||
elif tool_call.function.name == 'execute_ipython_cell':
|
||||
action = IPythonRunCellAction(**arguments)
|
||||
elif tool_call.function.name == 'delegate_to_browsing_agent':
|
||||
action = AgentDelegateAction(
|
||||
agent='BrowsingAgent',
|
||||
inputs=arguments,
|
||||
)
|
||||
elif tool_call.function.name == 'finish':
|
||||
action = AgentFinishAction()
|
||||
elif tool_call.function.name == 'edit_file':
|
||||
action = FileEditAction(**arguments)
|
||||
elif tool_call.function.name == 'str_replace_editor':
|
||||
# We implement this in agent_skills, which can be used via Jupyter
|
||||
# convert tool_call.function.arguments to kwargs that can be passed to file_editor
|
||||
code = f'print(file_editor(**{arguments}))'
|
||||
logger.debug(
|
||||
f'TOOL CALL: str_replace_editor -> file_editor with code: {code}'
|
||||
)
|
||||
action = IPythonRunCellAction(code=code, include_extra=False)
|
||||
elif tool_call.function.name == 'browser':
|
||||
action = BrowseInteractiveAction(browser_actions=arguments['code'])
|
||||
elif tool_call.function.name == 'web_read':
|
||||
action = BrowseURLAction(url=arguments['url'])
|
||||
else:
|
||||
raise FunctionCallNotExistsError(
|
||||
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
|
||||
)
|
||||
|
||||
# We only add thought to the first action
|
||||
if i == 0:
|
||||
action = combine_thought(action, thought)
|
||||
# Add metadata for tool calling
|
||||
action.tool_call_metadata = ToolCallMetadata(
|
||||
tool_call_id=tool_call.id,
|
||||
function_name=tool_call.function.name,
|
||||
model_response=response,
|
||||
total_calls_in_response=len(assistant_msg.tool_calls),
|
||||
)
|
||||
actions.append(action)
|
||||
else:
|
||||
actions.append(
|
||||
MessageAction(content=assistant_msg.content, wait_for_response=True)
|
||||
)
|
||||
|
||||
assert len(actions) >= 1
|
||||
return actions
|
||||
|
||||
|
||||
def get_tools(
|
||||
codeact_enable_browsing: bool = False,
|
||||
codeact_enable_llm_editor: bool = False,
|
||||
codeact_enable_jupyter: bool = False,
|
||||
) -> list[ChatCompletionToolParam]:
|
||||
tools = [CmdRunTool, FinishTool]
|
||||
if codeact_enable_browsing:
|
||||
tools.append(WebReadTool)
|
||||
tools.append(BrowserTool)
|
||||
if codeact_enable_jupyter:
|
||||
tools.append(IPythonTool)
|
||||
if codeact_enable_llm_editor:
|
||||
tools.append(LLMBasedFileEditTool)
|
||||
else:
|
||||
tools.append(StrReplaceEditorTool)
|
||||
return tools
|
||||
@@ -0,0 +1,230 @@
|
||||
from opendevin.runtime.plugins import SWEAgentCommandsRequirement
|
||||
|
||||
_SWEAGENT_BASH_DOCS = '\n'.join(
|
||||
filter(
|
||||
lambda x: not x.startswith('submit'),
|
||||
SWEAgentCommandsRequirement.documentation.split('\n'),
|
||||
)
|
||||
)
|
||||
# _SWEAGENT_BASH_DOCS content below:
|
||||
"""
|
||||
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
|
||||
goto <line_number> - moves the window to show <line_number>
|
||||
scroll_down - moves the window down {WINDOW} lines
|
||||
scroll_up - moves the window down {WINDOW} lines
|
||||
create <filename> - creates and opens a new file with the given name
|
||||
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
|
||||
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
|
||||
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
|
||||
edit <start_line>:<end_line>
|
||||
<replacement_text>
|
||||
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
|
||||
"""
|
||||
|
||||
_COMMAND_DOCS = (
|
||||
'\nApart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:\n'
|
||||
f'{_SWEAGENT_BASH_DOCS}'
|
||||
"Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
|
||||
)
|
||||
|
||||
SYSTEM_MESSAGE = f"""A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
|
||||
<execute_ipython>
|
||||
print("Hello World!")
|
||||
</execute_ipython>
|
||||
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
|
||||
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
|
||||
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
|
||||
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
|
||||
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
|
||||
If the assistant encounters an import error in IPython for a newly installed package, they should try to restart the kernel and import the package again. IPython kernel can be re-started by:
|
||||
<execute_ipython>
|
||||
import IPython
|
||||
IPython.Application.instance().kernel.do_shutdown(True) # Restart the kernel
|
||||
</execute_ipython>
|
||||
|
||||
{_COMMAND_DOCS}
|
||||
|
||||
The assistant's response should be concise, but do express their thoughts.
|
||||
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
|
||||
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
--- START OF EXAMPLE ---
|
||||
|
||||
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
|
||||
|
||||
ASSISTANT:
|
||||
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
|
||||
<execute_ipython>
|
||||
CODE='''
|
||||
from flask import Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
numbers = list(range(1, 11))
|
||||
return str(numbers)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=5000)
|
||||
'''
|
||||
with open('app.py', 'w') as f:
|
||||
f.write(CODE)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[Code executed successfully with no output]
|
||||
|
||||
ASSISTANT:
|
||||
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
|
||||
<execute_bash>
|
||||
python3 app.py > server.log 2>&1 &
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
|
||||
|
||||
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
|
||||
<execute_bash>
|
||||
cat server.log
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
Traceback (most recent call last):
|
||||
File "/workspace/app.py", line 2, in <module>
|
||||
from flask import Flask
|
||||
ModuleNotFoundError: No module named 'flask'
|
||||
|
||||
ASSISTANT:
|
||||
It seems that Flask is not installed. Let me install Flask for you:
|
||||
<execute_bash>
|
||||
pip install flask
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
Defaulting to user installation because normal site-packages is not writeable
|
||||
Collecting flask
|
||||
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
|
||||
Collecting blinker>=1.6.2
|
||||
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
|
||||
Collecting Werkzeug>=3.0.0
|
||||
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
|
||||
Collecting click>=8.1.3
|
||||
Using cached click-8.1.7-py3-none-any.whl (97 kB)
|
||||
Collecting itsdangerous>=2.1.2
|
||||
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
|
||||
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
|
||||
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
|
||||
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
|
||||
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
|
||||
|
||||
ASSISTANT:
|
||||
Now that Flask is installed, let me run the Python file again:
|
||||
<execute_bash>
|
||||
python3 app.py > server.log 2>&1 &
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[1] 124
|
||||
|
||||
ASSISTANT:
|
||||
Let me check the server log again:
|
||||
<execute_bash>
|
||||
cat server.log
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
* Running on http://127.0.0.1:5000
|
||||
Press CTRL+C to quit
|
||||
|
||||
ASSISTANT:
|
||||
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
|
||||
|
||||
USER: Now kill the server, make it display the numbers in a table format.
|
||||
|
||||
ASSISTANT:
|
||||
Sure! Let me stop the server first:
|
||||
<execute_bash>
|
||||
kill 124
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[1]+ Terminated python3 app.py > server.log 2>&1
|
||||
|
||||
ASSISTANT:
|
||||
The server has been stopped. Let me open the Python file and modify it.
|
||||
<execute_bash>
|
||||
open app.py
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
[File: /workspace/app.py (11 lines total)]
|
||||
1:
|
||||
2:from flask import Flask
|
||||
3:app = Flask(__name__)
|
||||
4:
|
||||
5:@app.route('/')
|
||||
6:def index():
|
||||
7: numbers = list(range(1, 11))
|
||||
8: return str(numbers)
|
||||
9:
|
||||
10:if __name__ == '__main__':
|
||||
11: app.run(port=5000)
|
||||
|
||||
ASSISTANT:
|
||||
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
|
||||
<execute_bash>
|
||||
edit 8:8 <<EOF
|
||||
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
EOF
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[File: /workspace/app.py (11 lines total)]
|
||||
1:
|
||||
2:from flask import Flask
|
||||
3:app = Flask(__name__)
|
||||
4:
|
||||
5:@app.route('/')
|
||||
6:def index():
|
||||
7: numbers = list(range(1, 11))
|
||||
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
9:
|
||||
10:if __name__ == '__main__':
|
||||
11: app.run(port=5000)
|
||||
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
|
||||
|
||||
ASSISTANT:
|
||||
The file has been updated. Let me run the Python file again with the new changes:
|
||||
<execute_bash>
|
||||
python3 app.py > server.log 2>&1 &
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[1] 126
|
||||
|
||||
ASSISTANT:
|
||||
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
|
||||
|
||||
--- END OF EXAMPLE ---
|
||||
"""
|
||||
|
||||
INVALID_INPUT_MESSAGE = (
|
||||
"I don't understand your input. \n"
|
||||
'If you want to execute a bash command, please use <execute_bash> YOUR_COMMAND_HERE </execute_bash>.\n'
|
||||
'If you want to execute a block of Python code, please use <execute_ipython> YOUR_COMMAND_HERE </execute_ipython>.\n'
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
|
||||
<IMPORTANT>
|
||||
* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
|
||||
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* The assistant MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
|
||||
</IMPORTANT>
|
||||
@@ -1,4 +1,5 @@
|
||||
from openhands.agenthub.delegator_agent.agent import DelegatorAgent
|
||||
from openhands.controller.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import DelegatorAgent
|
||||
|
||||
Agent.register('DelegatorAgent', DelegatorAgent)
|
||||
|
||||
@@ -1,62 +1,55 @@
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.events.action import Action, AgentDelegateAction, AgentFinishAction
|
||||
from openhands.events.observation import AgentDelegateObservation, Observation
|
||||
from openhands.llm.llm import LLM
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action, AgentDelegateAction, AgentFinishAction
|
||||
from opendevin.events.observation import AgentDelegateObservation
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
|
||||
class DelegatorAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
The Delegator Agent is responsible for delegating tasks to other agents based on the current task.
|
||||
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
|
||||
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
|
||||
"""
|
||||
|
||||
current_delegate: str = ''
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
"""Initialize the Delegator Agent with an LLM
|
||||
def __init__(self, llm: LLM):
|
||||
"""
|
||||
Initialize the Delegator Agent with an LLM
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
super().__init__(llm)
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""Checks to see if current step is completed, returns AgentFinishAction if True.
|
||||
Otherwise, delegates the task to the next agent in the pipeline.
|
||||
"""
|
||||
Checks to see if current step is completed, returns AgentFinishAction if True.
|
||||
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
|
||||
|
||||
Parameters:
|
||||
- state (State): The current state given the previous actions and observations
|
||||
|
||||
Returns:
|
||||
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
|
||||
- AgentDelegateAction: The next agent to delegate the task to
|
||||
- Action: The next action to take based on llm response
|
||||
"""
|
||||
if self.current_delegate == '':
|
||||
self.current_delegate = 'study'
|
||||
task, _ = state.get_current_user_intent()
|
||||
return AgentDelegateAction(
|
||||
agent='StudyRepoForTaskAgent', inputs={'task': task}
|
||||
agent='StudyRepoForTaskAgent', inputs={'task': state.plan.main_goal}
|
||||
)
|
||||
|
||||
# last observation in history should be from the delegate
|
||||
last_observation = None
|
||||
for event in reversed(state.history):
|
||||
if isinstance(event, Observation):
|
||||
last_observation = event
|
||||
break
|
||||
|
||||
last_observation = state.history[-1][1]
|
||||
if not isinstance(last_observation, AgentDelegateObservation):
|
||||
raise Exception('Last observation is not an AgentDelegateObservation')
|
||||
|
||||
goal, _ = state.get_current_user_intent()
|
||||
if self.current_delegate == 'study':
|
||||
self.current_delegate = 'coder'
|
||||
return AgentDelegateAction(
|
||||
agent='CoderAgent',
|
||||
inputs={
|
||||
'task': goal,
|
||||
'task': state.plan.main_goal,
|
||||
'summary': last_observation.outputs['summary'],
|
||||
},
|
||||
)
|
||||
@@ -65,7 +58,7 @@ class DelegatorAgent(Agent):
|
||||
return AgentDelegateAction(
|
||||
agent='VerifierAgent',
|
||||
inputs={
|
||||
'task': goal,
|
||||
'task': state.plan.main_goal,
|
||||
},
|
||||
)
|
||||
elif self.current_delegate == 'verifier':
|
||||
@@ -79,9 +72,12 @@ class DelegatorAgent(Agent):
|
||||
return AgentDelegateAction(
|
||||
agent='CoderAgent',
|
||||
inputs={
|
||||
'task': goal,
|
||||
'task': state.plan.main_goal,
|
||||
'summary': last_observation.outputs['summary'],
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise Exception('Invalid delegate state')
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return []
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from openhands.agenthub.dummy_agent.agent import DummyAgent
|
||||
from openhands.controller.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import DummyAgent
|
||||
|
||||
Agent.register('DummyAgent', DummyAgent)
|
||||
|
||||
+77
-124
@@ -1,15 +1,14 @@
|
||||
from typing import TypedDict, Union
|
||||
import time
|
||||
from typing import TypedDict
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events.action import (
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AddTaskAction,
|
||||
AgentFinishAction,
|
||||
AgentRecallAction,
|
||||
AgentRejectAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
@@ -17,20 +16,21 @@ from openhands.events.action import (
|
||||
MessageAction,
|
||||
ModifyTaskAction,
|
||||
)
|
||||
from openhands.events.observation import (
|
||||
AgentStateChangedObservation,
|
||||
from opendevin.events.observation import (
|
||||
AgentRecallObservation,
|
||||
CmdOutputObservation,
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.llm.llm import LLM
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
"""
|
||||
FIXME: There are a few problems this surfaced
|
||||
* FileWrites seem to add an unintended newline at the end of the file
|
||||
* command_id is sometimes a number, sometimes a string
|
||||
* Why isn't the output of the background command split between two steps?
|
||||
* Browser not working
|
||||
"""
|
||||
|
||||
@@ -38,52 +38,45 @@ ActionObs = TypedDict(
|
||||
'ActionObs', {'action': Action, 'observations': list[Observation]}
|
||||
)
|
||||
|
||||
BACKGROUND_CMD = 'echo "This is in the background" && sleep .1 && echo "This too"'
|
||||
|
||||
|
||||
class DummyAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
|
||||
without making any LLM calls.
|
||||
"""
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
super().__init__(llm, config)
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
self.steps: list[ActionObs] = [
|
||||
{
|
||||
'action': AddTaskAction(
|
||||
parent='None', goal='check the current directory'
|
||||
),
|
||||
'observations': [],
|
||||
'action': AddTaskAction(parent='0', goal='check the current directory'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': AddTaskAction(parent='0', goal='run ls'),
|
||||
'observations': [],
|
||||
'action': AddTaskAction(parent='0.0', goal='run ls'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': ModifyTaskAction(task_id='0', state='in_progress'),
|
||||
'observations': [],
|
||||
'action': ModifyTaskAction(id='0.0', state='in_progress'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': MessageAction('Time to get started!'),
|
||||
'observations': [],
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': CmdRunAction(command='echo "foo"'),
|
||||
'observations': [
|
||||
CmdOutputObservation(
|
||||
'foo', command_id=-1, command='echo "foo"', exit_code=0
|
||||
)
|
||||
CmdOutputObservation('foo', command_id=-1, command='echo "foo"')
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': FileWriteAction(
|
||||
content='echo "Hello, World!"', path='hello.sh'
|
||||
),
|
||||
'observations': [
|
||||
FileWriteObservation(
|
||||
content='echo "Hello, World!"', path='hello.sh'
|
||||
)
|
||||
],
|
||||
'observations': [FileWriteObservation('', path='hello.sh')],
|
||||
},
|
||||
{
|
||||
'action': FileReadAction(path='hello.sh'),
|
||||
@@ -95,117 +88,77 @@ class DummyAgent(Agent):
|
||||
'action': CmdRunAction(command='bash hello.sh'),
|
||||
'observations': [
|
||||
CmdOutputObservation(
|
||||
'bash: hello.sh: No such file or directory',
|
||||
command_id=-1,
|
||||
command='bash workspace/hello.sh',
|
||||
exit_code=127,
|
||||
'Hello, World!', command_id=-1, command='bash hello.sh'
|
||||
)
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': CmdRunAction(command=BACKGROUND_CMD, background=True),
|
||||
'observations': [
|
||||
CmdOutputObservation(
|
||||
'Background command started. To stop it, send a `kill` action with id 42',
|
||||
command_id='42', # type: ignore[arg-type]
|
||||
command=BACKGROUND_CMD,
|
||||
),
|
||||
CmdOutputObservation(
|
||||
'This is in the background\nThis too\n',
|
||||
command_id='42', # type: ignore[arg-type]
|
||||
command=BACKGROUND_CMD,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': AgentRecallAction(query='who am I?'),
|
||||
'observations': [
|
||||
AgentRecallObservation('', memories=['I am a computer.']),
|
||||
# CmdOutputObservation('This too\n', command_id='42', command=BACKGROUND_CMD),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': BrowseURLAction(url='https://google.com'),
|
||||
'observations': [
|
||||
# BrowserOutputObservation('<html><body>Simulated Google page</body></html>',url='https://google.com',screenshot=''),
|
||||
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': BrowseInteractiveAction(
|
||||
browser_actions='goto("https://google.com")'
|
||||
),
|
||||
'observations': [
|
||||
# BrowserOutputObservation('<html><body>Simulated Google page after interaction</body></html>',url='https://google.com',screenshot=''),
|
||||
],
|
||||
'action': AgentFinishAction(),
|
||||
'observations': [],
|
||||
},
|
||||
{
|
||||
'action': AgentRejectAction(),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': AgentFinishAction(
|
||||
outputs={}, thought='Task completed', action='finish'
|
||||
),
|
||||
'observations': [AgentStateChangedObservation('', AgentState.FINISHED)],
|
||||
'observations': [],
|
||||
},
|
||||
]
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
if state.iteration >= len(self.steps):
|
||||
return AgentFinishAction()
|
||||
|
||||
current_step = self.steps[state.iteration]
|
||||
action = current_step['action']
|
||||
|
||||
# If the action is AddTaskAction or ModifyTaskAction, update the parent ID or task_id
|
||||
if isinstance(action, AddTaskAction):
|
||||
if action.parent == 'None':
|
||||
action.parent = '' # Root task has no parent
|
||||
elif action.parent == '0':
|
||||
action.parent = state.root_task.id
|
||||
elif action.parent.startswith('0.'):
|
||||
action.parent = f'{state.root_task.id}{action.parent[1:]}'
|
||||
elif isinstance(action, ModifyTaskAction):
|
||||
if action.task_id == '0':
|
||||
action.task_id = state.root_task.id
|
||||
elif action.task_id.startswith('0.'):
|
||||
action.task_id = f'{state.root_task.id}{action.task_id[1:]}'
|
||||
# Ensure the task_id doesn't start with a dot
|
||||
if action.task_id.startswith('.'):
|
||||
action.task_id = action.task_id[1:]
|
||||
elif isinstance(action, (BrowseURLAction, BrowseInteractiveAction)):
|
||||
try:
|
||||
return self.simulate_browser_action(action)
|
||||
except (
|
||||
Exception
|
||||
): # This could be a specific exception for browser unavailability
|
||||
return self.handle_browser_unavailable(action)
|
||||
|
||||
time.sleep(0.1)
|
||||
if state.iteration > 0:
|
||||
prev_step = self.steps[state.iteration - 1]
|
||||
|
||||
if 'observations' in prev_step and prev_step['observations']:
|
||||
if 'observations' in prev_step:
|
||||
expected_observations = prev_step['observations']
|
||||
hist_events = state.history[-len(expected_observations) :]
|
||||
|
||||
if len(hist_events) < len(expected_observations):
|
||||
print(
|
||||
f'Warning: Expected {len(expected_observations)} observations, but got {len(hist_events)}'
|
||||
)
|
||||
|
||||
for i in range(min(len(expected_observations), len(hist_events))):
|
||||
hist_obs = event_to_dict(hist_events[i])
|
||||
expected_obs = event_to_dict(expected_observations[i])
|
||||
|
||||
# Remove dynamic fields for comparison
|
||||
for obs in [hist_obs, expected_obs]:
|
||||
obs.pop('id', None)
|
||||
obs.pop('timestamp', None)
|
||||
obs.pop('cause', None)
|
||||
obs.pop('source', None)
|
||||
if 'extras' in obs:
|
||||
obs['extras'].pop('command_id', None)
|
||||
|
||||
hist_start = len(state.history) - len(expected_observations)
|
||||
for i in range(len(expected_observations)):
|
||||
hist_obs = state.history[hist_start + i][1].to_dict()
|
||||
expected_obs = expected_observations[i].to_dict()
|
||||
if (
|
||||
'command_id' in hist_obs['extras']
|
||||
and hist_obs['extras']['command_id'] != -1
|
||||
):
|
||||
del hist_obs['extras']['command_id']
|
||||
hist_obs['content'] = ''
|
||||
if (
|
||||
'command_id' in expected_obs['extras']
|
||||
and expected_obs['extras']['command_id'] != -1
|
||||
):
|
||||
del expected_obs['extras']['command_id']
|
||||
expected_obs['content'] = ''
|
||||
if hist_obs != expected_obs:
|
||||
print(
|
||||
f'Warning: Observation mismatch. Expected {expected_obs}, got {hist_obs}'
|
||||
)
|
||||
print('\nactual', hist_obs)
|
||||
print('\nexpect', expected_obs)
|
||||
assert (
|
||||
hist_obs == expected_obs
|
||||
), f'Expected observation {expected_obs}, got {hist_obs}'
|
||||
return self.steps[state.iteration]['action']
|
||||
|
||||
return action
|
||||
|
||||
def simulate_browser_action(
|
||||
self, action: Union[BrowseURLAction, BrowseInteractiveAction]
|
||||
) -> Action:
|
||||
# Instead of simulating, we'll reject the browser action
|
||||
return self.handle_browser_unavailable(action)
|
||||
|
||||
def handle_browser_unavailable(
|
||||
self, action: Union[BrowseURLAction, BrowseInteractiveAction]
|
||||
) -> Action:
|
||||
# Create a message action to inform that browsing is not available
|
||||
message = 'Browser actions are not available in the DummyAgent environment.'
|
||||
if isinstance(action, BrowseURLAction):
|
||||
message += f' Unable to browse URL: {action.url}'
|
||||
elif isinstance(action, BrowseInteractiveAction):
|
||||
message += (
|
||||
f' Unable to perform interactive browsing: {action.browser_actions}'
|
||||
)
|
||||
return MessageAction(content=message)
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return ['I am a computer.']
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
## Introduction
|
||||
|
||||
This package contains definitions of micro-agents. A micro-agent is defined
|
||||
in the following structure:
|
||||
|
||||
```
|
||||
[AgentName]
|
||||
├── agent.yaml
|
||||
└── prompt.md
|
||||
```
|
||||
|
||||
Note that `prompt.md` could use jinja2 template syntax. During runtime, `prompt.md`
|
||||
is loaded and rendered, and used together with `agent.yaml` to initialize a
|
||||
micro-agent.
|
||||
@@ -0,0 +1,2 @@
|
||||
* `browse` - opens a web page. Arguments:
|
||||
* `url` - the URL to open
|
||||
@@ -0,0 +1,3 @@
|
||||
* `delegate` - send a task to another agent from the list provided. Arguments:
|
||||
* `agent` - the agent to which the task is delegated. MUST match a name in the list of agents provided.
|
||||
* `inputs` - a dictionary of input parameters to the agent, as specified in the list
|
||||
@@ -0,0 +1,2 @@
|
||||
* `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working. Arguments:
|
||||
* `outputs` - a dictionary representing the outputs of your task, if any
|
||||
@@ -0,0 +1,2 @@
|
||||
* `kill` - kills a background command
|
||||
* `id` - the ID of the background command to kill
|
||||
@@ -0,0 +1,2 @@
|
||||
* `message` - make a plan, set a goal, or record your thoughts. Arguments:
|
||||
* `content` - the thought to record
|
||||
@@ -0,0 +1,2 @@
|
||||
* `read` - reads the content of a file. Arguments:
|
||||
* `path` - the path of the file to read
|
||||
@@ -0,0 +1,2 @@
|
||||
* `reject` - reject the task. Arguments:
|
||||
* `outputs` - a dictionary representing the outputs of your task, if any
|
||||
@@ -0,0 +1,3 @@
|
||||
* `run` - runs a command on the command line in a Linux shell. Arguments:
|
||||
* `command` - the command to run
|
||||
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
|
||||
@@ -0,0 +1,3 @@
|
||||
* `write` - writes the content to a file. Arguments:
|
||||
* `path` - the path of the file to write
|
||||
* `content` - the content to write to the file
|
||||
@@ -0,0 +1,5 @@
|
||||
Your response MUST be in JSON format. It must be an object, and it must contain two fields:
|
||||
* `action`, which is one of the actions specified here
|
||||
* `args`, which is a map of key-value pairs, specifying the arguments for that action
|
||||
|
||||
You MUST NOT include any other text besides the JSON response
|
||||
@@ -0,0 +1,4 @@
|
||||
Here is a recent history of actions you've taken in service of this plan,
|
||||
as well as observations you've made. This only includes the MOST RECENT
|
||||
actions and observations--more may have happened before that.
|
||||
They are time-ordered, with your most recent action at the bottom.
|
||||
+55
-51
@@ -1,59 +1,67 @@
|
||||
import json
|
||||
|
||||
from jinja2 import BaseLoader, Environment
|
||||
|
||||
from openhands.agenthub.micro.instructions import instructions
|
||||
from openhands.agenthub.micro.registry import all_microagents
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import Action
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.serialization.event import event_to_memory
|
||||
from openhands.llm.llm import LLM
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.exceptions import LLMOutputError
|
||||
from opendevin.events.action import Action, action_from_dict
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
from .instructions import instructions
|
||||
from .registry import all_microagents
|
||||
|
||||
|
||||
def parse_response(orig_response: str) -> Action:
|
||||
# attempt to load the JSON dict from the response
|
||||
action_dict = json.loads(orig_response)
|
||||
depth = 0
|
||||
start = -1
|
||||
for i, char in enumerate(orig_response):
|
||||
if char == '{':
|
||||
if depth == 0:
|
||||
start = i
|
||||
depth += 1
|
||||
elif char == '}':
|
||||
depth -= 1
|
||||
if depth == 0 and start != -1:
|
||||
response = orig_response[start : i + 1]
|
||||
try:
|
||||
action_dict = json.loads(response)
|
||||
action = action_from_dict(action_dict)
|
||||
return action
|
||||
except json.JSONDecodeError as e:
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON in response. Please make sure the response is a valid JSON object.'
|
||||
) from e
|
||||
raise LLMOutputError('No valid JSON object found in response.')
|
||||
|
||||
# load the action from the dict
|
||||
return action_from_dict(action_dict)
|
||||
|
||||
def my_encoder(obj):
|
||||
"""
|
||||
Encodes objects as dictionaries
|
||||
|
||||
Parameters:
|
||||
- obj (Object): An object that will be converted
|
||||
|
||||
Returns:
|
||||
- dict: If the object can be converted it is returned in dict format
|
||||
"""
|
||||
if hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
|
||||
|
||||
def to_json(obj, **kwargs):
|
||||
"""Serialize an object to str format"""
|
||||
return json.dumps(obj, **kwargs)
|
||||
"""
|
||||
Serialize an object to str format
|
||||
"""
|
||||
return json.dumps(obj, default=my_encoder, **kwargs)
|
||||
|
||||
|
||||
class MicroAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
prompt = ''
|
||||
agent_definition: dict = {}
|
||||
|
||||
def history_to_json(self, history: list[Event], max_events: int = 20, **kwargs):
|
||||
"""
|
||||
Serialize and simplify history to str format
|
||||
"""
|
||||
processed_history = []
|
||||
event_count = 0
|
||||
|
||||
for event in reversed(history):
|
||||
if event_count >= max_events:
|
||||
break
|
||||
processed_history.append(
|
||||
event_to_memory(event, self.llm.config.max_message_chars)
|
||||
)
|
||||
event_count += 1
|
||||
|
||||
# history is in reverse order, let's fix it
|
||||
processed_history.reverse()
|
||||
|
||||
return json.dumps(processed_history, **kwargs)
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
super().__init__(llm, config)
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
if 'name' not in self.agent_definition:
|
||||
raise ValueError('Agent definition must contain a name')
|
||||
self.prompt_template = Environment(loader=BaseLoader).from_string(self.prompt)
|
||||
@@ -61,22 +69,18 @@ class MicroAgent(Agent):
|
||||
del self.delegates[self.agent_definition['name']]
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
last_user_message, last_image_urls = state.get_current_user_intent()
|
||||
prompt = self.prompt_template.render(
|
||||
state=state,
|
||||
instructions=instructions,
|
||||
to_json=to_json,
|
||||
history_to_json=self.history_to_json,
|
||||
delegates=self.delegates,
|
||||
latest_user_message=last_user_message,
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if self.llm.vision_is_active() and last_image_urls:
|
||||
content.append(ImageContent(image_urls=last_image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(
|
||||
messages=self.llm.format_messages_for_llm(message),
|
||||
)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
state.num_of_chars += len(prompt) + len(action_resp)
|
||||
action = parse_response(action_resp)
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return []
|
||||
|
||||
@@ -2,5 +2,5 @@ name: CoderAgent
|
||||
description: Given a particular task, and a detailed description of the codebase, accomplishes the task
|
||||
inputs:
|
||||
task: string
|
||||
summary: string
|
||||
codebase_summary: string
|
||||
outputs: {}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Task
|
||||
You are a software engineer. You've inherited an existing codebase, which you
|
||||
need to modify to complete this task:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
|
||||
{% if state.inputs.summary %}
|
||||
Here's a summary of the codebase, as it relates to this task:
|
||||
|
||||
{{ state.inputs.summary }}
|
||||
{% endif %}
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
Do NOT finish until you have completed the tasks.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -0,0 +1,25 @@
|
||||
## Introduction
|
||||
|
||||
CommitWriterAgent can help write git commit message. Example:
|
||||
|
||||
```bash
|
||||
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_TYPE="exec" \
|
||||
poetry run python opendevin/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
|
||||
```
|
||||
|
||||
This agent is special in the sense that it doesn't need a task. Once called,
|
||||
it attempts to read all diff in the git staging area and write a good commit
|
||||
message.
|
||||
|
||||
## Future work
|
||||
|
||||
### Feedback loop
|
||||
|
||||
The commit message could be (optionally) shown to the customer or
|
||||
other agents, so that CommitWriterAgent could gather feedback to further
|
||||
improve the commit message.
|
||||
|
||||
### Task rejection
|
||||
|
||||
When the agent cannot compile a commit message (e.g. not git repository), it
|
||||
should reject the task with an explanation.
|
||||
@@ -3,4 +3,3 @@ description: "Write a git commit message for files in the git staging area"
|
||||
inputs: {}
|
||||
outputs:
|
||||
answer: string
|
||||
reason: string
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Task
|
||||
You are a responsible software engineer and always write good commit messages.
|
||||
|
||||
Please analyze the diff in the staging area, understand the context and content
|
||||
of the updates from the diff only. Identify key elements like:
|
||||
- Which files are affected?
|
||||
- What types of changes were made (e.g., new features, bug fixes, refactoring, documentation, testing)?
|
||||
|
||||
Then you should generate a commit message that succinctly summarizes the staged
|
||||
changes. The commit message should include:
|
||||
- A summary line that clearly states the purpose of the changes.
|
||||
- Optionally, a detailed description if the changes are complex or need further explanation.
|
||||
|
||||
You should find the diff using `git diff --cached`, compile a commit message,
|
||||
and call the `finish` action with `outputs.answer` set to the answer. If current
|
||||
repo is not a valid git repo, or there is no diff in the staging area, please call
|
||||
the `reject` action with `outputs.answer` set to the reason.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
If the last item in the history is an error, you should try to fix it.
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.reject }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -3,6 +3,4 @@ description: Delegates tasks to microagents based on their area of expertise
|
||||
generates: Action
|
||||
inputs:
|
||||
task: string
|
||||
outputs:
|
||||
summary: string # if finished
|
||||
reason: string # if rejected
|
||||
outputs: {}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Task
|
||||
You are in charge of accomplishing the following task:
|
||||
{{ state.plan.main_goal }}
|
||||
|
||||
In order to accomplish this goal, you must delegate tasks to one or more agents, who
|
||||
can do the actual work. A description of each agent is provided below. You MUST
|
||||
select one of the delegates below to move towards accomplishing the task, and you MUST
|
||||
provide the correct inputs for the delegate you select.
|
||||
|
||||
## Agents
|
||||
{% for name, details in delegates.items() %}
|
||||
### {{ name }}
|
||||
{{ details.description }}
|
||||
#### Inputs
|
||||
{{ to_json(details.inputs) }}
|
||||
{% endfor %}
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.delegate }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Task
|
||||
You are a brilliant mathematician and programmer. You've been given the following problem to solve:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
|
||||
Please write a python script that solves this problem, and prints the answer to stdout.
|
||||
ONLY print the answer to stdout, nothing else.
|
||||
You should then run the python script with `python3`,
|
||||
and call the `finish` action with `outputs.answer` set to the answer.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
If the last item in the history is an error, you should try to fix it.
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Task
|
||||
You are a database engineer. You are working on an existing Postgres project, and have been given
|
||||
the following task:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
|
||||
You must:
|
||||
* Investigate the existing migrations to understand the current schema
|
||||
* Write a new migration to accomplish the task above
|
||||
* Test that the migrations work properly
|
||||
|
||||
## Actions
|
||||
You may take any of the following actions:
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.run }}
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -4,10 +4,7 @@ import yaml
|
||||
|
||||
all_microagents = {}
|
||||
|
||||
# Get the list of directories and sort them to preserve determinism
|
||||
dirs = sorted(os.listdir(os.path.dirname(__file__)))
|
||||
|
||||
for dir in dirs:
|
||||
for dir in os.listdir(os.path.dirname(__file__)):
|
||||
base = os.path.dirname(__file__) + '/' + dir
|
||||
if os.path.isfile(base):
|
||||
continue
|
||||
@@ -16,7 +13,8 @@ for dir in dirs:
|
||||
promptFile = base + '/prompt.md'
|
||||
agentFile = base + '/agent.yaml'
|
||||
if not os.path.isfile(promptFile) or not os.path.isfile(agentFile):
|
||||
raise Exception(f'Missing prompt or agent file in {base}. Please create them.')
|
||||
raise Exception(
|
||||
f'Missing prompt or agent file in {base}. Please create them.')
|
||||
with open(promptFile, 'r') as f:
|
||||
prompt = f.read()
|
||||
with open(agentFile, 'r') as f:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Task
|
||||
You are a software engineer. You've inherited an existing codebase, which you're
|
||||
learning about for the first time. Your goal is to produce a detailed summary
|
||||
of the codebase, including:
|
||||
* The overall purpose of the project
|
||||
* The directory structure
|
||||
* The main components of the codebase
|
||||
* How the components fit together
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
You should ONLY `run` commands that have no side-effects, like `ls` and `grep`.
|
||||
|
||||
Do NOT finish until you have a complete understanding of the codebase.
|
||||
When you're done, put your summary into the output of the `finish` action.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Task
|
||||
You are a software engineer. You've inherited an existing codebase, which you're
|
||||
learning about for the first time. You need to study the codebase to find all
|
||||
the information needed to complete this task:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
You must ONLY `run` commands that have no side-effects, like `ls` and `grep`.
|
||||
|
||||
Do NOT finish until you have a complete understanding of which parts of the
|
||||
codebase are relevant to the task, including particular files, functions, and classes.
|
||||
When you're done, put your summary in `outputs.summary` in the `finish` action.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -1,6 +1,5 @@
|
||||
name: TypoFixerAgent
|
||||
description: Fixes typos in files in the current working directory
|
||||
inputs:
|
||||
task: string
|
||||
inputs: {}
|
||||
outputs:
|
||||
summary: string
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Task
|
||||
You are a proofreader tasked with fixing typos in the files in your current working directory. Your goal is to:
|
||||
1. Scan the files for typos
|
||||
2. Overwrite the files with the typos fixed
|
||||
3. Provide a summary of the typos fixed
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
To complete this task:
|
||||
1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
|
||||
2. Use the `think` action to analyze the contents and identify typos.
|
||||
3. Use the `write` action to create new versions of the files with the typos fixed.
|
||||
- Overwrite the original files with the corrected content. Make sure to provide the file path in the format `'./file_name.ext'`.
|
||||
4. Use the `think` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
|
||||
5. Use the `finish` action to return the summary in the `outputs.summary` field.
|
||||
|
||||
Do NOT finish until you have fixed all the typos and generated a summary.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-5:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
For example, if you want to use the read action to read the contents of a file named example.txt, your response should look like this:
|
||||
{
|
||||
"action": "read",
|
||||
"args": {
|
||||
"path": "./example.txt"
|
||||
}
|
||||
}
|
||||
|
||||
Similarly, if you want to use the write action to write content to a file named output.txt, your response should look like this:
|
||||
{
|
||||
"action": "write",
|
||||
"args": {
|
||||
"path": "./output.txt",
|
||||
"content": "This is the content to be written to the file."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Task
|
||||
You are a quality assurance engineer. Another engineer has made changes to the
|
||||
codebase which are supposed to solve this task:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
|
||||
Your goal is to verify that the changes are correct and bug-free.
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
You must ONLY `run` commands that have no side-effects, like `ls`, `grep`, and test scripts.
|
||||
|
||||
Do NOT finish until you know whether the task is complete and correct.
|
||||
When you're done, add a `completed` boolean to the `outputs` of the `finish` action.
|
||||
If `completed` is `false`, you MUST also provide a `summary` in the `outputs` of the `finish` action
|
||||
explaining what the problem is.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -0,0 +1,2 @@
|
||||
.envrc
|
||||
workspace
|
||||
@@ -0,0 +1,8 @@
|
||||
# LLM control loop
|
||||
This is currently a standalone utility. It will need to be integrated into OpenDevin's backend.
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
# Run this in project root
|
||||
./agenthub/monologue_agent/build-and-run.sh "write a bash script that prints 'hello world'"
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# TODO
|
||||
There's a lot of low-hanging fruit for this agent:
|
||||
|
||||
* Strip `<script>`, `<style>`, and other non-text tags from the HTML before sending it to the LLM
|
||||
* Keep track of the working directory when the agent uses `cd`
|
||||
* Improve memory condensing--condense earlier memories more aggressively
|
||||
* Limit the time that `run` can wait (in case agent runs an interactive command and it's hanging)
|
||||
* Figure out how to run background processes, e.g. `node server.js` to start a server
|
||||
@@ -0,0 +1,5 @@
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import MonologueAgent
|
||||
|
||||
Agent.register('MonologueAgent', MonologueAgent)
|
||||
@@ -0,0 +1,259 @@
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
from agenthub.monologue_agent.utils.monologue import Monologue
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.config import config
|
||||
from opendevin.core.exceptions import AgentNoInstructionError
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentRecallAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
NullAction,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
AgentRecallObservation,
|
||||
BrowserOutputObservation,
|
||||
CmdOutputObservation,
|
||||
FileReadObservation,
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
if config.agent.memory_enabled:
|
||||
from agenthub.monologue_agent.utils.memory import LongTermMemory
|
||||
|
||||
MAX_TOKEN_COUNT_PADDING = 512
|
||||
MAX_OUTPUT_LENGTH = 5000
|
||||
|
||||
INITIAL_THOUGHTS = [
|
||||
'I exist!',
|
||||
'Hmm...looks like I can type in a command line prompt',
|
||||
'Looks like I have a web browser too!',
|
||||
"Here's what I want to do: $TASK",
|
||||
'How am I going to get there though?',
|
||||
'It seems like I have some kind of short term memory.',
|
||||
'Each of my thoughts seems to be stored in a JSON array.',
|
||||
'It seems whatever I say next will be added as an object to the list.',
|
||||
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
|
||||
'Fortunately I have long term memory!',
|
||||
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
|
||||
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
|
||||
"Let's try it out!",
|
||||
'RECALL what it is I want to do',
|
||||
"Here's what I want to do: $TASK",
|
||||
'How am I going to get there though?',
|
||||
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
|
||||
'RUN echo "hello world"',
|
||||
'hello world',
|
||||
'Cool! I bet I can write files too using the write action.',
|
||||
'WRITE echo "console.log(\'hello world\')" > test.js',
|
||||
'',
|
||||
"I just created test.js. I'll try and run it now.",
|
||||
'RUN node test.js',
|
||||
'hello world',
|
||||
'It works!',
|
||||
"I'm going to try reading it now using the read action.",
|
||||
'READ test.js',
|
||||
"console.log('hello world')",
|
||||
'Nice! I can read files too!',
|
||||
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
|
||||
"Let's try that...",
|
||||
'BROWSE google.com',
|
||||
'<form><input type="text"></input><button type="submit"></button></form>',
|
||||
'I can browse the web too!',
|
||||
'And once I have completed my task, I can use the finish action to stop working.',
|
||||
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
|
||||
'Very cool. Now to accomplish my task.',
|
||||
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
|
||||
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
|
||||
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
|
||||
'It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself.',
|
||||
]
|
||||
|
||||
|
||||
class MonologueAgent(Agent):
|
||||
"""
|
||||
The Monologue Agent utilizes long and short term memory to complete tasks.
|
||||
Long term memory is stored as a LongTermMemory object and the model uses it to search for examples from the past.
|
||||
Short term memory is stored as a Monologue object and the model can condense it as necessary.
|
||||
"""
|
||||
|
||||
_initialized = False
|
||||
monologue: Monologue
|
||||
memory: 'LongTermMemory | None'
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
"""
|
||||
Initializes the Monologue Agent with an llm, monologue, and memory.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm)
|
||||
|
||||
def _add_event(self, event: dict):
|
||||
"""
|
||||
Adds a new event to the agent's monologue and memory.
|
||||
Monologue automatically condenses when it gets too large.
|
||||
|
||||
Parameters:
|
||||
- event (dict): The event that will be added to monologue and memory
|
||||
"""
|
||||
|
||||
if (
|
||||
'args' in event
|
||||
and 'output' in event['args']
|
||||
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
|
||||
):
|
||||
event['args']['output'] = (
|
||||
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
|
||||
)
|
||||
|
||||
self.monologue.add_event(event)
|
||||
if self.memory is not None:
|
||||
self.memory.add_event(event)
|
||||
|
||||
# Test monologue token length
|
||||
prompt = prompts.get_request_action_prompt(
|
||||
'',
|
||||
self.monologue.get_thoughts(),
|
||||
[],
|
||||
)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
token_count = self.llm.get_token_count(messages)
|
||||
|
||||
if token_count + MAX_TOKEN_COUNT_PADDING > self.llm.max_input_tokens:
|
||||
self.monologue.condense(self.llm)
|
||||
|
||||
def _initialize(self, task: str):
|
||||
"""
|
||||
Utilizes the INITIAL_THOUGHTS list to give the agent a context for its capabilities
|
||||
and how to navigate the WORKSPACE_MOUNT_PATH_IN_SANDBOX in `config` (e.g., /workspace by default).
|
||||
Short circuited to return when already initialized.
|
||||
Will execute again when called after reset.
|
||||
|
||||
Parameters:
|
||||
- task (str): The initial goal statement provided by the user
|
||||
|
||||
Raises:
|
||||
- AgentNoInstructionError: If task is not provided
|
||||
"""
|
||||
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if task is None or task == '':
|
||||
raise AgentNoInstructionError()
|
||||
|
||||
self.monologue = Monologue()
|
||||
if config.agent.memory_enabled:
|
||||
self.memory = LongTermMemory()
|
||||
else:
|
||||
self.memory = None
|
||||
|
||||
self._add_initial_thoughts(task)
|
||||
self._initialized = True
|
||||
|
||||
def _add_initial_thoughts(self, task):
|
||||
previous_action = ''
|
||||
for thought in INITIAL_THOUGHTS:
|
||||
thought = thought.replace('$TASK', task)
|
||||
if previous_action != '':
|
||||
observation: Observation = NullObservation(content='')
|
||||
if previous_action in {ActionType.RUN, ActionType.PUSH}:
|
||||
observation = CmdOutputObservation(
|
||||
content=thought, command_id=0, command=''
|
||||
)
|
||||
elif previous_action == ActionType.READ:
|
||||
observation = FileReadObservation(content=thought, path='')
|
||||
elif previous_action == ActionType.RECALL:
|
||||
observation = AgentRecallObservation(content=thought, memories=[])
|
||||
elif previous_action == ActionType.BROWSE:
|
||||
observation = BrowserOutputObservation(
|
||||
content=thought, url='', screenshot=''
|
||||
)
|
||||
self._add_event(observation.to_memory())
|
||||
previous_action = ''
|
||||
else:
|
||||
action: Action = NullAction()
|
||||
if thought.startswith('RUN'):
|
||||
command = thought.split('RUN ')[1]
|
||||
action = CmdRunAction(command)
|
||||
previous_action = ActionType.RUN
|
||||
elif thought.startswith('WRITE'):
|
||||
parts = thought.split('WRITE ')[1].split(' > ')
|
||||
path = parts[1]
|
||||
content = parts[0]
|
||||
action = FileWriteAction(path=path, content=content)
|
||||
elif thought.startswith('READ'):
|
||||
path = thought.split('READ ')[1]
|
||||
action = FileReadAction(path=path)
|
||||
previous_action = ActionType.READ
|
||||
elif thought.startswith('RECALL'):
|
||||
query = thought.split('RECALL ')[1]
|
||||
action = AgentRecallAction(query=query)
|
||||
previous_action = ActionType.RECALL
|
||||
elif thought.startswith('BROWSE'):
|
||||
url = thought.split('BROWSE ')[1]
|
||||
action = BrowseURLAction(url=url)
|
||||
previous_action = ActionType.BROWSE
|
||||
else:
|
||||
action = MessageAction(thought)
|
||||
self._add_event(action.to_memory())
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
Modifies the current state by adding the most recent actions and observations, then prompts the model to think about it's next action to take using monologue, memory, and hint.
|
||||
|
||||
Parameters:
|
||||
- state (State): The current state based on previous steps taken
|
||||
|
||||
Returns:
|
||||
- Action: The next action to take based on LLM response
|
||||
"""
|
||||
self._initialize(state.plan.main_goal)
|
||||
for prev_action, obs in state.updated_info:
|
||||
self._add_event(prev_action.to_memory())
|
||||
self._add_event(obs.to_memory())
|
||||
|
||||
state.updated_info = []
|
||||
|
||||
prompt = prompts.get_request_action_prompt(
|
||||
state.plan.main_goal,
|
||||
self.monologue.get_thoughts(),
|
||||
state.background_commands_obs,
|
||||
)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
state.num_of_chars += len(prompt) + len(action_resp)
|
||||
action = prompts.parse_action_response(action_resp)
|
||||
self.latest_action = action
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
"""
|
||||
Uses VectorIndexRetriever to find related memories within the long term memory.
|
||||
Uses search to produce top 10 results.
|
||||
|
||||
Parameters:
|
||||
- query (str): The query that we want to find related memories for
|
||||
|
||||
Returns:
|
||||
- list[str]: A list of top 10 text results that matched the query
|
||||
"""
|
||||
if self.memory is None:
|
||||
return []
|
||||
return self.memory.search(query)
|
||||
|
||||
def reset(self) -> None:
|
||||
super().reset()
|
||||
|
||||
# Reset the initial monologue and memory
|
||||
self._initialized = False
|
||||
@@ -0,0 +1,38 @@
|
||||
import json
|
||||
|
||||
from json_repair import repair_json
|
||||
|
||||
|
||||
def my_encoder(obj):
|
||||
"""
|
||||
Encodes objects as dictionaries
|
||||
|
||||
Parameters:
|
||||
- obj (Object): An object that will be converted
|
||||
|
||||
Returns:
|
||||
- dict: If the object can be converted it is returned in dict format
|
||||
"""
|
||||
if hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
|
||||
|
||||
def dumps(obj, **kwargs):
|
||||
"""
|
||||
Serialize an object to str format
|
||||
"""
|
||||
|
||||
return json.dumps(obj, default=my_encoder, **kwargs)
|
||||
|
||||
|
||||
def loads(s, **kwargs):
|
||||
"""
|
||||
Create a JSON object from str
|
||||
"""
|
||||
json_start = s.find('{')
|
||||
json_end = s.rfind('}') + 1
|
||||
if json_start == -1 or json_end == -1:
|
||||
raise ValueError('Invalid response: no JSON found')
|
||||
s = s[json_start:json_end]
|
||||
s = repair_json(s)
|
||||
return json.loads(s, **kwargs)
|
||||
@@ -0,0 +1,181 @@
|
||||
import threading
|
||||
|
||||
import chromadb
|
||||
import llama_index.embeddings.openai.base as llama_openai
|
||||
from llama_index.core import Document, VectorStoreIndex
|
||||
from llama_index.core.retrievers import VectorIndexRetriever
|
||||
from llama_index.vector_stores.chroma import ChromaVectorStore
|
||||
from openai._exceptions import APIConnectionError, InternalServerError, RateLimitError
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
from opendevin.core.config import config
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
from . import json
|
||||
|
||||
num_retries = config.llm.num_retries
|
||||
retry_min_wait = config.llm.retry_min_wait
|
||||
retry_max_wait = config.llm.retry_max_wait
|
||||
|
||||
# llama-index includes a retry decorator around openai.get_embeddings() function
|
||||
# it is initialized with hard-coded values and errors
|
||||
# this non-customizable behavior is creating issues when it's retrying faster than providers' rate limits
|
||||
# this block attempts to banish it and replace it with our decorator, to allow users to set their own limits
|
||||
|
||||
if hasattr(llama_openai.get_embeddings, '__wrapped__'):
|
||||
original_get_embeddings = llama_openai.get_embeddings.__wrapped__
|
||||
else:
|
||||
logger.warning('Cannot set custom retry limits.')
|
||||
num_retries = 1
|
||||
original_get_embeddings = llama_openai.get_embeddings
|
||||
|
||||
|
||||
def attempt_on_error(retry_state):
|
||||
logger.error(
|
||||
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.',
|
||||
exc_info=False,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_attempt(num_retries),
|
||||
wait=wait_random_exponential(min=retry_min_wait, max=retry_max_wait),
|
||||
retry=retry_if_exception_type(
|
||||
(RateLimitError, APIConnectionError, InternalServerError)
|
||||
),
|
||||
after=attempt_on_error,
|
||||
)
|
||||
def wrapper_get_embeddings(*args, **kwargs):
|
||||
return original_get_embeddings(*args, **kwargs)
|
||||
|
||||
|
||||
llama_openai.get_embeddings = wrapper_get_embeddings
|
||||
|
||||
|
||||
class EmbeddingsLoader:
|
||||
"""Loader for embedding model initialization."""
|
||||
|
||||
@staticmethod
|
||||
def get_embedding_model(strategy: str):
|
||||
supported_ollama_embed_models = [
|
||||
'llama2',
|
||||
'mxbai-embed-large',
|
||||
'nomic-embed-text',
|
||||
'all-minilm',
|
||||
'stable-code',
|
||||
]
|
||||
if strategy in supported_ollama_embed_models:
|
||||
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||
|
||||
return OllamaEmbedding(
|
||||
model_name=strategy,
|
||||
base_url=config.llm.embedding_base_url,
|
||||
ollama_additional_kwargs={'mirostat': 0},
|
||||
)
|
||||
elif strategy == 'openai':
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
|
||||
return OpenAIEmbedding(
|
||||
model='text-embedding-ada-002',
|
||||
api_key=config.llm.api_key,
|
||||
)
|
||||
elif strategy == 'azureopenai':
|
||||
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
||||
|
||||
return AzureOpenAIEmbedding(
|
||||
model='text-embedding-ada-002',
|
||||
deployment_name=config.llm.embedding_deployment_name,
|
||||
api_key=config.llm.api_key,
|
||||
azure_endpoint=config.llm.base_url,
|
||||
api_version=config.llm.api_version,
|
||||
)
|
||||
elif (strategy is not None) and (strategy.lower() == 'none'):
|
||||
# TODO: this works but is not elegant enough. The incentive is when
|
||||
# monologue agent is not used, there is no reason we need to initialize an
|
||||
# embedding model
|
||||
return None
|
||||
else:
|
||||
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
||||
|
||||
return HuggingFaceEmbedding(model_name='BAAI/bge-small-en-v1.5')
|
||||
|
||||
|
||||
sema = threading.Semaphore(value=config.agent.memory_max_threads)
|
||||
|
||||
|
||||
class LongTermMemory:
|
||||
"""
|
||||
Handles storing information for the agent to access later, using chromadb.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the chromadb and set up ChromaVectorStore for later use.
|
||||
"""
|
||||
db = chromadb.Client(chromadb.Settings(anonymized_telemetry=False))
|
||||
self.collection = db.get_or_create_collection(name='memories')
|
||||
vector_store = ChromaVectorStore(chroma_collection=self.collection)
|
||||
embedding_strategy = config.llm.embedding_model
|
||||
embed_model = EmbeddingsLoader.get_embedding_model(embedding_strategy)
|
||||
self.index = VectorStoreIndex.from_vector_store(vector_store, embed_model)
|
||||
self.thought_idx = 0
|
||||
self._add_threads = []
|
||||
|
||||
def add_event(self, event: dict):
|
||||
"""
|
||||
Adds a new event to the long term memory with a unique id.
|
||||
|
||||
Parameters:
|
||||
- event (dict): The new event to be added to memory
|
||||
"""
|
||||
id = ''
|
||||
t = ''
|
||||
if 'action' in event:
|
||||
t = 'action'
|
||||
id = event['action']
|
||||
elif 'observation' in event:
|
||||
t = 'observation'
|
||||
id = event['observation']
|
||||
doc = Document(
|
||||
text=json.dumps(event),
|
||||
doc_id=str(self.thought_idx),
|
||||
extra_info={
|
||||
'type': t,
|
||||
'id': id,
|
||||
'idx': self.thought_idx,
|
||||
},
|
||||
)
|
||||
self.thought_idx += 1
|
||||
logger.debug('Adding %s event to memory: %d', t, self.thought_idx)
|
||||
thread = threading.Thread(target=self._add_doc, args=(doc,))
|
||||
self._add_threads.append(thread)
|
||||
thread.start() # We add the doc concurrently so we don't have to wait ~500ms for the insert
|
||||
|
||||
def _add_doc(self, doc):
|
||||
with sema:
|
||||
self.index.insert(doc)
|
||||
|
||||
def search(self, query: str, k: int = 10):
|
||||
"""
|
||||
Searches through the current memory using VectorIndexRetriever
|
||||
|
||||
Parameters:
|
||||
- query (str): A query to match search results to
|
||||
- k (int): Number of top results to return
|
||||
|
||||
Returns:
|
||||
- list[str]: list of top k results found in current memory
|
||||
"""
|
||||
retriever = VectorIndexRetriever(
|
||||
index=self.index,
|
||||
similarity_top_k=k,
|
||||
)
|
||||
results = retriever.retrieve(query)
|
||||
return [r.get_text() for r in results]
|
||||
@@ -0,0 +1,79 @@
|
||||
import agenthub.monologue_agent.utils.json as json
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
from opendevin.core.exceptions import AgentEventTypeError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
|
||||
class Monologue:
|
||||
"""
|
||||
The monologue is a representation for the agent's internal monologue where it can think.
|
||||
The agent has the capability of using this monologue for whatever it wants.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the empty list of thoughts
|
||||
"""
|
||||
self.thoughts = []
|
||||
|
||||
def add_event(self, t: dict):
|
||||
"""
|
||||
Adds an event to memory if it is a valid event.
|
||||
|
||||
Parameters:
|
||||
- t (dict): The thought that we want to add to memory
|
||||
|
||||
Raises:
|
||||
- AgentEventTypeError: If t is not a dict
|
||||
"""
|
||||
if not isinstance(t, dict):
|
||||
raise AgentEventTypeError()
|
||||
self.thoughts.append(t)
|
||||
|
||||
def get_thoughts(self):
|
||||
"""
|
||||
Get the current thoughts of the agent.
|
||||
|
||||
Returns:
|
||||
- list: The list of thoughts that the agent has.
|
||||
"""
|
||||
return self.thoughts
|
||||
|
||||
def get_total_length(self):
|
||||
"""
|
||||
Gives the total number of characters in all thoughts
|
||||
|
||||
Returns:
|
||||
- Int: Total number of chars in thoughts.
|
||||
"""
|
||||
total_length = 0
|
||||
for t in self.thoughts:
|
||||
try:
|
||||
total_length += len(json.dumps(t))
|
||||
except TypeError as e:
|
||||
logger.error('Error serializing thought: %s', str(e), exc_info=False)
|
||||
return total_length
|
||||
|
||||
def condense(self, llm: LLM):
|
||||
"""
|
||||
Attempts to condense the monologue by using the llm
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): llm to be used for summarization
|
||||
|
||||
Raises:
|
||||
- Exception: the same exception as it got from the llm or processing the response
|
||||
"""
|
||||
|
||||
try:
|
||||
prompt = prompts.get_summarize_monologue_prompt(self.thoughts)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = llm.completion(messages=messages)
|
||||
summary_resp = resp['choices'][0]['message']['content']
|
||||
self.thoughts = prompts.parse_summary_response(summary_resp)
|
||||
except Exception as e:
|
||||
logger.error('Error condensing thoughts: %s', str(e), exc_info=False)
|
||||
|
||||
# TODO If the llm fails with ContextWindowExceededError, we can try to condense the monologue chunk by chunk
|
||||
raise
|
||||
@@ -0,0 +1,216 @@
|
||||
import re
|
||||
from json import JSONDecodeError
|
||||
|
||||
from opendevin.core.config import config
|
||||
from opendevin.core.exceptions import LLMOutputError
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
action_from_dict,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
CmdOutputObservation,
|
||||
)
|
||||
|
||||
from . import json
|
||||
|
||||
ACTION_PROMPT = """
|
||||
You're a thoughtful robot. Your main task is this:
|
||||
%(task)s
|
||||
|
||||
Don't expand the scope of your task--just complete it as written.
|
||||
|
||||
This is your internal monologue, in JSON format:
|
||||
|
||||
%(monologue)s
|
||||
|
||||
|
||||
Your most recent thought is at the bottom of that monologue. Continue your train of thought.
|
||||
What is your next single thought or action? Your response must be in JSON format.
|
||||
It must be a single object, and it must contain two fields:
|
||||
* `action`, which is one of the actions below
|
||||
* `args`, which is a map of key-value pairs, specifying the arguments for that action
|
||||
|
||||
Here are the possible actions:
|
||||
* `read` - reads the content of a file. Arguments:
|
||||
* `path` - the path of the file to read
|
||||
* `write` - writes the content to a file. Arguments:
|
||||
* `path` - the path of the file to write
|
||||
* `content` - the content to write to the file
|
||||
* `run` - runs a command. Arguments:
|
||||
* `command` - the command to run
|
||||
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
|
||||
* `kill` - kills a background command
|
||||
* `id` - the ID of the background command to kill
|
||||
* `browse` - opens a web page. Arguments:
|
||||
* `url` - the URL to open
|
||||
* `push` - Push a branch from the current repo to github:
|
||||
* `owner` - the owner of the repo to push to
|
||||
* `repo` - the name of the repo to push to
|
||||
* `branch` - the name of the branch to push
|
||||
* `recall` - recalls a past memory. Arguments:
|
||||
* `query` - the query to search for
|
||||
* `message` - make a plan, set a goal, or record your thoughts. Arguments:
|
||||
* `content` - the message to record
|
||||
* `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
|
||||
|
||||
%(background_commands)s
|
||||
|
||||
You MUST take time to think in between read, write, run, browse, push, and recall actions--do this with the `message` action.
|
||||
You should never act twice in a row without thinking. But if your last several
|
||||
actions are all `message` actions, you should consider taking a different action.
|
||||
|
||||
Notes:
|
||||
* you are logged in as %(user)s, but sudo will always work without a password.
|
||||
* all non-background commands will be forcibly stopped if they remain running for over %(timeout)s seconds.
|
||||
* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
|
||||
* don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
|
||||
* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
|
||||
* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
|
||||
* whenever an action fails, always send a `message` about why it may have happened before acting again.
|
||||
|
||||
What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.
|
||||
|
||||
%(hint)s
|
||||
"""
|
||||
|
||||
MONOLOGUE_SUMMARY_PROMPT = """
|
||||
Below is the internal monologue of an automated LLM agent. Each
|
||||
thought is an item in a JSON array. The thoughts may be memories,
|
||||
actions taken by the agent, or outputs from those actions.
|
||||
Please return a new, smaller JSON array, which summarizes the
|
||||
internal monologue. You can summarize individual thoughts, and
|
||||
you can condense related thoughts together with a description
|
||||
of their content.
|
||||
|
||||
%(monologue)s
|
||||
|
||||
Make the summaries as pithy and informative as possible.
|
||||
Be specific about what happened and what was learned. The summary
|
||||
will be used as keywords for searching for the original memory.
|
||||
Be sure to preserve any key words or important information.
|
||||
|
||||
Your response must be in JSON format. It must be an object with the
|
||||
key `new_monologue`, which is a JSON array containing the summarized monologue.
|
||||
Each entry in the array must have an `action` key, and an `args` key.
|
||||
The action key may be `summarize`, and `args.summary` should contain the summary.
|
||||
You can also use the same action and args from the source monologue.
|
||||
"""
|
||||
|
||||
|
||||
def get_summarize_monologue_prompt(thoughts: list[dict]):
|
||||
"""
|
||||
Gets the prompt for summarizing the monologue
|
||||
|
||||
Returns:
|
||||
- str: A formatted string with the current monologue within the prompt
|
||||
"""
|
||||
return MONOLOGUE_SUMMARY_PROMPT % {
|
||||
'monologue': json.dumps({'old_monologue': thoughts}, indent=2),
|
||||
}
|
||||
|
||||
|
||||
def get_request_action_prompt(
|
||||
task: str,
|
||||
thoughts: list[dict],
|
||||
background_commands_obs: list[CmdOutputObservation] = [],
|
||||
):
|
||||
"""
|
||||
Gets the action prompt formatted with appropriate values.
|
||||
|
||||
Parameters:
|
||||
- task (str): The current task the agent is trying to accomplish
|
||||
- thoughts (list[dict]): The agent's current thoughts
|
||||
- background_commands_obs (list[CmdOutputObservation]): list of all observed background commands running
|
||||
|
||||
Returns:
|
||||
- str: Formatted prompt string with hint, task, monologue, and background included
|
||||
"""
|
||||
|
||||
hint = ''
|
||||
if len(thoughts) > 0:
|
||||
latest_thought = thoughts[-1]
|
||||
if 'action' in latest_thought:
|
||||
if latest_thought['action'] == 'message':
|
||||
if latest_thought['args']['content'].startswith('OK so my task is'):
|
||||
hint = "You're just getting started! What should you do first?"
|
||||
else:
|
||||
hint = "You've been thinking a lot lately. Maybe it's time to take action?"
|
||||
elif latest_thought['action'] == 'error':
|
||||
hint = 'Looks like that last command failed. Maybe you need to fix it, or try something else.'
|
||||
|
||||
bg_commands_message = ''
|
||||
if len(background_commands_obs) > 0:
|
||||
bg_commands_message = 'The following commands are running in the background:'
|
||||
for command_obs in background_commands_obs:
|
||||
bg_commands_message += (
|
||||
f'\n`{command_obs.command_id}`: {command_obs.command}'
|
||||
)
|
||||
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
|
||||
|
||||
user = 'opendevin' if config.run_as_devin else 'root'
|
||||
|
||||
return ACTION_PROMPT % {
|
||||
'task': task,
|
||||
'monologue': json.dumps(thoughts, indent=2),
|
||||
'background_commands': bg_commands_message,
|
||||
'hint': hint,
|
||||
'user': user,
|
||||
'timeout': config.sandbox_timeout,
|
||||
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
|
||||
}
|
||||
|
||||
|
||||
def parse_action_response(response: str) -> Action:
|
||||
"""
|
||||
Parses a string to find an action within it
|
||||
|
||||
Parameters:
|
||||
- response (str): The string to be parsed
|
||||
|
||||
Returns:
|
||||
- Action: The action that was found in the response string
|
||||
"""
|
||||
try:
|
||||
action_dict = json.loads(response)
|
||||
except JSONDecodeError:
|
||||
# Find response-looking json in the output and use the more promising one. Helps with weak llms
|
||||
response_json_matches = re.finditer(
|
||||
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
|
||||
response,
|
||||
) # Find all response-looking strings
|
||||
|
||||
def rank(match):
|
||||
return (
|
||||
len(match[2]) if match[1] == 'message' else 130
|
||||
) # Crudely rank multiple responses by length
|
||||
|
||||
try:
|
||||
action_dict = json.loads(
|
||||
max(response_json_matches, key=rank)[0]
|
||||
) # Use the highest ranked response
|
||||
except (ValueError, JSONDecodeError):
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
|
||||
)
|
||||
if 'content' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict['contents'] = action_dict.pop('content')
|
||||
return action_from_dict(action_dict)
|
||||
|
||||
|
||||
def parse_summary_response(response: str) -> list[dict]:
|
||||
"""
|
||||
Parses a summary of the monologue
|
||||
|
||||
Parameters:
|
||||
- response (str): The response string to be parsed
|
||||
|
||||
Returns:
|
||||
- list[dict]: The list of summaries output by the model
|
||||
"""
|
||||
parsed = json.loads(response)
|
||||
return parsed['new_monologue']
|
||||
@@ -1,4 +1,5 @@
|
||||
from openhands.agenthub.planner_agent.agent import PlannerAgent
|
||||
from openhands.controller.agent import Agent
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .agent import PlannerAgent
|
||||
|
||||
Agent.register('PlannerAgent', PlannerAgent)
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
from openhands.agenthub.planner_agent.prompt import get_prompt_and_images
|
||||
from openhands.agenthub.planner_agent.response_parser import PlannerResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import Action, AgentFinishAction
|
||||
from openhands.llm.llm import LLM
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action, AgentFinishAction
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
from .prompt import get_prompt, parse_response
|
||||
|
||||
|
||||
class PlannerAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
|
||||
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
|
||||
"""
|
||||
response_parser = PlannerResponseParser()
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
"""Initialize the Planner Agent with an LLM
|
||||
def __init__(self, llm: LLM):
|
||||
"""
|
||||
Initialize the Planner Agent with an LLM
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
super().__init__(llm)
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""Checks to see if current step is completed, returns AgentFinishAction if True.
|
||||
"""
|
||||
Checks to see if current step is completed, returns AgentFinishAction if True.
|
||||
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
|
||||
|
||||
Parameters:
|
||||
@@ -35,19 +33,16 @@ class PlannerAgent(Agent):
|
||||
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
|
||||
- Action: The next action to take based on llm response
|
||||
"""
|
||||
if state.root_task.state in [
|
||||
'completed',
|
||||
'verified',
|
||||
'abandoned',
|
||||
]:
|
||||
return AgentFinishAction()
|
||||
|
||||
prompt, image_urls = get_prompt_and_images(
|
||||
state, self.llm.config.max_message_chars
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if self.llm.vision_is_active() and image_urls:
|
||||
content.append(ImageContent(image_urls=image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(messages=self.llm.format_messages_for_llm(message))
|
||||
return self.response_parser.parse(resp)
|
||||
if state.plan.task.state in ['completed', 'verified', 'abandoned']:
|
||||
return AgentFinishAction()
|
||||
prompt = get_prompt(state.plan, state.history)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
state.num_of_chars += len(prompt) + len(action_resp)
|
||||
action = parse_response(action_resp)
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return []
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema import ActionType
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import (
|
||||
import json
|
||||
|
||||
from opendevin.controller.state.plan import Plan
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
NullAction,
|
||||
action_from_dict,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.serialization.event import event_to_memory
|
||||
|
||||
HISTORY_SIZE = 20
|
||||
HISTORY_SIZE = 10
|
||||
|
||||
prompt = """
|
||||
# Task
|
||||
@@ -74,21 +78,23 @@ It must be an object, and it must contain two fields:
|
||||
* `content` - the content to write to the file
|
||||
* `run` - runs a command on the command line in a Linux shell. Arguments:
|
||||
* `command` - the command to run
|
||||
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
|
||||
* `kill` - kills a background command
|
||||
* `id` - the ID of the background command to kill
|
||||
* `browse` - opens a web page. Arguments:
|
||||
* `url` - the URL to open
|
||||
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
|
||||
* `message` - make a plan, set a goal, or record your thoughts. Arguments:
|
||||
* `content` - the message to record
|
||||
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
|
||||
* `add_task` - add a task to your plan. Arguments:
|
||||
* `parent` - the ID of the parent task (leave empty if it should go at the top level)
|
||||
* `parent` - the ID of the parent task
|
||||
* `goal` - the goal of the task
|
||||
* `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
|
||||
* `modify_task` - close a task. Arguments:
|
||||
* `task_id` - the ID of the task to close
|
||||
* `id` - the ID of the task to close
|
||||
* `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
|
||||
* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
|
||||
|
||||
You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
|
||||
You MUST take time to think in between read, write, run, browse, and recall actions--do this with the `message` action.
|
||||
You should never act twice in a row without thinking. But if your last several
|
||||
actions are all `message` actions, you should consider taking a different action.
|
||||
|
||||
@@ -100,6 +106,7 @@ What is your next thought or action? Again, you must reply with JSON, and only w
|
||||
|
||||
def get_hint(latest_action_id: str) -> str:
|
||||
"""Returns action type hint based on given action_id"""
|
||||
|
||||
hints = {
|
||||
'': "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with.",
|
||||
ActionType.RUN: 'You should think about the command you just ran, what output it gave, and how that affects your plan.',
|
||||
@@ -107,6 +114,7 @@ def get_hint(latest_action_id: str) -> str:
|
||||
ActionType.WRITE: 'You just changed a file. You should think about how it affects your plan.',
|
||||
ActionType.BROWSE: 'You should think about the page you just visited, and what you learned from it.',
|
||||
ActionType.MESSAGE: "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action.",
|
||||
ActionType.RECALL: 'You should think about the information you just recalled, and how it should affect your plan.',
|
||||
ActionType.ADD_TASK: 'You should think about the next action to take.',
|
||||
ActionType.MODIFY_TASK: 'You should think about the next action to take.',
|
||||
ActionType.SUMMARIZE: '',
|
||||
@@ -115,74 +123,62 @@ def get_hint(latest_action_id: str) -> str:
|
||||
return hints.get(latest_action_id, '')
|
||||
|
||||
|
||||
def get_prompt_and_images(
|
||||
state: State, max_message_chars: int
|
||||
) -> tuple[str, list[str] | None]:
|
||||
"""Gets the prompt for the planner agent.
|
||||
|
||||
def get_prompt(plan: Plan, history: list[tuple[Action, Observation]]) -> str:
|
||||
"""
|
||||
Gets the prompt for the planner agent.
|
||||
Formatted with the most recent action-observation pairs, current task, and hint based on last action
|
||||
|
||||
Parameters:
|
||||
- state (State): The state of the current agent
|
||||
- plan (Plan): The original plan outlined by the user with LLM defined tasks
|
||||
- history (list[tuple[Action, Observation]]): list of corresponding action-observation pairs
|
||||
|
||||
Returns:
|
||||
- str: The formatted string prompt with historical values
|
||||
"""
|
||||
# the plan
|
||||
plan_str = json.dumps(state.root_task.to_dict(), indent=2)
|
||||
|
||||
# the history
|
||||
plan_str = json.dumps(plan.task.to_dict(), indent=2)
|
||||
sub_history = history[-HISTORY_SIZE:]
|
||||
history_dicts = []
|
||||
latest_action: Action = NullAction()
|
||||
|
||||
# retrieve the latest HISTORY_SIZE events
|
||||
for event_count, event in enumerate(reversed(state.history)):
|
||||
if event_count >= HISTORY_SIZE:
|
||||
break
|
||||
if latest_action == NullAction() and isinstance(event, Action):
|
||||
latest_action = event
|
||||
history_dicts.append(event_to_memory(event, max_message_chars))
|
||||
|
||||
# history_dicts is in reverse order, lets fix it
|
||||
history_dicts.reverse()
|
||||
|
||||
# and get it as a JSON string
|
||||
for action, observation in sub_history:
|
||||
if not isinstance(action, NullAction):
|
||||
history_dicts.append(action.to_memory())
|
||||
latest_action = action
|
||||
if not isinstance(observation, NullObservation):
|
||||
observation_dict = observation.to_memory()
|
||||
history_dicts.append(observation_dict)
|
||||
history_str = json.dumps(history_dicts, indent=2)
|
||||
|
||||
# the plan status
|
||||
current_task = state.root_task.get_current_task()
|
||||
current_task = plan.get_current_task()
|
||||
if current_task is not None:
|
||||
plan_status = f"You're currently working on this task:\n{current_task.goal}."
|
||||
if len(current_task.subtasks) == 0:
|
||||
plan_status += "\nIf it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW."
|
||||
else:
|
||||
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
|
||||
|
||||
# the hint, based on the last action
|
||||
hint = get_hint(event_to_memory(latest_action, max_message_chars).get('action', ''))
|
||||
logger.debug('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
|
||||
|
||||
# the last relevant user message (the task)
|
||||
message, image_urls = state.get_current_user_intent()
|
||||
|
||||
# finally, fill in the prompt
|
||||
hint = get_hint(latest_action.to_dict()['action'])
|
||||
logger.info('HINT:\n' + hint, extra={'msg_type': 'INFO'})
|
||||
return prompt % {
|
||||
'task': message,
|
||||
'task': plan.main_goal,
|
||||
'plan': plan_str,
|
||||
'history': history_str,
|
||||
'hint': hint,
|
||||
'plan_status': plan_status,
|
||||
}, image_urls
|
||||
}
|
||||
|
||||
|
||||
def parse_response(response: str) -> Action:
|
||||
"""Parses the model output to find a valid action to take
|
||||
"""
|
||||
Parses the model output to find a valid action to take
|
||||
|
||||
Parameters:
|
||||
- response (str): A response from the model that potentially contains an Action.
|
||||
|
||||
Returns:
|
||||
- Action: A valid next action to perform from model output
|
||||
"""
|
||||
json_start = response.find('{')
|
||||
json_end = response.rfind('}') + 1
|
||||
response = response[json_start:json_end]
|
||||
action_dict = json.loads(response)
|
||||
if 'contents' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from openhands.controller.action_parser import ResponseParser
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
)
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
|
||||
|
||||
class PlannerResponseParser(ResponseParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def parse(self, response: str) -> Action:
|
||||
action_str = self.parse_response(response)
|
||||
return self.parse_action(action_str)
|
||||
|
||||
def parse_response(self, response) -> str:
|
||||
# get the next action from the response
|
||||
return response['choices'][0]['message']['content']
|
||||
|
||||
def parse_action(self, action_str: str) -> Action:
|
||||
"""Parses a string to find an action within it
|
||||
|
||||
Parameters:
|
||||
- response (str): The string to be parsed
|
||||
|
||||
Returns:
|
||||
- Action: The action that was found in the response string
|
||||
"""
|
||||
# attempt to load the JSON dict from the response
|
||||
action_dict = json.loads(action_str)
|
||||
|
||||
if 'content' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict['contents'] = action_dict.pop('content')
|
||||
|
||||
return action_from_dict(action_dict)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Docker Containers
|
||||
Each folder here contains a Dockerfile, and a config.sh describing how to build
|
||||
the image and where to push it. These are images are built and pushed in GitHub Actions
|
||||
by the `ghcr.yml` workflow.
|
||||
|
||||
## Building Manually
|
||||
|
||||
```
|
||||
docker build -f containers/app/Dockerfile -t opendevin .
|
||||
docker build -f containers/sandbox/Dockerfile -t sandbox .
|
||||
docker build -f containers/evaluation/Dockerfile -t evaluation evaluation/SWE-bench/
|
||||
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
ARG OPEN_DEVIN_BUILD_VERSION=dev
|
||||
FROM node:21.7.2-bookworm-slim as frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm install -g npm@10.5.1
|
||||
RUN npm ci
|
||||
|
||||
COPY ./frontend ./
|
||||
RUN npm run make-i18n && npm run build
|
||||
|
||||
FROM python:3.12-slim as backend-builder
|
||||
|
||||
WORKDIR /app
|
||||
ENV PYTHONPATH '/app'
|
||||
|
||||
ENV POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=1 \
|
||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y curl make git build-essential \
|
||||
&& python3 -m pip install poetry==1.8.2 --break-system-packages
|
||||
|
||||
COPY ./pyproject.toml ./poetry.lock ./
|
||||
RUN touch README.md
|
||||
RUN poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR
|
||||
|
||||
FROM python:3.12-slim as runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV RUN_AS_DEVIN=true
|
||||
# A random number--we need this to be different from the user's UID on the host machine
|
||||
ENV OPENDEVIN_USER_ID=42420
|
||||
ENV USE_HOST_NETWORK=false
|
||||
ENV SSH_HOSTNAME=host.docker.internal
|
||||
ENV WORKSPACE_BASE=/opt/workspace_base
|
||||
ENV OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION
|
||||
RUN mkdir -p $WORKSPACE_BASE
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y curl ssh sudo
|
||||
|
||||
RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs # Default is 1000, but OSX is often 501
|
||||
|
||||
RUN groupadd app
|
||||
RUN useradd -l -m -u $OPENDEVIN_USER_ID -s /bin/bash opendevin && \
|
||||
usermod -aG app opendevin && \
|
||||
usermod -aG sudo opendevin && \
|
||||
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
RUN chown -R opendevin:app /app && chmod -R 770 /app
|
||||
USER opendevin
|
||||
|
||||
ENV VIRTUAL_ENV=/app/.venv \
|
||||
PATH="/app/.venv/bin:$PATH" \
|
||||
PYTHONPATH='/app'
|
||||
|
||||
COPY --chown=opendevin:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
RUN playwright install --with-deps chromium
|
||||
|
||||
COPY --chown=opendevin:app --chmod=770 ./opendevin ./opendevin
|
||||
COPY --chown=opendevin:app --chmod=777 ./opendevin/runtime/plugins ./opendevin/runtime/plugins
|
||||
COPY --chown=opendevin:app --chmod=770 ./agenthub ./agenthub
|
||||
|
||||
RUN python opendevin/core/download.py # No-op to download assets
|
||||
RUN chown -R opendevin:app /app/logs && chmod -R 770 /app/logs # This gets created by the download.py script
|
||||
|
||||
|
||||
COPY --chown=opendevin:app --chmod=770 --from=frontend-builder /app/dist ./frontend/dist
|
||||
COPY --chown=opendevin:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
USER root
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"]
|
||||
@@ -0,0 +1,4 @@
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_ORG=opendevin
|
||||
DOCKER_IMAGE=opendevin
|
||||
DOCKER_BASE_DIR="."
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
echo "Starting OpenDevin..."
|
||||
if [[ $NO_SETUP == "true" ]]; then
|
||||
echo "Skipping setup, running as $(whoami)"
|
||||
"$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "The OpenDevin entrypoint.sh must run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SANDBOX_USER_ID" ]; then
|
||||
echo "SANDBOX_USER_ID is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
|
||||
echo "Running OpenDevin as root"
|
||||
export RUN_AS_DEVIN=false
|
||||
mkdir -p /root/.cache/ms-playwright/
|
||||
mv /home/opendevin/.cache/ms-playwright/ /root/.cache/
|
||||
"$@"
|
||||
else
|
||||
echo "Setting up enduser with id $SANDBOX_USER_ID"
|
||||
if ! useradd -l -m -u $SANDBOX_USER_ID -s /bin/bash enduser; then
|
||||
echo "Failed to create user enduser with id $SANDBOX_USER_ID. Moving opendevin user."
|
||||
incremented_id=$(($SANDBOX_USER_ID + 1))
|
||||
usermod -u $incremented_id opendevin
|
||||
if ! useradd -l -m -u $SANDBOX_USER_ID -s /bin/bash enduser; then
|
||||
echo "Failed to create user enduser with id $SANDBOX_USER_ID for a second time. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
usermod -aG app enduser
|
||||
# get the user group of /var/run/docker.sock and set opendevin to that group
|
||||
DOCKER_SOCKET_GID=$(stat -c '%g' /var/run/docker.sock)
|
||||
echo "Docker socket group id: $DOCKER_SOCKET_GID"
|
||||
if getent group $DOCKER_SOCKET_GID; then
|
||||
echo "Group with id $DOCKER_SOCKET_GID already exists"
|
||||
else
|
||||
echo "Creating group with id $DOCKER_SOCKET_GID"
|
||||
groupadd -g $DOCKER_SOCKET_GID docker
|
||||
fi
|
||||
|
||||
mkdir -p /home/enduser/.cache/ms-playwright/
|
||||
mv /home/opendevin/.cache/ms-playwright/ /home/enduser/.cache/
|
||||
|
||||
usermod -aG $DOCKER_SOCKET_GID enduser
|
||||
echo "Running as enduser"
|
||||
su enduser /bin/bash -c "$*"
|
||||
fi
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
image_name=$1
|
||||
org_name=$2
|
||||
push=0
|
||||
if [[ $3 == "--push" ]]; then
|
||||
push=1
|
||||
fi
|
||||
|
||||
echo -e "Building: $image_name"
|
||||
tags=()
|
||||
|
||||
OPEN_DEVIN_BUILD_VERSION="dev"
|
||||
|
||||
cache_tag_base="buildcache"
|
||||
cache_tag="$cache_tag_base"
|
||||
|
||||
if [[ -n $GITHUB_REF_NAME ]]; then
|
||||
# check if ref name is a version number
|
||||
if [[ $GITHUB_REF_NAME =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
major_version=$(echo $GITHUB_REF_NAME | cut -d. -f1)
|
||||
minor_version=$(echo $GITHUB_REF_NAME | cut -d. -f1,2)
|
||||
tags+=($major_version $minor_version)
|
||||
fi
|
||||
sanitized=$(echo $GITHUB_REF_NAME | sed 's/[^a-zA-Z0-9.-]\+/-/g')
|
||||
OPEN_DEVIN_BUILD_VERSION=$sanitized
|
||||
cache_tag+="-${sanitized}"
|
||||
tags+=($sanitized)
|
||||
fi
|
||||
echo "Tags: ${tags[@]}"
|
||||
|
||||
dir=./containers/$image_name
|
||||
if [ ! -f $dir/Dockerfile ]; then
|
||||
echo "No Dockerfile found"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f $dir/config.sh ]; then
|
||||
echo "No config.sh found for Dockerfile"
|
||||
exit 1
|
||||
fi
|
||||
source $dir/config.sh
|
||||
if [[ -n "$org_name" ]]; then
|
||||
DOCKER_ORG="$org_name"
|
||||
fi
|
||||
DOCKER_REPOSITORY=$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE
|
||||
DOCKER_REPOSITORY=${DOCKER_REPOSITORY,,} # lowercase
|
||||
echo "Repo: $DOCKER_REPOSITORY"
|
||||
echo "Base dir: $DOCKER_BASE_DIR"
|
||||
|
||||
args=""
|
||||
for tag in ${tags[@]}; do
|
||||
args+=" -t $DOCKER_REPOSITORY:$tag"
|
||||
done
|
||||
if [[ $push -eq 1 ]]; then
|
||||
args+=" --push"
|
||||
args+=" --cache-to=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag,mode=max"
|
||||
fi
|
||||
|
||||
docker buildx build \
|
||||
$args \
|
||||
--build-arg OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION \
|
||||
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag \
|
||||
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag_base-main \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--provenance=false \
|
||||
-f $dir/Dockerfile $DOCKER_BASE_DIR
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# install basic packages
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
vim \
|
||||
nano \
|
||||
unzip \
|
||||
zip \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
build-essential \
|
||||
openssh-server \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -0,0 +1,15 @@
|
||||
# How to build custom E2B sandbox for OpenDevin
|
||||
|
||||
[E2B](https://e2b.dev) is an [open-source](https://github.com/e2b-dev/e2b) secure cloud environment (sandbox) made for running AI-generated code and agents. E2B offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
|
||||
|
||||
|
||||
1. Install the CLI with NPM.
|
||||
```sh
|
||||
npm install -g @e2b/cli@latest
|
||||
```
|
||||
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
|
||||
|
||||
1. Build the sandbox
|
||||
```sh
|
||||
e2b template build --dockerfile ./Dockerfile --name "open-devin"
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# This is a config for E2B sandbox template.
|
||||
# You can use 'template_id' (785n69crgahmz0lkdw9h) or 'template_name (open-devin) from this config to spawn a sandbox:
|
||||
|
||||
# Python SDK
|
||||
# from e2b import Sandbox
|
||||
# sandbox = Sandbox(template='open-devin')
|
||||
|
||||
# JS SDK
|
||||
# import { Sandbox } from 'e2b'
|
||||
# const sandbox = await Sandbox.create({ template: 'open-devin' })
|
||||
|
||||
dockerfile = "Dockerfile"
|
||||
template_name = "open-devin"
|
||||
template_id = "785n69crgahmz0lkdw9h"
|
||||
@@ -0,0 +1,41 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
# https://github.com/princeton-nlp/SWE-bench/issues/15#issuecomment-1815392192
|
||||
RUN apt-get update && \
|
||||
apt-get install -y bash gcc git jq wget && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN git config --global user.email "swebench@pnlp.org"
|
||||
RUN git config --global user.name "swebench"
|
||||
|
||||
RUN apt update && apt install -y build-essential
|
||||
|
||||
# Create new user
|
||||
RUN useradd -ms /bin/bash swe-bench
|
||||
USER swe-bench
|
||||
WORKDIR /home/swe-bench
|
||||
|
||||
# Setup Conda
|
||||
ENV PATH="/home/swe-bench/miniconda3/bin:${PATH}"
|
||||
ARG PATH="/home/swe-bench/miniconda3/bin:${PATH}"
|
||||
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-`uname -m`.sh -O miniconda.sh \
|
||||
&& mkdir ~/.conda \
|
||||
&& bash miniconda.sh -b \
|
||||
&& rm -f miniconda.sh
|
||||
RUN conda --version
|
||||
|
||||
# Setup SWE-Bench Env
|
||||
COPY environment.yml .
|
||||
RUN conda env create -f environment.yml
|
||||
|
||||
# Add commands
|
||||
COPY ./commands.sh .
|
||||
RUN . ./commands.sh
|
||||
|
||||
# Some missing packages
|
||||
RUN pip install datasets python-dotenv gitpython
|
||||
|
||||
RUN conda init bash
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -0,0 +1,4 @@
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_ORG=opendevin
|
||||
DOCKER_IMAGE=eval-swe-bench
|
||||
DOCKER_BASE_DIR=evaluation/SWE-bench
|
||||
@@ -0,0 +1,33 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# install basic packages
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
vim \
|
||||
nano \
|
||||
unzip \
|
||||
zip \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
build-essential \
|
||||
openssh-server \
|
||||
sudo \
|
||||
bash \
|
||||
gcc \
|
||||
jq \
|
||||
g++ \
|
||||
make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p -m0755 /var/run/sshd
|
||||
|
||||
# symlink python3 to python
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||
|
||||
# install basic dependencies for CodeActAgent
|
||||
RUN pip3 install --upgrade pip
|
||||
RUN pip3 install jupyterlab notebook jupyter_kernel_gateway flake8
|
||||
@@ -0,0 +1,4 @@
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_ORG=opendevin
|
||||
DOCKER_IMAGE=sandbox
|
||||
DOCKER_BASE_DIR="."
|
||||
@@ -1,5 +0,0 @@
|
||||
from openhands.controller.agent_controller import AgentController
|
||||
|
||||
__all__ = [
|
||||
'AgentController',
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user