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}")