From 7f763b57d023b8e94eed1abe24b21088d95c163b Mon Sep 17 00:00:00 2001
From: yuymf <1352948945@qq.com>
Date: Wed, 27 Sep 2023 17:25:51 +0800
Subject: [PATCH 1/3] Minecraft game add action_developer
---
Temp.md | 10 +
.../minecraft_tests/action_developer.png | Bin 0 -> 29220 bytes
mc_requirements.txt | 3 +
metagpt/actions/minecraft/generate_actions.py | 35 ++-
metagpt/minecraft_team.py | 70 +++--
metagpt/mineflayer_environment.py | 16 +-
.../minecraft/action_response_format.txt | 15 -
metagpt/prompts/minecraft/critic.txt | 127 --------
metagpt/prompts/minecraft/curriculum.txt | 42 ---
.../curriculum_qa_step2_answer_questions.txt | 8 -
.../curriculum_task_decomposition.txt | 12 -
metagpt/prompts/minecraft/skill.txt | 51 ----
metagpt/roles/minecraft/action_developer.py | 283 ++++++++++++++++--
metagpt/roles/minecraft/minecraft_base.py | 1 -
metagpt/utils/minecraft/__init__.py | 4 +-
metagpt/utils/minecraft/action_rsp_parser.py | 69 +++++
metagpt/utils/minecraft/file_utils.py | 4 +-
metagpt/utils/minecraft/json_utils.py | 4 +-
metagpt/utils/minecraft/load_prompts.py | 5 +-
.../minecraft/load_skills_code_context.py | 21 ++
.../skills_code_context/.prettierrc.json | 3 +
.../minecraft/skills_code_context/__init__.py | 0
.../skills_code_context/craftItem.js | 14 +
.../skills_code_context/exploreUntil.js | 31 ++
.../minecraft/skills_code_context/killMob.js | 12 +
.../skills_code_context/mineBlock.js | 15 +
.../skills_code_context/mineflayer.js | 22 ++
.../skills_code_context/placeItem.js | 28 ++
.../skills_code_context/smeltItem.js | 22 ++
.../minecraft/skills_code_context/useChest.js | 35 +++
30 files changed, 639 insertions(+), 323 deletions(-)
create mode 100644 docs/resources/workspace/minecraft_tests/action_developer.png
create mode 100644 mc_requirements.txt
delete mode 100644 metagpt/prompts/minecraft/action_response_format.txt
delete mode 100644 metagpt/prompts/minecraft/critic.txt
delete mode 100644 metagpt/prompts/minecraft/curriculum.txt
delete mode 100644 metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt
delete mode 100644 metagpt/prompts/minecraft/curriculum_task_decomposition.txt
delete mode 100644 metagpt/prompts/minecraft/skill.txt
create mode 100644 metagpt/utils/minecraft/action_rsp_parser.py
create mode 100644 metagpt/utils/minecraft/load_skills_code_context.py
create mode 100644 metagpt/utils/minecraft/skills_code_context/.prettierrc.json
create mode 100644 metagpt/utils/minecraft/skills_code_context/__init__.py
create mode 100644 metagpt/utils/minecraft/skills_code_context/craftItem.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/exploreUntil.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/killMob.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/mineBlock.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/mineflayer.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/placeItem.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/smeltItem.js
create mode 100644 metagpt/utils/minecraft/skills_code_context/useChest.js
diff --git a/Temp.md b/Temp.md
index f120d2249..b86073fd5 100644
--- a/Temp.md
+++ b/Temp.md
@@ -31,3 +31,13 @@ ### 0926: 环境信息获取和更新 on_event()实际内容
+
+
+### 0927:Action_developer 更新
+
+对应需实现 GenerateActionCode ,完成对应的和 GameEnvironment 的交
+互和 Environment 的信息传递
+
+测试结果
+
+
diff --git a/docs/resources/workspace/minecraft_tests/action_developer.png b/docs/resources/workspace/minecraft_tests/action_developer.png
new file mode 100644
index 0000000000000000000000000000000000000000..397b4bdee20e81fb7255d1100f79f0f871832298
GIT binary patch
literal 29220
zcma&O30P8H)F_Ni)~jV@rDbZ9*qEu*9h7$=6AkF3LRBD(F=bUU;|Fh*E
z5D2u|?##&xAkb0>2&AH1xdIree6>~uxTs(**d7Pv(zlKQH_K4R9FKuO`LSyA*OvqL
ztNhQnVL+fYKNkO0Xnq-9AW%TL-N|DYgFOX()n9fwCUwa%YP9&B+RK(~MmDr9-}+$f
z^zg4VEtAjJ)*iO0`?24C%gx8yF83JL+XAkowjMPXL~rMeHT(B2l`oM5C74o7FQ3f5
zvM%|OTjf3}d$!eMbMjR3tK!Svf-eg^vSJWM1-1dI>3+dqyjmE<=`LQ=PZlf%E&~Z?
zu#2~;>+9D5m-{X|x))LMKf4P+69jUK@KOaXHG5zF?~5O`&3>TH%NxlH`JO9uF#tK}
zQC_O)#|9{Un5J$bWuCJjU8tOAFDUX}6`%s1nxr-+k^PdG6VwcE+cI2OB1=5NZ1nT<
zODbjRCpq7-OsjQdF!I1j<~Yybyd*#JD?(c5K7dz=U_$A=M8c}zV_J$*v?
zV+5vHKo~C^ki{;1GFs?^4Hqm(6l_ObsUIa0`K92cZy0fQ!g%3)`y5%xUD(;6ULAg!
z<+?Db-(z(#9~QWC8rmIYpuR)+@JlmnZ4O4k8rd5&d%6d*VFAO^@@^nFn$l2ge2v(8R)y%4L&`x?;}R6;I1#fpc*s
z9rZC=iaLU7e{vrzsoH}3k=6+odlb8otm0ZV(o7IM!H_!4Oo&_C=0u!
zeqiEc`hwiI-YI;Ln9coB{fKV+Paqq7%jzkQ!%8Xhti%YoNJ{HWhA5;ke2(c{zDfVr
zHzbkD%~@|~ldP+c)U7J&GfPraHFQn!oxaeM|;h>L(1{)ac_d;4h1_UZlyM}PZo^p
zW7NJb@bd_3g^#A$wUz6AzUnTS5j#Cz`F$Z%t)We_;cFhP420;f&6v2x7aJzQ7AgUh
zn-NO8eu*#gob2~X+OuUe1i&8nNS-(hkhSRJJ&xlqd#?q@$B_{84i+#vu;x>
z#+Op;-uw=z6=m()pO!8T(DAGyP>VT3mBn^b^QAq7vTn_om!jmnQ@Krj%`X%tqu#B8
z$SQh-!ZkndRLCL5WyBXtQ9`WTp}Bqs#j=AM?-K2ogX5$|DDPUq47qK0{QhjGGctl>
z>=l$Q(d|syi=H5sG$)6d%urf_V|{48`Y7=yJu-Y}->9g`+He?uhkMDbUoYvWqS%tx
z)5f6i;&hX){d(JOxYhn>PWa=qTV0bn#53B0%%G-D_dn7?$8B||A5?k*RL(n)<3uzx
zY_4?vhiCF9sG{O?9LWPyo)MFB+wU;Srql^bDKoqk`mj9m$~=|&dXMu}LRfGd!=r3s
z0^kGnI)%qmgSQKYJN7K36J@14EHvzXz+`PiMElF7l0d2~<}{%fO7h}*)O|hDU9S*%
zPjP&wonl2R>*?te;2lQ~ke?aPPJwSs%?*>?A92Z?Sog*RtJ;9QHD_Zh@#Nxb6%_1G
zt%0tm9cdj5%2x!e+iiSP-?c5*wlArG{vNEn>Wg#lek`_V9md7v;-)2V%2$8M00iBiom$7q(zQ8@G4c
zq!-({eN5m(B7c}ump|dAXIB#81p-R}pNC?PdeKxn_O6Y7z^6X{7PPX&<6K|A%AT0W
zVRru?+=YFo!_aZ-AIYPS6?QU?>NB6~ma)s6%wgf4qr91*Ipq6&n|rEGpPwi@AI5{~
zFq_K>1)9d%$|q$`*x}{qK}jpv_?>dPc0Qo@KzGE0Vzr+q4zl|noay=amb~ZJHcyD~
zre3f*v^-NY>2>PJ$uGmK^00z7%9*p%%bTdb8n4+}8~zYEhZ!hf;HPTo9R8ap~Fs=yp4!EksF?mI5(W3kUnEt+LYjw8DV!8mgV1DvAhv`|Oa9*ZXu
zix1$@EfEUHP3bGbmS!$bs&7wZh01AtbT~|IW&qiKZX*ueIdJu^DB^i=)vXwp4Me}f
z-G}U~vF-g=^`rr@E(iOvT)xW6%|WFxmEI)B^%YS8Le6ai^2d9^JsW+B9t5R!_VnRteNX&V7uZ5eRSGq}tD<>FwywRCe`Oy1YLqI7S4^C*GSa;Z9
z=*?H(NWp8tW<~0%yW)W=&E9C)ke-iX9W%exx|mrV0Anf$ET)G(p-H;HP8UJ3E~j!Mi$mTW
zGX7Ib(HN?ZCr$coj`C4iSQqhqw}!N+M0qazJnez!mHoyK_!S$X
zSG~4X^y=Ut)kuNW5B%#@53SAmi{nJh@U4v8Hka}Ggd_jTSr}wCI=#ADL~tl5ykdMU
z&;I)%2kaX|(go?GkOVK(>e)8kxJR1H-MN0)es6Udn?I`<|5z7+ZnTPo*t~=(RpLm+
zO)zfafH^d86wy9{F`8$G`hJ#tSChbX6k0gmoNqBo+;+jk8Gr57_$u#^TyJ(ZCBa*4
zl`Uub4Xvb9Y1{W{<;QO9%{fjUn{`N)6nHkX^pZtI~)Vs;RRdw|j`WR&Gqp9RoM-es@bWk1i67nD~
zO5ek$B=6?9ktU_0`ZJZ}MYHm?5A!?}LqR_XtNK-D_+o&3uvsk-|Gv%;Cq2%sC3q9Y
z!(#`l<8`&NpVbgn{i>cV{skS-s6T2Pd9r)wE(k5=m_7D*U`Km$oEFj0w!5F#c-_uf
z{P2<;xc^(^g5<|c)wn9GK~xfKTr%Qgv)A%DtxSR;`UFt5%`S2Xf-f`w1iF
z2QV*n_4py#jhqwbI{I$s8OQU4tmi?pqnTE@TfKw(*(M<%3D--3cK;#T!KKeb#+-?j`?{d4;IVZU9U(6Mq{bnt2CX?#uJ
z3;w|2mA$1%`u#(}aRW{vTu9ZUtiDws$&T)0fXV#V44zL7f?5@SZKz7Y7v1eFoY9PZ
zC%ff{{y{UW^C<49k3Ab*JAya^LxP$$xES{|^&g57PaTZKBLYIBs6>FOu&(JCQf*
zG(E22mQ%p>e+DzV$Gs}9R4?^okZfJ&O@uNX9lYayz-o_Db;2Hh7?9`J_LbN^J{3>;
zX=p}zT(^&Pf_!L1=Si-wXuvCdj`WPzqoW+?%@ysLxvf;K1$Hu7Rv#nbAXR&$YLr;+
z`8ZPQp57g|ZA`mcD`2L3RJ`tpZO!kJwHqox4wRHUK?-|>yk@!x
z`)b=^)k(Q*C{~Mg*<|mYhLn@XN{aVDR~-2h`3ZummJ|r;onE&U(<|_z)F;q}Bl{l7
zzf>Py!K5?v^Y12C=1uK%w-51@_T#ULOkW>j`W}jv_vn!9vpgcIeBu|ql53F>5Xkng
zS&x`&Yd+N+S$=(RYD<^wXo&m$o2pTJSLR5l#}KC{N=hS+Un1Tieg-G{!g3_j;b@qk
zpej)q&{3CbnZ-ylJ+m;QIMr_0BSJsn;K_K?wJ{#FedC#oV+7Ca0=P5eX|-U4z_R+}
zA7vd&`|B)~ypd_UZ{F4THfwdy`0$T4GIi)|4qKsDT3QPJ04lcIP^7HI@dcFU3Z2ui
zXvwfy7OA8u$NN&ck8Q7GHx!lYl(fgoMYM$#ggX;h?OouqOEm!66r@d0zToF4KN)tL
zWzn6?N`ISu1R(HT46$(M(gRxj92JbthSB?0%c{}aEbY|xHA`}Njk#Xw%2}6x3Yz}|
z*j6{eVQ=NEsmT<+pb$qN=X{4}3+|vROid`6UWU!Z#cO_8@}dI}VYhW}u1_u(eOpDi
z+p`Q{mX=|y4Z{PB<$*!l_CNZ*_2*s{&L-oG4Juw=bE*#zx?l0z6_G6df4~8d`kejG
zZ~HK=gWWbbEa{rft9oQ7w3hHWo^&zY1bo@8t5b(Vk^C-6_CnnxKP>sHs|3bYABQHlv=tWBD)o`g7rKtjZGb;A}CxeQ#{NxA65rLxCg!VWbG}
z5!53C=WeN^Lo~~94R4SH(j|SS;3@%}Z`E^2h2lKPmssPr)OJp4r2Ae1?5fq1x8*2YHH*6rVZT1pU
zeTKt&u)?NhHxI6otPS#C7DuX7d(avtnd-6(&GX_0Shs8ATY`EkqXbvj3-eAw>KaA9
zep`|bz1SK1Gi3cEad*Xp4O8j^URF65dKo5sZuf_m^}4OC|3R3sZ7Sp{NgD11`U8P>
zXZD*kIV;C?3&p2us_$3g_Ia19cYar!4}}|vCO&4f9!KB?|3zjg+Lqf3U8s{eGFgh&B6rHGB-x|ORd3ncE4;4X>n5#6_oL)@}~O{luxz7iiYgU5C0Lq=ANVv
z7zpXnOWW?
zyl2Y!_#oa)xhMBhxA_c;Cc507uUC4Db=wVNb_peo{%jFbU%b@68X2!k9K>n;tT2M_
znXcXLX)zxU^EmlCV1%Mv;sxI|qc*xV$Jdoy)ip;*jN~g#+P#yZC0~hdw*6_632|Gs
z_N?joi6fZ9oiX52zu4`9%2GeUdNa`h2hOPdIYDBheNqQOz4Qr%~
z%}?c0t_Jm%So*MhX+8939^B;4cY+h|rM22*D)y=5I2qnDEPGMM%Ea7*PrTumx;{t_
zMvk^g4g3+Y$t2!Xz)r#3=9wN5vfqSUjqM3a@;R3jAz>cHbsH`p$<>%z=nL6AQwV`B
z1Uu-UZW%ID7;_cMNejN#0+n8iM}%T>{4~Tpgt6GcO?kvVOP$WmvOSF0ZK>u#Bv1_I
zS^*M1UjfbrH!iT$9#?hEE--2TxP!eeNZ%)VW~a6@_Sb!dVzsJjdl0UuUc=a(RV{9U
za&%w#zl7fb7|aInuOr=0&z9HFD6w?|dTgv>H~mu(u{2Np#+UF4N+hV}s(X++gvogI
zO3bZ6<<7KdF|aEApMKjG3fTw>JUk42+LEC??=qfu`rKH(E&B%A;)?SVcl1mb&
zpe;Paq1@F$zR8*(n&mv5KBMN-TO!!8#q;^_shhkf4n6b7{o_r37K|KEGv*@_L*Xfi
zL^Wm7?2>E6+>$=!4gJxQ;PXM_TB%P9gt=r*q<^emW%!4nUn9Qqo?KH@cQCrcgzo_Z
zmySvHk77^yxON>zxo|I7c(OXlg>Ypr?`^crIbLxcSGIxdGj~Or>hLWDqHGQ5L;eo9
zgqX)0vx<29cDVCa^WUrmiD++q__GBq%Ix49)gQej%FOFK
zZfW@2OaSkxC^=@sv=6h_5l+b|O7G?}v-a}h10b5kj30LX)O*U&5OFOOZt;V2KKxb(5_+5D!PO(G!owCe<=b=&mM(e_M@g
zDAlojtQfQ5F(*u*QdW(mzP^(XxjXY6)+`1r`Uo@MNNf{hAkASWebXK;uina$g$Pnt^X|RDY_kjUi7>mrb88wO
zRJMHco@ZzN>DzisAF*`O
z^Z%R$7+X{CH2B8x4{IXbs2d}ePSa3=${$>hm?
z%kEX))6eQZ2T=pXfcMaq_|+#B@~S>&>eA~I^NEWYg|s$#cvOyD?Z?i`Amz$$b8_iB
zCu}II4*t7NcQHRQi;)1MIt|6xs%LymUiEp{30+=5S&Z`;dFJL;
zf3qjn={P&=xr5K6J%&11I
z7)h@|Zi2pj-D91tAKF(hQuC3a1NO8^Sk=z!2D*jyPTBjDo-8x8?vLX?CY*3PgQXA<
z_%*aLh;?~3v%id-38v_uvaw@vO~HAD6Sb`MlVnM`8lt8=<-Bi2+
z?YQ>UQfhxdAh9~e^PJ^C{Q9|dxlJA1N58rf{cx-K(Nhrv7z3G))J4Cr4MLYC@8maswt&Nmh8v5jKsk|M#kwDs+33fXSi*u$=U+K&8ip&fPxTek?ZL2@O@D)$Zx
zfR=`Jy0-|e$ICFD`#gZIF{m<%rql5
zf6c5n`Foeh1Wjz7`c&Ri=CqU2d}qyRUV7|1y-H8%6&sYqGEeHQ+&V)r8c%MGIkGZ{
z9U2YnLK55p-m~(X>%fxg4F{isd{%$FF`4qPXfVLX?c%a`9ng4{JKduWZ*Pka36Z-X
zp)m2(D}l6*smF^=IfI=E$Bw%7WMMdjd;z?o6s=tuuhqnwsyQXjlYMGInD<%
zr}#I>NuQ+H00lqRbx{5;=fsvs3ugd$f4vL-n2O$|m3PgT*SiL)SmRdrlV6?5KMlNu
z^FZj%N~x=aR7H}7C)b{&LWrej-BmkR0#Z^tP8aTsA9g&jF!?3+O~+GxOxP}8qtaRJ
z?ZAnd`oE~i7B7^-X0tMQQG3;p`bOw6ilvK`0QrYW=d@GgcZSksp&p9^jF|tNRR1L3
z1tAmRo^u^Ri(ljJk-cbBs~9@C;KkeCS_QPOj_JG@3M|{Hyt$}7|3CTodr4m0Mn-8P
zPl{+aU0h}H{oP`gEicJjy813&a2Oc652<9(x)#42p#QhH5N8`lSUlE9Y2MXtPuup}
zjffTb13nb5+}65qUh$>!ZJBHS
zEkf%`QpMaJ6B>8v&dnPV>6lQ
z_TkTG4n|Zi5530`Z3L;)3?a(pyD4Fp;4MFvhaYf9;CpL(JeC3RhNADacpKxyIncGf
z-Rj`Fy|>=n1&iAde~3Q|K2Z@r)Kd2AGrM&vFKsH(0;f*Zy03T_TGO@p5j&S54u)n~
zWrg>qL02ehA?+iPosjkV3TX
zIOUf~hvHxPzZF1gs4gw3AI{^f9=TcI(lPOC3J5X8j#z5PUMg37*@iugnx6mNklulo
zLo4U19bfoLF*lW{pVz|6l^;I_3Kkx4bjZxoEr?M@X*0pFTg4xyx*H-;n{Gh#WWS)+GhN$|>!)aRm~HfzvNAP+IZCyzpdG^J+mR`B6wfY-wIj?VF@;aM~0-wK6V`=;0m`
z)#}Md5qsZR+khJ_Cvr|)mW=(APbJU4DW{Z8H8nRzorn1a`hKLiGFDU9Jd5_LI!&LS
zg_FORpF1~7XN)B(T^Y6@i{B6GoyyLA(dxgKpTrufWvtoy9l7v+9k@z-Don2SYI@0b
z@K2sv+<7t_r5{&O(_h6oCE1s+75EP!y4v>=uZwm%@|7$pp+BWSXlTE5lAFRSef{i?
zBG+U*Jt)ShO{P<-YfdQ_SJ%((qd1v=zFz$<*4)w=bnW*b4%@Egr=Iq&T3D}{<~i@d
z?|UC53XVnnW@uaA66X;j$Y?WV
z(hceF-3Z_}+(0!t#?jKUi0TAGg=mZ!*vCP^qJb3YS_&t?<9Oe_so
z@9=n7R^rO6))tG*xKG_#chm#WF*@vC89f#ZhuJcHToEF?jtRpBRF-pM;UR-{X{GLdf!}4Tt$iX#nm6!?)RHPKh(0PY=k~SaaZ&quX^OB!d!!%m7
zU(xBlDZwzU2)X+E+$94%$p6J+lwh4FJW1_F0{G`(?L9y!Z|I54&pSamlR-1+fI5cc
z@Vc0CAyG+E#gphSKENyLU8@Ixx)WVpVT*3|YMIU}DUT(f(
z)osc%T8?+CKfm0$qQsM}MgEh35iET&IA=PJT7VZU)&J1E;g*39Y4CMQD%iz4-zgbT
zQ*H^!;xuH#)VTIdcE}pJl>3RvEUU%{*90vc@(g=dg?GA0SjS57X&$x;K1KcwEfkF<
zh)Xa9#q{KG~ey9Li-`=|^0U$m&@`Eqkjy$*<#||dmrR!MESku
zTh~ok!p+HY+S*aLpER+aUd@!OsCSZPMfxS20TL}McR^aa<4JJTtq*I$?XXuXX#-1T
z9Li|16LxxbW*09YPL~(V#0mcZ1`#W?*DHNi`-S?8FStS`?w7~5k%tCu#!Kni!>-=7
zwuFz@{a-H`tCw7W^_7JypL^RAsdceJqY;i(7$6G1OLl-H821&N(4)QoNqKit=6Z=v
zCmZIiPF0`sP|P)r+zxwd2nl`oIWGq)DTWi2Pr{2Bwe8GrJr!0w?B!hJMH2eE$0qY<
zkJ_u@$FJ>Q;R=0W|KPG!Zm+AxY(X^+>jm@B&5Qbod
z-#*@on`I<+lOkd|tx8?7kFWetu
zm|E+iV&X|9f5md&wnKNg4caH#7?RoYxdcFo<
zCyRk%rP)YYo^~z-!f;9nvrG_4vaWzmEha|?O7ui(|7_ZuVITNn!;<3SyQ_z?@0%pl
z%Nhp2`fPAsaZtn2T>e7ii2L~eOG+k*;`WMm#0X-x>!mY-(tQKqxrfjUac}1lQRY@~
zqq!lZp^IsC!VL!{5JHHqi42|du15N$+y+5@k`HOB2MnC#ZJMW3i9`o9VK
z{D)UV_dUec+emEuDE|>Dy(TT3wRt7OoyX==JZJl%(_Q4}3kuTYr)Ae2fBQ`}S<_0a
z-qTOv)B(EKyyTyh6IeE6B>j9st)mhjcf(30ClUbz@#g)Qw+SO6R34wY8?jWMEo#0mOoxxpr9m!TBGmrb^w;Jb>z7YHlmKF7)d4
zKiSCA+&nMCp)qbNZ@RQ>nCK6l>+5~rzvBg#R*t(b=5$sbdomdla2~3g%2GJY+^3G)
z>(*=*#Vu7{1WZM<0EOA|mkd)ZE;Y#MXMTD?dr}T}e#TO?=QOXB;8_=RmFN3P1qhMd
z%k@I7I@${<5gI$LyaxAQzy8Uh{P@0hN!r$gEjTQ`+O+%3g}1L7daOuhFMu)`g0q~a
z?`Fh)5~ISrH*mGowRoLamQ2&+*~!&M4Yq2@zR-D2A!zaR@4~2RA*wrE+N+x?8oAPF
zMLuBz9(gTkiu6F)*aon#yNxfB;4t?rb_kMV9K7n7-J1Dv1rKDdzf+pEG_}NG_l_Rj
zho0MV^tF}@!*3SDve4jVkG#LP%(!$L5-y@{dU{e9y4La`*ZI84(THhb`GgP3!=7O(
zDIp6&J|t}B;%7^@JnBMpx{l}l=r2&riJ)i3uSqKAmJ)Ag7qbh6oLAtvL8nSFb90;Tc;0fgsXhRZWdlQWmX
zZMx4d+4&)R)te5qBp_GcG}uR8^vZOFfxnMzOp>{@ybOKQ6HX;i(ey55mmrg0I_y^)K)s933mJxXg1xE(;MHdJ#0DlTcP8dLZ?#K79
z+oY1K0l3oH`bAe&z7#iG`0hIzeaySV3DM^nZ1m`4x&Wo?kVuOjH4t#)HN8EhU7pOP
zF*c;IT1f@D0D$F_CVEGo6AiEH5bfYfD?;+e9eC!5#BtLM9>C}^lIzgAO-tGhx+Xo1
z*>6DR%fLM7`)q5o4i8mF)1x`#XHO=F!_{A8aT@OLTjt@-K7dAy-*-$5@#)LYxfyn?
z*`8JRO8+*72eBC)R-sWWyE9qqxx#_eQrdz#UpbKs6m4AL`I0qD+Kq7!fw)GxXcq)E
zuyk;r_B1QkVFRcNvT%9@4(LoM+aE`=9qNCX8%LTH9+_CN)VUFsXLk?aE{$Cfjn4{g
zod{sRps%Wb!@>WD_67I>rDO%W2&i-cS#OBbwU;$qd8asEN2qsF=AWpZZ|z`jF(j_V
zCD`>7$@bBe9gA2JrFP=QSwXDH{8tBU&i
z9vhS?w@esk+G1XIav%zL{PSSL
z`Zif&Y%}u9_+8l+-rW*LAWgdd%{g-%4tTo1A8>kZoFm1(0`FM4SfaFyvwap3uR^@5
z-s$~t=K^^H&SVL>=3>Od636kr?lI{h1EWkdQvE%Y#;|?m@CuDc=9iUJ>T?#k6jUR(
za0LSR!qhV}6Bh0DXM(p1)pH({C-V0MVdDX}Ejx+d9@w|t?xo!ckIzi5%w7D$!Rh9w
z;r)LM4nbQhDh>cUx>gw*&%a1KJh5@rS@r%Avjg49RfU|{qp*@ohCBMa(pk>?yG?{?
zx5Sp_jz?#_d=e%Bz#v=M+Ipw6E4&VR8txfQrvbH+f(q$
z85`qlF>bk1^pSmOG0-I(Tx4|97eegE0r>7C({x7?f0l;6VB&=Bf#@q=j|%_o0KnPR
zKS^wV%;77aUFat+-LCW&(l}4Rh492PcrSSicmYGGw7W-{-6Nohz!E8h2L6k_`B-3~a
zZ8)2bB_>Cj>J+_@m8*uJJ0d2Tzi3%hYuQ2VNo99f+MrCnQ
zKgl9(zFN5!a5PD#+gBbx9tb
zFC*%|Q1%a5BMV&E^%2Mc`r=7#vFam{d<$F*mj|%gc0n50{0zITz&L2)BC(L8xlHV2
zTgHozk9+a+F>QLm-mT)1X)^z?9njE1EYy_8m0nT+t^w$r1ygH-(edvbsBq)3*S6hy
z5e`Q?sgK-@>VGC3dAHdR;upk!WW93O)~`s)CxbX3%(kCe*-L*#Q=S3B(&hc*5V7uC~Ya^Ssu2~TY%
zGzoHuTR{W&7EPEpTt}>O65s-6X`>&4CjhoF*}7=*9F8UN?@jbC!zZi;4g9YG3+Q0$
zMEfjw-@uwRAbjv*N~M`YjB1|kJhI5FTmSC3idgu=)n#eTvtxkO)ne^e(547Wd$Fv#
zy4sUM$ZZ6HP>bzgE!OA)B;j%Ms{oWL2)gF)%6IW3&*L2G>!bEJ;ekJGyFdd!w*SS|
zw0o*P%KmBvY)j9A)g4d`ZIP-i){?~#^Xq}QH+pCyFnDy)%KNWKZFzD+)j2QVB&lTn
z)VfGvY$b(C8{;aNf0RS965~*WqJL=4|7K^^K@{k|6kFj%lPO5@ki-FF)!fTRhn6Pl
ztk`Fp)0!i{-AbE$Uhf3wjPklpAf&4A^ZPS7!O$yuoHs!>KAyt4-esVpJGw1@VwU5|
z>DnOIW2@?&c)&rMT3S$0miA<1wNO29Vr^@@jv*yPok3m~o!p-v+dTB@9)yMZH1`
z`Q>F;JuBDxHM_<~^Zrl4*z$`)_sJ3TwOt6jTu{z|A2ei7k>;Yw?Vb~5chvW;6{}3d
zUMVS(cPx*8hli*4>c!N5L+2UY1uv=7-qvU`{@^T1y)L!e=VQC&5oY&ASY%0dan5K{
zm~Pbeq7KAPdp;`Pmw83DmN!jE6z{V;TgXT_NFSO%A3gf%>W6lXG!1W38#hjq%m^Nb)MS~8p`Zl!3d=iDMmh^35#`euoi~FK+*kEY8_jRHI{+pFtdL-hj
zY_k%~Jw<*LPLzAQ%U6MF%wkjczpgc@UIGfT`qYpcUtD^SqG!!dJ%`-|0asfR
zvV;E39}YQA(}#7l
z0g$V|PY|PW_G%ux44!0ZKE|?A@Gt#}X`k*ls2cZJz)9|pAe(FIuVG7!R$vPB``%{h
zVf@rJF`eXzb2r7czh_){9}1hE^)b$sbdK=$YCf>xb{i3+c-%U`pCe3fom8QwsI5Cg
z!`#^S6)W%LCS($Y(|8Nv+}G$1M!D!JDJcR-*u=~{u8+}uAy{=>{;jSz5}##byxgs)
zvepLxUwJWIUYeFv)@&3>*gJl)2x(S#&;+HJOYAW_{U-bkw{bT0#RH$dSO9QTOD-St
zTD4m%Z%x-~ibfz22JY}k4K{A>EMLt~<)vDT|1YpCGnW;hnwM$&yCc9Aospl3=g|*K
z{ClsaYxoA*e2|<5Pv^)iy2Ce~`-FS!LCQt6ee-Zf>HR1%wS+oCZX(qkVJwdL$d~%?
zI&>z9o{t7yV{oNAwwuSTurDmCZ^hH=C7>d(~fLMBZgsBn3lP
z7Lc!o9GJN+=`EDI6?=HVSPD@|fE%By!nFY!N#hM5PQin(CIXY>mq*5yn=Ri)pZ+d8
zRW+;kil0rm@=He0^>LfObIp}|(w~`yS-mCheQb5XR}T$gk7PK%&%~kG?b82>;mvr_
zgz1;{Mw^A{&elIo=E|HZZ>X~vNl)D6$%@ZGqu!*-f~u`42eK6sFI&lp5~dBXzvotM
z5_s%^m;l-9W3Wo5vky=TUeJf;Y64}&dZyhf{jW6mUm%ch9hJ@gKFa7PL#x#RU-p@o
zYQ|jbSVFy1Ft=ZMOXvRT-?YIqfUT+uXon2~TG!%dP}=JMv#F~4hM!Hfc^A%#k?XEh
z{?P%J)R+L36NU@hu9f#0p1=$CEb6eOylq@;G9H;ofG&}lm186FM5pl%EIZ
zug1Rb9yU+y9?Z&I)S-Ofp-$3?kmKuTB#V^@<;IKh9mE2lciMKrH!s@kz^s5Iuk@sr
zS@7YjCuT@3GiGoA30WCAlKK*s<_5hlh
z_$iyam#WB-{`kbSc<dtnyz}jR5&a2~eQ1?g6fg$(+!PK9|I1tBSXuE9Gqn`XzbhEsrM&wJ#`q
zU_6OjlxcXW_#DEL4Yc3@&$H;S+y&Wh@Iqflm09hDN;tX8hu8<5T4U$5vcbzX%OwZW
zuVT{p#>>z;SMz1^u3Zytl0s-d5cZ?&3#H)%%Gn*jLk>=?0y;Z{w%+@?U#gTYP%Guz
z`F2>Z<1YBS=mMh{N6Ckj$L*qSogW+Po%R*VhVC&-*U?;K^%vpS?495jB_MD7e%EK?
zCa*lwZ5MQh^DnEZpew1ht7q;NW1{3&cnQmvVEn{#*l&^N{68*@
zC#_wkrJr?m>0}v$?nQ$dFH?G(wn$E`5J0;SjesM2!Svuu{>(UB{&It2?9~%N=+HF=
z3a5V7^!IN2OMOFSa8yqkZwlM@X%*(oA~W9K38eAqf+t6-svGZzvF6vA#r{1*w6Nb^
zsKJRnYY$S>%VVjxdnz3lQOL3JE8skjO$I9LUQ|LbwMpt9srz><{t7_Nu{6k{8wR?+
zXjB1z|9kiS@1?sTu``YyxIgv^12v`juB*n19&A6mw061QGj8+of)9qLXd6F
zVk=Y>L3nqK7ks|o&RvIPSAo8C2(ci(DOKd&JHK@^Ty5Tr61^S-(yWge`$c(NkrGG-
z)xND;4%zW~l}fCGf1*`*|L(33nU^h{r6M$uuzJFTo;W%$CPNgeLFQ{S9}dsa?E%k?
z_aL4$>sw{`=t{W_)m?wRO1@3<>YM*UcJIBfEjCEVqoK#_4THxDg>Va5)vX7gHw}+H
zKpdgeT6_(pl|AO$bmlo?{|$|-IIZYCgiW{dMPt}iA7wJ0t1h?tUrwk%92JvQmn0h-
zCE8V;_*-g^OGum!AXVqbHiW{%I^n7=TzvkTP!b1I`UP`GcXv?yd>7*;9t6T0X#l1N
zL|fR`IQd?;A+Tk$bR+mrW&haBgFs(!mi$7=0+6@1_$*B6OKMi|mx&%60S_*O#gVKh
z=BCOR`RE{ecKB0HDZSC6WrKVpkZTv}h7sTpi9obo$#$o{x6z;dOvsT0dgRE;3ZjLU
z6AVU637bvMG9I8uBzdCx^2#Q@8i|h~i<5xb1=VWqJr7i0Y0Fc5>McR9)1PP7uqBJ^
zk;9m<6d$wq=u|uG6XZq(SA%M=89?f)nquB{f38AWo0pyrdzOvx
zYG%CGEo^1Dal%4isMI98oyKCkvjuI+_P-xZ&)|fr;N{}WdXfh
z>O^ji)|btECwKE`Gc2zsA;2J1EHLX7(KPk0G+lBx3H
z@JfF?OW5ameaCD*i{G*Y=GK{rL5?w9OT=e$vmiH@Qdf)(KX6HO*a
zi`(e7GA25NaIY5$b0lRqphYH^k-sqByCApmon3}>Ez<9_!0-%Fs}B1o{wnwRetWUN
zinqLW~^~ocumGKBXNn~Tfy(N^NC9%Uwr*lGWtkAt2svdaVAY$WUBcwla=r`
z(APIwvc`>iCd>U6?d}X_qO18Ro?R8y$q?JG7%h**@cQonfzAL1jF$@Z;2T0RoqYJG
zlc6*_s!g&*vFN%@I!$DW6PBRiK2qREE$(X!tO6oW7OKwEPdpNzC4%2%Qb%`Y`JHHKRS4P1@|H?=yDX_(Ja
z7e31!*0^n@FU?%Y?ayao_BWO=?2p(6DkQsIYr~5dEiOxNttB`vBA}qHvpmMH!l>g@
zCM%1#s%P$C9kczf=woPc_Dj4+^;ZPd3NQykGF(
zrtSvM{P0ad(448&vT`536Kk}oyvR>6F8Y58`}TMy-~WHgp@XDUN)DA!kwYbiW$2`G
z>hP|dSB(%OX^z{FN+nhla+;2E&Y59Su@H0I%rNCJ%#3CZoAJA*&-?rRd_V8U_xJS=
zkJ;nC?)$p0`*pou&)4($ymu#h=4Vir&rdRKo4y&E2%~5*>2ce_n}QzCrYc*X0MsRp
zUDv^wCgcZJMOt>OVrY)P;#pRsaD!DQf#9ZmYR&1!^o`x7>+cFmu}TA_j~ncUzg^U(
zy)+dSxxHsEg#GyQ7Rvd_n-3teH1_tiCIBfc+Iu!w;x*2`0in2|rBYUT`9hQE3BGPA
zHf_{Ec{kNpnGMes71=JVN5Ka0e^{2>71oeIq6vPK|3(XgL0zY+`BQT9S9atO{l}V`
z*9A*?-$^ime}zwNLRn4a&p9k{mgYySP!`;+@pwL-6H40?UxybSRb4>BtcdqVQMJ2kq0`pAm*FTls8ygdM}o)l$N0Ec?sLF%)2;I
z387w~6=8)5)Ks|=5AJ34%I0`)YJB5{Sj7?{(g4yk598~FmzJ9?|M;+kaU8*spgAsI
z)m470csLt)=XRiHn)^24&lbuq-^}m;3988Fkl;cW;^Vbg5Jf6{E~jmlUD#Bvs=IKf
z2m;Bho>5)Nwv=j5s6j~esusN~lc%)4p+)as*{VobgBVI?2^dScSus{o?t8EPv-Pzkz9f`ZbBAxYYV6YoL$lV^
zH-G=7w&;c}k4vgH>r6TyCaekj5a8v(Wh=w#68a>i8~6YD9zSr>K6j-HPZ
zBN=D>kK(}Bhm-Vr0&;e$2Y?bHY;xX(7<%%|dbR9;w7N?C{?X}hTTPs_SS~hytMb*@
z@XPtyTBdiyUuY)4AB(zNR^vNumw35E{UW>cBji|3oA|pFjC&<4ke-_i>ALtwPaRaC#ylGz0Ig-BM??QTxsE8*Pl&?oS*1
z7^~`K#CnkNgLZR@@vM#SD&c3i0V6$B2(D*)eQl6UpAC6m86Ebv%};d=cPkpWJAelq
z&GvIMr>XUKal_Lry+6>WS>hIm^QDJ)Z@SbYN@aBB_+9H4c`uec1yN)|Irc^>{M!CeOK#7^zta+*)a{QamgTD
zQn?r3EK16fo;ppFI}N)TQF-@j`Xp!8cb{3l)X3tpUr2wNsZlgZ9vt~P)YYWeH!J+E
zkJMGMcBcrASD!gl=|=Qy-g6C*6@L_5sGIH@1j|lFiO~|%U26EzX}{Uq3iS0ESGDq{
z<5Q~qYP7;R6DL@Fn00n2%c9rK=$Z8j)941Qb74zQ#UW7aQ~vFYooY@ab~cRY#WeIS
z@<5vpJCsUDPnY&TF7r~!M>_C+e*tvgrHDP*+JggH8pMHlG(5~~IPZ#|SWIPv#5G`Pt>+-v_y_d!9{p
zdE}m`0o0m^i>}V!KvDi}Fvm2Qk1`_9Z-d08mJq}}_wwK7K5uvSwJc_8StSCu3K5ZX
zLnyVPQpWaW?Vd36GPc!9y4)J@1*F^b-D)DM(jL^DC>~RJayFz4MDksRZo-6nL*}rE
z(!Zp#$9a`|`Uk){zvQS1YatQnk^hK1|JEd)Ei>I(bNP+n!}ptWOXpZ2p-o+D8AbE4
zPgO=Z{hoKf89jAe7zXM4zzG%VdksEG>Qs%)<+7
zlg-C3bShqOJL7*R^>uV`3;WfC0ga?C+%>HId(FeXRW=#6X7&eZLDh!tU9{6OS=t;?
zn)O2x!903>`}g;zMHJ_B$p_WCC-_gs_e&a5v%F=4W(M+@P02k0*Q7WKL(+OdpYl
z?SnBbh?RIDi=}Bnphri@Q#_h2g!Sp>eI}v9_M^;JA&138KZ7I}nvpYcLWZf326PJp
zD#1U13CUGf<=FnjM$ubRd8qWbRk=dSr&5Y*2>axB4Blw`Xpeuct|m64UTIYoLeSSJ
zProMROV3EN+2L4e^yFl%63+Q1mfbNY*Jj?PPhWn9nirsPj-fC~WaIQ>P|RM}n%xT#
zqKy;`nMLF5xDS7V2@=+_>1@%s)T1)?`^~PdJ6Ce;jD*Y5GPmH1L3h^vT8ia)16_$~
z<;)3U{YiC+h>`<=K3t+(^ln7R?1Cn3ZNn+)q1@^HgQExvPbrsGiT-5|LV}AMQ=aN~
zMIG|9z}N&|WkVzJ=3jAK`G(*=L-9iL%QXv`a5_GBotL@j<#n^nbdQi-@5PcXe`X&s
zdu=EIpD3tq4P{x^<*tksEvR4=u`G!3!i{q;rMW4}R8X|vd0c56mQ?opK9+&E8S1|=JWQ>En0;Hx5KogSDG0m_v
z@u()5L%=B%1$6@robzrGA6d1D_v+~tPa$AsnSvZg;We*&=PV~6yx66lZl)q^5~24(
zK3^!V5!9ZZgd=;16c9n`|0`k$`&a*HF^@&4LZ-`3P0pGR&Uxm~N5}E>&-qiQxD9p6
zzshPdf9Eag`#~e(kxMa
zE(zGviCp#$_2Ka1Tu&Ear2f&h-e66m#D_&9G_rb
z_XN4o?VTH62~)FnlxCxNU+^y6svk=$iX%n#4Wd|W2{UGcS?>B(Uy0G}7(~eaN7nq8
zf*E8kzo}5>+K5XAqG_xofw+xk!=Lvp=DH&&9Jng3u;thXGuSTzBGvp8&m+a69?w>;
zyd2NFEp?dsSnzDEzf4PH$P7rX2IOs~bxZZa=Ga-0A{vr)!L2X_{q7Mj=|U&ijc4xM
z_X())B8S+6G5P&xv8)`?59!=850WI~a_Y1q|LFJli`0#D?@AkOnJcA4s4e;|?P&g2
zZvmz>;Lx|KdTuKj&F*#^8QzC+>~mqHF~Ksr9kN2^%a@u!r;nBLKYOj!3MFV}iM19%**!O+czvyye;|!~x3UxYIpyHtbvqbt&MNFvNW06YV)~b1kgMDfaSnYwJ
zqAgFGcTcd7Q6Gd}42Z(60}p(a_WzcP{D~|^N2qq8+4oJHg=NZZw}6TwxM1aP5rv;n
zWw;;As%lK`CzuzJ0izY9_rLu)`_xx@{EgZxAbR;*ASsHm3G6TbDlCBpv?k|tj}kEQ
z;P20hD-6JYT6?|+pfLKkKFbUvRyV4s~Jp
zH;c>qO}m;q(LdQLH^=SCH_^fk4VF=w%1C2T@-d6C4|NE{<@!n$GVn<|yzpKj%6tir
zaJDwrx>spkXx`>V%3$ftd)a-nB@NM12RfErX~-MJvv7p|jEUq2kD4R(w@{b-Dmr&F%wjH7
z#C1))?J1MS#DofFa27KIja)`LvvNHO@2EpLGri$6;@#K?VO={fBaAF_epUjZ@5`out<>Lr7kKp43Dw^6)
zlGzu()>6-W&oj%j_l?~OH9KK*CL{!e@LcVArfij7t%7(@0cqUudd`U)db3@kGQ~Pk
zzp^DbXN9>JQ4j2PGE^Y~PEKCYq=WkjsP)Xd@hitodMa-U7u|qi?;P9<>W;QsKDcUd
z+-b=9HiaVb2>EIvYU^IItAH?b&&)z<%LAj22RA=9&?cu07c9GPPHlBYres@`>b6?>
zqxjxZh6~?ot3HHUyPs{xOTVcJI@kyjv!I(V|G<$P!?^oH=pG|Cl35M?6N!%qRRZ_p
zSPWkca;P;Yf_0#^SIOnU-Z$<(dTZ1u=}w7jj++N~xm;ytFmbX=<^4vF3K$EzfI|#U&nhLt=)JLt@~|(m@E5l}qth^X
z;$7ii^isx;L?iHAhN=55Lm(Dmnc|V$qZ;KM`&m0&4Iiyr`WFpB}x#wtJi697w5T^_#L0Bgmo_}ffLUTP|U$pc^|8B}EOFi3?dkw7#C7N9a
zS6O9i(;{lKs&7pQcsYii%sR*)rIx2}i%KKm!!Qf0@D9=YrxVQDZ6{~ja1
znww1*os#g?__SR(*f3^%KFNqSnCqu=slGKR?}rC}>-~a>Y)}CYU?!);#qD;;)Q9c&
zxHml>Xy+|<8@kVusChiuU&l^Kw&qsFD7Gyjvr_b;-eeXO5;WdyHwuGlamyAX0;o4@
zqTd^GF
z%C7)e7^nkORsbwq9Jz-+R&!L>P(x!{i@kF9)nZc}rr2Q|tvTY9E$GydME7F3WaZRThEdx`u~)Dhvu=>EgDcqJVE?Rngej!m7}VBMceD4Ez_qciZx
z1Os#mopLqmmzF1+RC?}W3Qv~>49o3#N8kKw=O$4R(_vtQF#of@5);xDf!bOuSp7Q5
zZSA{k<-xlz!YTyUe=xmpq#X_<8}j$UfM`4nX}o15hF$^IZqgv=*X4f#tLz@G=ueAL
z@rcQ(!lDI$BxzD|+6eGIgNnD{^zI6Eg(}DGb?mA+n*Ci?X4sA2ZY&X>nZ
zi2L)~t$f2j<>%$R=5ILQP
zyIS*#6i9gXC$G?5eTpZtY7SFc`WHIub);KD#=)dk2$tld(=^OJ=8tk@gzfOr5F7;3
zb96c{Z{v*1ox_3{A8p%P5rr??b*_Caie2jwQ>`D6^Ajw_g#Q
zWv(6r@F~+WELkW(pdQ23Zzm&qplW1yk~|QA6Am2B_pMF?o7WeH
ze^1MRE=RQEt1dZn7WP0)zLE)u5mN>M{@+TQ%~$L5jHkHj9JkJ~B
z6^h-p7kI*cK|8|lW_8E|_+RL61
z7e{`Qz4vh@(C@R#ObplTmHZYv^bW}YJ3|Q~d*|xjJM=WXv7Ou2st*BleaUuEj*)1M
zLsjlV=%{t3$QZ*%*~1_4KQc1jPXb@IoC}c?Zk6ax#>K=FD`H~z{{2+nBWJ6
zDFWQY+i8rBg8DLG50S$?Xsbe1Mnu1ueFA?2{2#t3#IE2-(+w?s4?br}Z4*6)NBi6n
zQrN->vcMB=Sz1?AW=eBW@r{-81#5iO%LBe*{}4B%26Mdq&?h`eA!7Mp(#+2NzpKImGV{$}MRWnbIxonvLo}%8i87TpMFqJ-trJ?;(XemGkafB0LB}vwbU;
zYPNco%`h1VXBpbSF0ri(j#z9I>Uj{Ys%vJThV~tVeYE%}JT{`2QKe(bJ`i
zznvjIl?WUs9?QKO-ZNYmpgE&nRAo9&tPm{LX(Ht0N^VphshWE4|F!Ojg8FKU3iKxa
z9jA~<#c}5lUN>v@G~9L``>6!@4nXzi{GL;a56E&qlq9fPs`p)C4!jTG&E-AE^!9w<
zO_xM-Hq+#B9L28?3-b0X!V&mmzrS)qKHAKlWrl>L9M;9XIo7#@m&mk1J~CwGMIc)O
z>LiO1?44>`_;AHXlADj9*I%HZ01i2=A2VcBiOlC$Z1B9>
zp?8t@&YN~$4?iZ~g*er$*B{I&aM;E-oLGI^`A=!N=U(ycO}Di_ki{FEwgeL!1YDzF
zv-he_DZQt(y*S#x36pqMdA|ExDJz13Y%Q*UdEy3cf4$ZW)Op4-AQq$mpN2*
zk3BJhU)$zv+6eHd0A9RPKyPo=P+^-P%$qc^u%G74-$tX(9m|SXH)nv$KFdapaaG0C
z5XQs4uUUA|Vm#l14wxV)Uq@j=mH)c0+k7QD
z9rtlgoi!BNWMF(RYH3dI$#Sw}1~_?>oz>HGiJ4^-GW7@c=(g9XHgBzmQ#0-;X8V7e
z@5R*F<_45s;jVWYP4@9&%WuG5B4-vElbk{)mgojG!;WJN)GwwrXPD1$PWh_P^3>@T
z#g?{hq^E3C2_yMVN?XE>YL|+HM$EiF4Lw3JW1T3T)z33Gk{dR%MJ5d%COYFP0Z-2w
zfw&cfE5&iA#|=$(cgpq`_%Ki!W7-td_uwgmc_YwUF^75WI16<$o1Y=>&0*^3`2+JZ=klXQX7=(C
zdW6RXO&E7ZXr**i|Mna|9ccgDF~1pZf21CMLB0;kU+{zDgHvtaCg)!mia9uv{$>{W
zMr!`Mk`8sPoO;mf?>5)2mO5G{L?vr17m9_#dc{2|T34!cxd@h=`tTP8ZTu4bZd-!u
z4Vo1#d3$Al|NNWWm{abIj*o{4A`N&lX1{Cu?83TU-`{n&a^Zj9JpeG~gto|OPwLS#
z4y?URlVC8G7#9qzAttUS$%8+Dz_@*_YaWi!f{)q2e8%&LaeAtXlk*N8R2*w_ba3rK
z9SCNo=fi|BsArm6K0VZla+2nzz6l;8H7wt3g3TIy5#$>TzVn!$?P)v77^!m3u0SRNYhcLLP$4oY
z?(tv91#U=I|_9r|*kWW>vak+wkePGUThXqb{f<3BCOV{9m19zQG
zdwmO`c8n!Gc?Gv0cAGprBZOsB2p@m)v^~DV=0ANTG|~=)
z_^{^?DDxaXB!x0kF~c0zCy0Q;$&~x4p#`yeD1!eRxUc9*yk(YNC>(5p-?^v^8DKK^
z9=SqQDzlA5Aj=2PuVh~t1Yh4!rg|5!C?3<27KF3c&>~i;{d)34uez!3K4fjyn$ssf
zACOIXAA{r6jsqE!a%YVrVSjC9whC5b@5w=<6~M&rvq3^E4GJ1XA}v2bZG_6`k7J){
zVnuEL{NSXim(9)9^S@%wt`jjwuN`LkI`G=pLixv)d9{U5?K?Q2w9zBr9bXkqcjp>q
z7uFfb!0Hb&oMId*{K!JAWD*07A3FTvAPa+JMS^OE%!{V@>%WrOdLucNBW%n40HDZ_
z;?u=R)V?Os+r3xCTq^rBXU#%(Y>`C58fO8&eLuT!3osEbtiTbl_UFf+GU}*F6GK4(
z*zBWtzil)1Ze4cq2;5t>=f3D1W&Zuw`q;B6CR~LhWMeXW^TnDPAEN4p{0O7$3M;;J
z(?#XHP2-+%?m`f6%C`@oAiAwK%iF?laa(4{A{86dXosi?2iKkuva~wVfll~Re*ryy
zqWoG}R!51-%4(AxKb{nBnMZ8w{Q~I@{?#`7{>(b^F8joD{y9e>#pze$
zH;mL#Ure~e{o?&^^73Q)vQ|d=D^8LA@Z(jFj4l&Nfp3oiXtWhrdkSt+X64C20MeV<
z)VJIcv;Gl!%3eIEtCZCcvqG@?Dl^qoI-XuYmlIN~u4NM~e?-B>ORQe)IFIGiZTyXp
z2VAeJLkKP7TGq!Bo(#Rdo)SUEN8vL7$dOTa#UuIhhx}%L8tyPy84Q0S;F`9Czm@vo
zV%J@>Ht>C-PUrB_cD+Ea)txC@{&1+C>j*hi*YNO0jFy0(DL%leGN@n&In-!&UZ3<4
z>*awp(zGDO^+|qP+T7ibx56b{=_VOTb|JA1QJ$VDLXIe=8E|uq7~B**?5ukLEESAR
zVoAf!KieAawC+o++U>lUgyGZY6Ynwi7E>keVQOsMf-D!u2j7^ShWQg9%A3C0Ws7V_k0GnlNC?nZ2f?uMg#?PV`T&M%ZyaNs*RzM=PYivD;{0J
zFU+1#^gfv%jk5WWl{AnJ<|zv=B-Tll+gDJ++s*FgVyIjT*8Lcpg~mW%2LSn-jF9|3Zsp2mTl8
zO>%6iwh3JeBn%gTdK76@VE&vCm}>a~Axw5t6I
zIUs_~)kz8=;?Cdv@37Ax%-Mr5Ck?_)HNgqP&wAPSh=1-kas&1jDOCeyCS)6p7<(0R
zlwf`=*<|bE@RzLDSnZa`yU1io^%W;Ax-+nz%0N&`+;S^n5dqdug$wSCXmeGlg+@k)V>*RGnh0P$>OvD4m7b^abl
z+iojoZYB;dT@J8%0TH9crQSjzE868kP>4I1T72E0P7opX0wC`ENpFjNB<60Swu$sj
z;Gf&_1e>qQ?@4wJ2vtAJH*m-)E5d;B<^#doc7Px|3A;ZxJVagewrB|3pdwVewwo(W
zu+bVa&x*yfrpmp`C<)H5nDG89C2cH=bgZ`6gdx=z>!;Ya@bv6U;KtXWzWQf3GUrWN
zua8is@IM^uE!V7G={UUbM?XbuSQIEekZ~N@RU*~eH7G$#@oja;UhPqcn*wSOh-A_K>~
z&1u?Nfm=muFphaOa^>DO{=otA+qAxK6Gi$YZ#}8+dik02A>QU@V7`#JE0ME5_7U*C
z@ACK&1?;xC_2s}_8*#I*47XEw3myTpdc8od83s=qSjl7QvE>7_zMl=Q)q}rEPifaq
z(cmJm1nxY1{8~@8>;su+6j8r??O?l)UwdJ!sC_>5SDWqHY~WD%A>9NJfyTiV95x>7
zra5=RaB`V>CTo#C#%^Gw+s<*Y^8>5qRG-Z_-iceaEEAX~dQE+$l6CR$2v
z@`iRU$)>QspupZU+Tu2ZBQ+VqCS-Yqd_&(b?`tOV^bMBkTdN6eJgPp03XK7?{;kB2$4!!5d4&TxiaYU!&ZlkHD{uz)S;XNj0NS
zEhPiC@#xkByg?Lhm*#XloKb);f;G;$7nG8$sc&<1X|M3K7%I$5s_{;D0N!<
zO&8?xlK7bRm-UGu`fTji^+EkML2mhk!$Yt|H+pk6?3kQx;u`KmIdcnEyT_-r&evU=
z&{K6pd%!~}9&^C#N$AzPxey644+0s_M?S<{AztPR7L=PxKCY52ss2tnrL~bY$B(5o
zA%~_b4wap*CTJ#->e@JCL%}E7<9f83LTD7-EyyO!-Z4Wv#j8Iupb{o6$mCB-m7_7*
z@s9_47NVaFW@EH|`vQiMJq)G%LeH)YIj*22r|ijE@VwLO6Qs~`IYhDE&q*<%M=!tf
zdfIRWEk>i0W=sn9TrcwZ-I$9xnnIj|w
zp)d7tmG}MjZGk?M*JkTP>_oipgEAW+<^EWx0vI{PLp(8Qx{rTyiHRMXti9
zrh2#lw-W))CEP!RM)|{5jd11{9KY-DaI8s6FB}hXiOzSg3?`6(2}&L?z%VE8pm#bl
zh?zdz#fDNo{0ve9y~7d4Lq6oB2RKWn&c>sA36tF>Jd44ks_S_n%t?Ny)tMe$;A!0q
mAK^QYKnc;Yv@pCZviCyD);LeGcJQ!>oIPcIGVl1cd;brY>Xu~y
literal 0
HcmV?d00001
diff --git a/mc_requirements.txt b/mc_requirements.txt
new file mode 100644
index 000000000..b01171201
--- /dev/null
+++ b/mc_requirements.txt
@@ -0,0 +1,3 @@
+javascript
+requests
+psutil
\ No newline at end of file
diff --git a/metagpt/actions/minecraft/generate_actions.py b/metagpt/actions/minecraft/generate_actions.py
index e32aab485..275cc8b38 100644
--- a/metagpt/actions/minecraft/generate_actions.py
+++ b/metagpt/actions/minecraft/generate_actions.py
@@ -4,6 +4,7 @@
# @Desc :
from metagpt.logs import logger
from metagpt.actions import Action
+from metagpt.utils.minecraft import parse_action_response
class GenerateActionCode(Action):
@@ -11,23 +12,33 @@ class GenerateActionCode(Action):
Action class for generating action code.
Refer to the code in the voyager/agents/action.py for implementation details.
"""
-
+
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
-
- async def generate_code(self):
+
+ async def generate_code(self, human_msg, system_msg=[]):
"""
Generate action code logic.
Implement the logic for generating action code here.
"""
- return ""
-
- async def run(self, human_msg, system_msg=[], *args, **kwargs):
+ rsp = await self._aask(prompt=human_msg, system_msgs=system_msg)
+ parsed_result = parse_action_response(rsp)
+ # logger.info(f"parsed_result is HERE: {parsed_result}")
+
+ try:
+ return parsed_result["program_code"] + "\n" + parsed_result["exec_code"]
+ except:
+ logger.error(f"Failed to parse response: {parsed_result}")
+ return None
+
+ async def run(self, msg, *args, **kwargs):
logger.info(f"run {self.__repr__()}")
# Generate action code.
- generated_code = await self.generate_code()
-
+ generated_code = await self.generate_code(
+ human_msg=msg['human_msg'], system_msg=msg['system_msg']
+ )
+
# Return the generated code.
return generated_code
@@ -37,10 +48,10 @@ class SummarizeLog(Action):
Action class for parsing and summarizing logs.
Refer to the code in the voyager/agents/action.py for implementation details.
"""
-
+
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
-
+
async def summarize_logs(self):
"""
Summarize chatlogs.
@@ -48,10 +59,10 @@ class SummarizeLog(Action):
Implement the logic for summarizing chatlogs here.
"""
return ""
-
+
async def run(self, *args, **kwargs):
# Summarize chatlogs.
summary = await self.summarize_logs()
-
+
# Return the summary.
return summary
diff --git a/metagpt/minecraft_team.py b/metagpt/minecraft_team.py
index 9e99b0cd5..8c5ad03ed 100644
--- a/metagpt/minecraft_team.py
+++ b/metagpt/minecraft_team.py
@@ -6,6 +6,7 @@ from typing import Iterable, Dict, Any
from pydantic import BaseModel, Field
import requests
import json
+import asyncio
from metagpt.logs import logger
from metagpt.roles import Role
@@ -15,44 +16,53 @@ from metagpt.software_company import SoftwareCompany
from metagpt.actions.minecraft.player_action import PlayerActions
from metagpt.roles.minecraft.minecraft_base import Minecraft
from metagpt.environment import Environment
-from .mineflayer_environment import MineflayerEnv
+from metagpt.mineflayer_environment import MineflayerEnv
+
class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
"""
游戏环境的记忆,用于多个agent进行信息的共享和缓存,而不需要重复在自己的角色内维护缓存
"""
+
event: dict[str, Any] = Field(default_factory=dict)
current_task: str = Field(default="Craft 4 wooden planks")
task_execution_time: float = Field(default=float)
context: str = Field(default="")
-
- code: str = Field(default="")
+ code: str = Field(default=None)
programs: str = Field(default="")
+ critique: str = Field(default="")
+ skills: list[str] = Field(default_factory=list)
- mf_instance : MineflayerEnv = Field(default_factory=MineflayerEnv)
+ mf_instance: MineflayerEnv = Field(default_factory=MineflayerEnv)
def set_mc_port(self, mc_port):
self.mf_instance.set_mc_port(mc_port)
-
+
def register_roles(self, roles: Iterable[Minecraft]):
for role in roles:
role.set_memory(self)
-
+
def update_event(self, event: Dict):
self.event = event
-
+
def update_task(self, task: str):
self.current_task = task
-
+
def update_context(self, context: str):
self.context = context
def update_code(self, code: str):
- self.code = code
+ self.code = code # action_developer.gen to HERE
def update_programs(self, programs: str):
self.programs = programs
+ def update_critique(self, critique: str):
+ self.critique = critique # critic_agent.check_task_success to HERE
+
+ def update_skills(self, skills: list):
+ self.skills = skills # skill_manager.retrieve_skills to HERE
+
async def on_event(self, *args):
"""
Retrieve Minecraft events.
@@ -70,10 +80,12 @@ class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
if not self.mf_instance.has_reset:
# TODO Modify
logger.info("Environment has not been reset yet, is resetting")
- self.mf_instance.reset(options={
- "mode": "soft",
- "wait_ticks": 20,
- })
+ self.mf_instance.reset(
+ options={
+ "mode": "soft",
+ "wait_ticks": 20,
+ }
+ )
# raise {}
self.mf_instance.check_process()
self.mf_instance.unpause()
@@ -82,7 +94,9 @@ class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
"programs": self.programs,
}
res = requests.post(
- f"{self.mf_instance.server}/step", json=data, timeout=self.mf_instance.request_timeout
+ f"{self.mf_instance.server}/step",
+ json=data,
+ timeout=self.mf_instance.request_timeout,
)
if res.status_code != 200:
logger.error("Failed to step Minecraft server")
@@ -96,33 +110,37 @@ class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
logger.error(f"Failed to retrieve Minecraft events: {str(e)}")
raise {}
+
class MinecraftPlayer(SoftwareCompany):
"""
Software Company: Possesses a team, SOP (Standard Operating Procedures), and a platform for instant messaging,
dedicated to writing executable code.
"""
+
environment: Environment = Field(default_factory=Environment)
game_memory: GameEnvironment = Field(default_factory=GameEnvironment)
investment: float = Field(default=50.0)
task: str = Field(default="")
game_info: dict = Field(default={})
-
+
def set_port(self, mc_port):
self.game_memory.set_mc_port(mc_port)
def hire(self, roles: list[Role]):
self.environment.add_roles(roles)
self.game_memory.register_roles(roles)
-
+
def start(self, task):
"""Start a project from publishing boss requirement."""
self.task = task
- self.environment.publish_message(Message(role="Player", content=task, cause_by=PlayerActions))
+ self.environment.publish_message(
+ Message(role="Player", content=task, cause_by=PlayerActions)
+ )
logger.info(self.game_info)
-
+
def _save(self):
logger.info(self.json())
-
+
async def run(self, n_round=3):
"""Run company until target round or no money"""
while n_round > 0:
@@ -131,13 +149,19 @@ class MinecraftPlayer(SoftwareCompany):
logger.debug(f"{n_round=}")
self._check_balance()
await self.environment.run()
-
+
return self.environment.history
-if "__name__" == "__main__":
+
+async def main():
test_code = "bot.chat(`/time set ${getNextTime()}`);"
- mc_port = 1960
+ mc_port = 6286
ge = GameEnvironment()
ge.set_mc_port(mc_port)
ge.update_code(test_code)
- logger.info(ge.on_event())
\ No newline at end of file
+ result = await ge.on_event()
+ logger.info("On event test done")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/metagpt/mineflayer_environment.py b/metagpt/mineflayer_environment.py
index 1b208c8d5..52b54ebfb 100644
--- a/metagpt/mineflayer_environment.py
+++ b/metagpt/mineflayer_environment.py
@@ -11,6 +11,7 @@ from metagpt.logs import logger
import metagpt.utils.minecraft as U
from metagpt.utils.minecraft.process_monitor import SubprocessMonitor
+
class MineflayerEnv:
def __init__(
self,
@@ -66,18 +67,21 @@ class MineflayerEnv:
)
if res.status_code != 200:
self.mineflayer.stop()
- logger.error(
- f"Minecraft server reply with code {res.status_code}"
- )
+ logger.error(f"Minecraft server reply with code {res.status_code}")
raise {}
return res.json()
- def reset(self, *, seed=None, options=None, ):
+ def reset(
+ self,
+ *,
+ seed=None,
+ options=None,
+ ):
if options is None:
options = {}
if options.get("inventory", {}) and options.get("mode", "hard") != "hard":
logger.error("inventory can only be set when options is hard")
- raise{}
+ raise {}
self.reset_options = {
"port": self.mc_port,
@@ -100,7 +104,7 @@ class MineflayerEnv:
self.reset_options["reset"] = "soft"
self.pause()
return json.loads(returned_data)
-
+
def close(self):
self.unpause()
if self.connected:
diff --git a/metagpt/prompts/minecraft/action_response_format.txt b/metagpt/prompts/minecraft/action_response_format.txt
deleted file mode 100644
index 21d22371d..000000000
--- a/metagpt/prompts/minecraft/action_response_format.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Explain: ...
-Plan:
-1) ...
-2) ...
-3) ...
-...
-Code:
-```javascript
-// helper functions (only if needed, try to avoid them)
-...
-// main function after the helper functions
-async function yourMainFunctionName(bot) {
- // ...
-}
-```
diff --git a/metagpt/prompts/minecraft/critic.txt b/metagpt/prompts/minecraft/critic.txt
deleted file mode 100644
index 50dd71cc9..000000000
--- a/metagpt/prompts/minecraft/critic.txt
+++ /dev/null
@@ -1,127 +0,0 @@
-You are an assistant that assesses my progress of playing Minecraft and provides useful guidance.
-
-You are required to evaluate if I have met the task requirements. Exceeding the task requirements is also considered a success while failing to meet them requires you to provide critique to help me improve.
-
-I will give you the following information:
-
-Biome: The biome after the task execution.
-Time: The current time.
-Nearby blocks: The surrounding blocks. These blocks are not collected yet. However, this is useful for some placing or planting tasks.
-Health: My current health.
-Hunger: My current hunger level. For eating task, if my hunger level is 20.0, then I successfully ate the food.
-Position: My current position.
-Equipment: My final equipment. For crafting tasks, I sometimes equip the crafted item.
-Inventory (xx/36): My final inventory. For mining and smelting tasks, you only need to check inventory.
-Chests: If the task requires me to place items in a chest, you can find chest information here.
-Task: The objective I need to accomplish.
-Context: The context of the task.
-
-You should only respond in JSON format as described below:
-{
- "reasoning": "reasoning",
- "success": boolean,
- "critique": "critique",
-}
-Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
-
-Here are some examples:
-INPUT:
-Inventory (2/36): {'oak_log':2, 'spruce_log':2}
-
-Task: Mine 3 wood logs
-
-RESPONSE:
-{
- "reasoning": "You need to mine 3 wood logs. You have 2 oak logs and 2 spruce logs, which add up to 4 wood logs.",
- "success": true,
- "critique": ""
-}
-
-INPUT:
-Inventory (3/36): {'crafting_table': 1, 'spruce_planks': 6, 'stick': 4}
-
-Task: Craft a wooden pickaxe
-
-RESPONSE:
-{
- "reasoning": "You have enough materials to craft a wooden pickaxe, but you didn't craft it.",
- "success": false,
- "critique": "Craft a wooden pickaxe with a crafting table using 3 spruce planks and 2 sticks."
-}
-
-INPUT:
-Inventory (2/36): {'raw_iron': 5, 'stone_pickaxe': 1}
-
-Task: Mine 5 iron_ore
-
-RESPONSE:
-{
- "reasoning": "Mining iron_ore in Minecraft will get raw_iron. You have 5 raw_iron in your inventory.",
- "success": true,
- "critique": ""
-}
-
-INPUT:
-Biome: plains
-
-Nearby blocks: stone, dirt, grass_block, grass, farmland, wheat
-
-Inventory (26/36): ...
-
-Task: Plant 1 wheat seed.
-
-RESPONSE:
-{
- "reasoning": "For planting tasks, inventory information is useless. In nearby blocks, there is farmland and wheat, which means you succeed to plant the wheat seed.",
- "success": true,
- "critique": ""
-}
-
-INPUT:
-Inventory (11/36): {... ,'rotten_flesh': 1}
-
-Task: Kill 1 zombie
-
-Context: ...
-
-RESPONSE
-{
- "reasoning": "You have rotten flesh in your inventory, which means you successfully killed one zombie.",
- "success": true,
- "critique": ""
-}
-
-INPUT:
-Hunger: 20.0/20.0
-
-Inventory (11/36): ...
-
-Task: Eat 1 ...
-
-Context: ...
-
-RESPONSE
-{
- "reasoning": "For all eating task, if the player's hunger is 20.0, then the player successfully ate the food.",
- "success": true,
- "critique": ""
-}
-
-INPUT:
-Nearby blocks: chest
-
-Inventory (28/36): {'rail': 1, 'coal': 2, 'oak_planks': 13, 'copper_block': 1, 'diorite': 7, 'cooked_beef': 4, 'granite': 22, 'cobbled_deepslate': 23, 'feather': 4, 'leather': 2, 'cooked_chicken': 3, 'white_wool': 2, 'stick': 3, 'black_wool': 1, 'stone_sword': 2, 'stone_hoe': 1, 'stone_axe': 2, 'stone_shovel': 2, 'cooked_mutton': 4, 'cobblestone_wall': 18, 'crafting_table': 1, 'furnace': 1, 'iron_pickaxe': 1, 'stone_pickaxe': 1, 'raw_copper': 12}
-
-Chests:
-(81, 131, 16): {'andesite': 2, 'dirt': 2, 'cobblestone': 75, 'wooden_pickaxe': 1, 'wooden_sword': 1}
-
-Task: Deposit useless items into the chest at (81, 131, 16)
-
-Context: ...
-
-RESPONSE
-{
- "reasoning": "You have 28 items in your inventory after depositing, which is more than 20. You need to deposit more items from your inventory to the chest.",
- "success": false,
- "critique": "Deposit more useless items such as copper_block, diorite, granite, cobbled_deepslate, feather, and leather to meet the requirement of having only 20 occupied slots in your inventory."
-}
diff --git a/metagpt/prompts/minecraft/curriculum.txt b/metagpt/prompts/minecraft/curriculum.txt
deleted file mode 100644
index 279d24f82..000000000
--- a/metagpt/prompts/minecraft/curriculum.txt
+++ /dev/null
@@ -1,42 +0,0 @@
-You are a helpful assistant that tells me the next immediate task to do in Minecraft. My ultimate goal is to discover as many diverse things as possible, accomplish as many diverse tasks as possible and become the best Minecraft player in the world.
-
-I will give you the following information:
-Question 1: ...
-Answer: ...
-Question 2: ...
-Answer: ...
-Question 3: ...
-Answer: ...
-...
-Biome: ...
-Time: ...
-Nearby blocks: ...
-Other blocks that are recently seen: ...
-Nearby entities (nearest to farthest): ...
-Health: Higher than 15 means I'm healthy.
-Hunger: Higher than 15 means I'm not hungry.
-Position: ...
-Equipment: If I have better armor in my inventory, you should ask me to equip it.
-Inventory (xx/36): ...
-Chests: You can ask me to deposit or take items from these chests. There also might be some unknown chest, you should ask me to open and check items inside the unknown chest.
-Completed tasks so far: ...
-Failed tasks that are too hard: ...
-
-You must follow the following criteria:
-1) You should act as a mentor and guide me to the next task based on my current learning progress.
-2) Please be very specific about what resources I need to collect, what I need to craft, or what mobs I need to kill.
-3) The next task should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]" etc. It should be a single phrase. Do not propose multiple tasks at the same time. Do not mention anything else.
-4) The next task should not be too hard since I may not have the necessary resources or have learned enough skills to complete it yet.
-5) The next task should be novel and interesting. I should look for rare resources, upgrade my equipment and tools using better materials, and discover new things. I should not be doing the same thing over and over again.
-6) I may sometimes need to repeat some tasks if I need to collect more resources to complete more difficult tasks. Only repeat tasks if necessary.
-7) Do not ask me to build or dig shelter even if it's at night. I want to explore the world and discover new things. I don't want to stay in one place.
-8) Tasks that require information beyond the player's status to verify should be avoided. For instance, "Placing 4 torches" and "Dig a 2x1x2 hole" are not ideal since they require visual confirmation from the screen. All the placing, building, planting, and trading tasks should be avoided. Do not propose task starting with these keywords.
-
-You should only respond in the format as described below:
-RESPONSE FORMAT:
-Reasoning: Based on the information I listed above, do reasoning about what the next task should be.
-Task: The next task.
-
-Here's an example response:
-Reasoning: The inventory is empty now, chop down a tree to get some wood.
-Task: Obtain a wood log.
diff --git a/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt b/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt
deleted file mode 100644
index bb5e26bf1..000000000
--- a/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-You are a helpful assistant that answer my question about Minecraft.
-
-I will give you the following information:
-Question: ...
-
-You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft.
-1) Start your answer with "Answer: ".
-2) Answer "Answer: Unknown" if you don't know the answer.
diff --git a/metagpt/prompts/minecraft/curriculum_task_decomposition.txt b/metagpt/prompts/minecraft/curriculum_task_decomposition.txt
deleted file mode 100644
index bb5d6707e..000000000
--- a/metagpt/prompts/minecraft/curriculum_task_decomposition.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-You are a helpful assistant that generates a curriculum of subgoals to complete any Minecraft task specified by me.
-
-I'll give you a final task and my current inventory, you need to decompose the task into a list of subgoals based on my inventory.
-
-You must follow the following criteria:
-1) Return a Python list of subgoals that can be completed in order to complete the specified task.
-2) Each subgoal should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]".
-3) Include each level of necessary tools as a subgoal, such as wooden, stone, iron, diamond, etc.
-
-You should only respond in JSON format as described below:
-["subgoal1", "subgoal2", "subgoal3", ...]
-Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
diff --git a/metagpt/prompts/minecraft/skill.txt b/metagpt/prompts/minecraft/skill.txt
deleted file mode 100644
index efedcecc6..000000000
--- a/metagpt/prompts/minecraft/skill.txt
+++ /dev/null
@@ -1,51 +0,0 @@
-You are a helpful assistant that writes a description of the given function written in Mineflayer javascript code.
-
-1) Do not mention the function name.
-2) Do not mention anything about `bot.chat` or helper functions.
-3) There might be some helper functions before the main function, but you only need to describe the main function.
-4) Try to summarize the function in no more than 6 sentences.
-5) Your response should be a single line of text.
-
-For example, if the function is:
-
-async function mineCobblestone(bot) {
- // Check if the wooden pickaxe is in the inventory, if not, craft one
- let woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
- if (!woodenPickaxe) {
- bot.chat("Crafting a wooden pickaxe.");
- await craftWoodenPickaxe(bot);
- woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
- }
-
- // Equip the wooden pickaxe if it exists
- if (woodenPickaxe) {
- await bot.equip(woodenPickaxe, "hand");
-
- // Explore until we find a stone block
- await exploreUntil(bot, new Vec3(1, -1, 1), 60, () => {
- const stone = bot.findBlock({
- matching: mcData.blocksByName["stone"].id,
- maxDistance: 32
- });
- if (stone) {
- return true;
- }
- });
-
- // Mine 8 cobblestone blocks using the wooden pickaxe
- bot.chat("Found a stone block. Mining 8 cobblestone blocks.");
- await mineBlock(bot, "stone", 8);
- bot.chat("Successfully mined 8 cobblestone blocks.");
-
- // Save the event of mining 8 cobblestone
- bot.save("cobblestone_mined");
- } else {
- bot.chat("Failed to craft a wooden pickaxe. Cannot mine cobblestone.");
- }
-}
-
-The main function is `mineCobblestone`.
-
-Then you would write:
-
-The function is about mining 8 cobblestones using a wooden pickaxe. First check if a wooden pickaxe is in the inventory. If not, craft one. If the wooden pickaxe is available, equip the wooden pickaxe in the hand. Next, explore the environment until finding a stone block. Once a stone block is found, mine a total of 8 cobblestone blocks using the wooden pickaxe.
diff --git a/metagpt/roles/minecraft/action_developer.py b/metagpt/roles/minecraft/action_developer.py
index d89e823f3..30b80ea4a 100644
--- a/metagpt/roles/minecraft/action_developer.py
+++ b/metagpt/roles/minecraft/action_developer.py
@@ -2,13 +2,22 @@
# @Date : 2023/9/23 12:45
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
+import asyncio
+
from metagpt.logs import logger
from metagpt.roles.minecraft.minecraft_base import Minecraft as Base
from metagpt.schema import Message, HumanMessage, SystemMessage
from metagpt.roles.minecraft.minecraft_base import agent_registry
from metagpt.actions.minecraft.generate_actions import GenerateActionCode
from metagpt.actions.minecraft.design_curriculumn import DesignCurriculum
-from metagpt.actions.minecraft.manage_skills import GenerateSkillDescription, RetrieveSkills, AddNewSkills
+from metagpt.actions.minecraft.manage_skills import (
+ GenerateSkillDescription,
+ RetrieveSkills,
+ AddNewSkills,
+)
+import metagpt.utils.minecraft as utils
+from metagpt.config import CONFIG
+from metagpt.minecraft_team import GameEnvironment
@agent_registry.register("action_developer")
@@ -17,22 +26,153 @@ class ActionDeveloper(Base):
iterative prompting mechanism in paper.
generate action code based on environment observation and plan, as well as skills retrieval results
"""
-
+
def __init__(
- self,
- name: str = "Bob",
- profile: str = "Generate code for specified tasks",
- goal: str = "Produce accurate and efficient code solutions in Python and JavaScript",
- constraints: str = "Adhere to coding best practices and style guidelines",
+ self,
+ name: str = "Bob",
+ profile: str = "Generate code for specified tasks",
+ goal: str = "Produce accurate and efficient code solutions in Python and JavaScript",
+ constraints: str = "Adhere to coding best practices and style guidelines",
) -> None:
super().__init__(name, profile, goal, constraints)
# Initialize actions specific to the Action role
self._init_actions([GenerateActionCode])
-
+
# Set events or actions the ActionAgent should watch or be aware of
# 需要根据events进行自己chest_observation的更新
self._watch([RetrieveSkills])
-
+
+ def render_system_message(self, skills=[], *args, **kwargs):
+ """
+ According to basic skills context files to genenarate js skill codes.
+ Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
+ """
+
+ action_template = utils.load_prompt("action_template")
+ base_skills = [
+ "exploreUntil",
+ "mineBlock",
+ "craftItem",
+ "placeItem",
+ "smeltItem",
+ "killMob",
+ ]
+ if not CONFIG.openai_api_model == "gpt-3.5-turbo":
+ base_skills += [
+ "useChest",
+ "mineflayer",
+ ]
+ programs = "\n\n".join(utils.load_skills_code_context(base_skills) + skills)
+ response_format = utils.load_prompt("action_response_format")
+ system_action_prompt = action_template.format(
+ programs=programs, response_format=response_format
+ )
+ system_action_message = SystemMessage(content=system_action_prompt)
+ assert isinstance(system_action_message, SystemMessage)
+ return system_action_message
+
+ def render_human_message(
+ self, events, code="", task="", context="", critique="", *args, **kwargs
+ ):
+ """
+ Integrate observation about the environment(especially events), add to HumanMessage.
+ Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
+ """
+
+ # Deal with events info
+ chat_messages = []
+ error_messages = []
+ # damage_messages = [] # TODO: try to add damage_messages into prompt later
+ assert events[-1][0] == "observe", "Last event must be observe"
+
+ for i, (event_type, event) in enumerate(events):
+ if event_type == "onChat":
+ chat_messages.append(event["onChat"])
+ elif event_type == "onError":
+ error_messages.append(event["onError"])
+ elif event_type == "observe":
+ biome = event["status"]["biome"]
+ time_of_day = event["status"]["timeOfDay"]
+ voxels = event["voxels"]
+ entities = event["status"]["entities"]
+ health = event["status"]["health"]
+ hunger = event["status"]["food"]
+ position = event["status"]["position"]
+ equipment = event["status"]["equipment"]
+ inventory_used = event["status"]["inventoryUsed"]
+ inventory = event["inventory"]
+ assert i == len(events) - 1, "observe must be the last event"
+
+ # Collect all the environment information into a str: observation
+ observation = ""
+
+ observation = (
+ f"Code from the last round:\n{code or 'No code in the first round'}\n\n"
+ )
+
+ if error_messages:
+ error = "\n".join(error_messages)
+ observation += f"Execution error:\n{error}\n\n"
+ else:
+ observation += f"Execution error: No error\n\n"
+
+ if chat_messages:
+ chat_log = "\n".join(chat_messages)
+ observation += f"Chat log: {chat_log}\n\n"
+ else:
+ observation += f"Chat log: None\n\n"
+
+ observation += f"Biome: {biome}\n\n"
+ observation += f"Time: {time_of_day}\n\n"
+ observation += f"Nearby blocks: {', '.join(voxels) if voxels else 'None'}\n\n"
+
+ if entities:
+ nearby_entities = [
+ k for k, v in sorted(entities.items(), key=lambda x: x[1])
+ ]
+ observation += f"Nearby entities (nearest to farthest): {', '.join(nearby_entities)}\n\n"
+ else:
+ observation += f"Nearby entities (nearest to farthest): None\n\n"
+
+ observation += f"Health: {health:.1f}/20\n\n"
+ observation += f"Hunger: {hunger:.1f}/20\n\n"
+ observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n"
+ observation += f"Equipment: {equipment}\n\n"
+ observation += f"Inventory ({inventory_used}/36): {'Empty' if not inventory else ', '.join(inventory)}\n\n"
+
+ if not (
+ task == "Place and deposit useless items into a chest"
+ or task.startswith("Deposit useless items into the chest at")
+ ):
+ # TODO: observation += self.render_chest_observation()
+ logger.warning("chest_observation will add later")
+
+ observation += f"Task: {task}\n\n"
+ observation += f"Context: {context or 'None'}\n\n"
+ observation += f"Critique: {critique or 'None'}\n\n"
+
+ return HumanMessage(content=observation)
+
+ def encapsule_message(
+ self,
+ events,
+ code="",
+ task="",
+ context="",
+ critique="",
+ skills=[],
+ *args,
+ **kwargs,
+ ):
+ system_message = self.render_system_message(skills=skills)
+ human_message = self.render_human_message(
+ events=events, code=code, task=task, context=context, critique=critique
+ )
+ return {
+ "system_msg": [system_message.content],
+ "human_msg": human_message.content,
+ }
+
async def _observe(self) -> int:
await super()._observe()
for msg in self._rc.news:
@@ -42,26 +182,42 @@ class ActionDeveloper(Base):
] # only relevant msgs count as observed news
logger.info(len(self._rc.news))
return len(self._rc.news)
-
- async def generate_action_code(self, human_msg, system_msg, *args, **kwargs):
- code = await GenerateActionCode().run(human_msg)
- logger.info(code)
- msg = Message(content=f"test_action", instruct_content="generate_action_code", role=self.profile)
+
+ async def generate_action_code(self, msg, *args, **kwargs):
+ code = await GenerateActionCode().run(msg, *args, **kwargs)
+ # logger.warning(type(code))
+ # logger.info(f"Code is Here:{code}")
+ self.perform_game_info_callback(code, self.game_memory.update_code)
+ msg = Message(
+ content=f"{code}",
+ instruct_content="generate_action_code",
+ role=self.profile,
+ )
logger.info(msg)
return msg
-
+
async def _act(self) -> Message:
todo = self._rc.todo
logger.debug(f"Todo is {todo}")
-
+
# 获取最新的游戏周边信息
+ events = await self._obtain_events()
context = self.game_memory.context
task = self.game_memory.current_task
+ code = self.game_memory.code
+ critique = self.game_memory.critique
+ skills = self.game_memory.skills
- message = self.encapsule_message(task, context)
+ message = self.encapsule_message(
+ events=events,
+ code=code,
+ task=task,
+ context=context,
+ critique=critique,
+ skills=skills,
+ )
logger.info(todo)
handler_map = {
-
GenerateActionCode: self.generate_action_code,
}
handler = handler_map.get(type(todo))
@@ -69,10 +225,95 @@ class ActionDeveloper(Base):
if handler:
msg = await handler(**message)
- logger.info(msg)
msg.cause_by = type(todo)
logger.info(msg.send_to)
self._publish_message(msg)
return msg
-
- raise ValueError(f"Unknown todo type: {type(todo)}")
\ No newline at end of file
+
+ raise ValueError(f"Unknown todo type: {type(todo)}")
+
+
+async def main():
+ events = [
+ [
+ "observe",
+ {
+ "voxels": ["grass_block", "dirt", "grass"],
+ "status": {
+ "health": 20,
+ "food": 20,
+ "saturation": 5,
+ "oxygen": 20,
+ "position": {"x": 0.5, "y": 84, "z": -207.5},
+ "velocity": {"x": 0, "y": -0.0784000015258789, "z": 0},
+ "yaw": 3.141592653589793,
+ "pitch": 0,
+ "onGround": True,
+ "equipment": [None, None, None, None, None, None],
+ "name": "bot",
+ "isInWater": False,
+ "isInLava": False,
+ "isCollidedHorizontally": False,
+ "isCollidedVertically": True,
+ "biome": "plains",
+ "entities": {
+ "chicken": 29.071822119730644,
+ "sheep": 20.361212992763768,
+ },
+ "timeOfDay": "day",
+ "inventoryUsed": 0,
+ "elapsedTime": 41,
+ },
+ "inventory": {},
+ "nearbyChests": {},
+ "blockRecords": ["grass_block", "dirt", "grass"],
+ },
+ ]
+ ]
+ code = """
+ collectBamboo.code async function collectBamboo(bot) {
+ // Equip the iron sword
+ const ironSword = bot.inventory.findInventoryItem(mcData.itemsByName.iron_sword.id);
+ await bot.equip(ironSword, "hand");
+
+ // Find bamboo plants using the exploreUntil function
+ const bambooPlants = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
+ const bambooPlants = bot.findBlocks({
+ matching: block => block.name === "bamboo",
+ maxDistance: 32,
+ count: 10
+ });
+ return bambooPlants.length >= 10 ? bambooPlants : null;
+ });
+ if (!bambooPlants) {
+ bot.chat("Could not find enough bamboo plants.");
+ return;
+ }
+
+ // Break 10 bamboo plants using the iron sword
+ for (const bambooPlant of bambooPlants) {
+ const block = bot.blockAt(bambooPlant);
+ await bot.dig(block);
+ }
+ bot.chat("Broke 10 bamboo plants.");
+
+ // Collect the dropped bamboo items
+ for (const bambooPlant of bambooPlants) {
+ await bot.pathfinder.goto(new GoalBlock(bambooPlant.x, bambooPlant.y, bambooPlant.z));
+ }
+ bot.chat("Collected 10 bamboo.");
+ }
+ """
+ ad = ActionDeveloper()
+ ge = GameEnvironment()
+ ad.set_memory(shared_memory=ge)
+ msg = ad.encapsule_message(events=events, code=code)
+ logger.info(f"Encapsuled_message: {msg}")
+
+ parsed_result = await ad.generate_action_code(msg)
+
+ logger.info(f"Parsed_code_updating: {parsed_result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/metagpt/roles/minecraft/minecraft_base.py b/metagpt/roles/minecraft/minecraft_base.py
index c73837a36..6834de606 100644
--- a/metagpt/roles/minecraft/minecraft_base.py
+++ b/metagpt/roles/minecraft/minecraft_base.py
@@ -8,7 +8,6 @@ import json
from metagpt.logs import logger
from metagpt.roles.role import Role
from metagpt.schema import HumanMessage, SystemMessage
-
from typing import Dict
from pydantic import BaseModel
diff --git a/metagpt/utils/minecraft/__init__.py b/metagpt/utils/minecraft/__init__.py
index 4c7624d55..e61f130c4 100644
--- a/metagpt/utils/minecraft/__init__.py
+++ b/metagpt/utils/minecraft/__init__.py
@@ -4,4 +4,6 @@
# @Desc :
from .load_prompts import load_prompt
from .json_utils import *
-from .file_utils import *
\ No newline at end of file
+from .file_utils import *
+from .load_skills_code_context import load_skills_code_context
+from .action_rsp_parser import parse_action_response
diff --git a/metagpt/utils/minecraft/action_rsp_parser.py b/metagpt/utils/minecraft/action_rsp_parser.py
new file mode 100644
index 000000000..d2f150e44
--- /dev/null
+++ b/metagpt/utils/minecraft/action_rsp_parser.py
@@ -0,0 +1,69 @@
+import re
+import time
+from javascript import require
+
+
+def parse_action_response(msg: str):
+ """
+ Return:
+ {
+ "program_code": program_code,
+ "program_name": main_function["name"],
+ "exec_code": exec_code,
+ }
+ Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
+ """
+
+ retry = 3
+ error = None
+ while retry > 0:
+ try:
+ babel = require("@babel/core")
+ babel_generator = require("@babel/generator").default
+
+ code_pattern = re.compile(r"```(?:javascript|js)(.*?)```", re.DOTALL)
+ code = "\n".join(code_pattern.findall(msg))
+ parsed = babel.parse(code)
+ functions = []
+ assert len(list(parsed.program.body)) > 0, "No functions found"
+ for i, node in enumerate(parsed.program.body):
+ if node.type != "FunctionDeclaration":
+ continue
+ node_type = (
+ "AsyncFunctionDeclaration"
+ if node["async"]
+ else "FunctionDeclaration"
+ )
+ functions.append(
+ {
+ "name": node.id.name,
+ "type": node_type,
+ "body": babel_generator(node).code,
+ "params": list(node["params"]),
+ }
+ )
+ # find the last async function
+ main_function = None
+ for function in reversed(functions):
+ if function["type"] == "AsyncFunctionDeclaration":
+ main_function = function
+ break
+ assert (
+ main_function is not None
+ ), "No async function found. Your main function must be async."
+ assert (
+ len(main_function["params"]) == 1
+ and main_function["params"][0].name == "bot"
+ ), f"Main function {main_function['name']} must take a single argument named 'bot'"
+ program_code = "\n\n".join(function["body"] for function in functions)
+ exec_code = f"await {main_function['name']}(bot);"
+ return {
+ "program_code": program_code,
+ "program_name": main_function["name"],
+ "exec_code": exec_code,
+ }
+ except Exception as e:
+ retry -= 1
+ error = e
+ time.sleep(1)
+ return f"Error parsing action response (before program execution): {error}"
diff --git a/metagpt/utils/minecraft/file_utils.py b/metagpt/utils/minecraft/file_utils.py
index 8f199329b..ca82f99b6 100644
--- a/metagpt/utils/minecraft/file_utils.py
+++ b/metagpt/utils/minecraft/file_utils.py
@@ -15,6 +15,7 @@ is_dir = os.path.isdir
get_dir = os.path.dirname
+
def is_sequence(obj):
"""
Returns:
@@ -78,7 +79,8 @@ def load_text(*fpaths, by_lines=False):
def load_text_lines(*fpaths):
return load_text(*fpaths, by_lines=True)
+
# aliases to be consistent with other load_* and dump_*
text_load = load_text
read_text = load_text
-read_text_lines = load_text_lines
\ No newline at end of file
+read_text_lines = load_text_lines
diff --git a/metagpt/utils/minecraft/json_utils.py b/metagpt/utils/minecraft/json_utils.py
index 2814a5887..03f357cec 100644
--- a/metagpt/utils/minecraft/json_utils.py
+++ b/metagpt/utils/minecraft/json_utils.py
@@ -8,6 +8,7 @@ import re
from typing import Any, Dict, Union
from .file_utils import f_join
+
def json_load(*file_path, **kwargs):
file_path = f_join(file_path)
with open(file_path, "r") as fp:
@@ -144,6 +145,7 @@ def correct_json(json_str: str) -> str:
return balanced_str
return json_str
+
def fix_and_parse_json(
json_str: str, try_to_fix_with_gpt: bool = True
) -> Union[str, Dict[Any, Any]]:
@@ -164,4 +166,4 @@ def fix_and_parse_json(
json_str = json_str[: last_brace_index + 1]
return json.loads(json_str)
except json.JSONDecodeError as e: # noqa: F841
- raise e
\ No newline at end of file
+ raise e
diff --git a/metagpt/utils/minecraft/load_prompts.py b/metagpt/utils/minecraft/load_prompts.py
index fe7c282b3..3b315461f 100644
--- a/metagpt/utils/minecraft/load_prompts.py
+++ b/metagpt/utils/minecraft/load_prompts.py
@@ -4,7 +4,8 @@
# @Desc :
import pkg_resources
from .file_utils import load_text
-
+
+
def load_prompt(prompt):
package_path = pkg_resources.resource_filename("metagpt", "")
- return load_text(f"{package_path}/prompts/minecraft/{prompt}.txt")
\ No newline at end of file
+ return load_text(f"{package_path}/prompts/minecraft/{prompt}.txt")
diff --git a/metagpt/utils/minecraft/load_skills_code_context.py b/metagpt/utils/minecraft/load_skills_code_context.py
new file mode 100644
index 000000000..ffaaa0052
--- /dev/null
+++ b/metagpt/utils/minecraft/load_skills_code_context.py
@@ -0,0 +1,21 @@
+import os
+import metagpt.utils.minecraft as utils
+from metagpt.logs import logger
+
+
+def load_skills_code_context(skill_names=None):
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ skills_dir = os.path.join(current_dir, "skills_code_context")
+ if skill_names is None:
+ skill_names = [
+ skill[:-3] for skill in os.listdir(f"{skills_dir}") if skill.endswith(".js")
+ ]
+ skills = [
+ utils.load_text(os.path.join(skills_dir, f"{skill_name}.js"))
+ for skill_name in skill_names
+ ]
+ return skills
+
+
+if __name__ == "__main__":
+ logger.info(load_skills_code_context(["craftItem", "exploreUntil"]))
diff --git a/metagpt/utils/minecraft/skills_code_context/.prettierrc.json b/metagpt/utils/minecraft/skills_code_context/.prettierrc.json
new file mode 100644
index 000000000..0a02bcefd
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/.prettierrc.json
@@ -0,0 +1,3 @@
+{
+ "tabWidth": 4
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/__init__.py b/metagpt/utils/minecraft/skills_code_context/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/metagpt/utils/minecraft/skills_code_context/craftItem.js b/metagpt/utils/minecraft/skills_code_context/craftItem.js
new file mode 100644
index 000000000..806811d46
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/craftItem.js
@@ -0,0 +1,14 @@
+// Craft 8 oak_planks from 2 oak_log (do the recipe 2 times): craftItem(bot, "oak_planks", 2);
+// You must place a crafting table before calling this function
+async function craftItem(bot, name, count = 1) {
+ const item = mcData.itemsByName[name];
+ const craftingTable = bot.findBlock({
+ matching: mcData.blocksByName.crafting_table.id,
+ maxDistance: 32,
+ });
+ await bot.pathfinder.goto(
+ new GoalLookAtBlock(craftingTable.position, bot.world)
+ );
+ const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0];
+ await bot.craft(recipe, count, craftingTable);
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/exploreUntil.js b/metagpt/utils/minecraft/skills_code_context/exploreUntil.js
new file mode 100644
index 000000000..55c62a453
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/exploreUntil.js
@@ -0,0 +1,31 @@
+/*
+Explore until find an iron_ore, use Vec3(0, -1, 0) because iron ores are usually underground
+await exploreUntil(bot, new Vec3(0, -1, 0), 60, () => {
+ const iron_ore = bot.findBlock({
+ matching: mcData.blocksByName["iron_ore"].id,
+ maxDistance: 32,
+ });
+ return iron_ore;
+});
+
+Explore until find a pig, use Vec3(1, 0, 1) because pigs are usually on the surface
+let pig = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
+ const pig = bot.nearestEntity((entity) => {
+ return (
+ entity.name === "pig" &&
+ entity.position.distanceTo(bot.entity.position) < 32
+ );
+ });
+ return pig;
+});
+*/
+async function exploreUntil(bot, direction, maxTime = 60, callback) {
+ /*
+ Implementation of this function is omitted.
+ direction: Vec3, can only contain value of -1, 0 or 1
+ maxTime: number, the max time for exploration
+ callback: function, early stop condition, will be called each second, exploration will stop if return value is not null
+
+ Return: null if explore timeout, otherwise return the return value of callback
+ */
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/killMob.js b/metagpt/utils/minecraft/skills_code_context/killMob.js
new file mode 100644
index 000000000..670ca9753
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/killMob.js
@@ -0,0 +1,12 @@
+// Kill a pig and collect the dropped item: killMob(bot, "pig", 300);
+async function killMob(bot, mobName, timeout = 300) {
+ const entity = bot.nearestEntity(
+ (entity) =>
+ entity.name === mobName &&
+ entity.position.distanceTo(bot.entity.position) < 32
+ );
+ await bot.pvp.attack(entity);
+ await bot.pathfinder.goto(
+ new GoalBlock(entity.position.x, entity.position.y, entity.position.z)
+ );
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/mineBlock.js b/metagpt/utils/minecraft/skills_code_context/mineBlock.js
new file mode 100644
index 000000000..c6a7559e6
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/mineBlock.js
@@ -0,0 +1,15 @@
+// Mine 3 cobblestone: mineBlock(bot, "stone", 3);
+async function mineBlock(bot, name, count = 1) {
+ const blocks = bot.findBlocks({
+ matching: (block) => {
+ return block.name === name;
+ },
+ maxDistance: 32,
+ count: count,
+ });
+ const targets = [];
+ for (let i = 0; i < Math.min(blocks.length, count); i++) {
+ targets.push(bot.blockAt(blocks[i]));
+ }
+ await bot.collectBlock.collect(targets, { ignoreNoPath: true });
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/mineflayer.js b/metagpt/utils/minecraft/skills_code_context/mineflayer.js
new file mode 100644
index 000000000..43217885c
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/mineflayer.js
@@ -0,0 +1,22 @@
+await bot.pathfinder.goto(goal); // A very useful function. This function may change your main-hand equipment.
+// Following are some Goals you can use:
+new GoalNear(x, y, z, range); // Move the bot to a block within the specified range of the specified block. `x`, `y`, `z`, and `range` are `number`
+new GoalXZ(x, z); // Useful for long-range goals that don't have a specific Y level. `x` and `z` are `number`
+new GoalGetToBlock(x, y, z); // Not get into the block, but get directly adjacent to it. Useful for fishing, farming, filling bucket, and beds. `x`, `y`, and `z` are `number`
+new GoalFollow(entity, range); // Follow the specified entity within the specified range. `entity` is `Entity`, `range` is `number`
+new GoalPlaceBlock(position, bot.world, {}); // Position the bot in order to place a block. `position` is `Vec3`
+new GoalLookAtBlock(position, bot.world, {}); // Path into a position where a blockface of the block at position is visible. `position` is `Vec3`
+
+// These are other Mineflayer functions you can use:
+bot.isABed(bedBlock); // Return true if `bedBlock` is a bed
+bot.blockAt(position); // Return the block at `position`. `position` is `Vec3`
+
+// These are other Mineflayer async functions you can use:
+await bot.equip(item, destination); // Equip the item in the specified destination. `item` is `Item`, `destination` can only be "hand", "head", "torso", "legs", "feet", "off-hand"
+await bot.consume(); // Consume the item in the bot's hand. You must equip the item to consume first. Useful for eating food, drinking potions, etc.
+await bot.fish(); // Let bot fish. Before calling this function, you must first get to a water block and then equip a fishing rod. The bot will automatically stop fishing when it catches a fish
+await bot.sleep(bedBlock); // Sleep until sunrise. You must get to a bed block first
+await bot.activateBlock(block); // This is the same as right-clicking a block in the game. Useful for buttons, doors, etc. You must get to the block first
+await bot.lookAt(position); // Look at the specified position. You must go near the position before you look at it. To fill bucket with water, you must lookAt first. `position` is `Vec3`
+await bot.activateItem(); // This is the same as right-clicking to use the item in the bot's hand. Useful for using buckets, etc. You must equip the item to activate first
+await bot.useOn(entity); // This is the same as right-clicking an entity in the game. Useful for shearing sheep, equipping harnesses, etc. You must get to the entity first
diff --git a/metagpt/utils/minecraft/skills_code_context/placeItem.js b/metagpt/utils/minecraft/skills_code_context/placeItem.js
new file mode 100644
index 000000000..99e06089c
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/placeItem.js
@@ -0,0 +1,28 @@
+// Place a crafting_table near the player, Vec3(1, 0, 0) is just an example, you shouldn't always use that: placeItem(bot, "crafting_table", bot.entity.position.offset(1, 0, 0));
+async function placeItem(bot, name, position) {
+ const item = bot.inventory.findInventoryItem(mcData.itemsByName[name].id);
+ // find a reference block
+ const faceVectors = [
+ new Vec3(0, 1, 0),
+ new Vec3(0, -1, 0),
+ new Vec3(1, 0, 0),
+ new Vec3(-1, 0, 0),
+ new Vec3(0, 0, 1),
+ new Vec3(0, 0, -1),
+ ];
+ let referenceBlock = null;
+ let faceVector = null;
+ for (const vector of faceVectors) {
+ const block = bot.blockAt(position.minus(vector));
+ if (block?.name !== "air") {
+ referenceBlock = block;
+ faceVector = vector;
+ break;
+ }
+ }
+ // You must first go to the block position you want to place
+ await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {}));
+ // You must equip the item right before calling placeBlock
+ await bot.equip(item, "hand");
+ await bot.placeBlock(referenceBlock, faceVector);
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/smeltItem.js b/metagpt/utils/minecraft/skills_code_context/smeltItem.js
new file mode 100644
index 000000000..0a3c76257
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/smeltItem.js
@@ -0,0 +1,22 @@
+// Smelt 1 raw_iron into 1 iron_ingot using 1 oak_planks as fuel: smeltItem(bot, "raw_iron", "oak_planks");
+// You must place a furnace before calling this function
+async function smeltItem(bot, itemName, fuelName, count = 1) {
+ const item = mcData.itemsByName[itemName];
+ const fuel = mcData.itemsByName[fuelName];
+ const furnaceBlock = bot.findBlock({
+ matching: mcData.blocksByName.furnace.id,
+ maxDistance: 32,
+ });
+ await bot.pathfinder.goto(
+ new GoalLookAtBlock(furnaceBlock.position, bot.world)
+ );
+ const furnace = await bot.openFurnace(furnaceBlock);
+ for (let i = 0; i < count; i++) {
+ await furnace.putFuel(fuel.id, null, 1);
+ await furnace.putInput(item.id, null, 1);
+ // Wait 12 seconds for the furnace to smelt the item
+ await bot.waitForTicks(12 * 20);
+ await furnace.takeOutput();
+ }
+ await furnace.close();
+}
diff --git a/metagpt/utils/minecraft/skills_code_context/useChest.js b/metagpt/utils/minecraft/skills_code_context/useChest.js
new file mode 100644
index 000000000..e80af3fd9
--- /dev/null
+++ b/metagpt/utils/minecraft/skills_code_context/useChest.js
@@ -0,0 +1,35 @@
+// Get a torch from chest at (30, 65, 100): getItemFromChest(bot, new Vec3(30, 65, 100), {"torch": 1});
+// This function will work no matter how far the bot is from the chest.
+async function getItemFromChest(bot, chestPosition, itemsToGet) {
+ await moveToChest(bot, chestPosition);
+ const chestBlock = bot.blockAt(chestPosition);
+ const chest = await bot.openContainer(chestBlock);
+ for (const name in itemsToGet) {
+ const itemByName = mcData.itemsByName[name];
+ const item = chest.findContainerItem(itemByName.id);
+ await chest.withdraw(item.type, null, itemsToGet[name]);
+ }
+ await closeChest(bot, chestBlock);
+}
+// Deposit a torch into chest at (30, 65, 100): depositItemIntoChest(bot, new Vec3(30, 65, 100), {"torch": 1});
+// This function will work no matter how far the bot is from the chest.
+async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) {
+ await moveToChest(bot, chestPosition);
+ const chestBlock = bot.blockAt(chestPosition);
+ const chest = await bot.openContainer(chestBlock);
+ for (const name in itemsToDeposit) {
+ const itemByName = mcData.itemsByName[name];
+ const item = bot.inventory.findInventoryItem(itemByName.id);
+ await chest.deposit(item.type, null, itemsToDeposit[name]);
+ }
+ await closeChest(bot, chestBlock);
+}
+// Check the items inside the chest at (30, 65, 100): checkItemInsideChest(bot, new Vec3(30, 65, 100));
+// You only need to call this function once without any action to finish task of checking items inside the chest.
+async function checkItemInsideChest(bot, chestPosition) {
+ await moveToChest(bot, chestPosition);
+ const chestBlock = bot.blockAt(chestPosition);
+ await bot.openContainer(chestBlock);
+ // You must close the chest after opening it if you are asked to open a chest
+ await closeChest(bot, chestBlock);
+}
From 3a705a0e89cf86917c464202c313cffd3fc3eb0e Mon Sep 17 00:00:00 2001
From: yuymf <1352948945@qq.com>
Date: Thu, 28 Sep 2023 01:17:02 +0800
Subject: [PATCH 2/3] Move test func & add comment
---
.../control_primitives_context/__init__.py | 30 +++--
metagpt/minecraft_team.py | 14 --
.../minecraft/action_response_format.txt | 15 +++
metagpt/prompts/minecraft/critic.txt | 127 ++++++++++++++++++
metagpt/prompts/minecraft/curriculum.txt | 42 ++++++
.../curriculum_qa_step2_answer_questions.txt | 8 ++
.../curriculum_task_decomposition.txt | 12 ++
metagpt/prompts/minecraft/skill.txt | 51 +++++++
metagpt/roles/minecraft/action_developer.py | 96 +------------
metagpt/utils/minecraft/__init__.py | 3 +-
metagpt/utils/minecraft/action_rsp_parser.py | 40 ++++--
.../minecraft/load_skills_code_context.py | 21 ---
.../skills_code_context/.prettierrc.json | 3 -
.../minecraft/skills_code_context/__init__.py | 0
.../skills_code_context/craftItem.js | 14 --
.../skills_code_context/exploreUntil.js | 31 -----
.../minecraft/skills_code_context/killMob.js | 12 --
.../skills_code_context/mineBlock.js | 15 ---
.../skills_code_context/mineflayer.js | 22 ---
.../skills_code_context/placeItem.js | 28 ----
.../skills_code_context/smeltItem.js | 22 ---
.../minecraft/skills_code_context/useChest.js | 35 -----
.../roles/minecraft/test_action_developer.py | 91 +++++++++++++
tests/metagpt/test_minecraft_team.py | 21 +++
.../utils/minecraft/test_action_rsp_parser.py | 69 ++++++++++
25 files changed, 489 insertions(+), 333 deletions(-)
create mode 100644 metagpt/prompts/minecraft/action_response_format.txt
create mode 100644 metagpt/prompts/minecraft/critic.txt
create mode 100644 metagpt/prompts/minecraft/curriculum.txt
create mode 100644 metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt
create mode 100644 metagpt/prompts/minecraft/curriculum_task_decomposition.txt
create mode 100644 metagpt/prompts/minecraft/skill.txt
delete mode 100644 metagpt/utils/minecraft/load_skills_code_context.py
delete mode 100644 metagpt/utils/minecraft/skills_code_context/.prettierrc.json
delete mode 100644 metagpt/utils/minecraft/skills_code_context/__init__.py
delete mode 100644 metagpt/utils/minecraft/skills_code_context/craftItem.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/exploreUntil.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/killMob.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/mineBlock.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/mineflayer.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/placeItem.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/smeltItem.js
delete mode 100644 metagpt/utils/minecraft/skills_code_context/useChest.js
create mode 100644 tests/metagpt/roles/minecraft/test_action_developer.py
create mode 100644 tests/metagpt/test_minecraft_team.py
create mode 100644 tests/metagpt/utils/minecraft/test_action_rsp_parser.py
diff --git a/metagpt/actions/minecraft/control_primitives_context/__init__.py b/metagpt/actions/minecraft/control_primitives_context/__init__.py
index 2bee5e3a8..9c3d08169 100644
--- a/metagpt/actions/minecraft/control_primitives_context/__init__.py
+++ b/metagpt/actions/minecraft/control_primitives_context/__init__.py
@@ -1,18 +1,20 @@
-import pkg_resources
import os
-import voyager.utils as U
+import metagpt.utils.minecraft as utils
+from metagpt.logs import logger
-
-def load_control_primitives_context(primitive_names=None):
- package_path = pkg_resources.resource_filename("metagpt", "")
- if primitive_names is None:
- primitive_names = [
- primitive[:-3]
- for primitive in os.listdir(f"{package_path}/actions/minecraft/control_primitives_context")
- if primitive.endswith(".js")
+def load_skills_code_context(skill_names=None):
+ skills_dir = os.path.dirname(os.path.abspath(__file__))
+ if skill_names is None:
+ skill_names = [
+ skill[:-3] for skill in os.listdir(f"{skills_dir}") if skill.endswith(".js")
]
- primitives = [
- U.load_text(f"{package_path}/actions/minecraft/control_primitives_context/{primitive_name}.js")
- for primitive_name in primitive_names
+ skills = [
+ utils.load_text(os.path.join(skills_dir, f"{skill_name}.js"))
+ for skill_name in skill_names
]
- return primitives
+ return skills
+
+
+if __name__ == "__main__":
+ logger.info(load_skills_code_context(["craftItem", "exploreUntil"]))
+
diff --git a/metagpt/minecraft_team.py b/metagpt/minecraft_team.py
index 8c5ad03ed..f60e6af4f 100644
--- a/metagpt/minecraft_team.py
+++ b/metagpt/minecraft_team.py
@@ -151,17 +151,3 @@ class MinecraftPlayer(SoftwareCompany):
await self.environment.run()
return self.environment.history
-
-
-async def main():
- test_code = "bot.chat(`/time set ${getNextTime()}`);"
- mc_port = 6286
- ge = GameEnvironment()
- ge.set_mc_port(mc_port)
- ge.update_code(test_code)
- result = await ge.on_event()
- logger.info("On event test done")
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/metagpt/prompts/minecraft/action_response_format.txt b/metagpt/prompts/minecraft/action_response_format.txt
new file mode 100644
index 000000000..df3713a83
--- /dev/null
+++ b/metagpt/prompts/minecraft/action_response_format.txt
@@ -0,0 +1,15 @@
+Explain: ...
+Plan:
+1) ...
+2) ...
+3) ...
+...
+Code:
+```javascript
+// helper functions (only if needed, try to avoid them)
+...
+// main function after the helper functions
+async function yourMainFunctionName(bot) {
+ // ...
+}
+```
\ No newline at end of file
diff --git a/metagpt/prompts/minecraft/critic.txt b/metagpt/prompts/minecraft/critic.txt
new file mode 100644
index 000000000..c42950675
--- /dev/null
+++ b/metagpt/prompts/minecraft/critic.txt
@@ -0,0 +1,127 @@
+You are an assistant that assesses my progress of playing Minecraft and provides useful guidance.
+
+You are required to evaluate if I have met the task requirements. Exceeding the task requirements is also considered a success while failing to meet them requires you to provide critique to help me improve.
+
+I will give you the following information:
+
+Biome: The biome after the task execution.
+Time: The current time.
+Nearby blocks: The surrounding blocks. These blocks are not collected yet. However, this is useful for some placing or planting tasks.
+Health: My current health.
+Hunger: My current hunger level. For eating task, if my hunger level is 20.0, then I successfully ate the food.
+Position: My current position.
+Equipment: My final equipment. For crafting tasks, I sometimes equip the crafted item.
+Inventory (xx/36): My final inventory. For mining and smelting tasks, you only need to check inventory.
+Chests: If the task requires me to place items in a chest, you can find chest information here.
+Task: The objective I need to accomplish.
+Context: The context of the task.
+
+You should only respond in JSON format as described below:
+{
+ "reasoning": "reasoning",
+ "success": boolean,
+ "critique": "critique",
+}
+Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
+
+Here are some examples:
+INPUT:
+Inventory (2/36): {'oak_log':2, 'spruce_log':2}
+
+Task: Mine 3 wood logs
+
+RESPONSE:
+{
+ "reasoning": "You need to mine 3 wood logs. You have 2 oak logs and 2 spruce logs, which add up to 4 wood logs.",
+ "success": true,
+ "critique": ""
+}
+
+INPUT:
+Inventory (3/36): {'crafting_table': 1, 'spruce_planks': 6, 'stick': 4}
+
+Task: Craft a wooden pickaxe
+
+RESPONSE:
+{
+ "reasoning": "You have enough materials to craft a wooden pickaxe, but you didn't craft it.",
+ "success": false,
+ "critique": "Craft a wooden pickaxe with a crafting table using 3 spruce planks and 2 sticks."
+}
+
+INPUT:
+Inventory (2/36): {'raw_iron': 5, 'stone_pickaxe': 1}
+
+Task: Mine 5 iron_ore
+
+RESPONSE:
+{
+ "reasoning": "Mining iron_ore in Minecraft will get raw_iron. You have 5 raw_iron in your inventory.",
+ "success": true,
+ "critique": ""
+}
+
+INPUT:
+Biome: plains
+
+Nearby blocks: stone, dirt, grass_block, grass, farmland, wheat
+
+Inventory (26/36): ...
+
+Task: Plant 1 wheat seed.
+
+RESPONSE:
+{
+ "reasoning": "For planting tasks, inventory information is useless. In nearby blocks, there is farmland and wheat, which means you succeed to plant the wheat seed.",
+ "success": true,
+ "critique": ""
+}
+
+INPUT:
+Inventory (11/36): {... ,'rotten_flesh': 1}
+
+Task: Kill 1 zombie
+
+Context: ...
+
+RESPONSE
+{
+ "reasoning": "You have rotten flesh in your inventory, which means you successfully killed one zombie.",
+ "success": true,
+ "critique": ""
+}
+
+INPUT:
+Hunger: 20.0/20.0
+
+Inventory (11/36): ...
+
+Task: Eat 1 ...
+
+Context: ...
+
+RESPONSE
+{
+ "reasoning": "For all eating task, if the player's hunger is 20.0, then the player successfully ate the food.",
+ "success": true,
+ "critique": ""
+}
+
+INPUT:
+Nearby blocks: chest
+
+Inventory (28/36): {'rail': 1, 'coal': 2, 'oak_planks': 13, 'copper_block': 1, 'diorite': 7, 'cooked_beef': 4, 'granite': 22, 'cobbled_deepslate': 23, 'feather': 4, 'leather': 2, 'cooked_chicken': 3, 'white_wool': 2, 'stick': 3, 'black_wool': 1, 'stone_sword': 2, 'stone_hoe': 1, 'stone_axe': 2, 'stone_shovel': 2, 'cooked_mutton': 4, 'cobblestone_wall': 18, 'crafting_table': 1, 'furnace': 1, 'iron_pickaxe': 1, 'stone_pickaxe': 1, 'raw_copper': 12}
+
+Chests:
+(81, 131, 16): {'andesite': 2, 'dirt': 2, 'cobblestone': 75, 'wooden_pickaxe': 1, 'wooden_sword': 1}
+
+Task: Deposit useless items into the chest at (81, 131, 16)
+
+Context: ...
+
+RESPONSE
+{
+ "reasoning": "You have 28 items in your inventory after depositing, which is more than 20. You need to deposit more items from your inventory to the chest.",
+ "success": false,
+ "critique": "Deposit more useless items such as copper_block, diorite, granite, cobbled_deepslate, feather, and leather to meet the requirement of having only 20 occupied slots in your inventory."
+}
\ No newline at end of file
diff --git a/metagpt/prompts/minecraft/curriculum.txt b/metagpt/prompts/minecraft/curriculum.txt
new file mode 100644
index 000000000..66a33c626
--- /dev/null
+++ b/metagpt/prompts/minecraft/curriculum.txt
@@ -0,0 +1,42 @@
+You are a helpful assistant that tells me the next immediate task to do in Minecraft. My ultimate goal is to discover as many diverse things as possible, accomplish as many diverse tasks as possible and become the best Minecraft player in the world.
+
+I will give you the following information:
+Question 1: ...
+Answer: ...
+Question 2: ...
+Answer: ...
+Question 3: ...
+Answer: ...
+...
+Biome: ...
+Time: ...
+Nearby blocks: ...
+Other blocks that are recently seen: ...
+Nearby entities (nearest to farthest): ...
+Health: Higher than 15 means I'm healthy.
+Hunger: Higher than 15 means I'm not hungry.
+Position: ...
+Equipment: If I have better armor in my inventory, you should ask me to equip it.
+Inventory (xx/36): ...
+Chests: You can ask me to deposit or take items from these chests. There also might be some unknown chest, you should ask me to open and check items inside the unknown chest.
+Completed tasks so far: ...
+Failed tasks that are too hard: ...
+
+You must follow the following criteria:
+1) You should act as a mentor and guide me to the next task based on my current learning progress.
+2) Please be very specific about what resources I need to collect, what I need to craft, or what mobs I need to kill.
+3) The next task should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]" etc. It should be a single phrase. Do not propose multiple tasks at the same time. Do not mention anything else.
+4) The next task should not be too hard since I may not have the necessary resources or have learned enough skills to complete it yet.
+5) The next task should be novel and interesting. I should look for rare resources, upgrade my equipment and tools using better materials, and discover new things. I should not be doing the same thing over and over again.
+6) I may sometimes need to repeat some tasks if I need to collect more resources to complete more difficult tasks. Only repeat tasks if necessary.
+7) Do not ask me to build or dig shelter even if it's at night. I want to explore the world and discover new things. I don't want to stay in one place.
+8) Tasks that require information beyond the player's status to verify should be avoided. For instance, "Placing 4 torches" and "Dig a 2x1x2 hole" are not ideal since they require visual confirmation from the screen. All the placing, building, planting, and trading tasks should be avoided. Do not propose task starting with these keywords.
+
+You should only respond in the format as described below:
+RESPONSE FORMAT:
+Reasoning: Based on the information I listed above, do reasoning about what the next task should be.
+Task: The next task.
+
+Here's an example response:
+Reasoning: The inventory is empty now, chop down a tree to get some wood.
+Task: Obtain a wood log.
\ No newline at end of file
diff --git a/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt b/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt
new file mode 100644
index 000000000..860d986e4
--- /dev/null
+++ b/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt
@@ -0,0 +1,8 @@
+You are a helpful assistant that answer my question about Minecraft.
+
+I will give you the following information:
+Question: ...
+
+You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft.
+1) Start your answer with "Answer: ".
+2) Answer "Answer: Unknown" if you don't know the answer.
\ No newline at end of file
diff --git a/metagpt/prompts/minecraft/curriculum_task_decomposition.txt b/metagpt/prompts/minecraft/curriculum_task_decomposition.txt
new file mode 100644
index 000000000..0e3b1592d
--- /dev/null
+++ b/metagpt/prompts/minecraft/curriculum_task_decomposition.txt
@@ -0,0 +1,12 @@
+You are a helpful assistant that generates a curriculum of subgoals to complete any Minecraft task specified by me.
+
+I'll give you a final task and my current inventory, you need to decompose the task into a list of subgoals based on my inventory.
+
+You must follow the following criteria:
+1) Return a Python list of subgoals that can be completed in order to complete the specified task.
+2) Each subgoal should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]".
+3) Include each level of necessary tools as a subgoal, such as wooden, stone, iron, diamond, etc.
+
+You should only respond in JSON format as described below:
+["subgoal1", "subgoal2", "subgoal3", ...]
+Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
\ No newline at end of file
diff --git a/metagpt/prompts/minecraft/skill.txt b/metagpt/prompts/minecraft/skill.txt
new file mode 100644
index 000000000..dc846cdf6
--- /dev/null
+++ b/metagpt/prompts/minecraft/skill.txt
@@ -0,0 +1,51 @@
+You are a helpful assistant that writes a description of the given function written in Mineflayer javascript code.
+
+1) Do not mention the function name.
+2) Do not mention anything about `bot.chat` or helper functions.
+3) There might be some helper functions before the main function, but you only need to describe the main function.
+4) Try to summarize the function in no more than 6 sentences.
+5) Your response should be a single line of text.
+
+For example, if the function is:
+
+async function mineCobblestone(bot) {
+ // Check if the wooden pickaxe is in the inventory, if not, craft one
+ let woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
+ if (!woodenPickaxe) {
+ bot.chat("Crafting a wooden pickaxe.");
+ await craftWoodenPickaxe(bot);
+ woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
+ }
+
+ // Equip the wooden pickaxe if it exists
+ if (woodenPickaxe) {
+ await bot.equip(woodenPickaxe, "hand");
+
+ // Explore until we find a stone block
+ await exploreUntil(bot, new Vec3(1, -1, 1), 60, () => {
+ const stone = bot.findBlock({
+ matching: mcData.blocksByName["stone"].id,
+ maxDistance: 32
+ });
+ if (stone) {
+ return true;
+ }
+ });
+
+ // Mine 8 cobblestone blocks using the wooden pickaxe
+ bot.chat("Found a stone block. Mining 8 cobblestone blocks.");
+ await mineBlock(bot, "stone", 8);
+ bot.chat("Successfully mined 8 cobblestone blocks.");
+
+ // Save the event of mining 8 cobblestone
+ bot.save("cobblestone_mined");
+ } else {
+ bot.chat("Failed to craft a wooden pickaxe. Cannot mine cobblestone.");
+ }
+}
+
+The main function is `mineCobblestone`.
+
+Then you would write:
+
+The function is about mining 8 cobblestones using a wooden pickaxe. First check if a wooden pickaxe is in the inventory. If not, craft one. If the wooden pickaxe is available, equip the wooden pickaxe in the hand. Next, explore the environment until finding a stone block. Once a stone block is found, mine a total of 8 cobblestone blocks using the wooden pickaxe.
\ No newline at end of file
diff --git a/metagpt/roles/minecraft/action_developer.py b/metagpt/roles/minecraft/action_developer.py
index 30b80ea4a..231699b40 100644
--- a/metagpt/roles/minecraft/action_developer.py
+++ b/metagpt/roles/minecraft/action_developer.py
@@ -2,8 +2,6 @@
# @Date : 2023/9/23 12:45
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
-import asyncio
-
from metagpt.logs import logger
from metagpt.roles.minecraft.minecraft_base import Minecraft as Base
from metagpt.schema import Message, HumanMessage, SystemMessage
@@ -17,7 +15,9 @@ from metagpt.actions.minecraft.manage_skills import (
)
import metagpt.utils.minecraft as utils
from metagpt.config import CONFIG
-from metagpt.minecraft_team import GameEnvironment
+from metagpt.actions.minecraft.control_primitives_context import (
+ load_skills_code_context,
+)
@agent_registry.register("action_developer")
@@ -62,7 +62,7 @@ class ActionDeveloper(Base):
"useChest",
"mineflayer",
]
- programs = "\n\n".join(utils.load_skills_code_context(base_skills) + skills)
+ programs = "\n\n".join(load_skills_code_context(base_skills) + skills)
response_format = utils.load_prompt("action_response_format")
system_action_prompt = action_template.format(
programs=programs, response_format=response_format
@@ -193,7 +193,7 @@ class ActionDeveloper(Base):
instruct_content="generate_action_code",
role=self.profile,
)
- logger.info(msg)
+ # logger.info(msg)
return msg
async def _act(self) -> Message:
@@ -231,89 +231,3 @@ class ActionDeveloper(Base):
return msg
raise ValueError(f"Unknown todo type: {type(todo)}")
-
-
-async def main():
- events = [
- [
- "observe",
- {
- "voxels": ["grass_block", "dirt", "grass"],
- "status": {
- "health": 20,
- "food": 20,
- "saturation": 5,
- "oxygen": 20,
- "position": {"x": 0.5, "y": 84, "z": -207.5},
- "velocity": {"x": 0, "y": -0.0784000015258789, "z": 0},
- "yaw": 3.141592653589793,
- "pitch": 0,
- "onGround": True,
- "equipment": [None, None, None, None, None, None],
- "name": "bot",
- "isInWater": False,
- "isInLava": False,
- "isCollidedHorizontally": False,
- "isCollidedVertically": True,
- "biome": "plains",
- "entities": {
- "chicken": 29.071822119730644,
- "sheep": 20.361212992763768,
- },
- "timeOfDay": "day",
- "inventoryUsed": 0,
- "elapsedTime": 41,
- },
- "inventory": {},
- "nearbyChests": {},
- "blockRecords": ["grass_block", "dirt", "grass"],
- },
- ]
- ]
- code = """
- collectBamboo.code async function collectBamboo(bot) {
- // Equip the iron sword
- const ironSword = bot.inventory.findInventoryItem(mcData.itemsByName.iron_sword.id);
- await bot.equip(ironSword, "hand");
-
- // Find bamboo plants using the exploreUntil function
- const bambooPlants = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
- const bambooPlants = bot.findBlocks({
- matching: block => block.name === "bamboo",
- maxDistance: 32,
- count: 10
- });
- return bambooPlants.length >= 10 ? bambooPlants : null;
- });
- if (!bambooPlants) {
- bot.chat("Could not find enough bamboo plants.");
- return;
- }
-
- // Break 10 bamboo plants using the iron sword
- for (const bambooPlant of bambooPlants) {
- const block = bot.blockAt(bambooPlant);
- await bot.dig(block);
- }
- bot.chat("Broke 10 bamboo plants.");
-
- // Collect the dropped bamboo items
- for (const bambooPlant of bambooPlants) {
- await bot.pathfinder.goto(new GoalBlock(bambooPlant.x, bambooPlant.y, bambooPlant.z));
- }
- bot.chat("Collected 10 bamboo.");
- }
- """
- ad = ActionDeveloper()
- ge = GameEnvironment()
- ad.set_memory(shared_memory=ge)
- msg = ad.encapsule_message(events=events, code=code)
- logger.info(f"Encapsuled_message: {msg}")
-
- parsed_result = await ad.generate_action_code(msg)
-
- logger.info(f"Parsed_code_updating: {parsed_result}")
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/metagpt/utils/minecraft/__init__.py b/metagpt/utils/minecraft/__init__.py
index e61f130c4..6ee04c606 100644
--- a/metagpt/utils/minecraft/__init__.py
+++ b/metagpt/utils/minecraft/__init__.py
@@ -5,5 +5,4 @@
from .load_prompts import load_prompt
from .json_utils import *
from .file_utils import *
-from .load_skills_code_context import load_skills_code_context
-from .action_rsp_parser import parse_action_response
+from .action_rsp_parser import parse_js_code, parse_action_response
diff --git a/metagpt/utils/minecraft/action_rsp_parser.py b/metagpt/utils/minecraft/action_rsp_parser.py
index d2f150e44..91ccc141d 100644
--- a/metagpt/utils/minecraft/action_rsp_parser.py
+++ b/metagpt/utils/minecraft/action_rsp_parser.py
@@ -2,28 +2,47 @@ import re
import time
from javascript import require
+def parse_js_code(msg: str):
+ '''
+ Extract and Parse JavaScript code blocks
+ '''
+ babel = require("@babel/core")
+ code_pattern = re.compile(r"```(?:javascript|js)(.*?)```", re.DOTALL)
+ code = "\n".join(code_pattern.findall(msg))
+ parsed = babel.parse(code)
+ return parsed
def parse_action_response(msg: str):
"""
+ Input:
+ '''
+ Explain: ...
+ Plan: ...
+ Code:
+ ```javascript
+ ...
+ ```
+ '''
+
Return:
{
"program_code": program_code,
"program_name": main_function["name"],
"exec_code": exec_code,
- }
+ } or
+
+ "{error}"
+
Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
"""
retry = 3
- error = None
+ error = None # 3 times failed return error
+ babel_generator = require("@babel/generator").default
while retry > 0:
try:
- babel = require("@babel/core")
- babel_generator = require("@babel/generator").default
-
- code_pattern = re.compile(r"```(?:javascript|js)(.*?)```", re.DOTALL)
- code = "\n".join(code_pattern.findall(msg))
- parsed = babel.parse(code)
+ parsed = parse_js_code(msg)
+ # Collect func list: check if func & async
functions = []
assert len(list(parsed.program.body)) > 0, "No functions found"
for i, node in enumerate(parsed.program.body):
@@ -42,7 +61,8 @@ def parse_action_response(msg: str):
"params": list(node["params"]),
}
)
- # find the last async function
+
+ # Ensure main_function is the last async function
main_function = None
for function in reversed(functions):
if function["type"] == "AsyncFunctionDeclaration":
@@ -55,6 +75,8 @@ def parse_action_response(msg: str):
len(main_function["params"]) == 1
and main_function["params"][0].name == "bot"
), f"Main function {main_function['name']} must take a single argument named 'bot'"
+
+ # Split to program_code & exec_code for output
program_code = "\n\n".join(function["body"] for function in functions)
exec_code = f"await {main_function['name']}(bot);"
return {
diff --git a/metagpt/utils/minecraft/load_skills_code_context.py b/metagpt/utils/minecraft/load_skills_code_context.py
deleted file mode 100644
index ffaaa0052..000000000
--- a/metagpt/utils/minecraft/load_skills_code_context.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import os
-import metagpt.utils.minecraft as utils
-from metagpt.logs import logger
-
-
-def load_skills_code_context(skill_names=None):
- current_dir = os.path.dirname(os.path.abspath(__file__))
- skills_dir = os.path.join(current_dir, "skills_code_context")
- if skill_names is None:
- skill_names = [
- skill[:-3] for skill in os.listdir(f"{skills_dir}") if skill.endswith(".js")
- ]
- skills = [
- utils.load_text(os.path.join(skills_dir, f"{skill_name}.js"))
- for skill_name in skill_names
- ]
- return skills
-
-
-if __name__ == "__main__":
- logger.info(load_skills_code_context(["craftItem", "exploreUntil"]))
diff --git a/metagpt/utils/minecraft/skills_code_context/.prettierrc.json b/metagpt/utils/minecraft/skills_code_context/.prettierrc.json
deleted file mode 100644
index 0a02bcefd..000000000
--- a/metagpt/utils/minecraft/skills_code_context/.prettierrc.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "tabWidth": 4
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/__init__.py b/metagpt/utils/minecraft/skills_code_context/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/metagpt/utils/minecraft/skills_code_context/craftItem.js b/metagpt/utils/minecraft/skills_code_context/craftItem.js
deleted file mode 100644
index 806811d46..000000000
--- a/metagpt/utils/minecraft/skills_code_context/craftItem.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// Craft 8 oak_planks from 2 oak_log (do the recipe 2 times): craftItem(bot, "oak_planks", 2);
-// You must place a crafting table before calling this function
-async function craftItem(bot, name, count = 1) {
- const item = mcData.itemsByName[name];
- const craftingTable = bot.findBlock({
- matching: mcData.blocksByName.crafting_table.id,
- maxDistance: 32,
- });
- await bot.pathfinder.goto(
- new GoalLookAtBlock(craftingTable.position, bot.world)
- );
- const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0];
- await bot.craft(recipe, count, craftingTable);
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/exploreUntil.js b/metagpt/utils/minecraft/skills_code_context/exploreUntil.js
deleted file mode 100644
index 55c62a453..000000000
--- a/metagpt/utils/minecraft/skills_code_context/exploreUntil.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-Explore until find an iron_ore, use Vec3(0, -1, 0) because iron ores are usually underground
-await exploreUntil(bot, new Vec3(0, -1, 0), 60, () => {
- const iron_ore = bot.findBlock({
- matching: mcData.blocksByName["iron_ore"].id,
- maxDistance: 32,
- });
- return iron_ore;
-});
-
-Explore until find a pig, use Vec3(1, 0, 1) because pigs are usually on the surface
-let pig = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
- const pig = bot.nearestEntity((entity) => {
- return (
- entity.name === "pig" &&
- entity.position.distanceTo(bot.entity.position) < 32
- );
- });
- return pig;
-});
-*/
-async function exploreUntil(bot, direction, maxTime = 60, callback) {
- /*
- Implementation of this function is omitted.
- direction: Vec3, can only contain value of -1, 0 or 1
- maxTime: number, the max time for exploration
- callback: function, early stop condition, will be called each second, exploration will stop if return value is not null
-
- Return: null if explore timeout, otherwise return the return value of callback
- */
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/killMob.js b/metagpt/utils/minecraft/skills_code_context/killMob.js
deleted file mode 100644
index 670ca9753..000000000
--- a/metagpt/utils/minecraft/skills_code_context/killMob.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// Kill a pig and collect the dropped item: killMob(bot, "pig", 300);
-async function killMob(bot, mobName, timeout = 300) {
- const entity = bot.nearestEntity(
- (entity) =>
- entity.name === mobName &&
- entity.position.distanceTo(bot.entity.position) < 32
- );
- await bot.pvp.attack(entity);
- await bot.pathfinder.goto(
- new GoalBlock(entity.position.x, entity.position.y, entity.position.z)
- );
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/mineBlock.js b/metagpt/utils/minecraft/skills_code_context/mineBlock.js
deleted file mode 100644
index c6a7559e6..000000000
--- a/metagpt/utils/minecraft/skills_code_context/mineBlock.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// Mine 3 cobblestone: mineBlock(bot, "stone", 3);
-async function mineBlock(bot, name, count = 1) {
- const blocks = bot.findBlocks({
- matching: (block) => {
- return block.name === name;
- },
- maxDistance: 32,
- count: count,
- });
- const targets = [];
- for (let i = 0; i < Math.min(blocks.length, count); i++) {
- targets.push(bot.blockAt(blocks[i]));
- }
- await bot.collectBlock.collect(targets, { ignoreNoPath: true });
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/mineflayer.js b/metagpt/utils/minecraft/skills_code_context/mineflayer.js
deleted file mode 100644
index 43217885c..000000000
--- a/metagpt/utils/minecraft/skills_code_context/mineflayer.js
+++ /dev/null
@@ -1,22 +0,0 @@
-await bot.pathfinder.goto(goal); // A very useful function. This function may change your main-hand equipment.
-// Following are some Goals you can use:
-new GoalNear(x, y, z, range); // Move the bot to a block within the specified range of the specified block. `x`, `y`, `z`, and `range` are `number`
-new GoalXZ(x, z); // Useful for long-range goals that don't have a specific Y level. `x` and `z` are `number`
-new GoalGetToBlock(x, y, z); // Not get into the block, but get directly adjacent to it. Useful for fishing, farming, filling bucket, and beds. `x`, `y`, and `z` are `number`
-new GoalFollow(entity, range); // Follow the specified entity within the specified range. `entity` is `Entity`, `range` is `number`
-new GoalPlaceBlock(position, bot.world, {}); // Position the bot in order to place a block. `position` is `Vec3`
-new GoalLookAtBlock(position, bot.world, {}); // Path into a position where a blockface of the block at position is visible. `position` is `Vec3`
-
-// These are other Mineflayer functions you can use:
-bot.isABed(bedBlock); // Return true if `bedBlock` is a bed
-bot.blockAt(position); // Return the block at `position`. `position` is `Vec3`
-
-// These are other Mineflayer async functions you can use:
-await bot.equip(item, destination); // Equip the item in the specified destination. `item` is `Item`, `destination` can only be "hand", "head", "torso", "legs", "feet", "off-hand"
-await bot.consume(); // Consume the item in the bot's hand. You must equip the item to consume first. Useful for eating food, drinking potions, etc.
-await bot.fish(); // Let bot fish. Before calling this function, you must first get to a water block and then equip a fishing rod. The bot will automatically stop fishing when it catches a fish
-await bot.sleep(bedBlock); // Sleep until sunrise. You must get to a bed block first
-await bot.activateBlock(block); // This is the same as right-clicking a block in the game. Useful for buttons, doors, etc. You must get to the block first
-await bot.lookAt(position); // Look at the specified position. You must go near the position before you look at it. To fill bucket with water, you must lookAt first. `position` is `Vec3`
-await bot.activateItem(); // This is the same as right-clicking to use the item in the bot's hand. Useful for using buckets, etc. You must equip the item to activate first
-await bot.useOn(entity); // This is the same as right-clicking an entity in the game. Useful for shearing sheep, equipping harnesses, etc. You must get to the entity first
diff --git a/metagpt/utils/minecraft/skills_code_context/placeItem.js b/metagpt/utils/minecraft/skills_code_context/placeItem.js
deleted file mode 100644
index 99e06089c..000000000
--- a/metagpt/utils/minecraft/skills_code_context/placeItem.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// Place a crafting_table near the player, Vec3(1, 0, 0) is just an example, you shouldn't always use that: placeItem(bot, "crafting_table", bot.entity.position.offset(1, 0, 0));
-async function placeItem(bot, name, position) {
- const item = bot.inventory.findInventoryItem(mcData.itemsByName[name].id);
- // find a reference block
- const faceVectors = [
- new Vec3(0, 1, 0),
- new Vec3(0, -1, 0),
- new Vec3(1, 0, 0),
- new Vec3(-1, 0, 0),
- new Vec3(0, 0, 1),
- new Vec3(0, 0, -1),
- ];
- let referenceBlock = null;
- let faceVector = null;
- for (const vector of faceVectors) {
- const block = bot.blockAt(position.minus(vector));
- if (block?.name !== "air") {
- referenceBlock = block;
- faceVector = vector;
- break;
- }
- }
- // You must first go to the block position you want to place
- await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {}));
- // You must equip the item right before calling placeBlock
- await bot.equip(item, "hand");
- await bot.placeBlock(referenceBlock, faceVector);
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/smeltItem.js b/metagpt/utils/minecraft/skills_code_context/smeltItem.js
deleted file mode 100644
index 0a3c76257..000000000
--- a/metagpt/utils/minecraft/skills_code_context/smeltItem.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Smelt 1 raw_iron into 1 iron_ingot using 1 oak_planks as fuel: smeltItem(bot, "raw_iron", "oak_planks");
-// You must place a furnace before calling this function
-async function smeltItem(bot, itemName, fuelName, count = 1) {
- const item = mcData.itemsByName[itemName];
- const fuel = mcData.itemsByName[fuelName];
- const furnaceBlock = bot.findBlock({
- matching: mcData.blocksByName.furnace.id,
- maxDistance: 32,
- });
- await bot.pathfinder.goto(
- new GoalLookAtBlock(furnaceBlock.position, bot.world)
- );
- const furnace = await bot.openFurnace(furnaceBlock);
- for (let i = 0; i < count; i++) {
- await furnace.putFuel(fuel.id, null, 1);
- await furnace.putInput(item.id, null, 1);
- // Wait 12 seconds for the furnace to smelt the item
- await bot.waitForTicks(12 * 20);
- await furnace.takeOutput();
- }
- await furnace.close();
-}
diff --git a/metagpt/utils/minecraft/skills_code_context/useChest.js b/metagpt/utils/minecraft/skills_code_context/useChest.js
deleted file mode 100644
index e80af3fd9..000000000
--- a/metagpt/utils/minecraft/skills_code_context/useChest.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// Get a torch from chest at (30, 65, 100): getItemFromChest(bot, new Vec3(30, 65, 100), {"torch": 1});
-// This function will work no matter how far the bot is from the chest.
-async function getItemFromChest(bot, chestPosition, itemsToGet) {
- await moveToChest(bot, chestPosition);
- const chestBlock = bot.blockAt(chestPosition);
- const chest = await bot.openContainer(chestBlock);
- for (const name in itemsToGet) {
- const itemByName = mcData.itemsByName[name];
- const item = chest.findContainerItem(itemByName.id);
- await chest.withdraw(item.type, null, itemsToGet[name]);
- }
- await closeChest(bot, chestBlock);
-}
-// Deposit a torch into chest at (30, 65, 100): depositItemIntoChest(bot, new Vec3(30, 65, 100), {"torch": 1});
-// This function will work no matter how far the bot is from the chest.
-async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) {
- await moveToChest(bot, chestPosition);
- const chestBlock = bot.blockAt(chestPosition);
- const chest = await bot.openContainer(chestBlock);
- for (const name in itemsToDeposit) {
- const itemByName = mcData.itemsByName[name];
- const item = bot.inventory.findInventoryItem(itemByName.id);
- await chest.deposit(item.type, null, itemsToDeposit[name]);
- }
- await closeChest(bot, chestBlock);
-}
-// Check the items inside the chest at (30, 65, 100): checkItemInsideChest(bot, new Vec3(30, 65, 100));
-// You only need to call this function once without any action to finish task of checking items inside the chest.
-async function checkItemInsideChest(bot, chestPosition) {
- await moveToChest(bot, chestPosition);
- const chestBlock = bot.blockAt(chestPosition);
- await bot.openContainer(chestBlock);
- // You must close the chest after opening it if you are asked to open a chest
- await closeChest(bot, chestBlock);
-}
diff --git a/tests/metagpt/roles/minecraft/test_action_developer.py b/tests/metagpt/roles/minecraft/test_action_developer.py
new file mode 100644
index 000000000..badb5166b
--- /dev/null
+++ b/tests/metagpt/roles/minecraft/test_action_developer.py
@@ -0,0 +1,91 @@
+import asyncio
+
+from metagpt.minecraft_team import GameEnvironment
+from metagpt.roles.minecraft.action_developer import ActionDeveloper
+from metagpt.logs import logger
+
+
+async def main():
+ events = [
+ [
+ "observe",
+ {
+ "voxels": ["grass_block", "dirt", "grass"],
+ "status": {
+ "health": 20,
+ "food": 20,
+ "saturation": 5,
+ "oxygen": 20,
+ "position": {"x": 0.5, "y": 84, "z": -207.5},
+ "velocity": {"x": 0, "y": -0.0784000015258789, "z": 0},
+ "yaw": 3.141592653589793,
+ "pitch": 0,
+ "onGround": True,
+ "equipment": [None, None, None, None, None, None],
+ "name": "bot",
+ "isInWater": False,
+ "isInLava": False,
+ "isCollidedHorizontally": False,
+ "isCollidedVertically": True,
+ "biome": "plains",
+ "entities": {
+ "chicken": 29.071822119730644,
+ "sheep": 20.361212992763768,
+ },
+ "timeOfDay": "day",
+ "inventoryUsed": 0,
+ "elapsedTime": 41,
+ },
+ "inventory": {},
+ "nearbyChests": {},
+ "blockRecords": ["grass_block", "dirt", "grass"],
+ },
+ ]
+ ]
+ code = """
+ async function collectBamboo(bot) {
+ // Equip the iron sword
+ const ironSword = bot.inventory.findInventoryItem(mcData.itemsByName.iron_sword.id);
+ await bot.equip(ironSword, "hand");
+
+ // Find bamboo plants using the exploreUntil function
+ const bambooPlants = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
+ const bambooPlants = bot.findBlocks({
+ matching: block => block.name === "bamboo",
+ maxDistance: 32,
+ count: 10
+ });
+ return bambooPlants.length >= 10 ? bambooPlants : null;
+ });
+ if (!bambooPlants) {
+ bot.chat("Could not find enough bamboo plants.");
+ return;
+ }
+
+ // Break 10 bamboo plants using the iron sword
+ for (const bambooPlant of bambooPlants) {
+ const block = bot.blockAt(bambooPlant);
+ await bot.dig(block);
+ }
+ bot.chat("Broke 10 bamboo plants.");
+
+ // Collect the dropped bamboo items
+ for (const bambooPlant of bambooPlants) {
+ await bot.pathfinder.goto(new GoalBlock(bambooPlant.x, bambooPlant.y, bambooPlant.z));
+ }
+ bot.chat("Collected 10 bamboo.");
+ }
+ """
+ ad = ActionDeveloper()
+ ge = GameEnvironment()
+ ad.set_memory(shared_memory=ge)
+ msg = ad.encapsule_message(events=events, code=code)
+ logger.info(f"Encapsuled_message: {msg}")
+
+ parsed_result = await ad.generate_action_code(msg)
+
+ logger.info(f"Parsed_code_updating: {parsed_result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/metagpt/test_minecraft_team.py b/tests/metagpt/test_minecraft_team.py
new file mode 100644
index 000000000..da0126d8d
--- /dev/null
+++ b/tests/metagpt/test_minecraft_team.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# @Date : 2023/09/28 00:03
+# @Author : yuymf
+# @Desc :
+import asyncio
+from metagpt.logs import logger
+from metagpt.minecraft_team import GameEnvironment
+
+
+async def main():
+ test_code = "bot.chat(`/time set ${getNextTime()}`);"
+ mc_port = 2745
+ ge = GameEnvironment()
+ ge.set_mc_port(mc_port)
+ ge.update_code(test_code)
+ result = await ge.on_event()
+ logger.info("On event test done")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/metagpt/utils/minecraft/test_action_rsp_parser.py b/tests/metagpt/utils/minecraft/test_action_rsp_parser.py
new file mode 100644
index 000000000..e41ce8dd4
--- /dev/null
+++ b/tests/metagpt/utils/minecraft/test_action_rsp_parser.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# @Date : 2023/09/28 00:08
+# @Author : yuymf
+# @Desc :
+from metagpt.utils.minecraft import parse_js_code, parse_action_response
+from metagpt.logs import logger
+from typing import Any
+
+
+if __name__ == "__main__":
+ msg = '''
+ Explain: The code from the last round is a function called `collectBamboo` that is supposed to collect bamboo plants. It equips an iron sword, finds bamboo plants using the `exploreUntil` function, breaks 10 bamboo plants using the iron sword, and then collects the dropped bamboo items.
+
+ Plan:
+ 1) Check if the bot has an iron sword in its inventory. If not, collect the necessary materials and craft an iron sword using the `craftItem` function.
+ 2) Use the `exploreUntil` function to find at least 10 bamboo plants. If the function times out or cannot find enough bamboo plants, return and chat "Could not find enough bamboo plants."
+ 3) Equip the iron sword.
+ 4) Iterate over the found bamboo plants and break them using the iron sword.
+ 5) Chat "Broke 10 bamboo plants."
+ 6) Iterate over the found bamboo plants and collect the dropped bamboo items.
+ 7) Chat "Collected 10 bamboo."
+
+ Code:
+ ```javascript
+ async function collectBamboo(bot) {
+ // Check if the bot has an iron sword
+ const ironSword = bot.inventory.findInventoryItem(mcData.itemsByName.iron_sword.id);
+ if (!ironSword) {
+ // Collect the necessary materials to craft an iron sword
+ await mineBlock(bot, "iron_ore", 3);
+ await smeltItem(bot, "iron_ore", "oak_planks", 3);
+ await craftItem(bot, "iron_sword", 1);
+ }
+
+ // Find bamboo plants using the exploreUntil function
+ const bambooPlants = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
+ const bambooPlants = bot.findBlocks({
+ matching: block => block.name === "bamboo",
+ maxDistance: 32,
+ count: 10
+ });
+ return bambooPlants.length >= 10 ? bambooPlants : null;
+ });
+ if (!bambooPlants) {
+ bot.chat("Could not find enough bamboo plants.");
+ return;
+ }
+
+ // Equip the iron sword
+ await bot.equip(ironSword, "hand");
+
+ // Break 10 bamboo plants using the iron sword
+ for (const bambooPlant of bambooPlants) {
+ const block = bot.blockAt(bambooPlant);
+ await bot.dig(block);
+ }
+ bot.chat("Broke 10 bamboo plants.");
+
+ // Collect the dropped bamboo items
+ for (const bambooPlant of bambooPlants) {
+ await bot.pathfinder.goto(new GoalBlock(bambooPlant.x, bambooPlant.y, bambooPlant.z));
+ }
+ bot.chat("Collected 10 bamboo.");
+ }
+ ```
+ '''
+
+ logger.info(f"Parse_js_code result is HERE: {parse_js_code(msg)}")
+ logger.info(f"Parse_action_response result is HERE: {parse_action_response(msg)}")
\ No newline at end of file
From 73cd3688283d29b3cba78454944ca5635113b246 Mon Sep 17 00:00:00 2001
From: yuymf <1352948945@qq.com>
Date: Thu, 28 Sep 2023 21:09:13 +0800
Subject: [PATCH 3/3] Action developer add chest_memory & .run test pass
---
.gitignore | 1 +
metagpt/actions/minecraft/generate_actions.py | 4 +-
metagpt/minecraft_team.py | 40 +++++++++++++++++-
metagpt/mineflayer_environment.py | 3 ++
metagpt/roles/minecraft/action_developer.py | 42 +++++++++++++++----
metagpt/utils/minecraft/json_utils.py | 32 --------------
minecraft_run.py | 3 +-
.../roles/minecraft/test_action_developer.py | 8 ++--
8 files changed, 84 insertions(+), 49 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1337aa4d4..0d335455c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -114,6 +114,7 @@ venv/
ENV/
env.bak/
venv.bak/
+*/ckpt
# Spyder project settings
.spyderproject
diff --git a/metagpt/actions/minecraft/generate_actions.py b/metagpt/actions/minecraft/generate_actions.py
index 275cc8b38..8b27630e8 100644
--- a/metagpt/actions/minecraft/generate_actions.py
+++ b/metagpt/actions/minecraft/generate_actions.py
@@ -32,11 +32,11 @@ class GenerateActionCode(Action):
logger.error(f"Failed to parse response: {parsed_result}")
return None
- async def run(self, msg, *args, **kwargs):
+ async def run(self, human_msg, system_msg, *args, **kwargs):
logger.info(f"run {self.__repr__()}")
# Generate action code.
generated_code = await self.generate_code(
- human_msg=msg['human_msg'], system_msg=msg['system_msg']
+ human_msg=human_msg, system_msg=system_msg
)
# Return the generated code.
diff --git a/metagpt/minecraft_team.py b/metagpt/minecraft_team.py
index f60e6af4f..392d12092 100644
--- a/metagpt/minecraft_team.py
+++ b/metagpt/minecraft_team.py
@@ -6,7 +6,6 @@ from typing import Iterable, Dict, Any
from pydantic import BaseModel, Field
import requests
import json
-import asyncio
from metagpt.logs import logger
from metagpt.roles import Role
@@ -33,17 +32,31 @@ class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
critique: str = Field(default="")
skills: list[str] = Field(default_factory=list)
+ chest_memory: dict[str, Any] = Field(default_factory=dict)
+
mf_instance: MineflayerEnv = Field(default_factory=MineflayerEnv)
def set_mc_port(self, mc_port):
self.mf_instance.set_mc_port(mc_port)
+ def set_mc_resume(self, resume: bool = False):
+ if resume:
+ logger.info(
+ f"Loading Action Developer from {self.mf_instance.ckpt_dir}/action"
+ )
+ with open(
+ f"{self.mf_instance.ckpt_dir}/action/chest_memory.json", "r"
+ ) as f:
+ self.chest_memory = json.load(f)
+ # TODO: add skills resume
+
def register_roles(self, roles: Iterable[Minecraft]):
for role in roles:
role.set_memory(self)
def update_event(self, event: Dict):
self.event = event
+ self.update_chest_memory(event)
def update_task(self, task: str):
self.current_task = task
@@ -52,7 +65,7 @@ class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
self.context = context
def update_code(self, code: str):
- self.code = code # action_developer.gen to HERE
+ self.code = code # action_developer.gen_action_code to HERE
def update_programs(self, programs: str):
self.programs = programs
@@ -63,6 +76,26 @@ class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
def update_skills(self, skills: list):
self.skills = skills # skill_manager.retrieve_skills to HERE
+ def update_chest_memory(self, events: Dict):
+ """
+ Input: events: Dict
+ Result: self.chest_memory update & save to json
+ """
+ nearbyChests = events[-1][1]["nearbyChests"]
+ for position, chest in nearbyChests.items():
+ if position in self.chest_memory:
+ if isinstance(chest, dict):
+ self.chest_memory[position] = chest
+ if chest == "Invalid":
+ logger.info(f"Action Developer removing chest {position}: {chest}")
+ self.chest_memory.pop(position)
+ else:
+ if chest != "Invalid":
+ logger.info(f"Action Developer saving chest {position}: {chest}")
+ self.chest_memory[position] = chest
+ with open(f"{self.mf_instance.ckpt_dir}/action/chest_memory.json", "w") as f:
+ json.dump(self.chest_memory, f)
+
async def on_event(self, *args):
"""
Retrieve Minecraft events.
@@ -126,6 +159,9 @@ class MinecraftPlayer(SoftwareCompany):
def set_port(self, mc_port):
self.game_memory.set_mc_port(mc_port)
+ def set_resume(self, resume: bool = False):
+ self.game_memory.set_mc_resume(resume=resume)
+
def hire(self, roles: list[Role]):
self.environment.add_roles(roles)
self.game_memory.register_roles(roles)
diff --git a/metagpt/mineflayer_environment.py b/metagpt/mineflayer_environment.py
index 52b54ebfb..e10127fe1 100644
--- a/metagpt/mineflayer_environment.py
+++ b/metagpt/mineflayer_environment.py
@@ -29,6 +29,9 @@ class MineflayerEnv:
self.reset_options = None
self.connected = False
self.server_paused = False
+ self.ckpt_dir = "metagpt/ckpt"
+
+ os.makedirs(f"{self.ckpt_dir}/action", exist_ok=True)
def set_mc_port(self, mc_port):
self.mc_port = mc_port
diff --git a/metagpt/roles/minecraft/action_developer.py b/metagpt/roles/minecraft/action_developer.py
index 231699b40..1184b5da4 100644
--- a/metagpt/roles/minecraft/action_developer.py
+++ b/metagpt/roles/minecraft/action_developer.py
@@ -7,7 +7,6 @@ from metagpt.roles.minecraft.minecraft_base import Minecraft as Base
from metagpt.schema import Message, HumanMessage, SystemMessage
from metagpt.roles.minecraft.minecraft_base import agent_registry
from metagpt.actions.minecraft.generate_actions import GenerateActionCode
-from metagpt.actions.minecraft.design_curriculumn import DesignCurriculum
from metagpt.actions.minecraft.manage_skills import (
GenerateSkillDescription,
RetrieveSkills,
@@ -42,6 +41,30 @@ class ActionDeveloper(Base):
# 需要根据events进行自己chest_observation的更新
self._watch([RetrieveSkills])
+ def render_chest_observation(self):
+ """
+ Render game_memory.chest_memory to prompt text.
+ Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
+ """
+
+ chests = []
+ for chest_position, chest in self.game_memory.chest_memory.items():
+ if isinstance(chest, dict) and len(chest) > 0:
+ chests.append(f"{chest_position}: {chest}")
+ for chest_position, chest in self.game_memory.chest_memory.items():
+ if isinstance(chest, dict) and len(chest) == 0:
+ chests.append(f"{chest_position}: Empty")
+ for chest_position, chest in self.game_memory.chest_memory.items():
+ if isinstance(chest, str):
+ assert chest == "Unknown"
+ chests.append(f"{chest_position}: Unknown items inside")
+ assert len(chests) == len(self.game_memory.chest_memory)
+ if chests:
+ chests = "\n".join(chests)
+ return f"Chests:\n{chests}\n\n"
+ else:
+ return f"Chests: None\n\n"
+
def render_system_message(self, skills=[], *args, **kwargs):
"""
According to basic skills context files to genenarate js skill codes.
@@ -140,12 +163,12 @@ class ActionDeveloper(Base):
observation += f"Equipment: {equipment}\n\n"
observation += f"Inventory ({inventory_used}/36): {'Empty' if not inventory else ', '.join(inventory)}\n\n"
- if not (
- task == "Place and deposit useless items into a chest"
- or task.startswith("Deposit useless items into the chest at")
- ):
- # TODO: observation += self.render_chest_observation()
- logger.warning("chest_observation will add later")
+ # TODO: if task update, uncomment this
+ # if not (
+ # task == "Place and deposit useless items into a chest"
+ # or task.startswith("Deposit useless items into the chest at")
+ # ):
+ observation += self.render_chest_observation()
observation += f"Task: {task}\n\n"
observation += f"Context: {context or 'None'}\n\n"
@@ -183,8 +206,8 @@ class ActionDeveloper(Base):
logger.info(len(self._rc.news))
return len(self._rc.news)
- async def generate_action_code(self, msg, *args, **kwargs):
- code = await GenerateActionCode().run(msg, *args, **kwargs)
+ async def generate_action_code(self, human_msg, system_msg, *args, **kwargs):
+ code = await GenerateActionCode().run(human_msg, system_msg, *args, **kwargs)
# logger.warning(type(code))
# logger.info(f"Code is Here:{code}")
self.perform_game_info_callback(code, self.game_memory.update_code)
@@ -202,6 +225,7 @@ class ActionDeveloper(Base):
# 获取最新的游戏周边信息
events = await self._obtain_events()
+ self.perform_game_info_callback(events, self.game_memory.update_event)
context = self.game_memory.context
task = self.game_memory.current_task
code = self.game_memory.code
diff --git a/metagpt/utils/minecraft/json_utils.py b/metagpt/utils/minecraft/json_utils.py
index 03f357cec..0e9d9ba6e 100644
--- a/metagpt/utils/minecraft/json_utils.py
+++ b/metagpt/utils/minecraft/json_utils.py
@@ -6,38 +6,6 @@
import json
import re
from typing import Any, Dict, Union
-from .file_utils import f_join
-
-
-def json_load(*file_path, **kwargs):
- file_path = f_join(file_path)
- with open(file_path, "r") as fp:
- return json.load(fp, **kwargs)
-
-
-def json_loads(string, **kwargs):
- return json.loads(string, **kwargs)
-
-
-def json_dump(data, *file_path, **kwargs):
- file_path = f_join(file_path)
- with open(file_path, "w") as fp:
- json.dump(data, fp, **kwargs)
-
-
-def json_dumps(data, **kwargs):
- """
- Returns: string
- """
- return json.dumps(data, **kwargs)
-
-
-# ---------------- Aliases -----------------
-# add aliases where verb goes first, json_load -> load_json
-load_json = json_load
-loads_json = json_loads
-dump_json = json_dump
-dumps_json = json_dumps
def extract_char_position(error_message: str) -> int:
diff --git a/minecraft_run.py b/minecraft_run.py
index 50eada1fc..d7d2cf7c2 100644
--- a/minecraft_run.py
+++ b/minecraft_run.py
@@ -13,7 +13,8 @@ from metagpt.minecraft_team import MinecraftPlayer
async def learn(task="Start", investment: float = 50.0, n_round: int = 3):
mc_player = MinecraftPlayer()
- mc_player.set_port(2253) # Modify this to your LAN port
+ mc_player.set_port(1077) # Modify this to your Minecraft LAN port
+ # mc_player.set_resume(True) # If load json from ckpt dir(include chest_memory, skills, ...)
mc_player.hire(
[
CurriculumDesigner(),
diff --git a/tests/metagpt/roles/minecraft/test_action_developer.py b/tests/metagpt/roles/minecraft/test_action_developer.py
index badb5166b..f00bfb783 100644
--- a/tests/metagpt/roles/minecraft/test_action_developer.py
+++ b/tests/metagpt/roles/minecraft/test_action_developer.py
@@ -37,11 +37,12 @@ async def main():
"elapsedTime": 41,
},
"inventory": {},
- "nearbyChests": {},
+ "nearbyChests": {"(1344, 64, 1381)": "Unknown"},
"blockRecords": ["grass_block", "dirt", "grass"],
},
]
]
+
code = """
async function collectBamboo(bot) {
// Equip the iron sword
@@ -78,11 +79,12 @@ async def main():
"""
ad = ActionDeveloper()
ge = GameEnvironment()
+ ge.update_event(events)
ad.set_memory(shared_memory=ge)
- msg = ad.encapsule_message(events=events, code=code)
+ msg = ad.encapsule_message(events=ge.event, code=code)
logger.info(f"Encapsuled_message: {msg}")
- parsed_result = await ad.generate_action_code(msg)
+ parsed_result = await ad.generate_action_code(**msg)
logger.info(f"Parsed_code_updating: {parsed_result}")