From d4a40bd9885119965d12b43c91b7bc0838d65d64 Mon Sep 17 00:00:00 2001 From: better629 Date: Wed, 10 Apr 2024 16:56:31 +0800 Subject: [PATCH] fix code --- .../environment/minecraft/minecraft_env.py | 2 +- metagpt/environment/werewolf/const.py | 68 +++++++++++-------- metagpt/environment/werewolf/werewolf_env.py | 23 +++++-- .../environment/werewolf/werewolf_ext_env.py | 4 +- .../ext/werewolf/actions/common_actions.py | 6 +- metagpt/ext/werewolf/roles/base_player.py | 14 ++-- metagpt/ext/werewolf/roles/moderator.py | 54 ++++++++++----- metagpt/ext/werewolf/roles/witch.py | 3 +- metagpt/ext/werewolf/schema.py | 12 +++- 9 files changed, 126 insertions(+), 60 deletions(-) diff --git a/metagpt/environment/minecraft/minecraft_env.py b/metagpt/environment/minecraft/minecraft_env.py index 0f39c9ccd..31a48964b 100644 --- a/metagpt/environment/minecraft/minecraft_env.py +++ b/metagpt/environment/minecraft/minecraft_env.py @@ -19,7 +19,7 @@ from metagpt.logs import logger from metagpt.utils.common import load_mc_skills_code, read_json_file, write_json_file -class MinecraftEnv(Environment, MinecraftExtEnv): +class MinecraftEnv(MinecraftExtEnv, Environment): """MinecraftEnv, including shared memory of cache and information between roles""" model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/metagpt/environment/werewolf/const.py b/metagpt/environment/werewolf/const.py index 905ad3713..873b02c3a 100644 --- a/metagpt/environment/werewolf/const.py +++ b/metagpt/environment/werewolf/const.py @@ -30,78 +30,92 @@ class RoleActionRes(Enum): PASS = "pass" # ignore current action output +empty_set = set() + # the ordered rules by the moderator to announce to everyone each step STEP_INSTRUCTIONS = { 0: { "content": "It’s dark, everyone close your eyes. I will talk with you/your team secretly at night.", - "send_to": {"Moderator"}, # for moderator to continue speaking - "restricted_to": {}, + "send_to": {RoleType.MODERATOR.value}, # for moderator to continue speaking + "restricted_to": empty_set, }, 1: { "content": "Guard, please open your eyes!", - "send_to": {"Moderator"}, # for moderator to continue speaking - "restricted_to": {}, + "send_to": {RoleType.MODERATOR.value}, # for moderator to continue speaking + "restricted_to": empty_set, }, 2: { "content": """Guard, now tell me who you protect tonight? You only choose one from the following living options please: {living_players}. Or you can pass. For example: Protect ...""", - "send_to": {"Guard"}, - "restricted_to": {"Moderator", "Guard"}, + "send_to": {RoleType.GUARD.value}, + "restricted_to": {RoleType.MODERATOR.value, RoleType.GUARD.value}, + }, + 3: {"content": "Guard, close your eyes", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set}, + 4: { + "content": "Werewolves, please open your eyes!", + "send_to": {RoleType.MODERATOR.value}, + "restricted_to": empty_set, }, - 3: {"content": "Guard, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}}, - 4: {"content": "Werewolves, please open your eyes!", "send_to": {"Moderator"}, "restricted_to": {}}, 5: { "content": """Werewolves, I secretly tell you that {werewolf_players} are all of the 2 werewolves! Keep in mind you are teammates. The rest players are not werewolves. choose one from the following living options please: {living_players}. For example: Kill ...""", - "send_to": {"Werewolf"}, - "restricted_to": {"Moderator", "Werewolf"}, + "send_to": {RoleType.WEREWOLF.value}, + "restricted_to": {RoleType.MODERATOR.value, RoleType.WEREWOLF.value}, }, - 6: {"content": "Werewolves, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}}, - 7: {"content": "Witch, please open your eyes!", "send_to": {"Moderator"}, "restricted_to": {}}, + 6: {"content": "Werewolves, close your eyes", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set}, + 7: {"content": "Witch, please open your eyes!", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set}, 8: { "content": """Witch, tonight {player_hunted} has been killed by the werewolves. You have a bottle of antidote, would you like to save him/her? If so, say "Save", else, say "Pass".""", - "send_to": {"Witch"}, - "restricted_to": {"Moderator", "Witch"}, + "send_to": {RoleType.WITCH.value}, + "restricted_to": {RoleType.MODERATOR.value, RoleType.WITCH.value}, }, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 9: { "content": """Witch, you also have a bottle of poison, would you like to use it to kill one of the living players? Choose one from the following living options: {living_players}. If so, say ONLY "Poison PlayerX", replace PlayerX with the actual player name, else, say "Pass".""", - "send_to": {"Witch"}, - "restricted_to": {"Moderator", "Witch"}, + "send_to": {RoleType.WITCH.value}, + "restricted_to": {RoleType.MODERATOR.value, RoleType.WITCH.value}, }, # - 10: {"content": "Witch, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}}, - 11: {"content": "Seer, please open your eyes!", "send_to": {"Moderator"}, "restricted_to": {}}, + 10: {"content": "Witch, close your eyes", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set}, + 11: {"content": "Seer, please open your eyes!", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set}, 12: { "content": """Seer, you can check one player's identity. Who are you going to verify its identity tonight? Choose only one from the following living options:{living_players}.""", - "send_to": {"Seer"}, - "restricted_to": {"Moderator", "Seer"}, + "send_to": {RoleType.SEER.value}, + "restricted_to": {RoleType.MODERATOR.value, RoleType.SEER.value}, }, - 13: {"content": "Seer, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}}, + 13: {"content": "Seer, close your eyes", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set}, # The 1-st daytime 14: { "content": """It's daytime. Everyone woke up except those who had been killed.""", - "send_to": {"Moderator"}, - "restricted_to": {}, + "send_to": {RoleType.MODERATOR.value}, + "restricted_to": empty_set, + }, + 15: { + "content": "{player_current_dead} was killed last night!", + "send_to": {RoleType.MODERATOR.value}, + "restricted_to": empty_set, }, - 15: {"content": "{player_current_dead} was killed last night!", "send_to": {"Moderator"}, "restricted_to": {}}, 16: { "content": """Living players: {living_players}, now freely talk about the current situation based on your observation and reflection with a few sentences. Decide whether to reveal your identity based on your reflection.""", "send_to": {MESSAGE_ROUTE_TO_ALL}, # send to all to speak in daytime - "restricted_to": {}, + "restricted_to": empty_set, }, 17: { "content": """Now vote and tell me who you think is the werewolf. Don’t mention your role. You only choose one from the following living options please: {living_players}. Say ONLY: I vote to eliminate ...""", "send_to": {MESSAGE_ROUTE_TO_ALL}, - "restricted_to": {}, + "restricted_to": empty_set, + }, + 18: { + "content": """{player_current_dead} was eliminated.""", + "send_to": {RoleType.MODERATOR.value}, + "restricted_to": empty_set, }, - 18: {"content": """{player_current_dead} was eliminated.""", "send_to": {"Moderator"}, "restricted_to": {}}, } diff --git a/metagpt/environment/werewolf/werewolf_env.py b/metagpt/environment/werewolf/werewolf_env.py index 728f1309c..999ff63a1 100644 --- a/metagpt/environment/werewolf/werewolf_env.py +++ b/metagpt/environment/werewolf/werewolf_env.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # @Desc : MG Werewolf Env +from typing import Iterable + from pydantic import Field from metagpt.environment.base_env import Environment @@ -9,15 +11,26 @@ from metagpt.environment.werewolf.werewolf_ext_env import WerewolfExtEnv from metagpt.schema import Message -class WerewolfEnv(Environment, WerewolfExtEnv): - timestamp: int = Field(default=0) +class WerewolfEnv(WerewolfExtEnv, Environment): + round_cnt: int = Field(default=0) + + def add_roles(self, roles: Iterable["Role"]): + """增加一批在当前环境的角色 + Add a batch of characters in the current environment + """ + for role in roles: + self.roles[role.name] = role # use name as key here, due to multi-player can have same profile + + for role in roles: # setup system message with roles + role.context = self.context + role.set_env(self) def publish_message(self, message: Message, add_timestamp: bool = True): """Post information to the current environment""" if add_timestamp: # Because the content of the message may be repeated, for example, killing the same person in two nights - # Therefore, a unique timestamp prefix needs to be added so that the same message will not be automatically deduplicated when added to the memory. - message.content = f"{self.timestamp} | " + message.content + # Therefore, a unique round_cnt prefix needs to be added so that the same message will not be automatically deduplicated when added to the memory. + message.content = f"{self.round_cnt} | " + message.content super().publish_message(message) async def run(self, k=1): @@ -25,4 +38,4 @@ class WerewolfEnv(Environment, WerewolfExtEnv): for _ in range(k): for role in self.roles.values(): await role.run() - self.timestamp += 1 + self.round_cnt += 1 diff --git a/metagpt/environment/werewolf/werewolf_ext_env.py b/metagpt/environment/werewolf/werewolf_ext_env.py index ccf26c771..35d1f5563 100644 --- a/metagpt/environment/werewolf/werewolf_ext_env.py +++ b/metagpt/environment/werewolf/werewolf_ext_env.py @@ -24,7 +24,7 @@ class WerewolfExtEnv(ExtEnv): round_idx: int = Field(default=0) # the current round step_idx: int = Field(default=0) # the current step of current round - eval_step_idx: int = Field(default=0) + eval_step_idx: list[int] = Field(default=[]) per_round_steps: int = Field(default=len(STEP_INSTRUCTIONS)) # game global states @@ -259,7 +259,7 @@ class WerewolfExtEnv(ExtEnv): def wolf_kill_someone(self, wolf_name: str, player_name: str): if not self._check_valid_role(wolf_name, RoleType.WEREWOLF.value): return - if not self._check_player_continue(wolf_name, particular_step=5): # 5=step no + if not self._check_player_continue(wolf_name, particular_step=6): # 5=step no return self.round_hunts[wolf_name] = player_name diff --git a/metagpt/ext/werewolf/actions/common_actions.py b/metagpt/ext/werewolf/actions/common_actions.py index 9fd7adb0a..0f1b3b74c 100644 --- a/metagpt/ext/werewolf/actions/common_actions.py +++ b/metagpt/ext/werewolf/actions/common_actions.py @@ -7,6 +7,8 @@ import json from tenacity import retry, stop_after_attempt, wait_fixed from metagpt.actions import Action +from metagpt.logs import logger +from metagpt.utils.common import parse_json_code_block class Speak(Action): @@ -228,6 +230,8 @@ class Reflect(Action): rsp = await self._aask(prompt) rsp = rsp.replace("\n", " ") - rsp_json = json.loads(rsp) + logger.debug(f"{self.name} result: {rsp}") + json_blocks = parse_json_code_block(rsp) + rsp_json = json.loads(json_blocks[0]) return json.dumps(rsp_json["REFLECTION"]) diff --git a/metagpt/ext/werewolf/roles/base_player.py b/metagpt/ext/werewolf/roles/base_player.py index 391f274c3..562ffb7b4 100644 --- a/metagpt/ext/werewolf/roles/base_player.py +++ b/metagpt/ext/werewolf/roles/base_player.py @@ -1,6 +1,6 @@ import re -from pydantic import Field, SerializeAsAny +from pydantic import Field, SerializeAsAny, model_validator from metagpt.actions.action import Action from metagpt.environment.werewolf.const import RoleState, RoleType @@ -16,6 +16,7 @@ from metagpt.ext.werewolf.actions import ( from metagpt.ext.werewolf.schema import RoleExperience, WwMessage from metagpt.logs import logger from metagpt.roles import Role +from metagpt.utils.common import any_to_str class BasePlayer(Role): @@ -44,6 +45,12 @@ class BasePlayer(Role): logger.warning("You must enable use_reflection before using experience") self.use_experience = False + @model_validator(mode="after") + def check_addresses(self): + if not self.addresses: + self.addresses = {any_to_str(self), self.name, self.profile} if self.name else {any_to_str(self)} + return self + async def _observe(self, ignore_memory=False) -> int: if self.status != RoleState.ALIVE: # 死者不再参与游戏 @@ -71,7 +78,7 @@ class BasePlayer(Role): async def _think(self): news = self.rc.news[0] - assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 + assert news.cause_by == any_to_str(InstructSpeak) # 消息为来自Moderator的指令时,才去做动作 if not news.restricted_to: # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) self.rc.todo = Speak() @@ -115,14 +122,13 @@ class BasePlayer(Role): reflection=reflection, experiences=experiences, ) - restricted_to = {} + restricted_to = set() elif isinstance(todo, NighttimeWhispers): rsp = await todo.run( profile=self.profile, name=self.name, context=memories, reflection=reflection, experiences=experiences ) restricted_to = {RoleType.MODERATOR.value, self.profile} # 给Moderator发送使用特殊技能的加密消息 - msg = WwMessage( content=rsp, role=self.profile, diff --git a/metagpt/ext/werewolf/roles/moderator.py b/metagpt/ext/werewolf/roles/moderator.py index 97d671271..7de0b74e6 100644 --- a/metagpt/ext/werewolf/roles/moderator.py +++ b/metagpt/ext/werewolf/roles/moderator.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Union from metagpt.actions.add_requirement import UserRequirement -from metagpt.const import DEFAULT_WORKSPACE_ROOT +from metagpt.const import DEFAULT_WORKSPACE_ROOT, MESSAGE_ROUTE_TO_ALL from metagpt.environment.werewolf.const import ( STEP_INSTRUCTIONS, RoleActionRes, @@ -20,6 +20,7 @@ from metagpt.ext.werewolf.actions.moderator_actions import ( from metagpt.ext.werewolf.roles.base_player import BasePlayer from metagpt.ext.werewolf.schema import WwMessage from metagpt.logs import logger +from metagpt.utils.common import any_to_str class Moderator(BasePlayer): @@ -63,8 +64,6 @@ class Moderator(BasePlayer): role.record_experiences(round_id=timestamp, outcome=outcome, game_setup=self.game_setup) async def _parse_speak(self, memories): - logger.info(f"step_idx: {self.step_idx}") - latest_msg = memories[-1] latest_msg_content = latest_msg.content @@ -76,25 +75,25 @@ class Moderator(BasePlayer): restricted_to = set() msg_cause_by = latest_msg.cause_by - if msg_cause_by == Hunt: + if msg_cause_by == any_to_str(Hunt): self.rc.env.step( EnvAction( - action_type=EnvActionType.WOLF_KILL, player_name=latest_msg.send_from, target_player_name=target + action_type=EnvActionType.WOLF_KILL, player_name=latest_msg.sent_from, target_player_name=target ) ) - elif msg_cause_by == Protect: + elif msg_cause_by == any_to_str(Protect): self.rc.env.step( EnvAction( - action_type=EnvActionType.GUARD_PROTECT, player_name=latest_msg.send_from, target_player_name=target + action_type=EnvActionType.GUARD_PROTECT, player_name=latest_msg.sent_from, target_player_name=target ) ) - elif msg_cause_by == Verify: + elif msg_cause_by == any_to_str(Verify): if target in self.werewolf_players: msg_content = f"{target} is a werewolf" else: msg_content = f"{target} is a good guy" restricted_to = {RoleType.MODERATOR.value, RoleType.SEER.value} - elif msg_cause_by == Save: + elif msg_cause_by == any_to_str(Save): if RoleActionRes.PASS.value in latest_msg_content.lower(): # the role ignore to response, answer `pass` pass @@ -105,11 +104,11 @@ class Moderator(BasePlayer): self.rc.env.step( EnvAction( action_type=EnvActionType.WITCH_SAVE, - player_name=latest_msg.send_from, + player_name=latest_msg.sent_from, target_player_name=target, ) ) - elif msg_cause_by == Poison: + elif msg_cause_by == any_to_str(Poison): if RoleActionRes.PASS.value in latest_msg_content.lower(): pass elif not self.witch_poison_left: @@ -119,7 +118,7 @@ class Moderator(BasePlayer): self.rc.env.step( EnvAction( action_type=EnvActionType.WITCH_POISON, - player_name=latest_msg.send_from, + player_name=latest_msg.sent_from, target_player_name=target, ) ) @@ -132,12 +131,32 @@ class Moderator(BasePlayer): self.update_player_status(player_current_dead) def _record_game_history(self, step_idx: int): - if step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner: + if step_idx and step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner: logger.info("a night and day cycle completed, examine all history") logger.debug(f"all_memories: {self.get_all_memories()}") with open(DEFAULT_WORKSPACE_ROOT / "werewolf_transcript.txt", "w") as f: f.write(self.get_all_memories()) + async def _observe(self, ignore_memory=False) -> int: + news = [] + if not news: + news = self.rc.msg_buffer.pop_all() + old_messages = [] if ignore_memory else self.rc.memory.get() + for m in news: + if len(m.restricted_to) and self.profile not in m.restricted_to and self.name not in m.restricted_to: + # if the msg is not send to the whole audience ("") nor this role (self.profile or self.name), + # then this role should not be able to receive it and record it into its memory + continue + self.rc.memory.add(m) + # add `MESSAGE_ROUTE_TO_ALL in n.send_to` make it to run `ParseSpeak` + self.rc.news = [ + n + for n in news + if (n.cause_by in self.rc.watch or self.profile in n.send_to or MESSAGE_ROUTE_TO_ALL in n.send_to) + and n not in old_messages + ] + return len(self.rc.news) + async def _think(self): if self.winner: self.rc.todo = AnnounceGameResult() @@ -155,6 +174,7 @@ class Moderator(BasePlayer): def _init_fields_from_obj(self, obs: dict[str, Union[int, str, list[str]]]): self.game_setup = obs.get("game_setup", "") + self.step_idx = obs.get("step_idx", 0) self.winner = obs.get("winner") self.win_reason = obs.get("win_reason") self.werewolf_players = obs.get("werewolf_players", []) @@ -168,7 +188,6 @@ class Moderator(BasePlayer): memories = self.get_all_memories(mode="msg") obs, _, _, _, _ = self.rc.env.step(action=EnvAction(action_type=EnvActionType.NONE)) - step_idx = obs["step_idx"] living_players = obs["living_players"] werewolf_players = obs["werewolf_players"] player_hunted = obs["player_hunted"] @@ -176,17 +195,17 @@ class Moderator(BasePlayer): self._init_fields_from_obj(obs) # 若进行完一夜一日的循环,打印和记录一次完整发言历史 - self._record_game_history(step_idx) + self._record_game_history(self.step_idx) # 若一晚或一日周期结束,对当晚或当日的死者进行总结,并更新玩家状态 - self._update_player_status(step_idx, player_current_dead) + self._update_player_status(self.step_idx, player_current_dead) if self.winner: self._record_all_experiences() # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): msg_content, msg_to_send_to, msg_restricted_to = await InstructSpeak().run( - step_idx, + self.step_idx, living_players=living_players, werewolf_players=werewolf_players, player_hunted=player_hunted, @@ -201,6 +220,7 @@ class Moderator(BasePlayer): send_to=msg_to_send_to, restricted_to=msg_restricted_to, ) + logger.info(f"current step_idx: {self.step_idx}") self.rc.env.step(EnvAction(action_type=EnvActionType.PROGRESS_STEP)) # to update step_idx elif isinstance(todo, ParseSpeak): diff --git a/metagpt/ext/werewolf/roles/witch.py b/metagpt/ext/werewolf/roles/witch.py index a92b4849d..2b73ea629 100644 --- a/metagpt/ext/werewolf/roles/witch.py +++ b/metagpt/ext/werewolf/roles/witch.py @@ -1,6 +1,7 @@ from metagpt.environment.werewolf.const import RoleType from metagpt.ext.werewolf.actions import InstructSpeak, Poison, Save, Speak from metagpt.ext.werewolf.roles.base_player import BasePlayer +from metagpt.utils.common import any_to_str class Witch(BasePlayer): @@ -11,7 +12,7 @@ class Witch(BasePlayer): async def _think(self): """女巫涉及两个特殊技能,因此在此需要改写_think进行路由""" news = self.rc.news[0] - assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 + assert news.cause_by == any_to_str(InstructSpeak) # 消息为来自Moderator的指令时,才去做动作 if not news.restricted_to: # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) self.rc.todo = Speak() diff --git a/metagpt/ext/werewolf/schema.py b/metagpt/ext/werewolf/schema.py index 09e6c8184..ad55da516 100644 --- a/metagpt/ext/werewolf/schema.py +++ b/metagpt/ext/werewolf/schema.py @@ -1,6 +1,9 @@ -from pydantic import BaseModel, Field +from typing import Any + +from pydantic import BaseModel, Field, field_validator from metagpt.schema import Message +from metagpt.utils.common import any_to_str_set class RoleExperience(BaseModel): @@ -18,4 +21,9 @@ class RoleExperience(BaseModel): class WwMessage(Message): # Werewolf Message - restricted_to: set[str] = Field(default={}, validate_default=True) + restricted_to: set[str] = Field(default=set(), validate_default=True) + + @field_validator("restricted_to", mode="before") + @classmethod + def check_restricted_to(cls, restricted_to: Any): + return any_to_str_set(restricted_to if restricted_to else set())