mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-06-17 15:35:21 +02:00
Merge pull request #372 from yuymf/minecraft_dev
Minecraft game add action_developer
This commit is contained in:
commit
89bdd19a51
25 changed files with 633 additions and 126 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -114,6 +114,7 @@ venv/
|
|||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
*/ckpt
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
|
|
|||
10
Temp.md
10
Temp.md
|
|
@ -31,3 +31,13 @@ ### 0926: 环境信息获取和更新 on_event()实际内容
|
|||
|
||||
<img src="docs/resources/workspace/minecraft_tests/on_event.jpeg" style="zoom:67%;" />
|
||||
|
||||
|
||||
|
||||
### 0927:Action_developer 更新
|
||||
|
||||
对应需实现 GenerateActionCode ,完成对应的和 GameEnvironment 的交
|
||||
互和 Environment 的信息传递
|
||||
|
||||
测试结果
|
||||
|
||||

|
||||
|
|
|
|||
BIN
docs/resources/workspace/minecraft_tests/action_developer.png
Normal file
BIN
docs/resources/workspace/minecraft_tests/action_developer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
3
mc_requirements.txt
Normal file
3
mc_requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
javascript
|
||||
requests
|
||||
psutil
|
||||
|
|
@ -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"]))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
return self.environment.history
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ Code:
|
|||
async function yourMainFunctionName(bot) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)}")
|
||||
|
||||
raise ValueError(f"Unknown todo type: {type(todo)}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@
|
|||
# @Desc :
|
||||
from .load_prompts import load_prompt
|
||||
from .json_utils import *
|
||||
from .file_utils import *
|
||||
from .file_utils import *
|
||||
from .action_rsp_parser import parse_js_code, parse_action_response
|
||||
|
|
|
|||
91
metagpt/utils/minecraft/action_rsp_parser.py
Normal file
91
metagpt/utils/minecraft/action_rsp_parser.py
Normal file
|
|
@ -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}"
|
||||
|
|
@ -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
|
||||
read_text_lines = load_text_lines
|
||||
|
|
|
|||
|
|
@ -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
|
||||
raise e
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
return load_text(f"{package_path}/prompts/minecraft/{prompt}.txt")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
93
tests/metagpt/roles/minecraft/test_action_developer.py
Normal file
93
tests/metagpt/roles/minecraft/test_action_developer.py
Normal file
|
|
@ -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())
|
||||
21
tests/metagpt/test_minecraft_team.py
Normal file
21
tests/metagpt/test_minecraft_team.py
Normal file
|
|
@ -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())
|
||||
69
tests/metagpt/utils/minecraft/test_action_rsp_parser.py
Normal file
69
tests/metagpt/utils/minecraft/test_action_rsp_parser.py
Normal file
|
|
@ -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)}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue