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/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 的信息传递 + +测试结果 + +![action_developer](docs/resources/workspace/minecraft_tests/action_developer.png) 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 000000000..397b4bdee Binary files /dev/null and b/docs/resources/workspace/minecraft_tests/action_developer.png differ 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/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/actions/minecraft/generate_actions.py b/metagpt/actions/minecraft/generate_actions.py index e32aab485..8b27630e8 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, human_msg, system_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=human_msg, system_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..392d12092 100644 --- a/metagpt/minecraft_team.py +++ b/metagpt/minecraft_team.py @@ -15,44 +15,87 @@ 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) + 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 - + def update_context(self, context: str): self.context = context def update_code(self, code: str): - self.code = code + self.code = code # action_developer.gen_action_code 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 + + 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. @@ -70,10 +113,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 +127,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 +143,40 @@ 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 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) - + 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 +185,5 @@ class MinecraftPlayer(SoftwareCompany): logger.debug(f"{n_round=}") self._check_balance() await self.environment.run() - - return self.environment.history -if "__name__" == "__main__": - test_code = "bot.chat(`/time set ${getNextTime()}`);" - mc_port = 1960 - ge = GameEnvironment() - ge.set_mc_port(mc_port) - ge.update_code(test_code) - logger.info(ge.on_event()) \ No newline at end of file + return self.environment.history diff --git a/metagpt/mineflayer_environment.py b/metagpt/mineflayer_environment.py index 1b208c8d5..e10127fe1 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, @@ -28,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 @@ -66,18 +70,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 +107,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 index 21d22371d..df3713a83 100644 --- a/metagpt/prompts/minecraft/action_response_format.txt +++ b/metagpt/prompts/minecraft/action_response_format.txt @@ -12,4 +12,4 @@ Code: async function yourMainFunctionName(bot) { // ... } -``` +``` \ No newline at end of file diff --git a/metagpt/prompts/minecraft/critic.txt b/metagpt/prompts/minecraft/critic.txt index 50dd71cc9..c42950675 100644 --- a/metagpt/prompts/minecraft/critic.txt +++ b/metagpt/prompts/minecraft/critic.txt @@ -124,4 +124,4 @@ 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 index 279d24f82..66a33c626 100644 --- a/metagpt/prompts/minecraft/curriculum.txt +++ b/metagpt/prompts/minecraft/curriculum.txt @@ -39,4 +39,4 @@ 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. +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 index bb5e26bf1..860d986e4 100644 --- a/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt +++ b/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt @@ -5,4 +5,4 @@ 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. +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 index bb5d6707e..0e3b1592d 100644 --- a/metagpt/prompts/minecraft/curriculum_task_decomposition.txt +++ b/metagpt/prompts/minecraft/curriculum_task_decomposition.txt @@ -9,4 +9,4 @@ You must follow the following criteria: 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. +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 index efedcecc6..dc846cdf6 100644 --- a/metagpt/prompts/minecraft/skill.txt +++ b/metagpt/prompts/minecraft/skill.txt @@ -48,4 +48,4 @@ 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. +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 d89e823f3..1184b5da4 100644 --- a/metagpt/roles/minecraft/action_developer.py +++ b/metagpt/roles/minecraft/action_developer.py @@ -7,8 +7,16 @@ 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.actions.minecraft.control_primitives_context import ( + load_skills_code_context, +) @agent_registry.register("action_developer") @@ -17,22 +25,177 @@ 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_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. + 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(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" + + # 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" + 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 +205,43 @@ 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) - logger.info(msg) + 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) + 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() + 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 + 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 +249,9 @@ 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)}") 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..6ee04c606 100644 --- a/metagpt/utils/minecraft/__init__.py +++ b/metagpt/utils/minecraft/__init__.py @@ -4,4 +4,5 @@ # @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 .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 new file mode 100644 index 000000000..91ccc141d --- /dev/null +++ b/metagpt/utils/minecraft/action_rsp_parser.py @@ -0,0 +1,91 @@ +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 # 3 times failed return error + babel_generator = require("@babel/generator").default + while retry > 0: + try: + 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): + 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"]), + } + ) + + # Ensure main_function is 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'" + + # 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 { + "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..0e9d9ba6e 100644 --- a/metagpt/utils/minecraft/json_utils.py +++ b/metagpt/utils/minecraft/json_utils.py @@ -6,37 +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: @@ -144,6 +113,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 +134,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/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 new file mode 100644 index 000000000..f00bfb783 --- /dev/null +++ b/tests/metagpt/roles/minecraft/test_action_developer.py @@ -0,0 +1,93 @@ +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": {"(1344, 64, 1381)": "Unknown"}, + "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() + ge.update_event(events) + ad.set_memory(shared_memory=ge) + msg = ad.encapsule_message(events=ge.event, 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