From 2e6cc6540b1f4c86e70cd8abdc3ea6125653f70a Mon Sep 17 00:00:00 2001 From: yzlin Date: Sat, 23 Sep 2023 01:43:48 +0800 Subject: [PATCH 01/47] first pipeline framework --- examples/debate.py | 2 - examples/werewolf_game/actions/__init__.py | 8 ++ .../werewolf_game/actions/common_actions.py | 27 ++++++ .../actions/moderator_actions.py | 49 ++++++++++ .../werewolf_game/actions/werewolf_actions.py | 24 +++++ examples/werewolf_game/roles/__init__.py | 4 + examples/werewolf_game/roles/base_player.py | 78 ++++++++++++++++ examples/werewolf_game/roles/moderator.py | 92 +++++++++++++++++++ examples/werewolf_game/roles/villager.py | 38 ++++++++ examples/werewolf_game/roles/werewolf.py | 44 +++++++++ examples/werewolf_game/start_game.py | 41 +++++++++ examples/werewolf_game/werewolf_game.py | 27 ++++++ metagpt/environment.py | 6 +- metagpt/roles/role.py | 9 ++ tests/metagpt/test_environment.py | 2 +- 15 files changed, 445 insertions(+), 6 deletions(-) create mode 100644 examples/werewolf_game/actions/__init__.py create mode 100644 examples/werewolf_game/actions/common_actions.py create mode 100644 examples/werewolf_game/actions/moderator_actions.py create mode 100644 examples/werewolf_game/actions/werewolf_actions.py create mode 100644 examples/werewolf_game/roles/__init__.py create mode 100644 examples/werewolf_game/roles/base_player.py create mode 100644 examples/werewolf_game/roles/moderator.py create mode 100644 examples/werewolf_game/roles/villager.py create mode 100644 examples/werewolf_game/roles/werewolf.py create mode 100644 examples/werewolf_game/start_game.py create mode 100644 examples/werewolf_game/werewolf_game.py diff --git a/examples/debate.py b/examples/debate.py index 05db28070..f157e86bd 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -49,7 +49,6 @@ class Trump(Role): super().__init__(name, profile, **kwargs) self._init_actions([ShoutOut]) self._watch([ShoutOut]) - self.name = "Trump" self.opponent_name = "Biden" async def _observe(self) -> int: @@ -89,7 +88,6 @@ class Biden(Role): super().__init__(name, profile, **kwargs) self._init_actions([ShoutOut]) self._watch([BossRequirement, ShoutOut]) - self.name = "Biden" self.opponent_name = "Trump" async def _observe(self) -> int: diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py new file mode 100644 index 000000000..7c5af46df --- /dev/null +++ b/examples/werewolf_game/actions/__init__.py @@ -0,0 +1,8 @@ +from examples.werewolf_game.actions.moderator_actions import InstructSpeak +from examples.werewolf_game.actions.common_actions import Speak +from examples.werewolf_game.actions.werewolf_actions import Hunt + +ACTIONS = { + "Speak": Speak, + "Hunt": Hunt, +} \ No newline at end of file diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py new file mode 100644 index 000000000..a0cb1f0fc --- /dev/null +++ b/examples/werewolf_game/actions/common_actions.py @@ -0,0 +1,27 @@ +from metagpt.actions import Action + +class Speak(Action): + """Action: Any speak action in a game""" + + PROMPT_TEMPLATE = """ + ## BACKGROUND + It's a Werewolf game, you are {profile}, say whatever possible to increase your chance of win, + ## HISTORY + You have knowledge to the following conversation: + {context} + ## YOUR TURN + It's daytime and it is your turn to speak, you will say (in 100 words): + """ + + def __init__(self, name="Speak", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str, profile: str): + + prompt = self.PROMPT_TEMPLATE.format(context=context, profile=profile) + + rsp = await self._aask(prompt) + + return rsp + + diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py new file mode 100644 index 000000000..7850ab478 --- /dev/null +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -0,0 +1,49 @@ +from metagpt.actions import Action + +STAGE_INSTRUCTIONS = { + # 上帝需要介入的全部步骤和对应指令 + # The 1-st night + 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 continuen speaking + "restricted_to": ""}, + 1: {"content": "Werewolves, please open your eyes!", + "send_to": "Moderator", # for moderator to continuen speaking + "restricted_to": ""}, + 2: {"content": """Werewolves, I secretly tell you that Player 3 and Player 4 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: + [Player 1, Player2]. """, # send to werewolf restrictedly for a response + "send_to": "Werewolf", + "restricted_to": "Werewolf"}, + 3: {"content": "Werewolves, close your eyes", + "send_to": "Moderator", # for moderator to continuen speaking + "restricted_to": ""}, + 4: {"content": """It's daytime. No one dies last night. Now freely talk about roles of other players with each other based on your observation and reflection + with few sentences. Decide whether to reveal your identity based on your reflection.""", + "send_to": "", # send to all to speak in daytime + "restricted_to": ""} +} + +class InstructSpeak(Action): + def __init__(self, name="InstructSpeak", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context, stage_idx): + return STAGE_INSTRUCTIONS[stage_idx] + +class ParseSpeak(Action): + async def run(self): + return "" + +class SummarizeNight(Action): + """consider all events at night, conclude which player dies (can be a peaceful night)""" + pass + +class SummarizeDay(Action): + """consider all votes at day, conclude which player dies""" + pass + +class AnnounceGameResult(Action): + + async def run(self, winner: str): + return f"Game over! The winner is {winner}" diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py new file mode 100644 index 000000000..ab54bc98c --- /dev/null +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -0,0 +1,24 @@ +from metagpt.actions import Action + +class Hunt(Action): + """Action: choose a villager to kill""" + + PROMPT_TEMPLATE = """ + It's a werewolf game and you are a werewolf, + this is game history: + {context}. + Attention: if your previous werewolf have chosen, follow its choice. + Now, choose one to kill, you will: + """ + + def __init__(self, name="Speak", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str): + + prompt = self.PROMPT_TEMPLATE.format(context=context) + + rsp = await self._aask(prompt) + # rsp = "Kill Player 1" + + return rsp diff --git a/examples/werewolf_game/roles/__init__.py b/examples/werewolf_game/roles/__init__.py new file mode 100644 index 000000000..464563344 --- /dev/null +++ b/examples/werewolf_game/roles/__init__.py @@ -0,0 +1,4 @@ +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.roles.moderator import Moderator +from examples.werewolf_game.roles.villager import Villager +from examples.werewolf_game.roles.werewolf import Werewolf diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py new file mode 100644 index 000000000..05daa9797 --- /dev/null +++ b/examples/werewolf_game/roles/base_player.py @@ -0,0 +1,78 @@ +from metagpt.roles import Role +from metagpt.schema import Message +from metagpt.logs import logger +from examples.werewolf_game.actions import ACTIONS, Speak, InstructSpeak + +ROLE_STATES = { + # 存活状态 + 0: "Alive", # 开场 + 1: "Dead", # 结束 + 2: "Protected", # 被保护 + 3: "Poisoned", # 被毒 + 4: "Saved", # 被救 +} + +class BasePlayer(Role): + def __init__( + self, + name: str = "PlayerXYZ", + profile: str = "BasePlayer", + team: str = "good guys", + special_action_names: list[str] = [], + **kwargs, + ): + super().__init__(name, profile, **kwargs) + self._init_actions([Speak]) + self._watch([InstructSpeak]) + self.team = team + # 调用 get_status() 来检查存活状态,并通过 set_status() 更新状态。 + self.status = 0 # 初始状态为活着 + + # 技能和监听配置 + self._watch([InstructSpeak]) # 监听Moderator的指令以做行动 + special_actions = [ACTIONS[action_name] for action_name in special_action_names] + capable_actions = [Speak] + special_actions + self._init_actions(capable_actions) # 给角色赋予行动技能 + self.special_actions = special_actions + + async def _observe(self) -> int: + await super()._observe() + # 只有发给全体的("")或发给自己的(self.profile)消息需要走下面的_react流程, + # 其他的收听到即可,不用做动作 + self._rc.news = [msg for msg in self._rc.news if msg.send_to in ["", self.profile]] + return len(self._rc.news) + + async def _think(self): + news = self._rc.news[0] + assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 + if not news.restricted_to: + # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) + self._rc.todo = Speak() + elif self.profile in news.restricted_to.split(","): # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" + # Moderator加密发给自己的,意味着要执行角色的特殊动作 + self._rc.todo = self.special_actions[0]() + + async def _act(self): + """每个角色要改写此函数以实现该角色的动作""" + raise NotImplementedError + + def get_all_memories(self) -> str: + memories = self._rc.memory.get() + memories = [f"{m.sent_from}: {m.content}" for m in memories] + memories = "\n".join(memories) + return memories + + def get_name(self): + return self.name + + def get_profile(self): + return self.profile + + def get_team(self): + return self.team + + def get_status(self): + return self.status + + def set_status(self, new_status): + self.status = new_status diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py new file mode 100644 index 000000000..e5a61e6c1 --- /dev/null +++ b/examples/werewolf_game/roles/moderator.py @@ -0,0 +1,92 @@ +import asyncio + +from metagpt.roles import Role +from metagpt.schema import Message +from metagpt.logs import logger +from examples.werewolf_game.actions.moderator_actions import ( + InstructSpeak, ParseSpeak, AnnounceGameResult, STAGE_INSTRUCTIONS +) +from metagpt.actions import BossRequirement as UserRequirement + +class Moderator(Role): + + # 游戏状态属性 + is_game_over = False + winner = None + + def __init__( + self, + name: str = "Moderator", + profile: str = "Moderator", + **kwargs, + ): + super().__init__(name, profile, **kwargs) + self._watch([UserRequirement, InstructSpeak, ParseSpeak]) + self._init_actions([InstructSpeak, ParseSpeak, AnnounceGameResult]) + self.stage_idx = 0 + + async def _instruct_speak(self): + stage_idx = self.stage_idx % len(STAGE_INSTRUCTIONS) + + stage_info = await InstructSpeak().run(context="", stage_idx=stage_idx) + + self.stage_idx += 1 + + return stage_info["content"], stage_info["send_to"], stage_info["restricted_to"] + + async def _parse_speak(self): + # 解析玩家消息并返回结果 + parse_result = await ParseSpeak().run() + + # 理解结果,更新各角色状态、游戏状态 + + return "Player message processed" + + async def _think(self): + + if self.is_game_over: + self._rc.todo = AnnounceGameResult() + return + + # 确定当前是需要InstructSpeak还是ParseSpeak. 通过判断当前流程状态变量,以及消息的cause_by属性 + # 0: InstructSpeak, 1: ParseSpeak,且需要判断消息的cause_by属性 + if self._rc.memory.get()[-1].role in ["User", self.profile]: + # 1. 上一轮消息是用户指令,解析用户指令,开始游戏 + # 2. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说 + # 3. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令 + self._rc.todo = InstructSpeak() + + else: + # 上一轮消息是游戏角色的发言,解析角色的发言 + self._rc.todo = ParseSpeak() + + async def _act(self): + todo = self._rc.todo + logger.info(f"{self._setting} ready to {todo}") + + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 + if isinstance(todo, InstructSpeak): + msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak() + msg = Message(content=msg_content, role=self.profile, sent_from=self.name, + cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) + + elif isinstance(todo, ParseSpeak): + msg_content = await self._parse_speak() + msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak) + + elif isinstance(todo, AnnounceGameResult): + msg_content = await AnnounceGameResult().run(winner=self.winner) + msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=AnnounceGameResult) + + logger.info(f"{self._setting}: {msg_content}") + + return msg + + def get_all_memories(self) -> str: + memories = self._rc.memory.get() + memories = [str(m) for m in memories] + memories = "\n".join(memories) + return memories diff --git a/examples/werewolf_game/roles/villager.py b/examples/werewolf_game/roles/villager.py new file mode 100644 index 000000000..0e9047938 --- /dev/null +++ b/examples/werewolf_game/roles/villager.py @@ -0,0 +1,38 @@ +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.actions import Speak +from metagpt.schema import Message +from metagpt.logs import logger + +class Villager(BasePlayer): + def __init__( + self, + name: str = "", + profile: str = "Villager", + team: str = "good guys", + special_action_names: list[str] = [], + **kwargs, + ): + super().__init__(name, profile, team, special_action_names, **kwargs) + + async def _act(self): + + # todo为_think时确定的,在村民这里,就只有一种todo,即Speak + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {todo}") + + # 可以用这个函数获取该角色的全部记忆 + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 根据自己定义的角色Action,对应地去run + rsp = await todo.run(profile=self.profile, context=memories) + + # 返回消息,注意给Moderator发送的加密消息需要用restricted_to="Moderator" + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Speak, send_to="", restricted_to="", + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py new file mode 100644 index 000000000..5a3bc55ff --- /dev/null +++ b/examples/werewolf_game/roles/werewolf.py @@ -0,0 +1,44 @@ +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.actions import Speak, Hunt +from metagpt.schema import Message +from metagpt.logs import logger + +class Werewolf(BasePlayer): + def __init__( + self, + name: str = "", + profile: str = "Werewolf", + team: str = "werewolves", + special_action_names: list[str] = ["Hunt"], + **kwargs, + ): + super().__init__(name, profile, team, special_action_names, **kwargs) + + async def _act(self): + # todo为_think时确定的,有两种情况,Speak或Hunt + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {str(todo)}") + + # 可以用这个函数获取该角色的全部记忆 + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 根据自己定义的角色Action,对应地去run,run的入参可能不同 + if isinstance(todo, Speak): + rsp = await todo.run(profile=self.profile, context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Speak, send_to="", restricted_to="", + ) + + elif isinstance(todo, Hunt): + rsp = await todo.run(context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Hunt, send_to="", + restricted_to=f"Moderator,{self.profile}", # 给Moderator及狼阵营发送要杀的人的加密消息 + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py new file mode 100644 index 000000000..98d76aa9d --- /dev/null +++ b/examples/werewolf_game/start_game.py @@ -0,0 +1,41 @@ +import asyncio +import platform +import fire + +from examples.werewolf_game.werewolf_game import WerewolfGame +from examples.werewolf_game.roles import Moderator, Villager, Werewolf + +DEFAULT_PLAYER_SETUP = """ +Game setup: +Player1: Villager, +Player2: Villager, +Player3: Werewolf, +Player4: Werewolf. +""" + +async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): + game = WerewolfGame() + game.hire([ + Moderator(), + Villager(name="Player1"), + Villager(name="Player2"), + Werewolf(name="Player3"), + Werewolf(name="Player4"), + ]) + game.invest(investment) + game.start_project(idea) + await game.run(n_round=n_round) + + +def main(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 10): + """ + :param idea: game config instructions + :param investment: contribute a certain dollar amount to watch the debate + :param n_round: maximum rounds of the debate + :return: + """ + asyncio.run(start_game(idea, investment, n_round)) + + +if __name__ == '__main__': + fire.Fire(main) diff --git a/examples/werewolf_game/werewolf_game.py b/examples/werewolf_game/werewolf_game.py new file mode 100644 index 000000000..7e743772c --- /dev/null +++ b/examples/werewolf_game/werewolf_game.py @@ -0,0 +1,27 @@ +from metagpt.software_company import SoftwareCompany +from metagpt.environment import Environment +from metagpt.actions import BossRequirement as UserRequirement +from metagpt.schema import Message + +class WerewolfEnvironment(Environment): + + async def run(self, k=1): + """处理一次所有信息的运行,各角色顺序执行 + Process all Role runs at once + """ + for _ in range(k): + for role in self.roles.values(): + await role.run() + +class WerewolfGame(SoftwareCompany): + """Use the "software company paradigm" to hold a werewolf game""" + + environment = WerewolfEnvironment() + + def start_project(self, idea): + """Start a project from user instruction.""" + self.idea = idea + self.environment.publish_message( + Message(role="User", content=idea, cause_by=UserRequirement, restricted_to="Moderator") + ) + print("a") diff --git a/metagpt/environment.py b/metagpt/environment.py index 24e6ada2f..a4305aa8f 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -33,7 +33,7 @@ class Environment(BaseModel): Add a role in the current environment """ role.set_env(self) - self.roles[role.profile] = role + self.roles[str(role._setting)] = role def add_roles(self, roles: Iterable[Role]): """增加一批在当前环境的角色 @@ -72,8 +72,8 @@ class Environment(BaseModel): """ return self.roles - def get_role(self, name: str) -> Role: + def get_role(self, role_setting: str) -> Role: """获得环境内的指定角色 get all the environment roles """ - return self.roles.get(name, None) + return self.roles.get(role_setting, None) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 44bb3e976..159f3c2e7 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -137,6 +137,11 @@ class Role: """Get the role description (position)""" return self._setting.profile + @property + def name(self): + """Get the role name""" + return self._setting.name + def _get_prefix(self): """Get the role prefix""" if self._setting.desc: @@ -188,6 +193,10 @@ class Role: self._rc.news = self._rc.memory.find_news(observed) # find news (previously unseen messages) from observed messages for i in env_msgs: + if i.restricted_to != "" and self.profile not in i.restricted_to and self.name not in i.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.recv(i) news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news] diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index a0f1f6257..8017d085b 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -24,7 +24,7 @@ def env(): def test_add_role(env: Environment): role = ProductManager("Alice", "product manager", "create a new product", "limited resources") env.add_role(role) - assert env.get_role(role.profile) == role + assert env.get_role(str(role._setting)) == role def test_get_roles(env: Environment): From 0221028de2fd7060ec4274a30ec89c5621e7119f Mon Sep 17 00:00:00 2001 From: kevin-meng Date: Mon, 25 Sep 2023 00:13:27 +0800 Subject: [PATCH 02/47] add guard --- examples/werewolf_game/actions/__init__.py | 2 + .../werewolf_game/actions/guard_actions.py | 24 ++++++++++ examples/werewolf_game/roles/__init__.py | 2 + examples/werewolf_game/roles/guard.py | 44 +++++++++++++++++++ examples/werewolf_game/start_game.py | 5 ++- 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 examples/werewolf_game/actions/guard_actions.py create mode 100644 examples/werewolf_game/roles/guard.py diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 7c5af46df..3c69b3c99 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,8 +1,10 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak from examples.werewolf_game.actions.common_actions import Speak from examples.werewolf_game.actions.werewolf_actions import Hunt +from examples.werewolf_game.actions.guard_actions import Protect ACTIONS = { "Speak": Speak, "Hunt": Hunt, + "Protect": Protect, } \ No newline at end of file diff --git a/examples/werewolf_game/actions/guard_actions.py b/examples/werewolf_game/actions/guard_actions.py new file mode 100644 index 000000000..419bd0330 --- /dev/null +++ b/examples/werewolf_game/actions/guard_actions.py @@ -0,0 +1,24 @@ +from metagpt.actions import Action + +class Protect(Action): + """Action: choose a player to protect""" + + PROMPT_TEMPLATE = """ + It's a werewolf game and you are a guard, + this is game history: + {context}. + Attention: you can not protect the same player in a row. + Now, choose one to portect, you will: + """ + + def __init__(self, name="Speak", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str): + + prompt = self.PROMPT_TEMPLATE.format(context=context) + + rsp = await self._aask(prompt) + # rsp = "Protect Player 1" + + return rsp diff --git a/examples/werewolf_game/roles/__init__.py b/examples/werewolf_game/roles/__init__.py index 464563344..52cbec1f9 100644 --- a/examples/werewolf_game/roles/__init__.py +++ b/examples/werewolf_game/roles/__init__.py @@ -2,3 +2,5 @@ from examples.werewolf_game.roles.base_player import BasePlayer from examples.werewolf_game.roles.moderator import Moderator from examples.werewolf_game.roles.villager import Villager from examples.werewolf_game.roles.werewolf import Werewolf +from examples.werewolf_game.roles.guard import Guard + diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py new file mode 100644 index 000000000..5a3bc55ff --- /dev/null +++ b/examples/werewolf_game/roles/guard.py @@ -0,0 +1,44 @@ +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.actions import Speak, Hunt +from metagpt.schema import Message +from metagpt.logs import logger + +class Werewolf(BasePlayer): + def __init__( + self, + name: str = "", + profile: str = "Werewolf", + team: str = "werewolves", + special_action_names: list[str] = ["Hunt"], + **kwargs, + ): + super().__init__(name, profile, team, special_action_names, **kwargs) + + async def _act(self): + # todo为_think时确定的,有两种情况,Speak或Hunt + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {str(todo)}") + + # 可以用这个函数获取该角色的全部记忆 + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 根据自己定义的角色Action,对应地去run,run的入参可能不同 + if isinstance(todo, Speak): + rsp = await todo.run(profile=self.profile, context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Speak, send_to="", restricted_to="", + ) + + elif isinstance(todo, Hunt): + rsp = await todo.run(context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Hunt, send_to="", + restricted_to=f"Moderator,{self.profile}", # 给Moderator及狼阵营发送要杀的人的加密消息 + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 98d76aa9d..0b10a0a86 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -3,7 +3,7 @@ import platform import fire from examples.werewolf_game.werewolf_game import WerewolfGame -from examples.werewolf_game.roles import Moderator, Villager, Werewolf +from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard DEFAULT_PLAYER_SETUP = """ Game setup: @@ -11,6 +11,8 @@ Player1: Villager, Player2: Villager, Player3: Werewolf, Player4: Werewolf. +Player5: Guard. + """ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): @@ -21,6 +23,7 @@ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, Villager(name="Player2"), Werewolf(name="Player3"), Werewolf(name="Player4"), + Guard(name="Player5"), ]) game.invest(investment) game.start_project(idea) From 02cc5c274d3d573cb3f592cfd070404270a77e28 Mon Sep 17 00:00:00 2001 From: kevin-meng Date: Mon, 25 Sep 2023 00:26:33 +0800 Subject: [PATCH 03/47] add guard --- .../werewolf_game/actions/guard_actions.py | 5 +++-- examples/werewolf_game/roles/guard.py | 20 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/examples/werewolf_game/actions/guard_actions.py b/examples/werewolf_game/actions/guard_actions.py index 419bd0330..98dfc3b32 100644 --- a/examples/werewolf_game/actions/guard_actions.py +++ b/examples/werewolf_game/actions/guard_actions.py @@ -5,9 +5,10 @@ class Protect(Action): PROMPT_TEMPLATE = """ It's a werewolf game and you are a guard, + you can choose to protect a player, including yourself, then the protected player will not be killed by the Werewolves this night. this is game history: {context}. - Attention: you can not protect the same player in a row. + Attention: you can not protect the same player two nights in a row. Now, choose one to portect, you will: """ @@ -21,4 +22,4 @@ class Protect(Action): rsp = await self._aask(prompt) # rsp = "Protect Player 1" - return rsp + return rsp \ No newline at end of file diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index 5a3bc55ff..94b5b46dc 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -1,21 +1,21 @@ from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak, Hunt +from examples.werewolf_game.actions import Speak, Protect from metagpt.schema import Message from metagpt.logs import logger -class Werewolf(BasePlayer): +class Guard(BasePlayer): def __init__( self, name: str = "", - profile: str = "Werewolf", - team: str = "werewolves", - special_action_names: list[str] = ["Hunt"], + profile: str = "Guard", + team: str = "good guys", + special_action_names: list[str] = ["Protect"], **kwargs, ): super().__init__(name, profile, team, special_action_names, **kwargs) async def _act(self): - # todo为_think时确定的,有两种情况,Speak或Hunt + # todo为_think时确定的,有两种情况,Speak或Protect todo = self._rc.todo logger.info(f"{self._setting}: ready to {str(todo)}") @@ -31,14 +31,16 @@ class Werewolf(BasePlayer): cause_by=Speak, send_to="", restricted_to="", ) - elif isinstance(todo, Hunt): + elif isinstance(todo, Protect): rsp = await todo.run(context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, - cause_by=Hunt, send_to="", - restricted_to=f"Moderator,{self.profile}", # 给Moderator及狼阵营发送要杀的人的加密消息 + cause_by=Protect, send_to="", + restricted_to=f"Moderator,{self.profile}", # 给Moderator发送守卫要保护的人加密消息 ) logger.info(f"{self._setting}: {rsp}") return msg + + From 3c015349640d4a3456e283b142b06a86b3dbdbd8 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 25 Sep 2023 23:22:18 +0800 Subject: [PATCH 04/47] Modify moderator --- .../actions/moderator_actions.py | 109 +++++++++++++++--- examples/werewolf_game/roles/moderator.py | 11 +- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 7850ab478..9e51a9106 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -4,45 +4,128 @@ STAGE_INSTRUCTIONS = { # 上帝需要介入的全部步骤和对应指令 # The 1-st night 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 continuen speaking + "send_to": "Moderator", # for moderator to continuen speaking "restricted_to": ""}, - 1: {"content": "Werewolves, please open your eyes!", - "send_to": "Moderator", # for moderator to continuen speaking + 1: {"content": "Guard, please open your eyes!", + "send_to": "Moderator", # for moderator to continuen speaking "restricted_to": ""}, - 2: {"content": """Werewolves, I secretly tell you that Player 3 and Player 4 are + 2: {"content": """Guard, now tell me who you protect tonight? + You only choose one from the following living options please: {living_players}.""", + "send_to": "Guard", + "restricted_to": "Guard"}, + 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 xxx and xxx 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: - [Player 1, Player2]. """, # send to werewolf restrictedly for a response + {living_players}. """, # send to werewolf restrictedly for a response "send_to": "Werewolf", "restricted_to": "Werewolf"}, - 3: {"content": "Werewolves, close your eyes", - "send_to": "Moderator", # for moderator to continuen speaking + 6: {"content": "Werewolves, close your eyes", + "send_to": "Moderator", "restricted_to": ""}, - 4: {"content": """It's daytime. No one dies last night. Now freely talk about roles of other players with each other based on your observation and reflection - with few sentences. Decide whether to reveal your identity based on your reflection.""", - "send_to": "", # send to all to speak in daytime - "restricted_to": ""} + 7: {"content": "Seer, please open your eyes!", + "send_to": "Moderator", + "restricted_to": ""}, + 8: {"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": "Seer"}, + 9: {"content": "Seer, close your eyes", + "send_to": "Moderator", + "restricted_to": ""}, + 10: {"content": "Witch, please open your eyes!", + "send_to": "Moderator", + "restricted_to": ""}, + 11: {"content": """Witch, you have a bottle of poison, who are you going to kill tonight? + Choose one from the following living options: {living_players}.""", + "send_to": "Witch", + "restricted_to": "Witch"}, + 12: {"content": """Witch, you have a bottle of antidote and a bottle of poison. + Who are you going to save tonight or kill tonight? Choose one from the following living options: + {living_players}.""", + "send_to": "Witch", + "restricted_to": "Witch"}, + 13: {"content": "Witch, close your eyes", + "send_to": "Moderator", + "restricted_to": ""}, + # The 1-st daytime + 14: {"content": """It's daytime. Everyone woke up except those who had been killed.""", + "send_to": "Moderator", + "restricted_to": ""}, + 15: {"content": "xxxx was killed last night. Or, it was a peaceful night and no one died!", + "send_to": "Moderator", + "restricted_to": ""}, + 16: {"content": """Now freely talk about roles of other players with each other based on your observation and + reflection with few sentences. Decide whether to reveal your identity based on your reflection.""", + "send_to": "", # send to all to speak in daytime + "restricted_to": ""}, + 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}. Or you can pass. For example: I vote to kill xxxx""", + "send_to": "Moderator", + "restricted_to": ""}, + 18: {"content": """xxxx was eliminated.""", + "send_to": "Moderator", + "restricted_to": ""}, } +INSTRUCT_SPEAK_TEMPLATE = """ +## BACKGROUND +It's a Werewolf game, you are moderator. + +## STAGE +The current stage of the game is: +{} + +## CONTEXT +Here's the current context: +{} + +What would you like to instruct? +""" class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, context, stage_idx): - return STAGE_INSTRUCTIONS[stage_idx] + async def run(self, context,living_players, stage_idx): + + instruction_info = STAGE_INSTRUCTIONS.get(stage_idx, "Unknown instruction.") + # 利用上下文信息context和所处阶段的信息 + if "{living_players}" in instruction_info["content"]: + content = instruction_info["content"].format(living_players) + else: + content = instruction_info["content"] + + prompt = INSTRUCT_SPEAK_TEMPLATE.format(content, context) + + rsp = await self._aask(prompt) + + send_to = instruction_info["send_to"] + restricted_to = instruction_info["restricted_to"] + + return rsp, send_to, restricted_to + class ParseSpeak(Action): async def run(self): return "" + class SummarizeNight(Action): """consider all events at night, conclude which player dies (can be a peaceful night)""" pass + class SummarizeDay(Action): """consider all votes at day, conclude which player dies""" pass + class AnnounceGameResult(Action): async def run(self, winner: str): diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index e5a61e6c1..9b59300ef 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -24,15 +24,12 @@ class Moderator(Role): self._watch([UserRequirement, InstructSpeak, ParseSpeak]) self._init_actions([InstructSpeak, ParseSpeak, AnnounceGameResult]) self.stage_idx = 0 + self.living_players = "[Player 1, Player 2, Player 3, Player 4]" - async def _instruct_speak(self): + async def _instruct_speak(self,context): stage_idx = self.stage_idx % len(STAGE_INSTRUCTIONS) - - stage_info = await InstructSpeak().run(context="", stage_idx=stage_idx) - self.stage_idx += 1 - - return stage_info["content"], stage_info["send_to"], stage_info["restricted_to"] + return await InstructSpeak().run(context=context, stage_idx=stage_idx, living_players=self.living_players) async def _parse_speak(self): # 解析玩家消息并返回结果 @@ -69,7 +66,7 @@ class Moderator(Role): # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): - msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak() + msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak(memories) msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) From 1a7f415fa933c2e6f944d9538e06a0e64bf9faaa Mon Sep 17 00:00:00 2001 From: kevin-meng Date: Mon, 25 Sep 2023 23:27:12 +0800 Subject: [PATCH 05/47] modify pronunciation --- examples/werewolf_game/actions/guard_actions.py | 4 ++-- examples/werewolf_game/start_game.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/werewolf_game/actions/guard_actions.py b/examples/werewolf_game/actions/guard_actions.py index 98dfc3b32..10e550a49 100644 --- a/examples/werewolf_game/actions/guard_actions.py +++ b/examples/werewolf_game/actions/guard_actions.py @@ -9,10 +9,10 @@ class Protect(Action): this is game history: {context}. Attention: you can not protect the same player two nights in a row. - Now, choose one to portect, you will: + Now, choose one to protect, you will: """ - def __init__(self, name="Speak", context=None, llm=None): + def __init__(self, name="Protect", context=None, llm=None): super().__init__(name, context, llm) async def run(self, context: str): diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 0b10a0a86..eeb4dbf21 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -1,6 +1,8 @@ import asyncio import platform import fire +import sys +sys.path.append("../../") from examples.werewolf_game.werewolf_game import WerewolfGame from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard From be2580f5db1596160fe90beaa8482630220de384 Mon Sep 17 00:00:00 2001 From: kevin-meng Date: Tue, 26 Sep 2023 01:20:29 +0800 Subject: [PATCH 06/47] add arbiter prompt for evaluate --- metagpt/prompts/arbiter.py | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 metagpt/prompts/arbiter.py diff --git a/metagpt/prompts/arbiter.py b/metagpt/prompts/arbiter.py new file mode 100644 index 000000000..d8e0d0781 --- /dev/null +++ b/metagpt/prompts/arbiter.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/26 1:12 +@Author : kevin-meng +@File : arbiter.py +""" + + +ARBITER = """ +As an experienced Arbiter, you possess the necessary competence, sound judgment, and absolute objectivity. you promise that you will officiate in games with complete impartiality, respecting and adhering to the rules that govern them, in the true spirit of sportsmanship. + +Please always remember the general duties of the Arbiters in a competition: +a. Ensure fair play and adhere to the Anti-cheating regulations. +b. Supervise the progress of the competition. +c. Observe the game and enforce decisions made, imposing penalties on players where appropriate. +d. Ensure that the Laws of the game are observed. + +The rules governing this competition are as follows: +=== +{rules} +=== + +The scoring dimensions for judging in this game are as follows: +=== +{dimensions} +=== + +After the end of the competition, the Arbiter should submit a report, which includes: +a. A summary report for the game. +b. The final standings. +c. Each player and their final score for each assessment category, along with the reasons for the ratings. +d. Any other important information +for example: + +## Summary + ...... + +## Results and Standings (Top3) + Top 1: player 1 + Top 2: player 2 + Top 2: player 3 + +## Scoring and Assessment Dimensions + - player 1 : socre + - dimension 1 + score: xx + reason: xx + - dimension 2 + score: xx + reason: xx + ...... + - player 2 + ...... + +## Conclusion + ...... + +""" + + From c6a5b46ddd6d9a465acbdec6052928718bf97bd6 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 26 Sep 2023 09:45:28 +0800 Subject: [PATCH 07/47] add seer role and action, and update moderator role --- examples/werewolf_game/actions/__init__.py | 2 + .../actions/moderator_actions.py | 127 +++++++++++++----- .../werewolf_game/actions/seer_actions.py | 22 +++ examples/werewolf_game/roles/__init__.py | 1 + examples/werewolf_game/roles/moderator.py | 53 +++++--- examples/werewolf_game/roles/seer.py | 44 ++++++ examples/werewolf_game/start_game.py | 6 +- 7 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 examples/werewolf_game/actions/seer_actions.py create mode 100644 examples/werewolf_game/roles/seer.py diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 7c5af46df..5568f50f8 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,8 +1,10 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak from examples.werewolf_game.actions.common_actions import Speak from examples.werewolf_game.actions.werewolf_actions import Hunt +from examples.werewolf_game.actions.seer_actions import Verify ACTIONS = { "Speak": Speak, "Hunt": Hunt, + "Verify": Verify, } \ No newline at end of file diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 9e51a9106..3ca67c5c1 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -1,6 +1,9 @@ +import asyncio +from random import random + from metagpt.actions import Action -STAGE_INSTRUCTIONS = { +STEP_INSTRUCTIONS = { # 上帝需要介入的全部步骤和对应指令 # The 1-st night 0: {"content": "It’s dark, everyone close your eyes. I will talk with you/your team secretly at night.", @@ -19,7 +22,7 @@ STAGE_INSTRUCTIONS = { 4: {"content": "Werewolves, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 5: {"content": """Werewolves, I secretly tell you that xxx and xxx are + 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}. """, # send to werewolf restrictedly for a response @@ -57,7 +60,7 @@ STAGE_INSTRUCTIONS = { 14: {"content": """It's daytime. Everyone woke up except those who had been killed.""", "send_to": "Moderator", "restricted_to": ""}, - 15: {"content": "xxxx was killed last night. Or, it was a peaceful night and no one died!", + 15: {"content": "{killed_player} was killed last night. Or, it was a peaceful night and no one died!", "send_to": "Moderator", "restricted_to": ""}, 16: {"content": """Now freely talk about roles of other players with each other based on your observation and @@ -66,49 +69,39 @@ STAGE_INSTRUCTIONS = { "restricted_to": ""}, 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}. Or you can pass. For example: I vote to kill xxxx""", + {living_players}. Or you can pass. For example: I vote to kill ...""", "send_to": "Moderator", "restricted_to": ""}, - 18: {"content": """xxxx was eliminated.""", + 18: {"content": """{voted_out_player} was eliminated.""", "send_to": "Moderator", "restricted_to": ""}, } -INSTRUCT_SPEAK_TEMPLATE = """ -## BACKGROUND -It's a Werewolf game, you are moderator. -## STAGE -The current stage of the game is: -{} - -## CONTEXT -Here's the current context: -{} - -What would you like to instruct? -""" class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, context,living_players, stage_idx): + async def run(self, step_idx, living_players, werewolf_players, killed_player, voted_out_player): + instruction_info = STEP_INSTRUCTIONS.get(step_idx, { + "content": "Unknown instruction.", + "send_to": "", + "restricted_to": "" + }) + content = instruction_info["content"] + if "{living_players}" in content and "{werewolf_players}" in content: + content = content.format(living_players=",".join(living_players), + werewolf_players=",".join(werewolf_players)) + if "{living_players}" in content: + content = content.format(living_players=",".join(living_players)) + if "{werewolf_players}" in content: + content = content.format(werewolf_players=",".join(werewolf_players)) + if "{killed_player}" in content: + content = content.format(killed_player=killed_player) + if "{voted_out_player}" in content: + content = content.format(voted_out_player=voted_out_player) - instruction_info = STAGE_INSTRUCTIONS.get(stage_idx, "Unknown instruction.") - # 利用上下文信息context和所处阶段的信息 - if "{living_players}" in instruction_info["content"]: - content = instruction_info["content"].format(living_players) - else: - content = instruction_info["content"] - - prompt = INSTRUCT_SPEAK_TEMPLATE.format(content, context) - - rsp = await self._aask(prompt) - - send_to = instruction_info["send_to"] - restricted_to = instruction_info["restricted_to"] - - return rsp, send_to, restricted_to + return content, instruction_info["send_to"], instruction_info["restricted_to"] class ParseSpeak(Action): @@ -118,15 +111,77 @@ class ParseSpeak(Action): class SummarizeNight(Action): """consider all events at night, conclude which player dies (can be a peaceful night)""" - pass + + def __init__(self, name="SummarizeNight", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, events): + # 假设events是一个字典,代表夜晚发生的多个事件,key是事件类型,value是该事件对应的玩家 + # 例如被狼人杀的玩家:{"killed_by_werewolves": "Player1"} + killed_by_werewolves = events.get("killed_by_werewolves", "") + protected_by_guard = events.get("protected_by_guard", "") + saved_by_witch = events.get("saved_by_witch", "") + poisoned_by_witch = events.get("poisoned_by_witch", "") + + # 若狼人杀的人和守卫守的人是同一个人,那么该人就会活着; + if protected_by_guard and killed_by_werewolves and protected_by_guard == killed_by_werewolves: + return "It was a peaceful night. No one was killed." + + # 若守卫和女巫都救了同一个人,那么该人就会死 + if protected_by_guard and saved_by_witch and protected_by_guard == saved_by_witch: + return f"{protected_by_guard} was killed by the werewolves." + + if saved_by_witch: + return f"{saved_by_witch} was saved by the witch." + + if poisoned_by_witch: + return f"{poisoned_by_witch} was poisoned by the witch." + + if killed_by_werewolves: + return f"{killed_by_werewolves} was killed by the werewolves." class SummarizeDay(Action): """consider all votes at day, conclude which player dies""" - pass + + def __init__(self, name="SummarizeDay", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, votes): + # 假设votes是一个字典,代表白天投票的结果,key是被投票的玩家,value是得票数 + # 例如:{"Player1": 2, "Player2": 1, "Player3": 1, "Player4": 0} + # 表示Player1得到2票,Player2和Player3各得到1票,Player4得到0票 + # 若平票,则无人死亡 + if not votes: + return "No votes were cast. No one was killed." + + max_votes = max(votes.values()) + players_with_max_votes = [player for player, vote_count in votes.items() if vote_count == max_votes] + + if len(players_with_max_votes) == 1: + eliminated_player = players_with_max_votes[0] + return f"{eliminated_player} was voted out and eliminated." + else: + # 若平票,则随机选一个人出局 + eliminated_player = players_with_max_votes[int(random() * len(players_with_max_votes))] + return f"There was a tie in the votes. {eliminated_player} was randomly chosen and eliminated." class AnnounceGameResult(Action): async def run(self, winner: str): return f"Game over! The winner is {winner}" + + +async def main(): + rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) + rst2 = await SummarizeNight().run({"killed_by_werewolves": "Player1"}) + # 表示第2个步骤的指令,living_players是所有活着的玩家,werewolf_players是所有狼人,killed_player是被杀的玩家,voted_out_player是被投票出局的玩家 + rst3 = await InstructSpeak().run(2, ["Player1", "Player2", "Player3", "Player4"], + ["Player3", "Player4"], "Player4", "Player3") + print(rst1) + print(rst2) + print(rst3) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/werewolf_game/actions/seer_actions.py b/examples/werewolf_game/actions/seer_actions.py new file mode 100644 index 000000000..b4cb55b15 --- /dev/null +++ b/examples/werewolf_game/actions/seer_actions.py @@ -0,0 +1,22 @@ +from metagpt.actions import Action + + +class Verify(Action): + """Action: Seer verifies a player's identity at night""" + + PROMPT_TEMPLATE = """ + It's a werewolf game and you are a seer. + You can choose to verify the identity of a player. + Here's the game history: + {context}. + Now, choose one player to verify: + """ + + def __init__(self, name="Verify", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str): + prompt = self.PROMPT_TEMPLATE.format(context=context) + rsp = await self._aask(prompt) + + return rsp diff --git a/examples/werewolf_game/roles/__init__.py b/examples/werewolf_game/roles/__init__.py index 464563344..ac79f07d7 100644 --- a/examples/werewolf_game/roles/__init__.py +++ b/examples/werewolf_game/roles/__init__.py @@ -2,3 +2,4 @@ from examples.werewolf_game.roles.base_player import BasePlayer from examples.werewolf_game.roles.moderator import Moderator from examples.werewolf_game.roles.villager import Villager from examples.werewolf_game.roles.werewolf import Werewolf +from examples.werewolf_game.roles.seer import Seer diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 9b59300ef..7cb85c50b 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -4,37 +4,46 @@ from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger from examples.werewolf_game.actions.moderator_actions import ( - InstructSpeak, ParseSpeak, AnnounceGameResult, STAGE_INSTRUCTIONS + InstructSpeak, ParseSpeak, AnnounceGameResult, STEP_INSTRUCTIONS ) from metagpt.actions import BossRequirement as UserRequirement -class Moderator(Role): +class Moderator(Role): # 游戏状态属性 is_game_over = False winner = None def __init__( - self, - name: str = "Moderator", - profile: str = "Moderator", - **kwargs, + self, + name: str = "Moderator", + profile: str = "Moderator", + **kwargs, ): super().__init__(name, profile, **kwargs) self._watch([UserRequirement, InstructSpeak, ParseSpeak]) self._init_actions([InstructSpeak, ParseSpeak, AnnounceGameResult]) - self.stage_idx = 0 - self.living_players = "[Player 1, Player 2, Player 3, Player 4]" - - async def _instruct_speak(self,context): - stage_idx = self.stage_idx % len(STAGE_INSTRUCTIONS) - self.stage_idx += 1 - return await InstructSpeak().run(context=context, stage_idx=stage_idx, living_players=self.living_players) + self.step_idx = 0 + self.living_players = ["Player1", "Player2", "Player3", "Player4", "Player5"] + self.werewolf_players = ["Player1", "Player2"] + self.killed_player = "Player 4" # 夜晚阶段,死掉的玩家 + self.voted_out_player = "Player 3" # 白天阶段,被投票出局的玩家 + # 假设votes代表白天投票的结果,key是被投票的玩家,value是得票数 + self.votes = {"Player1": 1, "Player2": 2, "Player3": 1, "Player4": 0, "Player5": 0} + + async def _instruct_speak(self, context): + step_idx = self.step_idx % len(STEP_INSTRUCTIONS) + self.step_idx += 1 + return await InstructSpeak().run(step_idx, + living_players=self.living_players, + werewolf_players=self.werewolf_players, + killed_player=self.killed_player, + voted_out_player=self.voted_out_player) async def _parse_speak(self): # 解析玩家消息并返回结果 parse_result = await ParseSpeak().run() - + # 理解结果,更新各角色状态、游戏状态 return "Player message processed" @@ -44,7 +53,7 @@ class Moderator(Role): if self.is_game_over: self._rc.todo = AnnounceGameResult() return - + # 确定当前是需要InstructSpeak还是ParseSpeak. 通过判断当前流程状态变量,以及消息的cause_by属性 # 0: InstructSpeak, 1: ParseSpeak,且需要判断消息的cause_by属性 if self._rc.memory.get()[-1].role in ["User", self.profile]: @@ -52,7 +61,7 @@ class Moderator(Role): # 2. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说 # 3. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令 self._rc.todo = InstructSpeak() - + else: # 上一轮消息是游戏角色的发言,解析角色的发言 self._rc.todo = ParseSpeak() @@ -68,20 +77,20 @@ class Moderator(Role): if isinstance(todo, InstructSpeak): msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak(memories) msg = Message(content=msg_content, role=self.profile, sent_from=self.name, - cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) - + cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) + elif isinstance(todo, ParseSpeak): msg_content = await self._parse_speak() msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak) - + elif isinstance(todo, AnnounceGameResult): msg_content = await AnnounceGameResult().run(winner=self.winner) msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=AnnounceGameResult) - + logger.info(f"{self._setting}: {msg_content}") - + return msg - + def get_all_memories(self) -> str: memories = self._rc.memory.get() memories = [str(m) for m in memories] diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py new file mode 100644 index 000000000..b4ff41c8c --- /dev/null +++ b/examples/werewolf_game/roles/seer.py @@ -0,0 +1,44 @@ +from examples.werewolf_game.actions.seer_actions import Verify +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.actions import Speak +from metagpt.schema import Message +from metagpt.logs import logger + + +class Seer(BasePlayer): + def __init__( + self, + name: str = "", + profile: str = "Seer", + team: str = "good guys", + special_action_names: list[str] = ["Verify"], + **kwargs, + ): + super().__init__(name, profile, team, special_action_names, **kwargs) + + async def _act(self): + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {str(todo)}") + + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 基于todo的类型,调用不同的action + if isinstance(todo, Speak): + rsp = await todo.run(profile=self.profile, context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Speak, send_to="", restricted_to="", + ) + + elif isinstance(todo, Verify): + rsp = await todo.run(context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Verify, send_to="", + restricted_to="Moderator", + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 98d76aa9d..b272dc13b 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -3,14 +3,15 @@ import platform import fire from examples.werewolf_game.werewolf_game import WerewolfGame -from examples.werewolf_game.roles import Moderator, Villager, Werewolf +from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Seer DEFAULT_PLAYER_SETUP = """ Game setup: Player1: Villager, Player2: Villager, Player3: Werewolf, -Player4: Werewolf. +Player4: Werewolf, +Player5: Seer. """ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): @@ -21,6 +22,7 @@ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, Villager(name="Player2"), Werewolf(name="Player3"), Werewolf(name="Player4"), + Seer(name="Player5"), ]) game.invest(investment) game.start_project(idea) From 4584435bc64a8c83316e075b2d554be72d9ba55d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 26 Sep 2023 13:00:44 +0800 Subject: [PATCH 08/47] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BA=86=E8=AE=BA?= =?UTF-8?q?=E6=96=87=E4=B8=AD=E7=9A=84=E4=B8=80=E4=BA=9B=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/moderator_actions.py | 41 ++++----- examples/werewolf_game/prompts/prompts.py | 85 +++++++++++++++++++ examples/werewolf_game/roles/moderator.py | 4 +- 3 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 examples/werewolf_game/prompts/prompts.py diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 3ca67c5c1..78b6fd1af 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -13,7 +13,7 @@ STEP_INSTRUCTIONS = { "send_to": "Moderator", # for moderator to continuen speaking "restricted_to": ""}, 2: {"content": """Guard, now tell me who you protect tonight? - You only choose one from the following living options please: {living_players}.""", + You only choose one from the following living options please: {living_players}. Or you can pass. For example: I protect ...""", "send_to": "Guard", "restricted_to": "Guard"}, 3: {"content": "Guard, close your eyes", @@ -25,35 +25,35 @@ STEP_INSTRUCTIONS = { 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}. """, # send to werewolf restrictedly for a response + {living_players}. For example: I kill ...""", "send_to": "Werewolf", "restricted_to": "Werewolf"}, 6: {"content": "Werewolves, close your eyes", "send_to": "Moderator", "restricted_to": ""}, - 7: {"content": "Seer, please open your eyes!", + 7: {"content": "Witch, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 8: {"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": "Seer"}, - 9: {"content": "Seer, close your eyes", - "send_to": "Moderator", - "restricted_to": ""}, - 10: {"content": "Witch, please open your eyes!", - "send_to": "Moderator", - "restricted_to": ""}, - 11: {"content": """Witch, you have a bottle of poison, who are you going to kill tonight? + 8: {"content": """Witch, you have a bottle of poison, who are you going to kill tonight? Choose one from the following living options: {living_players}.""", - "send_to": "Witch", - "restricted_to": "Witch"}, - 12: {"content": """Witch, you have a bottle of antidote and a bottle of poison. + "send_to": "Witch", + "restricted_to": "Witch"}, + 9: {"content": """Witch, you have a bottle of antidote and a bottle of poison. Who are you going to save tonight or kill tonight? Choose one from the following living options: {living_players}.""", - "send_to": "Witch", - "restricted_to": "Witch"}, - 13: {"content": "Witch, close your eyes", + "send_to": "Witch", + "restricted_to": "Witch"}, + 10: {"content": "Witch, close your eyes", + "send_to": "Moderator", + "restricted_to": ""}, + 11: {"content": "Seer, please open your eyes!", + "send_to": "Moderator", + "restricted_to": ""}, + 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": "Seer"}, + 13: {"content": "Seer, close your eyes", "send_to": "Moderator", "restricted_to": ""}, # The 1-st daytime @@ -183,5 +183,6 @@ async def main(): print(rst2) print(rst3) + if __name__ == '__main__': asyncio.run(main()) diff --git a/examples/werewolf_game/prompts/prompts.py b/examples/werewolf_game/prompts/prompts.py new file mode 100644 index 000000000..5f618798d --- /dev/null +++ b/examples/werewolf_game/prompts/prompts.py @@ -0,0 +1,85 @@ +# 论文中出现的提示语,利用了思维链 +# 1. 基于游戏规则和对话内容,选择5个问题 +# 2. 生成2个问题 +# 3. 生成可能的回答 +# 4. 进行反思 +# 5. 生成最终回复 + +GAME_RULE = '''You are playing a game called the Werewolf with some other players. This game is based on text conversations. Here are +the game rules: Roles: The moderator is also host, he organised this game and you need to answer his instructions correctly. +Don’t talk with the moderator. There are five roles in the game, werewolf, villager, seer, guard and witch. There are two +alternate phases in this game, daytime and dark. When it’s dark: Your talking content with moderator is confidential. You +needn’t worry about other players and moderator knowing what you say and do. No need to worry about suspicions from +others during the night. If you are werewolf, you can know what your teammates want to kill and you should vote one player +to kill based on your analysis. Player who receives the most votes after all werewolves voting will be killed. No one will be +killed if there is no consensus! If you are witch, you have a bottle of antidote that can save a player targeted by werewolves +after dark, and a bottle of poison that can poison a player after dark. Both poison and antidote can be used only once. If you +are seer, you can verify whether a player is a werewolf every night, which is a very important thing. If you are guard, you +can protect a player every night to prevent the player from being killed by werewolves, but guard cannot resist the witch’s +poison and guard cannot protect the same player on two consecutive nights. Villagers can’t do anything at night. During the +daytime: you discuss with all players including your enemies. At the end of the discussion, players vote to eliminate one +player they suspect of being a werewolf. The player with the most votes will be eliminated. The moderator will tell who is +killed, otherwise there is no one killed. Note that villager, seer, guard and witch are all in villager side, they have the same +objective. Objectives: If you are werewolf, your goal is to cooperate with other werewolves to kill all players who are not +werewolves at last. If you are not werewolf, you need to kill all werewolves with your partner once you find out that certain +players are suspicious to be werewolves. This could greatly improve your chances of winning, although it is somewhat risky.If +one player is killed, he can’t do anything anymore and will be out of the game. Tips: To complete the objective: During +night, you should analyze and use your ability correctly. During daytime, you need to reason carefully about the roles of other +players and be careful not to reveal your own role casually unless you’re cheating other players. Only give the player’s name +when making a decision/voting, and don’t generate other players’ conversation.Reasoning based on facts you have observed +and you cannot perceive information (such as acoustic info) other than text. You are Player {name}, the {profile}. +You’re playing with 6 other players. Do not pretend you are other players or the moderator. +''' + +SELECT_QUESTIONS = ''' +Now its the {t}-th {day_or_night}. Given the game rules and conversations above, assuming you are {agent_name}, the +{role}, and to complete the instructions of the moderator, you need to think about a few questions clearly first, so that you can +make an accurate decision on the next step. Choose only five that you think are the most important in the current situation +from the list of questions below: {questions_prepared_for_specific_role} Please repeat the five important questions of your +choice, separating them with ‘##’. +''' + +# 为特定的角色,准备的问题 +questions_prepared_for_specific_role_sample = ''' +1. What is my player name and what is my role? What is my final objective in this game? +2. Based on the chat history, can you guess what some players’ role might be? +3. What is the current phase, daytime or night? what should I do at this phase according to the game rules? +4. Based on the conversation and my inference, who is most likely to be an alive werewolf? +5. I want to know who the most suspicious player, and why? +6. I also want to know if any player’s behavior has changed suspiciously compared to the previous days, and if so, who and why? +7. What is the best strategy I should use right now to uncover werewolves without revealing my own role? Should I accuse someone directly, ask probing questions, or stay silent for now? +8. Have any players claimed specific roles that can be verified or disputed? +''' + +ASK_QUESTIONS = ''' +Now its the {t}-th {day_or_night}. Given the game rules and conversations above, assuming you are {agent_name}, the +{role}, and to complete the instructions of the moderator, you need to think about a few questions clearly first, so that you can +make an accurate decision on the next step. {selected_questions} Do not answer these queations. In addition to the above +questions, please make a bold guess, what else do you want to know about the current situation? Please ask two important +questions in first person, separating them with ‘##’. +''' + +GENERATE_POSSIBLE_ANSWER = ''' +Now its the {t}-th {day_or_night}. Given the game rules and conversations above, assuming you are {agent_name}, the +{role}, for question: {question} There are some possible answers: {candidate_answers} Generate the correct answer +based on the context. If there is not direct answer, you should think and generate the answer based on the context. No need to +give options. The answer should in first person using no more than 2 sentences and without any analysis and item numbers. +''' + +REFLECTION = ''' +Now its the {t}-th {day_or_night}. Assuming you are {agent_name}, the {role}, what insights can you summarize +with few sentences based on the above conversations and {At} in heart for helping continue the talking and achieving your +objective? For example: As the {role}, I observed that... I think that... But I am... So... +''' + +# 得到最终的回复,再抽取出最终的content +GENERATE_FINAL_RESPONSE = ''' +Now its the {t}-th {day_or_night}. Think about what to say based on the game rules and context, especially the just now +reflection {R}. +Give your step-by-step thought process and your derived consise talking content (no more than 2 sentences) at last, separating them with ‘##’. +For example: +## Thought process +My step-by-step thought process:... +## Content +My concise talking content: ... +''' diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 7cb85c50b..2069c5f41 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -26,8 +26,8 @@ class Moderator(Role): self.step_idx = 0 self.living_players = ["Player1", "Player2", "Player3", "Player4", "Player5"] self.werewolf_players = ["Player1", "Player2"] - self.killed_player = "Player 4" # 夜晚阶段,死掉的玩家 - self.voted_out_player = "Player 3" # 白天阶段,被投票出局的玩家 + self.killed_player = "Player4" # 夜晚阶段,死掉的玩家 + self.voted_out_player = "Player3" # 白天阶段,被投票出局的玩家 # 假设votes代表白天投票的结果,key是被投票的玩家,value是得票数 self.votes = {"Player1": 1, "Player2": 2, "Player3": 1, "Player4": 0, "Player5": 0} From f9179c377d626ea974b8e93188faa900b6c2e490 Mon Sep 17 00:00:00 2001 From: Kevin Meng Date: Tue, 26 Sep 2023 20:15:27 +0800 Subject: [PATCH 09/47] Update start_game.py delete two lines of local testing code --- examples/werewolf_game/start_game.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index eeb4dbf21..0b10a0a86 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -1,8 +1,6 @@ import asyncio import platform import fire -import sys -sys.path.append("../../") from examples.werewolf_game.werewolf_game import WerewolfGame from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard From e5140fd6a581febc7461dff952a6bee71daf659d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 26 Sep 2023 23:05:45 +0800 Subject: [PATCH 10/47] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86ivan=E5=86=99?= =?UTF-8?q?=E7=9A=84parse=20speak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/moderator_actions.py | 79 ++++++++++++++++++- examples/werewolf_game/roles/base_player.py | 10 +-- examples/werewolf_game/roles/moderator.py | 33 +++++--- 3 files changed, 100 insertions(+), 22 deletions(-) diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 78b6fd1af..5fd3ede93 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -1,4 +1,5 @@ import asyncio +import collections from random import random from metagpt.actions import Action @@ -77,6 +78,42 @@ STEP_INSTRUCTIONS = { "restricted_to": ""}, } +ROLE_STATES = { + # 存活状态 + 0: "Alive", # 开场 + 1: "Dead", # 结束 + 2: "Protected", # 被保护 + 3: "Poisoned", # 被毒 + 4: "Saved", # 被救 + 5: "Killed" # 被刀 +} + +VOTE_PROMPT = """ + Welcome to the daytime discussion phase in the Werewolf game. + + During the day, players discuss and share information about who they suspect might be a werewolf. + Players can also cast their votes to eliminate a player they believe is a werewolf. + + Here are the conversations from the daytime: + + {vote_message} + + Now it's time to cast your votes. + + You can vote for a player by typing their name. + Example: "Vote for Player2" + + Here are the voting options: +""" + +PARSE_INSTRUCTIONS = { + 0: "Now it's time to vote", + 1: "The {winner} have won! They successfully eliminated all the {loser}}." + "The game has ended. Thank you for playing Werewolf!", + 2: "The night has ended, and it's time to reveal the casualties." + "During the night, the Werewolves made their move. Unfortunately, they targeted {PlayerName}, who is now dead." +} + class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): @@ -105,8 +142,46 @@ class InstructSpeak(Action): class ParseSpeak(Action): - async def run(self): - return "" + def __init__(self, name="ParseSpeak", context=None, llm=None): + super().__init__(name, context, llm) + self.daytime_info = collections.defaultdict(list) + self.night_info = collections.defaultdict(list) + self.vote_message = [] + + async def run(self, dead_history, context, env): + + for m in env.memory.get(): + role = m.sent_from if hasattr(m, 'sent_from') else "" + content = m.content if hasattr(m, 'content') else "" + target = m.sent_to if hasattr(m, 'sent_to') else "" + restricted = m.restricted_to if hasattr(m, 'restricted_to') else "" + if target == 'all': + self.daytime_info[role] = [content, target, restricted] + else: + self.night_info[role] = [content, target, restricted] + + # collect info from the night and identify the dead player + for role in self.night_info: + if "kill" in self.night_info[role][0] and self.night_info[role][1]: + target = self.night_info[role][1] + print("env.get_roles[target]", env, env.env.roles) + env.env.roles[target].set_status(ROLE_STATES[5]) + for role in self.night_info: + if ("save" or "guard") in self.night_info[role][0]: + save_target = self.night_info[role][1] + if save_target == target: + env.env.roles[target].set_status(ROLE_STATES[0]) + else: + dead_history.append(target) + + # collect message from the daytime and identify the vote player + for role in self.daytime_info: + self.vote_message += f"\n{self.daytime_info[role][0]}" + + vote_player = await self.llm.aask(VOTE_PROMPT.format(vote_message=self.vote_message)) + dead_history.append(vote_player) + + return dead_history, vote_player, PARSE_INSTRUCTIONS class SummarizeNight(Action): diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 05daa9797..c3e60372c 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -3,14 +3,6 @@ from metagpt.schema import Message from metagpt.logs import logger from examples.werewolf_game.actions import ACTIONS, Speak, InstructSpeak -ROLE_STATES = { - # 存活状态 - 0: "Alive", # 开场 - 1: "Dead", # 结束 - 2: "Protected", # 被保护 - 3: "Poisoned", # 被毒 - 4: "Saved", # 被救 -} class BasePlayer(Role): def __init__( @@ -24,7 +16,7 @@ class BasePlayer(Role): super().__init__(name, profile, **kwargs) self._init_actions([Speak]) self._watch([InstructSpeak]) - self.team = team + self.team = team # 调用 get_status() 来检查存活状态,并通过 set_status() 更新状态。 self.status = 0 # 初始状态为活着 diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 2069c5f41..4c1334b3c 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -26,8 +26,8 @@ class Moderator(Role): self.step_idx = 0 self.living_players = ["Player1", "Player2", "Player3", "Player4", "Player5"] self.werewolf_players = ["Player1", "Player2"] - self.killed_player = "Player4" # 夜晚阶段,死掉的玩家 - self.voted_out_player = "Player3" # 白天阶段,被投票出局的玩家 + self.good_guys = ["Player3", "Player4", "Player5"] + self.dead_players = [] # 夜晚阶段,死掉的玩家 # 假设votes代表白天投票的结果,key是被投票的玩家,value是得票数 self.votes = {"Player1": 1, "Player2": 2, "Player3": 1, "Player4": 0, "Player5": 0} @@ -37,16 +37,26 @@ class Moderator(Role): return await InstructSpeak().run(step_idx, living_players=self.living_players, werewolf_players=self.werewolf_players, - killed_player=self.killed_player, - voted_out_player=self.voted_out_player) + killed_player=self.dead_players, + voted_out_player="Player3") - async def _parse_speak(self): - # 解析玩家消息并返回结果 - parse_result = await ParseSpeak().run() + async def _parse_speak(self, memories, env): - # 理解结果,更新各角色状态、游戏状态 + self.dead_players, vote_player, parse_info = await ParseSpeak().run(dead_history=self.dead_players, + context=memories, env=env) - return "Player message processed" + # decide to move the game into the next phase + if not vote_player: + msg_content, send_to = parse_info[0], self.profile + # game's termination condition + elif all(item in self.dead_players for item in self.werewolf_players) or all( + item in self.dead_players for item in self.good_guys): + self.is_game_over = True + msg_content, send_to = parse_info[1], "all" + else: + # game's termination condition + msg_content, send_to = parse_info[2], "" + return msg_content, send_to async def _think(self): @@ -80,8 +90,9 @@ class Moderator(Role): cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) elif isinstance(todo, ParseSpeak): - msg_content = await self._parse_speak() - msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak) + msg_content, send_to = await self._parse_speak(memories, self._rc) + msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak, + send_to=send_to) elif isinstance(todo, AnnounceGameResult): msg_content = await AnnounceGameResult().run(winner=self.winner) From fd03751e1d1f0b04ed80f49689c3f4c8eebe9dc9 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 28 Sep 2023 23:47:35 +0800 Subject: [PATCH 11/47] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86STEP=5FINSTRUC?= =?UTF-8?q?TIONS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/moderator_actions.py | 24 +++++++++++-------- examples/werewolf_game/roles/moderator.py | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 5fd3ede93..6a79e307d 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -35,15 +35,14 @@ STEP_INSTRUCTIONS = { 7: {"content": "Witch, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 8: {"content": """Witch, you have a bottle of poison, who are you going to kill tonight? - Choose one from the following living options: {living_players}.""", + 8: {"content": """Witch, tonight {killed_player} has been killed by the werewolves. + You have a bottle of antidote, would you like to save him/her? If not, simply Pass.""", "send_to": "Witch", - "restricted_to": "Witch"}, - 9: {"content": """Witch, you have a bottle of antidote and a bottle of poison. - Who are you going to save tonight or kill tonight? Choose one from the following living options: - {living_players}.""", + "restricted_to": "Witch"}, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 + 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 not, simply Pass.""", "send_to": "Witch", - "restricted_to": "Witch"}, + "restricted_to": "Witch"}, # 10: {"content": "Witch, close your eyes", "send_to": "Moderator", "restricted_to": ""}, @@ -71,7 +70,7 @@ STEP_INSTRUCTIONS = { 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}. Or you can pass. For example: I vote to kill ...""", - "send_to": "Moderator", + "send_to": "", "restricted_to": ""}, 18: {"content": """{voted_out_player} was eliminated.""", "send_to": "Moderator", @@ -108,7 +107,7 @@ VOTE_PROMPT = """ PARSE_INSTRUCTIONS = { 0: "Now it's time to vote", - 1: "The {winner} have won! They successfully eliminated all the {loser}}." + 1: "The {winner} have won! They successfully eliminated all the {loser}." "The game has ended. Thank you for playing Werewolf!", 2: "The night has ended, and it's time to reveal the casualties." "During the night, the Werewolves made their move. Unfortunately, they targeted {PlayerName}, who is now dead." @@ -193,6 +192,11 @@ class SummarizeNight(Action): async def run(self, events): # 假设events是一个字典,代表夜晚发生的多个事件,key是事件类型,value是该事件对应的玩家 # 例如被狼人杀的玩家:{"killed_by_werewolves": "Player1"} + # 被守卫守护的玩家:{"protected_by_guard": "Player2"} + # 被女巫救的玩家:{"saved_by_witch": "Player3"} + # 被女巫毒的玩家:{"poisoned_by_witch": "Player4"} + # 被预言家查验的玩家:{"verified_by_seer": "Player5"} + # 若没有事件发生,则events为空字典 killed_by_werewolves = events.get("killed_by_werewolves", "") protected_by_guard = events.get("protected_by_guard", "") saved_by_witch = events.get("saved_by_witch", "") @@ -226,7 +230,7 @@ class SummarizeDay(Action): # 假设votes是一个字典,代表白天投票的结果,key是被投票的玩家,value是得票数 # 例如:{"Player1": 2, "Player2": 1, "Player3": 1, "Player4": 0} # 表示Player1得到2票,Player2和Player3各得到1票,Player4得到0票 - # 若平票,则无人死亡 + # 若平票,则随机选一个人出局 if not votes: return "No votes were cast. No one was killed." diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 4c1334b3c..49380919e 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -31,7 +31,7 @@ class Moderator(Role): # 假设votes代表白天投票的结果,key是被投票的玩家,value是得票数 self.votes = {"Player1": 1, "Player2": 2, "Player3": 1, "Player4": 0, "Player5": 0} - async def _instruct_speak(self, context): + async def _instruct_speak(self): step_idx = self.step_idx % len(STEP_INSTRUCTIONS) self.step_idx += 1 return await InstructSpeak().run(step_idx, @@ -85,7 +85,7 @@ class Moderator(Role): # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): - msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak(memories) + msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak() msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) From ce388827f80c817dc61dde1eec29c19b770df6a4 Mon Sep 17 00:00:00 2001 From: chaleeluo Date: Sat, 30 Sep 2023 10:56:29 +0800 Subject: [PATCH 12/47] witch role --- examples/werewolf_game/actions/__init__.py | 3 + .../werewolf_game/actions/witch_action.py | 47 +++++++++++++ examples/werewolf_game/roles/__init__.py | 1 + examples/werewolf_game/roles/witch.py | 67 +++++++++++++++++++ examples/werewolf_game/start_game.py | 6 +- 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 examples/werewolf_game/actions/witch_action.py create mode 100644 examples/werewolf_game/roles/witch.py diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 7c5af46df..92f6f0f8d 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,8 +1,11 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak from examples.werewolf_game.actions.common_actions import Speak from examples.werewolf_game.actions.werewolf_actions import Hunt +from examples.werewolf_game.actions.witch_actions import Save, Poison ACTIONS = { "Speak": Speak, "Hunt": Hunt, + "Save": Save, + "Poison": Poison } \ No newline at end of file diff --git a/examples/werewolf_game/actions/witch_action.py b/examples/werewolf_game/actions/witch_action.py new file mode 100644 index 000000000..144ba24bd --- /dev/null +++ b/examples/werewolf_game/actions/witch_action.py @@ -0,0 +1,47 @@ +from metagpt.actions import Action + +class Save(Action): + """Action: choose a villager to Save""" + + PROMPT_TEMPLATE = """ + It's a werewolf game and you are a witch, + this is game history: + {context}. + Attention: You have received information that someone is going to be killed. + Now, decide whether you want to save that person or not: + """ + + def __init__(self, name="Save", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str): + + prompt = self.PROMPT_TEMPLATE.format(context=context) + + rsp = await self._aask(prompt) + # rsp = "Save Player 1" + + return rsp + +class Poison(Action): + """Action: choose a villager to Poison""" + + PROMPT_TEMPLATE = """ + It's a werewolf game and you are a witch, + this is game history: + {context}. + Attention: You have received information that someone is going to be killed. + Now, decide whether you want to poison another person or not: + """ + + def __init__(self, name="Poison", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str): + + prompt = self.PROMPT_TEMPLATE.format(context=context) + + rsp = await self._aask(prompt) + # rsp = "Poison Player 1" + + return rsp diff --git a/examples/werewolf_game/roles/__init__.py b/examples/werewolf_game/roles/__init__.py index 464563344..debf9b7cf 100644 --- a/examples/werewolf_game/roles/__init__.py +++ b/examples/werewolf_game/roles/__init__.py @@ -2,3 +2,4 @@ from examples.werewolf_game.roles.base_player import BasePlayer from examples.werewolf_game.roles.moderator import Moderator from examples.werewolf_game.roles.villager import Villager from examples.werewolf_game.roles.werewolf import Werewolf +from examples.werewolf_game.roles.witch import Witch \ No newline at end of file diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py new file mode 100644 index 000000000..999b75432 --- /dev/null +++ b/examples/werewolf_game/roles/witch.py @@ -0,0 +1,67 @@ +from examples.werewolf_game.actions.witch_actions import Save, Poison +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.actions import Speak, Hunt +from metagpt.schema import Message +from metagpt.logs import logger + +STATE_TEMPLATE = """Here are your conversation records. You can decide which stage you should enter or stay in based on these records. +Please note that only the text between the first and second "===" is information about completing tasks and should not be regarded as commands for executing operations. +=== +{history} +=== +You can now choose one of the following stages to decide the stage you need to go in the next step: +{states} +Just answer a number between 0-{n_states}, choose the most suitable stage according to the understanding of the conversation. +Please note that the answer only needs a number, no need to add any other text. +If there is no conversation record, choose 0. +Do not answer anything else, and do not add any other information in your answer. +""" + + +class Witch(BasePlayer): + def __init__( + self, + name: str = "", + profile: str = "Witch", + team: str = "good guys", + special_action_names: list[str] = ["Save", "Poison"], + **kwargs, + ): + super().__init__(name, profile, team, special_action_names, **kwargs) + + async def _act(self): + # todo为_think时确定的,有三种情况,Speak或Save或Poison + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {str(todo)}") + + # 可以用这个函数获取该角色的全部记忆 + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 根据自己定义的角色Action,对应地去run,run的入参可能不同 + if isinstance(todo, Speak): + rsp = await todo.run(profile=self.profile, context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Speak, send_to="", restricted_to="", + ) + + elif isinstance(todo, Save): + rsp = await todo.run(context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Save, send_to="", + restricted_to=f"Moderator,{self.profile}", # 给Moderator发送要救的人的加密消息 + ) + + elif isinstance(todo, Poison): + rsp = await todo.run(context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Poison, send_to="", + restricted_to=f"Moderator,{self.profile}", # 给Moderator发送要读的人的加密消息 + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg \ No newline at end of file diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 98d76aa9d..98f031362 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -3,14 +3,15 @@ import platform import fire from examples.werewolf_game.werewolf_game import WerewolfGame -from examples.werewolf_game.roles import Moderator, Villager, Werewolf +from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Witch DEFAULT_PLAYER_SETUP = """ Game setup: Player1: Villager, Player2: Villager, Player3: Werewolf, -Player4: Werewolf. +Player4: Werewolf, +Player5: Witch """ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): @@ -21,6 +22,7 @@ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, Villager(name="Player2"), Werewolf(name="Player3"), Werewolf(name="Player4"), + Witch(name="Player5"), ]) game.invest(investment) game.start_project(idea) From de153405b3e2221bc53ea30ed41a706056093888 Mon Sep 17 00:00:00 2001 From: chaleeluo Date: Sat, 30 Sep 2023 11:05:05 +0800 Subject: [PATCH 13/47] fix --- examples/werewolf_game/start_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 8ae6afe26..5be3186f7 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -3,7 +3,7 @@ import platform import fire from examples.werewolf_game.werewolf_game import WerewolfGame -from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard, Seer, witch +from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard, Seer, Witch DEFAULT_PLAYER_SETUP = """ Game setup: From 6f8b1f71168eff35b5f6135be501f76e39af0c67 Mon Sep 17 00:00:00 2001 From: chaleeluo Date: Sat, 30 Sep 2023 11:16:12 +0800 Subject: [PATCH 14/47] fix --- .../werewolf_game/actions/{witch_action.py => witch_actions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/werewolf_game/actions/{witch_action.py => witch_actions.py} (100%) diff --git a/examples/werewolf_game/actions/witch_action.py b/examples/werewolf_game/actions/witch_actions.py similarity index 100% rename from examples/werewolf_game/actions/witch_action.py rename to examples/werewolf_game/actions/witch_actions.py From f7ed6ef2ee6b568ffd02444f0bd9af104136633b Mon Sep 17 00:00:00 2001 From: yzlin Date: Sat, 30 Sep 2023 12:08:57 +0800 Subject: [PATCH 15/47] game setup --- examples/werewolf_game/start_game.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 5be3186f7..83d3b3385 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -12,8 +12,8 @@ Player2: Villager, Player3: Werewolf, Player4: Werewolf, Player5: Guard, -Player6: Seer. -Player7: Witch +Player6: Seer, +Player7: Witch. """ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): @@ -33,7 +33,7 @@ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, await game.run(n_round=n_round) -def main(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 10): +def main(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 100): """ :param idea: game config instructions :param investment: contribute a certain dollar amount to watch the debate From 4d39bb2815c06f644129941c4691f8d9c195f5aa Mon Sep 17 00:00:00 2001 From: yzlin Date: Sat, 30 Sep 2023 12:24:45 +0800 Subject: [PATCH 16/47] moderator parsespeak & 1st v of runnable pipeline --- examples/werewolf_game/actions/__init__.py | 4 +- .../werewolf_game/actions/common_actions.py | 5 +- .../werewolf_game/actions/guard_actions.py | 3 +- .../actions/moderator_actions.py | 89 ++------- .../werewolf_game/actions/seer_actions.py | 4 +- .../werewolf_game/actions/werewolf_actions.py | 5 +- .../werewolf_game/actions/witch_actions.py | 6 +- examples/werewolf_game/roles/__init__.py | 2 +- examples/werewolf_game/roles/base_player.py | 7 +- examples/werewolf_game/roles/guard.py | 2 +- examples/werewolf_game/roles/moderator.py | 177 ++++++++++++++---- examples/werewolf_game/roles/seer.py | 2 +- examples/werewolf_game/roles/villager.py | 2 +- examples/werewolf_game/roles/werewolf.py | 2 +- examples/werewolf_game/roles/witch.py | 38 ++-- 15 files changed, 206 insertions(+), 142 deletions(-) diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 00a276be3..cf358e92b 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -11,5 +11,5 @@ ACTIONS = { "Protect": Protect, "Verify": Verify, "Save": Save, - "Poison": Poison -} \ No newline at end of file + "Poison": Poison, +} diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index a0cb1f0fc..84936ebde 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -10,7 +10,10 @@ class Speak(Action): You have knowledge to the following conversation: {context} ## YOUR TURN - It's daytime and it is your turn to speak, you will say (in 100 words): + Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, + 1. If the instruction is to speak, speak in 100 words; + 2. If the instruction is to vote, you MUST vote and ONLY say "I vote to eliminate PlayerX", where X is the player index, DO NOT include any other words. + Your will say: """ def __init__(self, name="Speak", context=None, llm=None): diff --git a/examples/werewolf_game/actions/guard_actions.py b/examples/werewolf_game/actions/guard_actions.py index 10e550a49..5b98d7e0e 100644 --- a/examples/werewolf_game/actions/guard_actions.py +++ b/examples/werewolf_game/actions/guard_actions.py @@ -8,7 +8,8 @@ class Protect(Action): you can choose to protect a player, including yourself, then the protected player will not be killed by the Werewolves this night. this is game history: {context}. - Attention: you can not protect the same player two nights in a row. + Attention: you can not protect the same player two nights in a row. + Format: "Protect PlayerX", where X is the player index. Now, choose one to protect, you will: """ diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 6a79e307d..3d60563ce 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -16,7 +16,7 @@ STEP_INSTRUCTIONS = { 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: I protect ...""", "send_to": "Guard", - "restricted_to": "Guard"}, + "restricted_to": "Moderator,Guard"}, 3: {"content": "Guard, close your eyes", "send_to": "Moderator", "restricted_to": ""}, @@ -28,21 +28,21 @@ STEP_INSTRUCTIONS = { choose one from the following living options please: {living_players}. For example: I kill ...""", "send_to": "Werewolf", - "restricted_to": "Werewolf"}, + "restricted_to": "Moderator,Werewolf"}, 6: {"content": "Werewolves, close your eyes", "send_to": "Moderator", "restricted_to": ""}, 7: {"content": "Witch, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 8: {"content": """Witch, tonight {killed_player} has been killed by the werewolves. - You have a bottle of antidote, would you like to save him/her? If not, simply Pass.""", + 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": "Witch"}, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 + "restricted_to": "Moderator,Witch"}, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 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 not, simply Pass.""", + Choose one from the following living options: {living_players}. If so, say "Poison PlayerX", where X is the player index, else, say "Pass".""", "send_to": "Witch", - "restricted_to": "Witch"}, # + "restricted_to": "Moderator,Witch"}, # 10: {"content": "Witch, close your eyes", "send_to": "Moderator", "restricted_to": ""}, @@ -52,7 +52,7 @@ STEP_INSTRUCTIONS = { 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": "Seer"}, + "restricted_to": "Moderator,Seer"}, 13: {"content": "Seer, close your eyes", "send_to": "Moderator", "restricted_to": ""}, @@ -60,7 +60,7 @@ STEP_INSTRUCTIONS = { 14: {"content": """It's daytime. Everyone woke up except those who had been killed.""", "send_to": "Moderator", "restricted_to": ""}, - 15: {"content": "{killed_player} was killed last night. Or, it was a peaceful night and no one died!", + 15: {"content": "{player_current_dead} was killed last night!", "send_to": "Moderator", "restricted_to": ""}, 16: {"content": """Now freely talk about roles of other players with each other based on your observation and @@ -72,21 +72,11 @@ STEP_INSTRUCTIONS = { {living_players}. Or you can pass. For example: I vote to kill ...""", "send_to": "", "restricted_to": ""}, - 18: {"content": """{voted_out_player} was eliminated.""", + 18: {"content": """{player_current_dead} was eliminated.""", "send_to": "Moderator", "restricted_to": ""}, } -ROLE_STATES = { - # 存活状态 - 0: "Alive", # 开场 - 1: "Dead", # 结束 - 2: "Protected", # 被保护 - 3: "Poisoned", # 被毒 - 4: "Saved", # 被救 - 5: "Killed" # 被刀 -} - VOTE_PROMPT = """ Welcome to the daytime discussion phase in the Werewolf game. @@ -118,7 +108,7 @@ class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, step_idx, living_players, werewolf_players, killed_player, voted_out_player): + async def run(self, step_idx, living_players, werewolf_players, player_hunted, player_current_dead): instruction_info = STEP_INSTRUCTIONS.get(step_idx, { "content": "Unknown instruction.", "send_to": "", @@ -132,56 +122,20 @@ class InstructSpeak(Action): content = content.format(living_players=",".join(living_players)) if "{werewolf_players}" in content: content = content.format(werewolf_players=",".join(werewolf_players)) - if "{killed_player}" in content: - content = content.format(killed_player=killed_player) - if "{voted_out_player}" in content: - content = content.format(voted_out_player=voted_out_player) + if "{player_hunted}" in content: + player_hunted = "No one" if not player_hunted else player_hunted + content = content.format(player_hunted=player_hunted) + if "{player_current_dead}" in content: + content = content.format(player_current_dead=player_current_dead) return content, instruction_info["send_to"], instruction_info["restricted_to"] - class ParseSpeak(Action): def __init__(self, name="ParseSpeak", context=None, llm=None): super().__init__(name, context, llm) - self.daytime_info = collections.defaultdict(list) - self.night_info = collections.defaultdict(list) - self.vote_message = [] - - async def run(self, dead_history, context, env): - - for m in env.memory.get(): - role = m.sent_from if hasattr(m, 'sent_from') else "" - content = m.content if hasattr(m, 'content') else "" - target = m.sent_to if hasattr(m, 'sent_to') else "" - restricted = m.restricted_to if hasattr(m, 'restricted_to') else "" - if target == 'all': - self.daytime_info[role] = [content, target, restricted] - else: - self.night_info[role] = [content, target, restricted] - - # collect info from the night and identify the dead player - for role in self.night_info: - if "kill" in self.night_info[role][0] and self.night_info[role][1]: - target = self.night_info[role][1] - print("env.get_roles[target]", env, env.env.roles) - env.env.roles[target].set_status(ROLE_STATES[5]) - for role in self.night_info: - if ("save" or "guard") in self.night_info[role][0]: - save_target = self.night_info[role][1] - if save_target == target: - env.env.roles[target].set_status(ROLE_STATES[0]) - else: - dead_history.append(target) - - # collect message from the daytime and identify the vote player - for role in self.daytime_info: - self.vote_message += f"\n{self.daytime_info[role][0]}" - - vote_player = await self.llm.aask(VOTE_PROMPT.format(vote_message=self.vote_message)) - dead_history.append(vote_player) - - return dead_history, vote_player, PARSE_INSTRUCTIONS + async def run(self): + pass class SummarizeNight(Action): """consider all events at night, conclude which player dies (can be a peaceful night)""" @@ -249,19 +203,14 @@ class SummarizeDay(Action): class AnnounceGameResult(Action): async def run(self, winner: str): - return f"Game over! The winner is {winner}" + return f"Game over! The winner is the {winner}" async def main(): rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) rst2 = await SummarizeNight().run({"killed_by_werewolves": "Player1"}) - # 表示第2个步骤的指令,living_players是所有活着的玩家,werewolf_players是所有狼人,killed_player是被杀的玩家,voted_out_player是被投票出局的玩家 - rst3 = await InstructSpeak().run(2, ["Player1", "Player2", "Player3", "Player4"], - ["Player3", "Player4"], "Player4", "Player3") print(rst1) print(rst2) - print(rst3) - if __name__ == '__main__': asyncio.run(main()) diff --git a/examples/werewolf_game/actions/seer_actions.py b/examples/werewolf_game/actions/seer_actions.py index b4cb55b15..4c54debe2 100644 --- a/examples/werewolf_game/actions/seer_actions.py +++ b/examples/werewolf_game/actions/seer_actions.py @@ -9,7 +9,9 @@ class Verify(Action): You can choose to verify the identity of a player. Here's the game history: {context}. - Now, choose one player to verify: + Now, choose one player to verify + Format: "Verify PlayerX", where X is the player index. + You will: """ def __init__(self, name="Verify", context=None, llm=None): diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index ab54bc98c..0a750a02d 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -7,11 +7,12 @@ class Hunt(Action): It's a werewolf game and you are a werewolf, this is game history: {context}. - Attention: if your previous werewolf have chosen, follow its choice. + Attention: if your previous werewolf has chosen, follow its choice. + Format: "Kill PlayerX", where X is the player index. Now, choose one to kill, you will: """ - def __init__(self, name="Speak", context=None, llm=None): + def __init__(self, name="Hunt", context=None, llm=None): super().__init__(name, context, llm) async def run(self, context: str): diff --git a/examples/werewolf_game/actions/witch_actions.py b/examples/werewolf_game/actions/witch_actions.py index 144ba24bd..dec39f466 100644 --- a/examples/werewolf_game/actions/witch_actions.py +++ b/examples/werewolf_game/actions/witch_actions.py @@ -7,8 +7,7 @@ class Save(Action): It's a werewolf game and you are a witch, this is game history: {context}. - Attention: You have received information that someone is going to be killed. - Now, decide whether you want to save that person or not: + Follow the Moderator's instruction, decide whether you want to save that person or not: """ def __init__(self, name="Save", context=None, llm=None): @@ -30,8 +29,7 @@ class Poison(Action): It's a werewolf game and you are a witch, this is game history: {context}. - Attention: You have received information that someone is going to be killed. - Now, decide whether you want to poison another person or not: + Follow the Moderator's instruction, decide whether you want to poison another person or not: """ def __init__(self, name="Poison", context=None, llm=None): diff --git a/examples/werewolf_game/roles/__init__.py b/examples/werewolf_game/roles/__init__.py index 249c84970..6afbafadc 100644 --- a/examples/werewolf_game/roles/__init__.py +++ b/examples/werewolf_game/roles/__init__.py @@ -4,4 +4,4 @@ from examples.werewolf_game.roles.villager import Villager from examples.werewolf_game.roles.werewolf import Werewolf from examples.werewolf_game.roles.guard import Guard from examples.werewolf_game.roles.seer import Seer -from examples.werewolf_game.roles.witch import Witch \ No newline at end of file +from examples.werewolf_game.roles.witch import Witch diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index c3e60372c..38e6b2374 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -18,7 +18,7 @@ class BasePlayer(Role): self._watch([InstructSpeak]) self.team = team # 调用 get_status() 来检查存活状态,并通过 set_status() 更新状态。 - self.status = 0 # 初始状态为活着 + self.status = 0 # 0代表活着,1代表死亡 # 技能和监听配置 self._watch([InstructSpeak]) # 监听Moderator的指令以做行动 @@ -28,6 +28,10 @@ class BasePlayer(Role): self.special_actions = special_actions async def _observe(self) -> int: + if self.status == 1: + # 死者不再参与游戏 + return 0 + await super()._observe() # 只有发给全体的("")或发给自己的(self.profile)消息需要走下面的_react流程, # 其他的收听到即可,不用做动作 @@ -50,6 +54,7 @@ class BasePlayer(Role): def get_all_memories(self) -> str: memories = self._rc.memory.get() + # NOTE: 除Moderator外,其他角色使用memory,只能用m.sent_from(玩家名)不能用m.role(玩家角色),因为他们不知道说话者的身份 memories = [f"{m.sent_from}: {m.content}" for m in memories] memories = "\n".join(memories) return memories diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index 94b5b46dc..89d22c153 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -21,7 +21,7 @@ class Guard(BasePlayer): # 可以用这个函数获取该角色的全部记忆 memories = self.get_all_memories() - print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 49380919e..9d0323221 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -1,4 +1,6 @@ import asyncio +import re +from collections import Counter from metagpt.roles import Role from metagpt.schema import Message @@ -6,6 +8,7 @@ from metagpt.logs import logger from examples.werewolf_game.actions.moderator_actions import ( InstructSpeak, ParseSpeak, AnnounceGameResult, STEP_INSTRUCTIONS ) +from examples.werewolf_game.actions import Hunt, Protect, Verify, Save, Poison from metagpt.actions import BossRequirement as UserRequirement @@ -24,52 +27,146 @@ class Moderator(Role): self._watch([UserRequirement, InstructSpeak, ParseSpeak]) self._init_actions([InstructSpeak, ParseSpeak, AnnounceGameResult]) self.step_idx = 0 - self.living_players = ["Player1", "Player2", "Player3", "Player4", "Player5"] - self.werewolf_players = ["Player1", "Player2"] - self.good_guys = ["Player3", "Player4", "Player5"] - self.dead_players = [] # 夜晚阶段,死掉的玩家 - # 假设votes代表白天投票的结果,key是被投票的玩家,value是得票数 - self.votes = {"Player1": 1, "Player2": 2, "Player3": 1, "Player4": 0, "Player5": 0} + + # game states + self.living_players = [] + self.werewolf_players = [] + self.good_guys = [] + self.winner = None + self.witch_poison_left = 1 + self.witch_antidote_left = 1 + + # player states of current night + self.player_hunted = None + self.player_protected = None + self.is_hunted_player_saved = False + self.player_poisoned = None + self.player_current_dead = [] + + def _parse_game_setup(self, game_setup: str): + self.living_players = re.findall(r"Player[0-9]+", game_setup) + self.werewolf_players = re.findall(r"Player[0-9]+: Werewolf", game_setup) + self.werewolf_players = [p.replace(": Werewolf", "") for p in self.werewolf_players] + self.good_guys = [p for p in self.living_players if p not in self.werewolf_players] + + def update_player_status(self, player_names: list[str]): + if not player_names: + return + roles_in_env = self._rc.env.get_roles() + for role_setting, role in roles_in_env.items(): + for player_name in player_names: + if player_name in role_setting: + role.set_status(new_status=1) # 更新为死亡 async def _instruct_speak(self): + print("*" * 10, "STEP: ", self.step_idx, "*" * 10) step_idx = self.step_idx % len(STEP_INSTRUCTIONS) self.step_idx += 1 return await InstructSpeak().run(step_idx, living_players=self.living_players, werewolf_players=self.werewolf_players, - killed_player=self.dead_players, - voted_out_player="Player3") + player_hunted=self.player_hunted, + player_current_dead=self.player_current_dead) - async def _parse_speak(self, memories, env): + async def _parse_speak(self, memories): + logger.info(self.step_idx) - self.dead_players, vote_player, parse_info = await ParseSpeak().run(dead_history=self.dead_players, - context=memories, env=env) + latest_msg = memories[-1] - # decide to move the game into the next phase - if not vote_player: - msg_content, send_to = parse_info[0], self.profile + match = re.search(r"Player[0-9]+", latest_msg.content[-10:]) + target = match.group(0) if match else "" + + # default return + msg_content = "Understood" + restricted_to = "" + + source_role = latest_msg.role + msg_cause_by = latest_msg.cause_by + if msg_cause_by == Hunt: + self.player_hunted = target + # breakpoint() + elif msg_cause_by == Protect: + self.player_protected = target + elif msg_cause_by == 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 = "Moderator,Seer" + elif msg_cause_by == Save: + if not self.witch_antidote_left and latest_msg.content != "Pass": + msg_content = "You have no antidote left and thus can not save the player" + restricted_to = "Moderator,Witch" + else: + self.witch_antidote_left -= 1 + self.is_hunted_player_saved = latest_msg.content == "Save" + elif msg_cause_by == Poison: + if not self.witch_poison_left and latest_msg.content != "Pass": + msg_content = "You have no poison left and thus can not poison the player" + restricted_to = "Moderator,Witch" + else: + self.witch_poison_left -= 1 + self.player_poisoned = target # "" if not poisoned and "PlayerX" if poisoned + + step_idx = self.step_idx % len(STEP_INSTRUCTIONS) + if step_idx == 13: # FIXME: hard code + # night ends: after all special roles acted, process the whole night + self.player_current_dead = [] # reset + + if self.player_hunted != self.player_protected and not self.is_hunted_player_saved: + self.player_current_dead.append(self.player_hunted) + if self.player_poisoned: + self.player_current_dead.append(self.player_poisoned) + + self.living_players = [p for p in self.living_players if p not in self.player_current_dead] + self.update_player_status(self.player_current_dead) + # reset + self.player_hunted = None + self.player_protected = None + self.is_hunted_player_saved = False + self.player_poisoned = None + + elif step_idx == 18: # FIXME: hard code + # day ends: after all roles voted, process all votings + voting_msgs = memories[-len(self.living_players):] + voted_all = [] + for msg in voting_msgs: + voted = re.search(r"Player[0-9]+", msg.content[-10:]) + if not voted: + continue + voted_all.append(voted.group(0)) + # breakpoint() + self.player_current_dead = [Counter(voted_all).most_common()[0][0]] # 平票时,杀序号小的 + self.living_players = [p for p in self.living_players if p not in self.player_current_dead] + self.update_player_status(self.player_current_dead) + msg_content = "Voting done" + # game's termination condition - elif all(item in self.dead_players for item in self.werewolf_players) or all( - item in self.dead_players for item in self.good_guys): - self.is_game_over = True - msg_content, send_to = parse_info[1], "all" - else: - # game's termination condition - msg_content, send_to = parse_info[2], "" - return msg_content, send_to + living_werewolf = [p for p in self.werewolf_players if p in self.living_players] + living_good_guys = [p for p in self.good_guys if p in self.living_players] + if not living_werewolf: + self.winner = "good guys" + elif not living_good_guys: + self.winner = "werewolf" + + return msg_content, restricted_to async def _think(self): - if self.is_game_over: + if self.winner is not None: self._rc.todo = AnnounceGameResult() return + + latest_msg = self._rc.memory.get()[-1] + if latest_msg.role in ["User"]: + # 上一轮消息是用户指令,解析用户指令,开始游戏 + game_setup = latest_msg.content + self._parse_game_setup(game_setup) + self._rc.todo = InstructSpeak() - # 确定当前是需要InstructSpeak还是ParseSpeak. 通过判断当前流程状态变量,以及消息的cause_by属性 - # 0: InstructSpeak, 1: ParseSpeak,且需要判断消息的cause_by属性 - if self._rc.memory.get()[-1].role in ["User", self.profile]: - # 1. 上一轮消息是用户指令,解析用户指令,开始游戏 - # 2. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说 - # 3. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令 + elif latest_msg.role in [self.profile]: + # 1. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说 + # 2. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令 self._rc.todo = InstructSpeak() else: @@ -80,19 +177,24 @@ class Moderator(Role): todo = self._rc.todo logger.info(f"{self._setting} ready to {todo}") - memories = self.get_all_memories() - print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + memories = self.get_all_memories(mode="msg") + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: + # 进行完一夜一日的循环,打印一次完整发言历史 + print(self.get_all_memories()) # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak() + msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) elif isinstance(todo, ParseSpeak): - msg_content, send_to = await self._parse_speak(memories, self._rc) - msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak, - send_to=send_to) + msg_content, msg_restriced_to = await self._parse_speak(memories) + msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 + msg = Message(content=msg_content, role=self.profile, sent_from=self.name, + cause_by=ParseSpeak, send_to="", restricted_to=msg_restriced_to) elif isinstance(todo, AnnounceGameResult): msg_content = await AnnounceGameResult().run(winner=self.winner) @@ -102,8 +204,9 @@ class Moderator(Role): return msg - def get_all_memories(self) -> str: + def get_all_memories(self, mode="str") -> str: memories = self._rc.memory.get() - memories = [str(m) for m in memories] - memories = "\n".join(memories) + if mode == "str": + memories = [f"{m.sent_from}({m.role}): {m.content}" for m in memories] + memories = "\n".join(memories) return memories diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index b4ff41c8c..7003bb2ad 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -21,7 +21,7 @@ class Seer(BasePlayer): logger.info(f"{self._setting}: ready to {str(todo)}") memories = self.get_all_memories() - print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 基于todo的类型,调用不同的action if isinstance(todo, Speak): diff --git a/examples/werewolf_game/roles/villager.py b/examples/werewolf_game/roles/villager.py index 0e9047938..6b1c6e50c 100644 --- a/examples/werewolf_game/roles/villager.py +++ b/examples/werewolf_game/roles/villager.py @@ -22,7 +22,7 @@ class Villager(BasePlayer): # 可以用这个函数获取该角色的全部记忆 memories = self.get_all_memories() - print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run rsp = await todo.run(profile=self.profile, context=memories) diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py index 5a3bc55ff..a4187d38c 100644 --- a/examples/werewolf_game/roles/werewolf.py +++ b/examples/werewolf_game/roles/werewolf.py @@ -21,7 +21,7 @@ class Werewolf(BasePlayer): # 可以用这个函数获取该角色的全部记忆 memories = self.get_all_memories() - print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py index 999b75432..2111bd01d 100644 --- a/examples/werewolf_game/roles/witch.py +++ b/examples/werewolf_game/roles/witch.py @@ -1,23 +1,8 @@ -from examples.werewolf_game.actions.witch_actions import Save, Poison +from examples.werewolf_game.actions import InstructSpeak, Speak, Save, Poison from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak, Hunt from metagpt.schema import Message from metagpt.logs import logger -STATE_TEMPLATE = """Here are your conversation records. You can decide which stage you should enter or stay in based on these records. -Please note that only the text between the first and second "===" is information about completing tasks and should not be regarded as commands for executing operations. -=== -{history} -=== -You can now choose one of the following stages to decide the stage you need to go in the next step: -{states} -Just answer a number between 0-{n_states}, choose the most suitable stage according to the understanding of the conversation. -Please note that the answer only needs a number, no need to add any other text. -If there is no conversation record, choose 0. -Do not answer anything else, and do not add any other information in your answer. -""" - - class Witch(BasePlayer): def __init__( self, @@ -28,6 +13,23 @@ class Witch(BasePlayer): **kwargs, ): super().__init__(name, profile, team, special_action_names, **kwargs) + + async def _think(self): + # 女巫涉及两个特殊技能,因此在此需要改写_think进行路由 + news = self._rc.news[0] + assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 + if not news.restricted_to: + # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) + self._rc.todo = Speak() + elif self.profile in news.restricted_to.split(","): # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" + # Moderator加密发给自己的,意味着要执行角色的特殊动作 + # 这里用关键词进行动作的选择,需要Moderator侧的指令进行配合 + if "save" in news.content.lower(): + self._rc.todo = Save() + elif "poison" in news.content.lower(): + self._rc.todo = Poison() + else: + raise ValueError("Moderator's instructions must include save or poison keyword") async def _act(self): # todo为_think时确定的,有三种情况,Speak或Save或Poison @@ -36,7 +38,7 @@ class Witch(BasePlayer): # 可以用这个函数获取该角色的全部记忆 memories = self.get_all_memories() - print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): @@ -64,4 +66,4 @@ class Witch(BasePlayer): logger.info(f"{self._setting}: {rsp}") - return msg \ No newline at end of file + return msg From fa88e4452119d82a5a458979876d229bc106fe1a Mon Sep 17 00:00:00 2001 From: yzlin Date: Sat, 30 Sep 2023 15:02:19 +0800 Subject: [PATCH 17/47] rm redundant & add random game setup --- .../actions/moderator_actions.py | 89 +++---------------- examples/werewolf_game/roles/base_player.py | 34 +++---- examples/werewolf_game/roles/guard.py | 11 +-- examples/werewolf_game/roles/moderator.py | 31 +++---- examples/werewolf_game/roles/seer.py | 3 +- examples/werewolf_game/roles/villager.py | 5 +- examples/werewolf_game/roles/werewolf.py | 9 +- examples/werewolf_game/roles/witch.py | 8 +- examples/werewolf_game/start_game.py | 51 ++++++----- examples/werewolf_game/werewolf_game.py | 16 +++- 10 files changed, 95 insertions(+), 162 deletions(-) diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 3d60563ce..fc897474e 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -13,8 +13,9 @@ STEP_INSTRUCTIONS = { 1: {"content": "Guard, please open your eyes!", "send_to": "Moderator", # for moderator to continuen speaking "restricted_to": ""}, - 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: I protect ...""", + 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"}, 3: {"content": "Guard, close your eyes", @@ -26,7 +27,7 @@ STEP_INSTRUCTIONS = { 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: I kill ...""", + {living_players}. For example: Kill ...""", "send_to": "Werewolf", "restricted_to": "Moderator,Werewolf"}, 6: {"content": "Werewolves, close your eyes", @@ -35,12 +36,13 @@ STEP_INSTRUCTIONS = { 7: {"content": "Witch, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 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".""", + 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"}, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 - 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 "Poison PlayerX", where X is the player index, else, say "Pass".""", + 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 "Poison PlayerX", where X is the player index, else, say "Pass".""", "send_to": "Witch", "restricted_to": "Moderator,Witch"}, # 10: {"content": "Witch, close your eyes", @@ -49,8 +51,8 @@ STEP_INSTRUCTIONS = { 11: {"content": "Seer, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 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}.""", + 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"}, 13: {"content": "Seer, close your eyes", @@ -77,33 +79,6 @@ STEP_INSTRUCTIONS = { "restricted_to": ""}, } -VOTE_PROMPT = """ - Welcome to the daytime discussion phase in the Werewolf game. - - During the day, players discuss and share information about who they suspect might be a werewolf. - Players can also cast their votes to eliminate a player they believe is a werewolf. - - Here are the conversations from the daytime: - - {vote_message} - - Now it's time to cast your votes. - - You can vote for a player by typing their name. - Example: "Vote for Player2" - - Here are the voting options: -""" - -PARSE_INSTRUCTIONS = { - 0: "Now it's time to vote", - 1: "The {winner} have won! They successfully eliminated all the {loser}." - "The game has ended. Thank you for playing Werewolf!", - 2: "The night has ended, and it's time to reveal the casualties." - "During the night, the Werewolves made their move. Unfortunately, they targeted {PlayerName}, who is now dead." -} - - class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): super().__init__(name, context, llm) @@ -123,9 +98,9 @@ class InstructSpeak(Action): if "{werewolf_players}" in content: content = content.format(werewolf_players=",".join(werewolf_players)) if "{player_hunted}" in content: - player_hunted = "No one" if not player_hunted else player_hunted content = content.format(player_hunted=player_hunted) if "{player_current_dead}" in content: + player_current_dead = "No one" if not player_current_dead else player_current_dead content = content.format(player_current_dead=player_current_dead) return content, instruction_info["send_to"], instruction_info["restricted_to"] @@ -137,43 +112,6 @@ class ParseSpeak(Action): async def run(self): pass -class SummarizeNight(Action): - """consider all events at night, conclude which player dies (can be a peaceful night)""" - - def __init__(self, name="SummarizeNight", context=None, llm=None): - super().__init__(name, context, llm) - - async def run(self, events): - # 假设events是一个字典,代表夜晚发生的多个事件,key是事件类型,value是该事件对应的玩家 - # 例如被狼人杀的玩家:{"killed_by_werewolves": "Player1"} - # 被守卫守护的玩家:{"protected_by_guard": "Player2"} - # 被女巫救的玩家:{"saved_by_witch": "Player3"} - # 被女巫毒的玩家:{"poisoned_by_witch": "Player4"} - # 被预言家查验的玩家:{"verified_by_seer": "Player5"} - # 若没有事件发生,则events为空字典 - killed_by_werewolves = events.get("killed_by_werewolves", "") - protected_by_guard = events.get("protected_by_guard", "") - saved_by_witch = events.get("saved_by_witch", "") - poisoned_by_witch = events.get("poisoned_by_witch", "") - - # 若狼人杀的人和守卫守的人是同一个人,那么该人就会活着; - if protected_by_guard and killed_by_werewolves and protected_by_guard == killed_by_werewolves: - return "It was a peaceful night. No one was killed." - - # 若守卫和女巫都救了同一个人,那么该人就会死 - if protected_by_guard and saved_by_witch and protected_by_guard == saved_by_witch: - return f"{protected_by_guard} was killed by the werewolves." - - if saved_by_witch: - return f"{saved_by_witch} was saved by the witch." - - if poisoned_by_witch: - return f"{poisoned_by_witch} was poisoned by the witch." - - if killed_by_werewolves: - return f"{killed_by_werewolves} was killed by the werewolves." - - class SummarizeDay(Action): """consider all votes at day, conclude which player dies""" @@ -205,12 +143,9 @@ class AnnounceGameResult(Action): async def run(self, winner: str): return f"Game over! The winner is the {winner}" - async def main(): rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) - rst2 = await SummarizeNight().run({"killed_by_werewolves": "Player1"}) print(rst1) - print(rst2) if __name__ == '__main__': asyncio.run(main()) diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 38e6b2374..df4cf48bf 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -1,3 +1,5 @@ +import re + from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger @@ -9,67 +11,55 @@ class BasePlayer(Role): self, name: str = "PlayerXYZ", profile: str = "BasePlayer", - team: str = "good guys", special_action_names: list[str] = [], **kwargs, ): super().__init__(name, profile, **kwargs) self._init_actions([Speak]) self._watch([InstructSpeak]) - self.team = team - # 调用 get_status() 来检查存活状态,并通过 set_status() 更新状态。 + # 通过 set_status() 更新状态。 self.status = 0 # 0代表活着,1代表死亡 - + # 技能和监听配置 self._watch([InstructSpeak]) # 监听Moderator的指令以做行动 special_actions = [ACTIONS[action_name] for action_name in special_action_names] capable_actions = [Speak] + special_actions self._init_actions(capable_actions) # 给角色赋予行动技能 self.special_actions = special_actions - + async def _observe(self) -> int: if self.status == 1: # 死者不再参与游戏 return 0 - + await super()._observe() # 只有发给全体的("")或发给自己的(self.profile)消息需要走下面的_react流程, # 其他的收听到即可,不用做动作 self._rc.news = [msg for msg in self._rc.news if msg.send_to in ["", self.profile]] return len(self._rc.news) - + async def _think(self): news = self._rc.news[0] assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 if not news.restricted_to: # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) self._rc.todo = Speak() - elif self.profile in news.restricted_to.split(","): # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" + elif self.profile in news.restricted_to.split(","): + # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" # Moderator加密发给自己的,意味着要执行角色的特殊动作 self._rc.todo = self.special_actions[0]() async def _act(self): """每个角色要改写此函数以实现该角色的动作""" raise NotImplementedError - + def get_all_memories(self) -> str: memories = self._rc.memory.get() + time_stamp_pattern = r'[0-9]+ \| ' # NOTE: 除Moderator外,其他角色使用memory,只能用m.sent_from(玩家名)不能用m.role(玩家角色),因为他们不知道说话者的身份 - memories = [f"{m.sent_from}: {m.content}" for m in memories] + memories = [f"{m.sent_from}: {re.sub(time_stamp_pattern, '', m.content)}" for m in memories] # regex去掉时间戳 memories = "\n".join(memories) return memories - - def get_name(self): - return self.name - - def get_profile(self): - return self.profile - - def get_team(self): - return self.team - - def get_status(self): - return self.status def set_status(self, new_status): self.status = new_status diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index 89d22c153..1f53dd795 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -8,12 +8,11 @@ class Guard(BasePlayer): self, name: str = "", profile: str = "Guard", - team: str = "good guys", special_action_names: list[str] = ["Protect"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) - + super().__init__(name, profile, special_action_names, **kwargs) + async def _act(self): # todo为_think时确定的,有两种情况,Speak或Protect todo = self._rc.todo @@ -30,7 +29,7 @@ class Guard(BasePlayer): content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", ) - + elif isinstance(todo, Protect): rsp = await todo.run(context=memories) msg = Message( @@ -38,9 +37,7 @@ class Guard(BasePlayer): cause_by=Protect, send_to="", restricted_to=f"Moderator,{self.profile}", # 给Moderator发送守卫要保护的人加密消息 ) - + logger.info(f"{self._setting}: {rsp}") return msg - - diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 9d0323221..c02400918 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -1,4 +1,3 @@ -import asyncio import re from collections import Counter @@ -13,9 +12,6 @@ from metagpt.actions import BossRequirement as UserRequirement class Moderator(Role): - # 游戏状态属性 - is_game_over = False - winner = None def __init__( self, @@ -35,7 +31,7 @@ class Moderator(Role): self.winner = None self.witch_poison_left = 1 self.witch_antidote_left = 1 - + # player states of current night self.player_hunted = None self.player_protected = None @@ -48,7 +44,7 @@ class Moderator(Role): self.werewolf_players = re.findall(r"Player[0-9]+: Werewolf", game_setup) self.werewolf_players = [p.replace(": Werewolf", "") for p in self.werewolf_players] self.good_guys = [p for p in self.living_players if p not in self.werewolf_players] - + def update_player_status(self, player_names: list[str]): if not player_names: return @@ -72,15 +68,15 @@ class Moderator(Role): logger.info(self.step_idx) latest_msg = memories[-1] + latest_msg_content = latest_msg.content - match = re.search(r"Player[0-9]+", latest_msg.content[-10:]) + match = re.search(r"Player[0-9]+", latest_msg_content[-10:]) # FIXME: hard code truncation target = match.group(0) if match else "" # default return msg_content = "Understood" restricted_to = "" - source_role = latest_msg.role msg_cause_by = latest_msg.cause_by if msg_cause_by == Hunt: self.player_hunted = target @@ -94,14 +90,18 @@ class Moderator(Role): msg_content = f"{target} is a good guy" restricted_to = "Moderator,Seer" elif msg_cause_by == Save: - if not self.witch_antidote_left and latest_msg.content != "Pass": + if "pass" in latest_msg_content.lower(): + pass + elif not self.witch_antidote_left: msg_content = "You have no antidote left and thus can not save the player" restricted_to = "Moderator,Witch" else: self.witch_antidote_left -= 1 - self.is_hunted_player_saved = latest_msg.content == "Save" + self.is_hunted_player_saved = True elif msg_cause_by == Poison: - if not self.witch_poison_left and latest_msg.content != "Pass": + if "pass" in latest_msg_content.lower(): + pass + elif not self.witch_poison_left: msg_content = "You have no poison left and thus can not poison the player" restricted_to = "Moderator,Witch" else: @@ -140,7 +140,7 @@ class Moderator(Role): self.living_players = [p for p in self.living_players if p not in self.player_current_dead] self.update_player_status(self.player_current_dead) msg_content = "Voting done" - + # game's termination condition living_werewolf = [p for p in self.werewolf_players if p in self.living_players] living_good_guys = [p for p in self.good_guys if p in self.living_players] @@ -156,7 +156,7 @@ class Moderator(Role): if self.winner is not None: self._rc.todo = AnnounceGameResult() return - + latest_msg = self._rc.memory.get()[-1] if latest_msg.role in ["User"]: # 上一轮消息是用户指令,解析用户指令,开始游戏 @@ -181,18 +181,19 @@ class Moderator(Role): # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: # 进行完一夜一日的循环,打印一次完整发言历史 + logger.info("a night and day cycle completed, examine all history") print(self.get_all_memories()) # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak() - msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 + # msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) elif isinstance(todo, ParseSpeak): msg_content, msg_restriced_to = await self._parse_speak(memories) - msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 + # msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak, send_to="", restricted_to=msg_restriced_to) diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index 7003bb2ad..3d9bdf92e 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -10,11 +10,10 @@ class Seer(BasePlayer): self, name: str = "", profile: str = "Seer", - team: str = "good guys", special_action_names: list[str] = ["Verify"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) + super().__init__(name, profile, special_action_names, **kwargs) async def _act(self): todo = self._rc.todo diff --git a/examples/werewolf_game/roles/villager.py b/examples/werewolf_game/roles/villager.py index 6b1c6e50c..ececa96ea 100644 --- a/examples/werewolf_game/roles/villager.py +++ b/examples/werewolf_game/roles/villager.py @@ -8,11 +8,10 @@ class Villager(BasePlayer): self, name: str = "", profile: str = "Villager", - team: str = "good guys", special_action_names: list[str] = [], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) + super().__init__(name, profile, special_action_names, **kwargs) async def _act(self): @@ -34,5 +33,5 @@ class Villager(BasePlayer): ) logger.info(f"{self._setting}: {rsp}") - + return msg diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py index a4187d38c..4a7393d77 100644 --- a/examples/werewolf_game/roles/werewolf.py +++ b/examples/werewolf_game/roles/werewolf.py @@ -8,12 +8,11 @@ class Werewolf(BasePlayer): self, name: str = "", profile: str = "Werewolf", - team: str = "werewolves", special_action_names: list[str] = ["Hunt"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) - + super().__init__(name, profile, special_action_names, **kwargs) + async def _act(self): # todo为_think时确定的,有两种情况,Speak或Hunt todo = self._rc.todo @@ -30,7 +29,7 @@ class Werewolf(BasePlayer): content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", ) - + elif isinstance(todo, Hunt): rsp = await todo.run(context=memories) msg = Message( @@ -38,7 +37,7 @@ class Werewolf(BasePlayer): cause_by=Hunt, send_to="", restricted_to=f"Moderator,{self.profile}", # 给Moderator及狼阵营发送要杀的人的加密消息 ) - + logger.info(f"{self._setting}: {rsp}") return msg diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py index 2111bd01d..13b677d7e 100644 --- a/examples/werewolf_game/roles/witch.py +++ b/examples/werewolf_game/roles/witch.py @@ -8,12 +8,11 @@ class Witch(BasePlayer): self, name: str = "", profile: str = "Witch", - team: str = "good guys", special_action_names: list[str] = ["Save", "Poison"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) - + super().__init__(name, profile, special_action_names, **kwargs) + async def _think(self): # 女巫涉及两个特殊技能,因此在此需要改写_think进行路由 news = self._rc.news[0] @@ -21,7 +20,8 @@ class Witch(BasePlayer): if not news.restricted_to: # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) self._rc.todo = Speak() - elif self.profile in news.restricted_to.split(","): # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" + elif self.profile in news.restricted_to.split(","): + # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" # Moderator加密发给自己的,意味着要执行角色的特殊动作 # 这里用关键词进行动作的选择,需要Moderator侧的指令进行配合 if "save" in news.content.lower(): diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 83d3b3385..541066117 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -1,46 +1,45 @@ import asyncio import platform import fire +import random from examples.werewolf_game.werewolf_game import WerewolfGame from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard, Seer, Witch -DEFAULT_PLAYER_SETUP = """ -Game setup: -Player1: Villager, -Player2: Villager, -Player3: Werewolf, -Player4: Werewolf, -Player5: Guard, -Player6: Seer, -Player7: Witch. -""" +def init_game_setup(shuffle=False): + roles = [ + Villager, + Villager, + Werewolf, + Werewolf, + Guard, + Seer, + Witch + ] + if shuffle: + random.seed(2023) + random.shuffle(roles) + players = [role(name=f"Player{i+1}") for i, role in enumerate(roles)] + game_setup = ["Game setup:"] + [f"{player.name}: {player.profile}," for player in players] + game_setup = "\n".join(game_setup) + return game_setup, players -async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): +async def start_game(investment: float = 3.0, n_round: int = 5): game = WerewolfGame() - game.hire([ - Moderator(), - Villager(name="Player1"), - Villager(name="Player2"), - Werewolf(name="Player3"), - Werewolf(name="Player4"), - Guard(name="Player5"), - Seer(name="Player6"), - Witch(name="Player7"), - ]) + game_setup, players = init_game_setup(shuffle=True) + players = [Moderator()] + players + game.hire(players) game.invest(investment) - game.start_project(idea) + game.start_project(game_setup) await game.run(n_round=n_round) - -def main(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 100): +def main(investment: float = 3.0, n_round: int = 100): """ - :param idea: game config instructions :param investment: contribute a certain dollar amount to watch the debate :param n_round: maximum rounds of the debate :return: """ - asyncio.run(start_game(idea, investment, n_round)) + asyncio.run(start_game(investment, n_round)) if __name__ == '__main__': diff --git a/examples/werewolf_game/werewolf_game.py b/examples/werewolf_game/werewolf_game.py index 7e743772c..feb2dca77 100644 --- a/examples/werewolf_game/werewolf_game.py +++ b/examples/werewolf_game/werewolf_game.py @@ -5,6 +5,20 @@ from metagpt.schema import Message class WerewolfEnvironment(Environment): + timestamp: int = 0 + + def publish_message(self, message: Message, add_timestamp: bool = True): + """向当前环境发布信息 + Post information to the current environment + """ + # self.message_queue.put(message) + if add_timestamp: + # 因消息内容可能重复,例如,连续两晚杀同一个人, + # 因此需要加一个unique的time_stamp以使得相同的message在加入记忆时不被自动去重 + message.content = f"{self.timestamp} | " + message.content + self.memory.add(message) + self.history += f"\n{message}" + async def run(self, k=1): """处理一次所有信息的运行,各角色顺序执行 Process all Role runs at once @@ -12,6 +26,7 @@ class WerewolfEnvironment(Environment): for _ in range(k): for role in self.roles.values(): await role.run() + self.timestamp += 1 class WerewolfGame(SoftwareCompany): """Use the "software company paradigm" to hold a werewolf game""" @@ -24,4 +39,3 @@ class WerewolfGame(SoftwareCompany): self.environment.publish_message( Message(role="User", content=idea, cause_by=UserRequirement, restricted_to="Moderator") ) - print("a") From 91c6c6b200a6f7f9b9e9a44240ef51e51ea2567e Mon Sep 17 00:00:00 2001 From: yzlin Date: Sat, 30 Sep 2023 23:36:54 +0800 Subject: [PATCH 18/47] example of modifying role strategy --- examples/werewolf_game/actions/__init__.py | 3 ++- .../werewolf_game/actions/werewolf_actions.py | 22 +++++++++++++++++++ examples/werewolf_game/roles/moderator.py | 3 +++ examples/werewolf_game/roles/werewolf.py | 5 +++-- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index cf358e92b..16b9391b3 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,6 +1,6 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak from examples.werewolf_game.actions.common_actions import Speak -from examples.werewolf_game.actions.werewolf_actions import Hunt +from examples.werewolf_game.actions.werewolf_actions import Hunt, Impersonate from examples.werewolf_game.actions.guard_actions import Protect from examples.werewolf_game.actions.seer_actions import Verify from examples.werewolf_game.actions.witch_actions import Save, Poison @@ -12,4 +12,5 @@ ACTIONS = { "Verify": Verify, "Save": Save, "Poison": Poison, + "Impersonate": Impersonate, } diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index 0a750a02d..410221cb0 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -1,4 +1,5 @@ from metagpt.actions import Action +from examples.werewolf_game.actions.common_actions import Speak class Hunt(Action): """Action: choose a villager to kill""" @@ -23,3 +24,24 @@ class Hunt(Action): # rsp = "Kill Player 1" return rsp + +class Impersonate(Speak): + """Action: werewolf impersonating a good guy in daytime speak""" + + PROMPT_TEMPLATE = """ + ## BACKGROUND + It's a Werewolf game, you are {profile}, say whatever possible to increase your chance of win, + ## HISTORY + You have knowledge to the following conversation: + {context} + ## ATTENTION: Try continuously impersonating a role with special ability, such as a Seer or a Witch, in order to mislead + other players, make them trust you, and thus hiding your werewolf identity + ## YOUR TURN + Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, + 1. If the instruction is to speak, speak in 100 words; + 2. If the instruction is to vote, you MUST vote and ONLY say "I vote to eliminate PlayerX", where X is the player index, DO NOT include any other words. + Your will say: + """ + + def __init__(self, name="Impersonate", context=None, llm=None): + super().__init__(name, context, llm) diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index c02400918..df0b870cd 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -1,6 +1,7 @@ import re from collections import Counter +from metagpt.const import WORKSPACE_ROOT from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger @@ -183,6 +184,8 @@ class Moderator(Role): # 进行完一夜一日的循环,打印一次完整发言历史 logger.info("a night and day cycle completed, examine all history") print(self.get_all_memories()) + with open(WORKSPACE_ROOT / 'werewolf_transcript.txt', "w") as f: + f.write(self.get_all_memories()) # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py index 4a7393d77..5426ca7c4 100644 --- a/examples/werewolf_game/roles/werewolf.py +++ b/examples/werewolf_game/roles/werewolf.py @@ -1,5 +1,5 @@ from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak, Hunt +from examples.werewolf_game.actions import Speak, Hunt, Impersonate from metagpt.schema import Message from metagpt.logs import logger @@ -24,7 +24,8 @@ class Werewolf(BasePlayer): # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, context=memories) + # rsp = await todo.run(profile=self.profile, context=memories) + rsp = await Impersonate().run(profile=self.profile, context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", From 81e8ee83fbb206745b3999cd961a98465e28bc80 Mon Sep 17 00:00:00 2001 From: yzlin Date: Sun, 1 Oct 2023 11:47:59 +0800 Subject: [PATCH 19/47] small bug fixed --- examples/werewolf_game/roles/moderator.py | 31 +++++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index df0b870cd..95af332db 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -109,8 +109,15 @@ class Moderator(Role): self.witch_poison_left -= 1 self.player_poisoned = target # "" if not poisoned and "PlayerX" if poisoned + return msg_content, restricted_to + + def _update_game_states(self, memories): + step_idx = self.step_idx % len(STEP_INSTRUCTIONS) - if step_idx == 13: # FIXME: hard code + if step_idx not in [15, 18]: # FIXME: hard code + return + + if step_idx == 15: # FIXME: hard code # night ends: after all special roles acted, process the whole night self.player_current_dead = [] # reset @@ -136,11 +143,9 @@ class Moderator(Role): if not voted: continue voted_all.append(voted.group(0)) - # breakpoint() self.player_current_dead = [Counter(voted_all).most_common()[0][0]] # 平票时,杀序号小的 self.living_players = [p for p in self.living_players if p not in self.player_current_dead] self.update_player_status(self.player_current_dead) - msg_content = "Voting done" # game's termination condition living_werewolf = [p for p in self.werewolf_players if p in self.living_players] @@ -150,7 +155,12 @@ class Moderator(Role): elif not living_good_guys: self.winner = "werewolf" - return msg_content, restricted_to + def _record_game_history(self): + if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: + logger.info("a night and day cycle completed, examine all history") + print(self.get_all_memories()) + with open(WORKSPACE_ROOT / 'werewolf_transcript.txt', "w") as f: + f.write(self.get_all_memories()) async def _think(self): @@ -179,13 +189,12 @@ class Moderator(Role): logger.info(f"{self._setting} ready to {todo}") memories = self.get_all_memories(mode="msg") - # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) - if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: - # 进行完一夜一日的循环,打印一次完整发言历史 - logger.info("a night and day cycle completed, examine all history") - print(self.get_all_memories()) - with open(WORKSPACE_ROOT / 'werewolf_transcript.txt', "w") as f: - f.write(self.get_all_memories()) + + # 若进行完一夜一日的循环,打印和记录一次完整发言历史 + self._record_game_history() + + # 若一晚或一日周期结束,对当晚或当日的死者进行总结,并更新游戏状态 + self._update_game_states(memories) # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): From 91faec6bb18427c5ff9a0e22be20c63dfbe8076f Mon Sep 17 00:00:00 2001 From: yzlin Date: Sun, 1 Oct 2023 23:10:59 +0800 Subject: [PATCH 20/47] add human player --- examples/werewolf_game/roles/human_player.py | 40 ++++++++++++++++++++ examples/werewolf_game/roles/seer.py | 2 +- examples/werewolf_game/start_game.py | 24 +++++++++--- 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 examples/werewolf_game/roles/human_player.py diff --git a/examples/werewolf_game/roles/human_player.py b/examples/werewolf_game/roles/human_player.py new file mode 100644 index 000000000..8b04ae821 --- /dev/null +++ b/examples/werewolf_game/roles/human_player.py @@ -0,0 +1,40 @@ +from examples.werewolf_game.actions import Speak +from examples.werewolf_game.roles import BasePlayer +from metagpt.schema import Message +from metagpt.logs import logger + +async def _act(self): + todo = self._rc.todo + + memories = self.get_all_memories() + + input_instruction = f""" + ## As a reminder, you have access to the following game history: + {memories} + ## You are {self.name}({self.profile}) + ## Guidance: + 1. If you are performing a special action or exercising a vote, + end your response with "PlayerX" where X is the player index, e.g., "..., kill/protect/poison/.../vote Player1". + 2. If it is a daytime free speech, you can speak in whatever format. + Now, please speak: + """ + rsp = input(input_instruction) # wait for human input + + msg_cause_by = type(todo) + msg_restricted_to = "" if isinstance(todo, Speak) \ + else f"Moderator,{self.profile}" + + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=msg_cause_by, send_to="", + restricted_to=msg_restricted_to, # 给Moderator及自身阵营发送加密消息 + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg + +def prepare_human_player(player_class: BasePlayer): + # Dynamically define a human player class that inherits from a certain role class + HumanPlayer = type('HumanPlayer', (player_class,), {'_act': _act}) + return HumanPlayer diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index 3d9bdf92e..1d58b70bf 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -35,7 +35,7 @@ class Seer(BasePlayer): msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Verify, send_to="", - restricted_to="Moderator", + restricted_to=f"Moderator,{self.profile}", ) logger.info(f"{self._setting}: {rsp}") diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 541066117..12552452c 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -3,10 +3,12 @@ import platform import fire import random +from metagpt.logs import logger from examples.werewolf_game.werewolf_game import WerewolfGame from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard, Seer, Witch +from examples.werewolf_game.roles.human_player import prepare_human_player -def init_game_setup(shuffle=False): +def init_game_setup(shuffle=True, add_human=False): roles = [ Villager, Villager, @@ -17,29 +19,39 @@ def init_game_setup(shuffle=False): Witch ] if shuffle: - random.seed(2023) + # random.seed(2023) random.shuffle(roles) + if add_human: + assigned_role_idx = random.randint(0, len(roles) - 1) + assigned_role = roles[assigned_role_idx] + roles[assigned_role_idx] = prepare_human_player(assigned_role) + players = [role(name=f"Player{i+1}") for i, role in enumerate(roles)] + + if add_human: + logger.info(f"You are assigned {players[assigned_role_idx].name}({players[assigned_role_idx].profile})") + game_setup = ["Game setup:"] + [f"{player.name}: {player.profile}," for player in players] game_setup = "\n".join(game_setup) + return game_setup, players -async def start_game(investment: float = 3.0, n_round: int = 5): +async def start_game(investment: float = 3.0, n_round: int = 5, shuffle : bool = True, add_human: bool = False): game = WerewolfGame() - game_setup, players = init_game_setup(shuffle=True) + game_setup, players = init_game_setup(shuffle=shuffle, add_human=add_human) players = [Moderator()] + players game.hire(players) game.invest(investment) game.start_project(game_setup) await game.run(n_round=n_round) -def main(investment: float = 3.0, n_round: int = 100): +def main(investment: float = 3.0, n_round: int = 100, shuffle : bool = True, add_human: bool = False): """ :param investment: contribute a certain dollar amount to watch the debate :param n_round: maximum rounds of the debate :return: """ - asyncio.run(start_game(investment, n_round)) + asyncio.run(start_game(investment, n_round, shuffle, add_human)) if __name__ == '__main__': From ef81791f3268a519751e7ca0aad3b5d902a187ee Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:42:31 +0800 Subject: [PATCH 21/47] =?UTF-8?q?#=20feat=EF=BC=9Aconvert=20txt=20file=20t?= =?UTF-8?q?o=20json=20for=20evals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/werewolf_game/evals/utils.py | 123 ++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 examples/werewolf_game/evals/utils.py diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py new file mode 100644 index 000000000..8705c1992 --- /dev/null +++ b/examples/werewolf_game/evals/utils.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + + +""" +__info:"utils for eval" +__author:"[Aria](https://github.com/ariafyy)" +__update:"convert txt to json" +""" +from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT +import re, json + + +class Utils(object): + def __init__(self): + pass + + def _action(self, text: str) -> str: + """ + # get action + input: I vote to eliminate Player3 + output: vote + """ + text = text.lower() + if "vote" in text: + action = "vote" + return action + if "verify" in text: + action = "verify" + return action + if "kill" in text: + action = "kill" + return action + else: + action = "chat" + return action + + def _life(self, text: str) -> str: + """ + # get life + input: Kill Player6 + output: dead + """ + text = text.lower() + if re.search(r'\b(eliminated|killed|kill)\b', text, re.I): + life = 'dead' + dead_role = re.findall(r'\[(.*?)\]', text) + if re.search(r'no one was killed', text, re.I): + return "alive", [] + else: + return life, dead_role + else: + life = "alive" + return life, [] + + def txt2data(self, file): + """ + input: .txt file + output: data for json format + """ + result = {} + count = 0 + day = -1 + flag = False + + with open(file, "r") as f: + lines = f.readlines() + for line in lines: + if "Moderator(Moderator): 0" in line: + flag = True + if flag: + if "It’s dark, everyone close your eyes." in line: + day += 1 + count = 0 + data = {} + parts = line.split("|") + data["role"] = parts[0].strip().split(":")[0] + data["day"] = day + data["turn"] = count + if len(parts) > 1: + data["text"] = parts[1].strip() + data["action"] = self._action(data["text"]) + data["life"], data["dead_role"] = self._life(data["text"]) + else: + continue + key = "day_{}".format(day) + if key not in result: + result[key] = [] + result[key].append(data) + count += 1 + return result + + def data2json(self, in_file): + """ + input:.txt + output:.json + + + output examples: + { + "day_0": [ + { + "role": "Moderator(Moderator)", + "day": 0, + "turn": 0, + "text": "It’s dark, everyone close your eyes. I will talk with you/your team secretly at night.", + "action": "chat", + "life": "alive", + "dead_role": [] + },{}] + ... + } + """ + result = self.txt2data(in_file) + with open(WORKSPACE_ROOT / 'werewolf_transcript.json', "w", encoding='utf-8') as f: + json.dump(result, f, ensure_ascii=False, indent=2) + f.write('\n') + + +if __name__ == '__main__': + txt_path = WORKSPACE_ROOT / "werewolf_transcript.txt" + log_path = PROJECT_ROOT / "logs/log.txt" + Utils().data2json(txt_path) From b8743fc8906675bb66201e9dd23ca1b25d934f28 Mon Sep 17 00:00:00 2001 From: MrL <332199893@qq.com> Date: Wed, 4 Oct 2023 11:12:23 +0800 Subject: [PATCH 22/47] Changes --- .../werewolf_game/actions/common_actions.py | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index 84936ebde..e69de29bb 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -1,30 +0,0 @@ -from metagpt.actions import Action - -class Speak(Action): - """Action: Any speak action in a game""" - - PROMPT_TEMPLATE = """ - ## BACKGROUND - It's a Werewolf game, you are {profile}, say whatever possible to increase your chance of win, - ## HISTORY - You have knowledge to the following conversation: - {context} - ## YOUR TURN - Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, - 1. If the instruction is to speak, speak in 100 words; - 2. If the instruction is to vote, you MUST vote and ONLY say "I vote to eliminate PlayerX", where X is the player index, DO NOT include any other words. - Your will say: - """ - - def __init__(self, name="Speak", context=None, llm=None): - super().__init__(name, context, llm) - - async def run(self, context: str, profile: str): - - prompt = self.PROMPT_TEMPLATE.format(context=context, profile=profile) - - rsp = await self._aask(prompt) - - return rsp - - From 17a0bd5de1b16a2f154998b9fcd07ac86b5cf71b Mon Sep 17 00:00:00 2001 From: MrL <332199893@qq.com> Date: Wed, 4 Oct 2023 11:14:16 +0800 Subject: [PATCH 23/47] Changes --- .../werewolf_game/actions/common_actions.py | 208 ++++++++++++++++++ .../werewolf_game/actions/werewolf_actions.py | 63 +++--- 2 files changed, 235 insertions(+), 36 deletions(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index e69de29bb..14e3cc0c0 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -0,0 +1,208 @@ +from metagpt.actions import Action +import json +from metagpt.const import WORKSPACE_ROOT + + +class Speak(Action): + """Action: Any speak action in a game""" + + # PROMPT_TEMPLATE = """ + # ## BACKGROUND + # It's a Werewolf game, you are {profile}, say whatever possible to increase your chance of win, + # ## HISTORY + # You have knowledge to the following conversation: + # {context} + # ## YOUR TURN + # Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, + # 1. If the instruction is to speak, speak in 100 words; + # 2. If the instruction is to vote, you MUST vote and ONLY say "I vote to eliminate PlayerX", where X is the player index, DO NOT include any other words. + # Your will say: + # """ + + PROMPT_TEMPLATE = """ + { + "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" + ,"HISTORY": "You have knowledge to the following conversation: __context__" + ,"ATTENTION": "You can not VOTE a player who is NOT ALIVE now! And be careful of revealing your identity !" + ,"YOUR_TURN": "Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, + 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; + 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', where X is the player index. + DO NOT include any other words. + " + ,"OUTPUT_FORMAT": + { + "ROLE": "Your role." + ,"NUMBER": "Your player number." + ,"IDENTITY": "You are? What is you identity? You are player1 or player2 or player3 or player4 or player5 or player6 or player7?" + ,"LIVING_PLAYERS": "List the players who is alive. Return a LIST datatype." + ,"THOUGHTS": "It is day time. Return the thinking steps of your decision of giving VOTE to other player from `LIVING_PLAYERS`. And return the reason why you choose to VOTE this player from `LIVING_PLAYERS`." + ,"SPEECH_OR_VOTE": "Follow the instruction of `YOUR_TURN` above and the `THOUGHTS` you have just now, give a speech or your vote." + } + + } + """ + + def __init__(self, name="Speak", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str, profile: str): + + # prompt = self.PROMPT_TEMPLATE.format(context=context, profile=profile) + prompt = self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) + + rsp = await self._aask(prompt) + re_run = 2 + while re_run > 0: + try: + rsp_json = json.loads(rsp) + break + except: + re_run -= 1 + + with open(WORKSPACE_ROOT / 'speak.txt', 'a') as f: + f.write(rsp) + + return rsp_json['SPEECH_OR_VOTE'] + + +class NighttimeWhispers(Action): + """ + + Action: nighttime whispers with thinking processes + + Usage Example: + + class Hunt(NighttimeWhispers): + ROLE = "Werewolf" + ACTION = "KILL" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True + + class Protect(NighttimeWhispers): + ROLE = "Guard" + ACTION = "PROTECT" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True + + class Verify(NighttimeWhispers): + ROLE = "Seer" + ACTION = "VERIFY" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True + + class Save(NighttimeWhispers): + ROLE = "Witch" + ACTION = "SAVE" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True + + def subclass_renew_prompt(self, prompt_json): + del prompt_json['ACTION'] + del prompt_json['ATTENTION'] + + prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST be killed at this night." + prompt_json["OUTPUT_FORMAT"]["OUTPUT"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." + + return prompt_json + + class Poison(NighttimeWhispers): + ROLE = "Witch" + ACTION = "POISON" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True + + def subclass_renew_prompt(self, prompt_json): + prompt_json["OUTPUT_FORMAT"]["OUTPUT"] += "Or if you want to PASS, then return PASS." + return prompt_json + + """ + + ROLE = "Werewolf" + ACTION = "KILL" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True + PROMPT_TEMPLATE = """ + { + "ROLE": "__role__" + ,"ACTION": "Choose one living player to __action__." + ,"ATTENTION": "You can only __action__ a player who is alive at this night! And you can not __action__ a player who is dead as this night!" + ,"PHASE": "Night" + ,"BACKGROUND": "It's a werewolf game and you are a __role__. Here's the game history:{__context__}." + ,"OUTPUT_FORMAT": + { + "ROLE": "Your role." + ,"NUMBER": "Your player number." + ,"IDENTITY": "You are? What is you identity? You are player1 or player2 or player3 or player4 or player5 or player6 or player7?" + ,"LIVING_PLAYERS": "List the players who is alive. Return a LIST datatype." + ,"THOUGHTS": "It is night time. Return the thinking steps of your decision of choosing one living player from `LIVING_PLAYERS` to __action__ this night. And return the reason why you choose to __action__ this player." + ,"OUTPUT": "As a __role__, you should choose one living player from `LIVING_PLAYERS` to __action__ this night according to the THOUGHTS you have just now. Return the number of the player you choose and return this NUMBER ONLY." + } + } + """ + + def __init__(self, name="NightTimeWhispers", context=None, llm=None): + super().__init__(name, context, llm) + + def _renew_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): + + def replace_string(prompt_json: dict): + k: str + for k in prompt_json.keys(): + if isinstance(prompt_json[k], dict): + prompt_json[k] = replace_string(prompt_json[k]) + continue + prompt_json[k] = prompt_json[k].replace("__role__", role) + prompt_json[k] = prompt_json[k].replace("__action__", action) + + return prompt_json + + prompt_json = replace_string(prompt_json) + + prompt_json["BACKGROUND"] = prompt_json["BACKGROUND"].replace("__context__", context) + + return prompt_json + + def subclass_renew_prompt(self, prompt_json: dict): + return prompt_json + + async def run(self, context: str): + """ + Note: `final_prompt` could be undefined and will raise error if `IF_RENEW` is true and `IF_JSON_INPUT` is False + """ + + if not self.IF_RENEW: + final_prompt = self.PROMPT_TEMPLATE.replace("__context__", context) + rsp_content = await self._aask(final_prompt) + return rsp_content + + if self.IF_JSON_INPUT: + prompt_json = json.loads(self.PROMPT_TEMPLATE) + prompt_json = self._renew_prompt_json(prompt_json=prompt_json, role=self.ROLE, action=self.ACTION, + context=context) + prompt_json = self.subclass_renew_prompt(prompt_json) # can be defined in subclass + final_prompt = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) + + rsp_content = await self._aask(final_prompt) + + with open(WORKSPACE_ROOT / f'{self.ACTION}.txt', 'a') as f: + f.write(rsp_content) + + if self.IF_JSON_OUTPUT: + re_run = 2 + while re_run > 0: + try: + rsp = json.loads(rsp_content) + break + except: + re_run -= 1 + return f"{self.ACTION} Player" + str(rsp["OUTPUT"]) + + return rsp_content + + diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index 410221cb0..cb30ca7eb 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -1,46 +1,37 @@ from metagpt.actions import Action -from examples.werewolf_game.actions.common_actions import Speak +from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispers -class Hunt(Action): - """Action: choose a villager to kill""" - - PROMPT_TEMPLATE = """ - It's a werewolf game and you are a werewolf, - this is game history: - {context}. - Attention: if your previous werewolf has chosen, follow its choice. - Format: "Kill PlayerX", where X is the player index. - Now, choose one to kill, you will: - """ - - def __init__(self, name="Hunt", context=None, llm=None): - super().__init__(name, context, llm) - - async def run(self, context: str): - - prompt = self.PROMPT_TEMPLATE.format(context=context) - - rsp = await self._aask(prompt) - # rsp = "Kill Player 1" - - return rsp +class Hunt(NighttimeWhispers): + ROLE = "Werewolf" + ACTION = "KILL" + IF_RENEW = True + IF_JSON_INPUT = True + IF_JSON_OUTPUT = True class Impersonate(Speak): """Action: werewolf impersonating a good guy in daytime speak""" PROMPT_TEMPLATE = """ - ## BACKGROUND - It's a Werewolf game, you are {profile}, say whatever possible to increase your chance of win, - ## HISTORY - You have knowledge to the following conversation: - {context} - ## ATTENTION: Try continuously impersonating a role with special ability, such as a Seer or a Witch, in order to mislead - other players, make them trust you, and thus hiding your werewolf identity - ## YOUR TURN - Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, - 1. If the instruction is to speak, speak in 100 words; - 2. If the instruction is to vote, you MUST vote and ONLY say "I vote to eliminate PlayerX", where X is the player index, DO NOT include any other words. - Your will say: + { + "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" + ,"HISTORY": "You have knowledge to the following conversation: __context__" + ,"ATTENTION": "Try continuously impersonating a role with special ability, such as a Seer or a Witch, in order to mislead + other players, make them trust you, and thus hiding your werewolf identity. You can not VOTE a player who is NOT ALIVE now!" + ,"YOUR_TURN": "Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, + 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; + 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', where X is the player index. + DO NOT include any other words. + " + ,"OUTPUT_FORMAT": + { + "ROLE": "Your role." + ,"NUMBER": "Your player number." + ,"IDENTITY": "You are? What is you identity? You are player1 or player2 or player3 or player4 or player5 or player6 or player7?" + ,"LIVING_PLAYERS": "List the players who is alive. Return a LIST datatype." + ,"THOUGHTS": "It is day time. Return the thinking steps of your decision of giving VOTE to other player from `LIVING_PLAYERS`. And return the reason why you choose to VOTE this player from `LIVING_PLAYERS`." + ,"SPEECH_OR_VOTE": "Follow the instruction of `YOUR_TURN` above and the `THOUGHTS` you have just now, give a speech or your vote. Remember, you are a WEREWOLF!!! But just keep it in mind, don't tell other players." + } + } """ def __init__(self, name="Impersonate", context=None, llm=None): From e694848b48ed63f74fba73d0deeeed480be811c1 Mon Sep 17 00:00:00 2001 From: MrL <332199893@qq.com> Date: Wed, 4 Oct 2023 11:31:05 +0800 Subject: [PATCH 24/47] make roles to speak their mind while they act --- examples/werewolf_game/actions/common_actions.py | 1 - examples/werewolf_game/actions/werewolf_actions.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index 14e3cc0c0..af477d397 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -64,7 +64,6 @@ class Speak(Action): return rsp_json['SPEECH_OR_VOTE'] - class NighttimeWhispers(Action): """ diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index cb30ca7eb..74905ff16 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -1,6 +1,7 @@ from metagpt.actions import Action from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispers + class Hunt(NighttimeWhispers): ROLE = "Werewolf" ACTION = "KILL" From 546c58aa6a02ba706e780bbf6ce90c1fb3330c98 Mon Sep 17 00:00:00 2001 From: MrL <332199893@qq.com> Date: Wed, 4 Oct 2023 11:35:51 +0800 Subject: [PATCH 25/47] make roles to speak their mind while they act --- examples/werewolf_game/actions/common_actions.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index af477d397..5f56806da 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -6,19 +6,6 @@ from metagpt.const import WORKSPACE_ROOT class Speak(Action): """Action: Any speak action in a game""" - # PROMPT_TEMPLATE = """ - # ## BACKGROUND - # It's a Werewolf game, you are {profile}, say whatever possible to increase your chance of win, - # ## HISTORY - # You have knowledge to the following conversation: - # {context} - # ## YOUR TURN - # Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, - # 1. If the instruction is to speak, speak in 100 words; - # 2. If the instruction is to vote, you MUST vote and ONLY say "I vote to eliminate PlayerX", where X is the player index, DO NOT include any other words. - # Your will say: - # """ - PROMPT_TEMPLATE = """ { "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" From 55794a572bca46077ce439dd987978fa2176f555 Mon Sep 17 00:00:00 2001 From: MrL <332199893@qq.com> Date: Wed, 4 Oct 2023 11:40:41 +0800 Subject: [PATCH 26/47] make roles to speak their mind while they act --- examples/werewolf_game/actions/common_actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index 5f56806da..9a5e628eb 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -34,7 +34,6 @@ class Speak(Action): async def run(self, context: str, profile: str): - # prompt = self.PROMPT_TEMPLATE.format(context=context, profile=profile) prompt = self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) rsp = await self._aask(prompt) From 26ca9641bf291f9cba08ad4bdca390479efaff3e Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:21:30 +0800 Subject: [PATCH 27/47] # fix: merge comments from mg --- examples/werewolf_game/evals/utils.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index 8705c1992..2e83b38f6 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -1,17 +1,13 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- - - -""" -__info:"utils for eval" -__author:"[Aria](https://github.com/ariafyy)" -__update:"convert txt to json" -""" +''' +Filename: MetaGPT/examples/werewolf_game/evals/utils.py +Created Date: Oct 2, 2023 +Author: [Aria](https://github.com/ariafyy) +''' from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT import re, json -class Utils(object): +class Utils: def __init__(self): pass @@ -92,10 +88,6 @@ class Utils(object): def data2json(self, in_file): """ - input:.txt - output:.json - - output examples: { "day_0": [ @@ -111,9 +103,14 @@ class Utils(object): ... } """ + result = self.txt2data(in_file) + self._save_json(result) + return result + + def _save_json(self, data): with open(WORKSPACE_ROOT / 'werewolf_transcript.json', "w", encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) + json.dump(data, f, ensure_ascii=False, indent=2) f.write('\n') From 010d11600baaefc24320ffe87efd58c0670e4c24 Mon Sep 17 00:00:00 2001 From: davidlee21 <36806941+davidlee21@users.noreply.github.com> Date: Sun, 8 Oct 2023 22:02:42 +0800 Subject: [PATCH 28/47] Update the proposal according to the reviewer's suggestions for improvement --- .../werewolf_game/actions/common_actions.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index 9a5e628eb..d54b1957a 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -36,10 +36,10 @@ class Speak(Action): prompt = self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) - rsp = await self._aask(prompt) re_run = 2 while re_run > 0: try: + rsp = await self._aask(prompt) rsp_json = json.loads(rsp) break except: @@ -85,14 +85,14 @@ class NighttimeWhispers(Action): IF_JSON_INPUT = True IF_JSON_OUTPUT = True - def subclass_renew_prompt(self, prompt_json): + def _construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): del prompt_json['ACTION'] del prompt_json['ATTENTION'] prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST be killed at this night." prompt_json["OUTPUT_FORMAT"]["OUTPUT"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." - return prompt_json + return self._default_construct_prompt_json(prompt_json, role, action, context) class Poison(NighttimeWhispers): ROLE = "Witch" @@ -101,9 +101,9 @@ class NighttimeWhispers(Action): IF_JSON_INPUT = True IF_JSON_OUTPUT = True - def subclass_renew_prompt(self, prompt_json): + def _construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): prompt_json["OUTPUT_FORMAT"]["OUTPUT"] += "Or if you want to PASS, then return PASS." - return prompt_json + return self._default_construct_prompt_json(prompt_json, role, action, context) """ @@ -134,7 +134,7 @@ class NighttimeWhispers(Action): def __init__(self, name="NightTimeWhispers", context=None, llm=None): super().__init__(name, context, llm) - def _renew_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): + def _default_construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): def replace_string(prompt_json: dict): k: str @@ -153,12 +153,12 @@ class NighttimeWhispers(Action): return prompt_json - def subclass_renew_prompt(self, prompt_json: dict): - return prompt_json + def _construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): + return self._default_construct_prompt_json(prompt_json, role, action, context) async def run(self, context: str): """ - Note: `final_prompt` could be undefined and will raise error if `IF_RENEW` is true and `IF_JSON_INPUT` is False + Note: `final_prompt` could be undefined and will raise error if `IF_RENEW` is True and `IF_JSON_INPUT` is False """ if not self.IF_RENEW: @@ -168,24 +168,23 @@ class NighttimeWhispers(Action): if self.IF_JSON_INPUT: prompt_json = json.loads(self.PROMPT_TEMPLATE) - prompt_json = self._renew_prompt_json(prompt_json=prompt_json, role=self.ROLE, action=self.ACTION, - context=context) - prompt_json = self.subclass_renew_prompt(prompt_json) # can be defined in subclass + prompt_json = self._construct_prompt_json(prompt_json=prompt_json, role=self.ROLE, action=self.ACTION, + context=context) # can be defined in subclass final_prompt = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) - rsp_content = await self._aask(final_prompt) - - with open(WORKSPACE_ROOT / f'{self.ACTION}.txt', 'a') as f: - f.write(rsp_content) - if self.IF_JSON_OUTPUT: re_run = 2 while re_run > 0: try: + rsp_content = await self._aask(final_prompt) rsp = json.loads(rsp_content) break except: re_run -= 1 + + with open(WORKSPACE_ROOT / f'{self.ACTION}.txt', 'a') as f: + f.write(rsp_content) + return f"{self.ACTION} Player" + str(rsp["OUTPUT"]) return rsp_content From 0c238d2b0d04aaf0bd533ff765d117792fcadf1d Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 9 Oct 2023 14:36:55 +0800 Subject: [PATCH 29/47] update win mechanism --- .../actions/moderator_actions.py | 18 ++++++------- examples/werewolf_game/roles/moderator.py | 25 ++++++++++++++----- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index fc897474e..c0b540a44 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -65,13 +65,13 @@ STEP_INSTRUCTIONS = { 15: {"content": "{player_current_dead} was killed last night!", "send_to": "Moderator", "restricted_to": ""}, - 16: {"content": """Now freely talk about roles of other players with each other based on your observation and - reflection with few sentences. Decide whether to reveal your identity based on your reflection.""", + 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": "", # send to all to speak in daytime "restricted_to": ""}, 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}. Or you can pass. For example: I vote to kill ...""", + {living_players}. Say ONLY: I vote to eliminate ...""", "send_to": "", "restricted_to": ""}, 18: {"content": """{player_current_dead} was eliminated.""", @@ -91,12 +91,12 @@ class InstructSpeak(Action): }) content = instruction_info["content"] if "{living_players}" in content and "{werewolf_players}" in content: - content = content.format(living_players=",".join(living_players), - werewolf_players=",".join(werewolf_players)) + content = content.format(living_players=living_players, + werewolf_players=werewolf_players) if "{living_players}" in content: - content = content.format(living_players=",".join(living_players)) + content = content.format(living_players=living_players) if "{werewolf_players}" in content: - content = content.format(werewolf_players=",".join(werewolf_players)) + content = content.format(werewolf_players=werewolf_players) if "{player_hunted}" in content: content = content.format(player_hunted=player_hunted) if "{player_current_dead}" in content: @@ -140,8 +140,8 @@ class SummarizeDay(Action): class AnnounceGameResult(Action): - async def run(self, winner: str): - return f"Game over! The winner is the {winner}" + async def run(self, winner: str, win_reason: str): + return f"Game over! {win_reason}. The winner is the {winner}" async def main(): rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 95af332db..f4bad6c96 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -24,12 +24,15 @@ class Moderator(Role): self._watch([UserRequirement, InstructSpeak, ParseSpeak]) self._init_actions([InstructSpeak, ParseSpeak, AnnounceGameResult]) self.step_idx = 0 + self.eval_step_idx = [] # game states self.living_players = [] self.werewolf_players = [] - self.good_guys = [] + self.villager_players = [] + self.special_role_players = [] self.winner = None + self.win_reason = None self.witch_poison_left = 1 self.witch_antidote_left = 1 @@ -44,7 +47,10 @@ class Moderator(Role): self.living_players = re.findall(r"Player[0-9]+", game_setup) self.werewolf_players = re.findall(r"Player[0-9]+: Werewolf", game_setup) self.werewolf_players = [p.replace(": Werewolf", "") for p in self.werewolf_players] - self.good_guys = [p for p in self.living_players if p not in self.werewolf_players] + self.villager_players = re.findall(r"Player[0-9]+: Villager", game_setup) + self.villager_players = [p.replace(": Villager", "") for p in self.villager_players] + self.special_role_players = [p for p in self.living_players \ + if p not in self.werewolf_players + self.villager_players] def update_player_status(self, player_names: list[str]): if not player_names: @@ -114,8 +120,10 @@ class Moderator(Role): def _update_game_states(self, memories): step_idx = self.step_idx % len(STEP_INSTRUCTIONS) - if step_idx not in [15, 18]: # FIXME: hard code + if step_idx not in [15, 18] or self.step_idx in self.eval_step_idx: # FIXME: hard code return + else: + self.eval_step_idx.append(self.step_idx) # record evaluation, avoid repetitive evaluation at the same step if step_idx == 15: # FIXME: hard code # night ends: after all special roles acted, process the whole night @@ -135,6 +143,7 @@ class Moderator(Role): self.player_poisoned = None elif step_idx == 18: # FIXME: hard code + print("*" * 10, step_idx) # day ends: after all roles voted, process all votings voting_msgs = memories[-len(self.living_players):] voted_all = [] @@ -144,16 +153,20 @@ class Moderator(Role): continue voted_all.append(voted.group(0)) self.player_current_dead = [Counter(voted_all).most_common()[0][0]] # 平票时,杀序号小的 + # print("*" * 10, "dead", self.player_current_dead) self.living_players = [p for p in self.living_players if p not in self.player_current_dead] self.update_player_status(self.player_current_dead) # game's termination condition living_werewolf = [p for p in self.werewolf_players if p in self.living_players] - living_good_guys = [p for p in self.good_guys if p in self.living_players] + living_villagers = [p for p in self.villager_players if p in self.living_players] + living_special_roles = [p for p in self.special_role_players if p in self.living_players] if not living_werewolf: self.winner = "good guys" - elif not living_good_guys: + self.win_reason = "werewolves all dead" + elif not living_villagers or not living_special_roles: self.winner = "werewolf" + self.win_reason = "villagers all dead" if not living_villagers else "special roles all dead" def _record_game_history(self): if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: @@ -210,7 +223,7 @@ class Moderator(Role): cause_by=ParseSpeak, send_to="", restricted_to=msg_restriced_to) elif isinstance(todo, AnnounceGameResult): - msg_content = await AnnounceGameResult().run(winner=self.winner) + msg_content = await AnnounceGameResult().run(winner=self.winner, win_reason=self.win_reason) msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=AnnounceGameResult) logger.info(f"{self._setting}: {msg_content}") From 343ad8d6533dca07eb862a7720e29aa876cf9b32 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 9 Oct 2023 14:56:56 +0800 Subject: [PATCH 30/47] add more instruction in speak prompt & simplify nighttimewhisper --- .../werewolf_game/actions/common_actions.py | 142 +++++++----------- .../werewolf_game/actions/werewolf_actions.py | 29 +--- examples/werewolf_game/roles/base_player.py | 5 +- examples/werewolf_game/roles/guard.py | 5 +- examples/werewolf_game/roles/seer.py | 4 +- examples/werewolf_game/roles/villager.py | 5 +- examples/werewolf_game/roles/werewolf.py | 9 +- examples/werewolf_game/roles/witch.py | 3 +- 8 files changed, 78 insertions(+), 124 deletions(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index d54b1957a..e37749da2 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -10,36 +10,41 @@ class Speak(Action): { "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" ,"HISTORY": "You have knowledge to the following conversation: __context__" - ,"ATTENTION": "You can not VOTE a player who is NOT ALIVE now! And be careful of revealing your identity !" - ,"YOUR_TURN": "Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, + ,"ATTENTION": "You can NOT VOTE a player who is NOT ALIVE now!" + ,"STRATEGY": __strategy__ + ,"MODERATOR_INSTRUCTION": __latest_instruction__, + ,"RULE": "Please follow the moderator's latest instruction, figure out if you need to speak your opinion or directly to vote, 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; - 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', where X is the player index. - DO NOT include any other words. - " + 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', where X is the player index, DO NOT include any other words." ,"OUTPUT_FORMAT": { - "ROLE": "Your role." - ,"NUMBER": "Your player number." - ,"IDENTITY": "You are? What is you identity? You are player1 or player2 or player3 or player4 or player5 or player6 or player7?" - ,"LIVING_PLAYERS": "List the players who is alive. Return a LIST datatype." - ,"THOUGHTS": "It is day time. Return the thinking steps of your decision of giving VOTE to other player from `LIVING_PLAYERS`. And return the reason why you choose to VOTE this player from `LIVING_PLAYERS`." - ,"SPEECH_OR_VOTE": "Follow the instruction of `YOUR_TURN` above and the `THOUGHTS` you have just now, give a speech or your vote." + "ROLE": "Your role, in this case, __profile__" + ,"PLAYER_NAME": "Your name, in this case, __name__" + ,"LIVING_PLAYERS": "List living players based on MODERATOR_INSTRUCTION. Return a LIST datatype." + ,"THOUGHTS": "Based on `MODERATOR_INSTRUCTION` and `RULE`, carefully think about what to say or vote so that your chance of win as __profile__ maximizes. Return the thinking process." + ,"RESPONSE": "Based on `MODERATOR_INSTRUCTION`, `RULE`, and the 'THOUGHTS' you had, express your opinion or cast a vote." } - } """ + STRATEGY = """ + Decide whether to reveal your identity based on benefits vs. risks, provide useful information, and vote to eliminate the most suspicious. + """ def __init__(self, name="Speak", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, context: str, profile: str): + async def run(self, profile: str, name: str, context: str, latest_instruction: str): - prompt = self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) + prompt = ( + self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) + .replace("__name__", name).replace("__latest_instruction__", latest_instruction) + .replace("__strategy__", self.STRATEGY) + ) re_run = 2 while re_run > 0: + rsp = await self._aask(prompt) try: - rsp = await self._aask(prompt) rsp_json = json.loads(rsp) break except: @@ -48,7 +53,7 @@ class Speak(Action): with open(WORKSPACE_ROOT / 'speak.txt', 'a') as f: f.write(rsp) - return rsp_json['SPEECH_OR_VOTE'] + return rsp_json['RESPONSE'] class NighttimeWhispers(Action): """ @@ -58,75 +63,50 @@ class NighttimeWhispers(Action): Usage Example: class Hunt(NighttimeWhispers): - ROLE = "Werewolf" ACTION = "KILL" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True class Protect(NighttimeWhispers): - ROLE = "Guard" ACTION = "PROTECT" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True class Verify(NighttimeWhispers): - ROLE = "Seer" ACTION = "VERIFY" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True class Save(NighttimeWhispers): - ROLE = "Witch" ACTION = "SAVE" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True - def _construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): + def _construct_prompt_json(self, prompt_json: dict, profile: str, action: str, context: str): del prompt_json['ACTION'] del prompt_json['ATTENTION'] prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST be killed at this night." - prompt_json["OUTPUT_FORMAT"]["OUTPUT"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." + prompt_json["OUTPUT_FORMAT"]["RESPONSE"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." - return self._default_construct_prompt_json(prompt_json, role, action, context) + return self._default_construct_prompt_json(prompt_json, profile, name, action, context) class Poison(NighttimeWhispers): - ROLE = "Witch" ACTION = "POISON" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True - def _construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): - prompt_json["OUTPUT_FORMAT"]["OUTPUT"] += "Or if you want to PASS, then return PASS." - return self._default_construct_prompt_json(prompt_json, role, action, context) + def _construct_prompt_json(self, prompt_json: dict, profile: str, action: str, context: str): + prompt_json["OUTPUT_FORMAT"]["RESPONSE"] += "Or if you want to PASS, then return PASS." + return self._default_construct_prompt_json(prompt_json, profile, name, action, context) """ - ROLE = "Werewolf" ACTION = "KILL" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True PROMPT_TEMPLATE = """ { - "ROLE": "__role__" + "ROLE": "__profile__" ,"ACTION": "Choose one living player to __action__." ,"ATTENTION": "You can only __action__ a player who is alive at this night! And you can not __action__ a player who is dead as this night!" ,"PHASE": "Night" - ,"BACKGROUND": "It's a werewolf game and you are a __role__. Here's the game history:{__context__}." + ,"BACKGROUND": "It's a werewolf game and you are a __profile__. Here's the game history:__context__." ,"OUTPUT_FORMAT": { - "ROLE": "Your role." - ,"NUMBER": "Your player number." - ,"IDENTITY": "You are? What is you identity? You are player1 or player2 or player3 or player4 or player5 or player6 or player7?" - ,"LIVING_PLAYERS": "List the players who is alive. Return a LIST datatype." + "ROLE": "Your role, in this case, __profile__" + ,"PLAYER_NAME": "Your name, in this case, __name__" + ,"LIVING_PLAYERS": "List the players who is alive based on moderator's latest instruction. Return a LIST datatype." ,"THOUGHTS": "It is night time. Return the thinking steps of your decision of choosing one living player from `LIVING_PLAYERS` to __action__ this night. And return the reason why you choose to __action__ this player." - ,"OUTPUT": "As a __role__, you should choose one living player from `LIVING_PLAYERS` to __action__ this night according to the THOUGHTS you have just now. Return the number of the player you choose and return this NUMBER ONLY." + ,"OUTPUT": "As a __profile__, you should choose one living player from `LIVING_PLAYERS` to __action__ this night according to the THOUGHTS you have just now. Return the player name ONLY." } } """ @@ -134,7 +114,7 @@ class NighttimeWhispers(Action): def __init__(self, name="NightTimeWhispers", context=None, llm=None): super().__init__(name, context, llm) - def _default_construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): + def _default_construct_prompt_json(self, prompt_json: dict, profile: str, name:str, action: str, context: str): def replace_string(prompt_json: dict): k: str @@ -142,7 +122,8 @@ class NighttimeWhispers(Action): if isinstance(prompt_json[k], dict): prompt_json[k] = replace_string(prompt_json[k]) continue - prompt_json[k] = prompt_json[k].replace("__role__", role) + prompt_json[k] = prompt_json[k].replace("__profile__", profile) + prompt_json[k] = prompt_json[k].replace("__name__", name) prompt_json[k] = prompt_json[k].replace("__action__", action) return prompt_json @@ -153,40 +134,27 @@ class NighttimeWhispers(Action): return prompt_json - def _construct_prompt_json(self, prompt_json: dict, role: str, action: str, context: str): - return self._default_construct_prompt_json(prompt_json, role, action, context) + def _construct_prompt_json(self, prompt_json: dict, profile: str, name: str, action: str, context: str): + return self._default_construct_prompt_json(prompt_json, profile, name, action, context) - async def run(self, context: str): - """ - Note: `final_prompt` could be undefined and will raise error if `IF_RENEW` is True and `IF_JSON_INPUT` is False - """ + async def run(self, context: str, profile: str, name: str): - if not self.IF_RENEW: - final_prompt = self.PROMPT_TEMPLATE.replace("__context__", context) + prompt_json = json.loads(self.PROMPT_TEMPLATE) + prompt_json = self._construct_prompt_json( + prompt_json=prompt_json, profile=profile, name=name, action=self.ACTION, context=context + ) + final_prompt = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) + + re_run = 2 + while re_run > 0: rsp_content = await self._aask(final_prompt) - return rsp_content - - if self.IF_JSON_INPUT: - prompt_json = json.loads(self.PROMPT_TEMPLATE) - prompt_json = self._construct_prompt_json(prompt_json=prompt_json, role=self.ROLE, action=self.ACTION, - context=context) # can be defined in subclass - final_prompt = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) - - if self.IF_JSON_OUTPUT: - re_run = 2 - while re_run > 0: - try: - rsp_content = await self._aask(final_prompt) - rsp = json.loads(rsp_content) - break - except: - re_run -= 1 - - with open(WORKSPACE_ROOT / f'{self.ACTION}.txt', 'a') as f: - f.write(rsp_content) - - return f"{self.ACTION} Player" + str(rsp["OUTPUT"]) - - return rsp_content + try: + rsp = json.loads(rsp_content) + break + except: + re_run -= 1 + with open(WORKSPACE_ROOT / f'{self.ACTION}.txt', 'a') as f: + f.write(rsp_content) + return f"{self.ACTION} " + str(rsp["OUTPUT"]) diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index 74905ff16..bbcdafc3c 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -3,36 +3,15 @@ from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispe class Hunt(NighttimeWhispers): - ROLE = "Werewolf" ACTION = "KILL" - IF_RENEW = True - IF_JSON_INPUT = True - IF_JSON_OUTPUT = True class Impersonate(Speak): """Action: werewolf impersonating a good guy in daytime speak""" - PROMPT_TEMPLATE = """ - { - "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" - ,"HISTORY": "You have knowledge to the following conversation: __context__" - ,"ATTENTION": "Try continuously impersonating a role with special ability, such as a Seer or a Witch, in order to mislead - other players, make them trust you, and thus hiding your werewolf identity. You can not VOTE a player who is NOT ALIVE now!" - ,"YOUR_TURN": "Please follow the moderator's latest instruction, FIGURE OUT if you need to speak your opinion or directly to vote, - 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; - 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', where X is the player index. - DO NOT include any other words. - " - ,"OUTPUT_FORMAT": - { - "ROLE": "Your role." - ,"NUMBER": "Your player number." - ,"IDENTITY": "You are? What is you identity? You are player1 or player2 or player3 or player4 or player5 or player6 or player7?" - ,"LIVING_PLAYERS": "List the players who is alive. Return a LIST datatype." - ,"THOUGHTS": "It is day time. Return the thinking steps of your decision of giving VOTE to other player from `LIVING_PLAYERS`. And return the reason why you choose to VOTE this player from `LIVING_PLAYERS`." - ,"SPEECH_OR_VOTE": "Follow the instruction of `YOUR_TURN` above and the `THOUGHTS` you have just now, give a speech or your vote. Remember, you are a WEREWOLF!!! But just keep it in mind, don't tell other players." - } - } + STRATEGY = """ + Try continuously impersonating a role with special ability, such as a Seer or a Witch, in order to mislead + other players, make them trust you, and thus hiding your werewolf identity. However, pay attention to what your werewolf partner said, + if your werewolf partner has claimed to be a Seer or Witch, DONT claim to be the same role. Remmber NOT to reveal your real identity as a werewolf! """ def __init__(self, name="Impersonate", context=None, llm=None): diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index df4cf48bf..1a044673d 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -15,8 +15,6 @@ class BasePlayer(Role): **kwargs, ): super().__init__(name, profile, **kwargs) - self._init_actions([Speak]) - self._watch([InstructSpeak]) # 通过 set_status() 更新状态。 self.status = 0 # 0代表活着,1代表死亡 @@ -60,6 +58,9 @@ class BasePlayer(Role): memories = [f"{m.sent_from}: {re.sub(time_stamp_pattern, '', m.content)}" for m in memories] # regex去掉时间戳 memories = "\n".join(memories) return memories + + def get_latest_instruction(self) -> str: + return self._rc.important_memory[-1].content # 角色监听着Moderator的InstructSpeak,是其重要记忆,直接获取即可 def set_status(self, new_status): self.status = new_status diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index 1f53dd795..fd899d35b 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -18,13 +18,14 @@ class Guard(BasePlayer): todo = self._rc.todo logger.info(f"{self._setting}: ready to {str(todo)}") - # 可以用这个函数获取该角色的全部记忆 + # 可以用这个函数获取该角色的全部记忆和最新的instruction memories = self.get_all_memories() + latest_instruction = self.get_latest_instruction() # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index 1d58b70bf..8b496d689 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -19,12 +19,14 @@ class Seer(BasePlayer): todo = self._rc.todo logger.info(f"{self._setting}: ready to {str(todo)}") + # 可以用这个函数获取该角色的全部记忆和最新的instruction memories = self.get_all_memories() + latest_instruction = self.get_latest_instruction() # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 基于todo的类型,调用不同的action if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", diff --git a/examples/werewolf_game/roles/villager.py b/examples/werewolf_game/roles/villager.py index ececa96ea..e6e59a51e 100644 --- a/examples/werewolf_game/roles/villager.py +++ b/examples/werewolf_game/roles/villager.py @@ -19,12 +19,13 @@ class Villager(BasePlayer): todo = self._rc.todo logger.info(f"{self._setting}: ready to {todo}") - # 可以用这个函数获取该角色的全部记忆 + # 可以用这个函数获取该角色的全部记忆和最新的instruction memories = self.get_all_memories() + latest_instruction = self.get_latest_instruction() # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run - rsp = await todo.run(profile=self.profile, context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) # 返回消息,注意给Moderator发送的加密消息需要用restricted_to="Moderator" msg = Message( diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py index 5426ca7c4..786e37691 100644 --- a/examples/werewolf_game/roles/werewolf.py +++ b/examples/werewolf_game/roles/werewolf.py @@ -18,21 +18,22 @@ class Werewolf(BasePlayer): todo = self._rc.todo logger.info(f"{self._setting}: ready to {str(todo)}") - # 可以用这个函数获取该角色的全部记忆 + # 可以用这个函数获取该角色的全部记忆和最新的instruction memories = self.get_all_memories() + latest_instruction = self.get_latest_instruction() # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): - # rsp = await todo.run(profile=self.profile, context=memories) - rsp = await Impersonate().run(profile=self.profile, context=memories) + # rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) + rsp = await Impersonate().run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", ) elif isinstance(todo, Hunt): - rsp = await todo.run(context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Hunt, send_to="", diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py index 13b677d7e..9b74d69be 100644 --- a/examples/werewolf_game/roles/witch.py +++ b/examples/werewolf_game/roles/witch.py @@ -38,11 +38,12 @@ class Witch(BasePlayer): # 可以用这个函数获取该角色的全部记忆 memories = self.get_all_memories() + latest_instruction = self.get_latest_instruction() # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", From ef1a9a4609c5f16c209fd0c94609aa5d9e48d25c Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 9 Oct 2023 21:33:59 +0800 Subject: [PATCH 31/47] introduce nighttime thought to all roles & add simple strategies into action --- examples/werewolf_game/actions/__init__.py | 2 +- .../werewolf_game/actions/common_actions.py | 88 +++++++++++-------- .../werewolf_game/actions/guard_actions.py | 23 +---- .../actions/moderator_actions.py | 2 +- .../werewolf_game/actions/seer_actions.py | 22 +---- .../werewolf_game/actions/werewolf_actions.py | 3 +- .../werewolf_game/actions/witch_actions.py | 45 ++++------ examples/werewolf_game/roles/guard.py | 2 +- examples/werewolf_game/roles/human_player.py | 2 +- examples/werewolf_game/roles/seer.py | 2 +- examples/werewolf_game/roles/witch.py | 4 +- 11 files changed, 79 insertions(+), 116 deletions(-) diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 16b9391b3..21a053980 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,5 +1,5 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak -from examples.werewolf_game.actions.common_actions import Speak +from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispers from examples.werewolf_game.actions.werewolf_actions import Hunt, Impersonate from examples.werewolf_game.actions.guard_actions import Protect from examples.werewolf_game.actions.seer_actions import Verify diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index e37749da2..42f2223ba 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -13,21 +13,22 @@ class Speak(Action): ,"ATTENTION": "You can NOT VOTE a player who is NOT ALIVE now!" ,"STRATEGY": __strategy__ ,"MODERATOR_INSTRUCTION": __latest_instruction__, - ,"RULE": "Please follow the moderator's latest instruction, figure out if you need to speak your opinion or directly to vote, - 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; - 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', where X is the player index, DO NOT include any other words." + ,"RULE": "Please follow the moderator's latest instruction, figure out if you need to speak your opinion or directly to vote: + 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; + 2. If the instruction is to VOTE, you MUST vote and ONLY say 'I vote to eliminate PlayerX', replace PlayerX with the actual player name, DO NOT include any other words." ,"OUTPUT_FORMAT": { "ROLE": "Your role, in this case, __profile__" ,"PLAYER_NAME": "Your name, in this case, __name__" ,"LIVING_PLAYERS": "List living players based on MODERATOR_INSTRUCTION. Return a LIST datatype." - ,"THOUGHTS": "Based on `MODERATOR_INSTRUCTION` and `RULE`, carefully think about what to say or vote so that your chance of win as __profile__ maximizes. Return the thinking process." + ,"THOUGHTS": "Based on `MODERATOR_INSTRUCTION` and `RULE`, carefully think about what to say or vote so that your chance of win as __profile__ maximizes. Give your step-by-step thought process, you should think no more than 3 steps. For example: My step-by-step thought process:..." ,"RESPONSE": "Based on `MODERATOR_INSTRUCTION`, `RULE`, and the 'THOUGHTS' you had, express your opinion or cast a vote." } } """ STRATEGY = """ Decide whether to reveal your identity based on benefits vs. risks, provide useful information, and vote to eliminate the most suspicious. + If you have special abilities, pay attention to those who falsely claims your role, for they are probably werewolves. """ def __init__(self, name="Speak", context=None, llm=None): @@ -45,6 +46,7 @@ class Speak(Action): while re_run > 0: rsp = await self._aask(prompt) try: + rsp = rsp.replace("\n", " ") rsp_json = json.loads(rsp) break except: @@ -63,58 +65,63 @@ class NighttimeWhispers(Action): Usage Example: class Hunt(NighttimeWhispers): - ACTION = "KILL" + def __init__(self, name="Hunt", context=None, llm=None): + super().__init__(name, context, llm) class Protect(NighttimeWhispers): - ACTION = "PROTECT" + def __init__(self, name="Protect", context=None, llm=None): + super().__init__(name, context, llm) class Verify(NighttimeWhispers): - ACTION = "VERIFY" + def __init__(self, name="Verify", context=None, llm=None): + super().__init__(name, context, llm) class Save(NighttimeWhispers): - ACTION = "SAVE" + def __init__(self, name="Save", context=None, llm=None): + super().__init__(name, context, llm) - def _construct_prompt_json(self, prompt_json: dict, profile: str, action: str, context: str): + def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, **kwargs): del prompt_json['ACTION'] del prompt_json['ATTENTION'] - prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST be killed at this night." prompt_json["OUTPUT_FORMAT"]["RESPONSE"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." - - return self._default_construct_prompt_json(prompt_json, profile, name, action, context) + return prompt_json class Poison(NighttimeWhispers): - ACTION = "POISON" - - def _construct_prompt_json(self, prompt_json: dict, profile: str, action: str, context: str): - prompt_json["OUTPUT_FORMAT"]["RESPONSE"] += "Or if you want to PASS, then return PASS." - return self._default_construct_prompt_json(prompt_json, profile, name, action, context) + def __init__(self, name="Poison", context=None, llm=None): + super().__init__(name, context, llm) + def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, **kwargs): + prompt_json["OUTPUT_FORMAT"]["RESPONSE"] += "Or if you want to PASS, return PASS." + return prompt_json """ - ACTION = "KILL" PROMPT_TEMPLATE = """ { "ROLE": "__profile__" ,"ACTION": "Choose one living player to __action__." - ,"ATTENTION": "You can only __action__ a player who is alive at this night! And you can not __action__ a player who is dead as this night!" - ,"PHASE": "Night" - ,"BACKGROUND": "It's a werewolf game and you are a __profile__. Here's the game history:__context__." + ,"ATTENTION": "1. You can only __action__ a player who is alive this night! And you can not __action__ a player who is dead this night! 2. `HISTORY` is all the information you observed, DONT hallucinate other player actions!" + ,"STRATEGY": "__strategy__" + ,"BACKGROUND": "It's a werewolf game and you are a __profile__. Here's the game history: __context__." ,"OUTPUT_FORMAT": { "ROLE": "Your role, in this case, __profile__" ,"PLAYER_NAME": "Your name, in this case, __name__" ,"LIVING_PLAYERS": "List the players who is alive based on moderator's latest instruction. Return a LIST datatype." - ,"THOUGHTS": "It is night time. Return the thinking steps of your decision of choosing one living player from `LIVING_PLAYERS` to __action__ this night. And return the reason why you choose to __action__ this player." - ,"OUTPUT": "As a __profile__, you should choose one living player from `LIVING_PLAYERS` to __action__ this night according to the THOUGHTS you have just now. Return the player name ONLY." + ,"THOUGHTS": "Choose one living player from `LIVING_PLAYERS` to __action__ this night. Return the reason why you choose to __action__ this player. If you observe nothing at first night, DONT imagine unexisting player actions! Give your step-by-step thought process, you should think no more than 3 steps. For example: My step-by-step thought process:..." + ,"RESPONSE": "As a __profile__, you should choose one living player from `LIVING_PLAYERS` to __action__ this night according to the THOUGHTS you have just now. Return the player name ONLY." } } """ + STRATEGY = """ + Decide which player is most threatening to you or most needs your support, take your action correspondingly. + """ def __init__(self, name="NightTimeWhispers", context=None, llm=None): super().__init__(name, context, llm) - def _default_construct_prompt_json(self, prompt_json: dict, profile: str, name:str, action: str, context: str): + def _construct_prompt_json(self, role_profile: str, role_name: str, context: str, **kwargs): + prompt_template = self.PROMPT_TEMPLATE def replace_string(prompt_json: dict): k: str @@ -122,39 +129,46 @@ class NighttimeWhispers(Action): if isinstance(prompt_json[k], dict): prompt_json[k] = replace_string(prompt_json[k]) continue - prompt_json[k] = prompt_json[k].replace("__profile__", profile) - prompt_json[k] = prompt_json[k].replace("__name__", name) - prompt_json[k] = prompt_json[k].replace("__action__", action) + prompt_json[k] = prompt_json[k].replace("__profile__", role_profile) + prompt_json[k] = prompt_json[k].replace("__name__", role_name) + prompt_json[k] = prompt_json[k].replace("__context__", context) + prompt_json[k] = prompt_json[k].replace("__action__", self.name) + prompt_json[k] = prompt_json[k].replace("__strategy__", self.STRATEGY) return prompt_json + + prompt_json: dict = json.loads(prompt_template) prompt_json = replace_string(prompt_json) - prompt_json["BACKGROUND"] = prompt_json["BACKGROUND"].replace("__context__", context) + prompt_json: dict = self._update_prompt_json(prompt_json, role_profile, role_name, context, **kwargs) + assert isinstance(prompt_json, dict) + prompt: str = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) + + return prompt + + def _update_prompt_json(self, prompt_json: dict, role_profile: str, role_name: str, context: str) -> dict: + # one can modify the prompt_json dictionary here return prompt_json - def _construct_prompt_json(self, prompt_json: dict, profile: str, name: str, action: str, context: str): - return self._default_construct_prompt_json(prompt_json, profile, name, action, context) - async def run(self, context: str, profile: str, name: str): - prompt_json = json.loads(self.PROMPT_TEMPLATE) - prompt_json = self._construct_prompt_json( - prompt_json=prompt_json, profile=profile, name=name, action=self.ACTION, context=context + final_prompt = self._construct_prompt_json( + role_profile=profile, role_name=name, context=context ) - final_prompt = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) re_run = 2 while re_run > 0: rsp_content = await self._aask(final_prompt) try: + rsp_content = rsp_content.replace("\n", " ") rsp = json.loads(rsp_content) break except: re_run -= 1 - with open(WORKSPACE_ROOT / f'{self.ACTION}.txt', 'a') as f: + with open(WORKSPACE_ROOT / f'{self.name}.txt', 'a') as f: f.write(rsp_content) - return f"{self.ACTION} " + str(rsp["OUTPUT"]) + return f"{self.name} " + str(rsp["RESPONSE"]) diff --git a/examples/werewolf_game/actions/guard_actions.py b/examples/werewolf_game/actions/guard_actions.py index 5b98d7e0e..310d1b278 100644 --- a/examples/werewolf_game/actions/guard_actions.py +++ b/examples/werewolf_game/actions/guard_actions.py @@ -1,26 +1,7 @@ from metagpt.actions import Action +from examples.werewolf_game.actions import NighttimeWhispers -class Protect(Action): - """Action: choose a player to protect""" - - PROMPT_TEMPLATE = """ - It's a werewolf game and you are a guard, - you can choose to protect a player, including yourself, then the protected player will not be killed by the Werewolves this night. - this is game history: - {context}. - Attention: you can not protect the same player two nights in a row. - Format: "Protect PlayerX", where X is the player index. - Now, choose one to protect, you will: - """ +class Protect(NighttimeWhispers): def __init__(self, name="Protect", context=None, llm=None): super().__init__(name, context, llm) - - async def run(self, context: str): - - prompt = self.PROMPT_TEMPLATE.format(context=context) - - rsp = await self._aask(prompt) - # rsp = "Protect Player 1" - - return rsp \ No newline at end of file diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index c0b540a44..638c3d658 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -42,7 +42,7 @@ STEP_INSTRUCTIONS = { "restricted_to": "Moderator,Witch"}, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 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 "Poison PlayerX", where X is the player index, else, say "Pass".""", + If so, say ONLY "Poison PlayerX", replace PlayerX with the actual player name, else, say "Pass".""", "send_to": "Witch", "restricted_to": "Moderator,Witch"}, # 10: {"content": "Witch, close your eyes", diff --git a/examples/werewolf_game/actions/seer_actions.py b/examples/werewolf_game/actions/seer_actions.py index 4c54debe2..6318de85f 100644 --- a/examples/werewolf_game/actions/seer_actions.py +++ b/examples/werewolf_game/actions/seer_actions.py @@ -1,24 +1,6 @@ from metagpt.actions import Action +from examples.werewolf_game.actions import NighttimeWhispers - -class Verify(Action): - """Action: Seer verifies a player's identity at night""" - - PROMPT_TEMPLATE = """ - It's a werewolf game and you are a seer. - You can choose to verify the identity of a player. - Here's the game history: - {context}. - Now, choose one player to verify - Format: "Verify PlayerX", where X is the player index. - You will: - """ - +class Verify(NighttimeWhispers): def __init__(self, name="Verify", context=None, llm=None): super().__init__(name, context, llm) - - async def run(self, context: str): - prompt = self.PROMPT_TEMPLATE.format(context=context) - rsp = await self._aask(prompt) - - return rsp diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index bbcdafc3c..24272c79e 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -3,7 +3,8 @@ from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispe class Hunt(NighttimeWhispers): - ACTION = "KILL" + def __init__(self, name="Hunt", context=None, llm=None): + super().__init__(name, context, llm) class Impersonate(Speak): """Action: werewolf impersonating a good guy in daytime speak""" diff --git a/examples/werewolf_game/actions/witch_actions.py b/examples/werewolf_game/actions/witch_actions.py index dec39f466..af8032a42 100644 --- a/examples/werewolf_game/actions/witch_actions.py +++ b/examples/werewolf_game/actions/witch_actions.py @@ -1,45 +1,30 @@ from metagpt.actions import Action +from examples.werewolf_game.actions import NighttimeWhispers -class Save(Action): - """Action: choose a villager to Save""" - - PROMPT_TEMPLATE = """ - It's a werewolf game and you are a witch, - this is game history: - {context}. - Follow the Moderator's instruction, decide whether you want to save that person or not: - """ - +class Save(NighttimeWhispers): def __init__(self, name="Save", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, context: str): + def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, **kwargs): + del prompt_json['ACTION'] + del prompt_json['ATTENTION'] - prompt = self.PROMPT_TEMPLATE.format(context=context) + prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST be killed at this night." + prompt_json["OUTPUT_FORMAT"]["RESPONSE"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." - rsp = await self._aask(prompt) - # rsp = "Save Player 1" + return prompt_json - return rsp - -class Poison(Action): - """Action: choose a villager to Poison""" - - PROMPT_TEMPLATE = """ - It's a werewolf game and you are a witch, - this is game history: - {context}. - Follow the Moderator's instruction, decide whether you want to poison another person or not: +class Poison(NighttimeWhispers): + STRATEGY = """ + Only poison a player if you are confident he/she is a werewolf. Don't poison a player randomly or at first night. + If someone claims to be the witch, poison him/her, because you are the only witch, he/she can only be a werewolf. """ def __init__(self, name="Poison", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, context: str): + def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, **kwargs): - prompt = self.PROMPT_TEMPLATE.format(context=context) + prompt_json["OUTPUT_FORMAT"]["RESPONSE"] += "Or if you want to PASS, return PASS." - rsp = await self._aask(prompt) - # rsp = "Poison Player 1" - - return rsp + return prompt_json diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index fd899d35b..580d16cd9 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -32,7 +32,7 @@ class Guard(BasePlayer): ) elif isinstance(todo, Protect): - rsp = await todo.run(context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Protect, send_to="", diff --git a/examples/werewolf_game/roles/human_player.py b/examples/werewolf_game/roles/human_player.py index 8b04ae821..fce90b05a 100644 --- a/examples/werewolf_game/roles/human_player.py +++ b/examples/werewolf_game/roles/human_player.py @@ -14,7 +14,7 @@ async def _act(self): ## You are {self.name}({self.profile}) ## Guidance: 1. If you are performing a special action or exercising a vote, - end your response with "PlayerX" where X is the player index, e.g., "..., kill/protect/poison/.../vote Player1". + end your response with "PlayerX", replace PlayerX with the actual player name, e.g., "..., kill/protect/poison/.../vote Player1". 2. If it is a daytime free speech, you can speak in whatever format. Now, please speak: """ diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index 8b496d689..54a15689d 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -33,7 +33,7 @@ class Seer(BasePlayer): ) elif isinstance(todo, Verify): - rsp = await todo.run(context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Verify, send_to="", diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py index 9b74d69be..a570677df 100644 --- a/examples/werewolf_game/roles/witch.py +++ b/examples/werewolf_game/roles/witch.py @@ -50,7 +50,7 @@ class Witch(BasePlayer): ) elif isinstance(todo, Save): - rsp = await todo.run(context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Save, send_to="", @@ -58,7 +58,7 @@ class Witch(BasePlayer): ) elif isinstance(todo, Poison): - rsp = await todo.run(context=memories) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories) msg = Message( content=rsp, role=self.profile, sent_from=self.name, cause_by=Poison, send_to="", From c55608cb7691a3e2d95605ecf4762889742859e4 Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:29:19 +0800 Subject: [PATCH 32/47] feat: polish log files for evals --- examples/werewolf_game/evals/utils.py | 148 +++++++++----------------- 1 file changed, 48 insertions(+), 100 deletions(-) diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index 2e83b38f6..ac2eecdf6 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -1,120 +1,68 @@ ''' Filename: MetaGPT/examples/werewolf_game/evals/utils.py -Created Date: Oct 2, 2023 +Created Date: Oct 10, 2023 Author: [Aria](https://github.com/ariafyy) ''' from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT -import re, json +import re class Utils: + """Utils: utils of logs""" + def __init__(self): pass - def _action(self, text: str) -> str: - """ - # get action - input: I vote to eliminate Player3 - output: vote - """ - text = text.lower() - if "vote" in text: - action = "vote" - return action - if "verify" in text: - action = "verify" - return action - if "kill" in text: - action = "kill" - return action - else: - action = "chat" - return action + def polish_log(self, in_logfile, out_txtfile): + """polish logs for evaluation""" + pattern_text = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \| (\w+) +\| ([\w\.]+:\w+:\d+) - (.*\S)" + pattern_player = r"(Player(\d{1}): \w+)" + pattern_start = False + json_start = False - def _life(self, text: str) -> str: - """ - # get life - input: Kill Player6 - output: dead - """ - text = text.lower() - if re.search(r'\b(eliminated|killed|kill)\b', text, re.I): - life = 'dead' - dead_role = re.findall(r'\[(.*?)\]', text) - if re.search(r'no one was killed', text, re.I): - return "alive", [] - else: - return life, dead_role - else: - life = "alive" - return life, [] + with open(in_logfile, "r") as f, open(out_txtfile, "w") as out: + for line in f.readlines(): + matches = re.match(pattern_text, line) + if matches: + message = matches.group(4).strip() + pattern_start = True + json_start = False - def txt2data(self, file): - """ - input: .txt file - output: data for json format - """ - result = {} - count = 0 - day = -1 - flag = False - - with open(file, "r") as f: - lines = f.readlines() - for line in lines: - if "Moderator(Moderator): 0" in line: - flag = True - if flag: - if "It’s dark, everyone close your eyes." in line: - day += 1 - count = 0 - data = {} - parts = line.split("|") - data["role"] = parts[0].strip().split(":")[0] - data["day"] = day - data["turn"] = count - if len(parts) > 1: - data["text"] = parts[1].strip() - data["action"] = self._action(data["text"]) - data["life"], data["dead_role"] = self._life(data["text"]) + if "Moderator(Moderator) ready to InstructSpeak" not in message and "Moderator(Moderator) ready to ParseSpeak" not in message and "Total running cost:" not in message: + out.write("- " + message + '\n') else: - continue - key = "day_{}".format(day) - if key not in result: - result[key] = [] - result[key].append(data) - count += 1 - return result + out.write('\n') - def data2json(self, in_file): - """ - output examples: - { - "day_0": [ - { - "role": "Moderator(Moderator)", - "day": 0, - "turn": 0, - "text": "It’s dark, everyone close your eyes. I will talk with you/your team secretly at night.", - "action": "chat", - "life": "alive", - "dead_role": [] - },{}] - ... - } - """ + elif pattern_start and not matches: + if "gpt-4 may update over time" in line: + line = "" + out.write(line) - result = self.txt2data(in_file) - self._save_json(result) - return result + elif line.strip().startswith("{"): + out.write(line.strip()) + json_start = True - def _save_json(self, data): - with open(WORKSPACE_ROOT / 'werewolf_transcript.json', "w", encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) - f.write('\n') + elif json_start and not line.strip().endswith("}"): + out.write(line.strip()) + + elif json_start and line.strip().endswith("}"): + out.write(line.strip()) + json_start = False + + elif line.startswith("(User):"): + out.write(line) + + elif line.startswith("********** STEP:"): + out.write(line) + + elif re.search(pattern_player, line): + out.write(line) + + else: + out.write("\n") if __name__ == '__main__': - txt_path = WORKSPACE_ROOT / "werewolf_transcript.txt" - log_path = PROJECT_ROOT / "logs/log.txt" - Utils().data2json(txt_path) + in_logfile = PROJECT_ROOT / "logs/log.txt" + out_txtfile = "input your wish path" + Utils().polish_log(in_logfile, out_txtfile) From 6f641d63107f82071beb19899504e0cbbacf19d5 Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Wed, 11 Oct 2023 10:45:58 +0800 Subject: [PATCH 33/47] #refactor: refactor Utils class for log parsing --- examples/werewolf_game/evals/utils.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index ac2eecdf6..d788496a3 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -1,6 +1,6 @@ ''' Filename: MetaGPT/examples/werewolf_game/evals/utils.py -Created Date: Oct 10, 2023 +Created Date: Oct 11, 2023 Author: [Aria](https://github.com/ariafyy) ''' from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT @@ -10,10 +10,8 @@ import re class Utils: """Utils: utils of logs""" - def __init__(self): - pass - - def polish_log(self, in_logfile, out_txtfile): + @staticmethod + def polish_log(in_logfile, out_txtfile): """polish logs for evaluation""" pattern_text = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \| (\w+) +\| ([\w\.]+:\w+:\d+) - (.*\S)" pattern_player = r"(Player(\d{1}): \w+)" @@ -49,13 +47,7 @@ class Utils: out.write(line.strip()) json_start = False - elif line.startswith("(User):"): - out.write(line) - - elif line.startswith("********** STEP:"): - out.write(line) - - elif re.search(pattern_player, line): + elif line.startswith("(User):") or line.startswith("********** STEP:") or re.search(pattern_player,line): out.write(line) else: From d015c81fc46d33c7844827f19baffcdd0918bc70 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Wed, 11 Oct 2023 16:30:07 +0800 Subject: [PATCH 34/47] introduce simple reflection & abstract common _act --- examples/werewolf_game/actions/__init__.py | 2 +- .../werewolf_game/actions/common_actions.py | 89 ++++++++++++------- .../werewolf_game/actions/witch_actions.py | 15 +++- examples/werewolf_game/roles/base_player.py | 39 +++++++- examples/werewolf_game/roles/guard.py | 33 ------- examples/werewolf_game/roles/seer.py | 34 ------- examples/werewolf_game/roles/villager.py | 27 ------ examples/werewolf_game/roles/werewolf.py | 39 ++------ examples/werewolf_game/roles/witch.py | 42 +-------- 9 files changed, 117 insertions(+), 203 deletions(-) diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 21a053980..784715907 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,5 +1,5 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak -from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispers +from examples.werewolf_game.actions.common_actions import Speak, NighttimeWhispers, Reflect from examples.werewolf_game.actions.werewolf_actions import Hunt, Impersonate from examples.werewolf_game.actions.guard_actions import Protect from examples.werewolf_game.actions.seer_actions import Verify diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index 42f2223ba..2b9600260 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -1,7 +1,7 @@ from metagpt.actions import Action import json from metagpt.const import WORKSPACE_ROOT - +from tenacity import retry, stop_after_attempt, wait_fixed class Speak(Action): """Action: Any speak action in a game""" @@ -11,6 +11,7 @@ class Speak(Action): "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"ATTENTION": "You can NOT VOTE a player who is NOT ALIVE now!" + ,"REFLECTION": "__reflection__" ,"STRATEGY": __strategy__ ,"MODERATOR_INSTRUCTION": __latest_instruction__, ,"RULE": "Please follow the moderator's latest instruction, figure out if you need to speak your opinion or directly to vote: @@ -34,23 +35,18 @@ class Speak(Action): def __init__(self, name="Speak", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, profile: str, name: str, context: str, latest_instruction: str): + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + async def run(self, profile: str, name: str, context: str, latest_instruction: str, reflection: str = ""): prompt = ( self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) .replace("__name__", name).replace("__latest_instruction__", latest_instruction) - .replace("__strategy__", self.STRATEGY) + .replace("__strategy__", self.STRATEGY).replace("__reflection__", reflection) ) - re_run = 2 - while re_run > 0: - rsp = await self._aask(prompt) - try: - rsp = rsp.replace("\n", " ") - rsp_json = json.loads(rsp) - break - except: - re_run -= 1 + rsp = await self._aask(prompt) + rsp = rsp.replace("\n", " ") + rsp_json = json.loads(rsp) with open(WORKSPACE_ROOT / 'speak.txt', 'a') as f: f.write(rsp) @@ -101,8 +97,9 @@ class NighttimeWhispers(Action): "ROLE": "__profile__" ,"ACTION": "Choose one living player to __action__." ,"ATTENTION": "1. You can only __action__ a player who is alive this night! And you can not __action__ a player who is dead this night! 2. `HISTORY` is all the information you observed, DONT hallucinate other player actions!" - ,"STRATEGY": "__strategy__" ,"BACKGROUND": "It's a werewolf game and you are a __profile__. Here's the game history: __context__." + ,"REFLECTION": "__reflection__" + ,"STRATEGY": "__strategy__" ,"OUTPUT_FORMAT": { "ROLE": "Your role, in this case, __profile__" @@ -120,7 +117,7 @@ class NighttimeWhispers(Action): def __init__(self, name="NightTimeWhispers", context=None, llm=None): super().__init__(name, context, llm) - def _construct_prompt_json(self, role_profile: str, role_name: str, context: str, **kwargs): + def _construct_prompt_json(self, role_profile: str, role_name: str, context: str, reflection: str, **kwargs): prompt_template = self.PROMPT_TEMPLATE def replace_string(prompt_json: dict): @@ -134,6 +131,7 @@ class NighttimeWhispers(Action): prompt_json[k] = prompt_json[k].replace("__context__", context) prompt_json[k] = prompt_json[k].replace("__action__", self.name) prompt_json[k] = prompt_json[k].replace("__strategy__", self.STRATEGY) + prompt_json[k] = prompt_json[k].replace("__reflection__", reflection) return prompt_json @@ -141,34 +139,65 @@ class NighttimeWhispers(Action): prompt_json = replace_string(prompt_json) - prompt_json: dict = self._update_prompt_json(prompt_json, role_profile, role_name, context, **kwargs) + prompt_json: dict = self._update_prompt_json(prompt_json, role_profile, role_name, context, reflection, **kwargs) assert isinstance(prompt_json, dict) prompt: str = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) return prompt - def _update_prompt_json(self, prompt_json: dict, role_profile: str, role_name: str, context: str) -> dict: + def _update_prompt_json(self, prompt_json: dict, role_profile: str, role_name: str, context: str, reflection: str) -> dict: # one can modify the prompt_json dictionary here return prompt_json - async def run(self, context: str, profile: str, name: str): + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + async def run(self, context: str, profile: str, name: str, reflection: str = ""): - final_prompt = self._construct_prompt_json( - role_profile=profile, role_name=name, context=context + prompt = self._construct_prompt_json( + role_profile=profile, role_name=name, context=context, reflection=reflection ) - re_run = 2 - while re_run > 0: - rsp_content = await self._aask(final_prompt) - try: - rsp_content = rsp_content.replace("\n", " ") - rsp = json.loads(rsp_content) - break - except: - re_run -= 1 + rsp = await self._aask(prompt) + rsp = rsp.replace("\n", " ") + rsp_json = json.loads(rsp) with open(WORKSPACE_ROOT / f'{self.name}.txt', 'a') as f: - f.write(rsp_content) + f.write(rsp) - return f"{self.name} " + str(rsp["RESPONSE"]) + return f"{self.name} " + str(rsp_json["RESPONSE"]) + +class Reflect(Action): + PROMPT_TEMPLATE = """ + { + "BACKGROUND": "It's a Werewolf game, you are __profile__" + ,"HISTORY": "You have knowledge to the following conversation: __context__" + ,"MODERATOR_INSTRUCTION": __latest_instruction__, + ,"OUTPUT_FORMAT": + { + "ROLE": "Your role, in this case, __profile__" + ,"PLAYER_NAME": "Your name, in this case, __name__" + ,"LIVING_PLAYERS": "List living players based on MODERATOR_INSTRUCTION. Return a LIST datatype." + ,"REFLECTION": "You are about to follow `MODERATOR_INSTRUCTION`, but before taking any action, think about + what insights you can draw from `HISTORY` for achieving your objective? + Try to figure out the role of each player including living or dead, and summarize the game states. Give your reflection in no more than three sentences." + ,"STRATEGY": Based on your reflection, think at high level what strategy you will take, in one sentence. + } + } + """ + + def __init__(self, name="Reflect", context=None, llm=None): + super().__init__(name, context, llm) + + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + async def run(self, profile: str, name: str, context: str, latest_instruction: str): + + prompt = ( + self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) + .replace("__name__", name).replace("__latest_instruction__", latest_instruction) + ) + + rsp = await self._aask(prompt) + rsp = rsp.replace("\n", " ") + rsp_json = json.loads(rsp) + + return rsp_json['REFLECTION'] diff --git a/examples/werewolf_game/actions/witch_actions.py b/examples/werewolf_game/actions/witch_actions.py index af8032a42..d5d8aa5a2 100644 --- a/examples/werewolf_game/actions/witch_actions.py +++ b/examples/werewolf_game/actions/witch_actions.py @@ -5,7 +5,7 @@ class Save(NighttimeWhispers): def __init__(self, name="Save", context=None, llm=None): super().__init__(name, context, llm) - def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, **kwargs): + def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, reflection: str, **kwargs): del prompt_json['ACTION'] del prompt_json['ATTENTION'] @@ -13,6 +13,11 @@ class Save(NighttimeWhispers): prompt_json["OUTPUT_FORMAT"]["RESPONSE"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." return prompt_json + + async def run(self, *args, **kwargs): + rsp = await super().run(*args, **kwargs) + action_name, rsp = rsp.split() + return rsp # 只需回复SAVE或PASS,不需要带上action名 class Poison(NighttimeWhispers): STRATEGY = """ @@ -23,8 +28,14 @@ class Poison(NighttimeWhispers): def __init__(self, name="Poison", context=None, llm=None): super().__init__(name, context, llm) - def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, **kwargs): + def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, reflection: str, **kwargs): prompt_json["OUTPUT_FORMAT"]["RESPONSE"] += "Or if you want to PASS, return PASS." return prompt_json + + async def run(self, *args, **kwargs): + rsp = await super().run(*args, **kwargs) + if "pass" in rsp.lower(): + action_name, rsp = rsp.split() # 带PASS,只需回复PASS,不需要带上action名,否则是Poison PlayerX,无需改动 + return rsp diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 1a044673d..734228d46 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -3,7 +3,7 @@ import re from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger -from examples.werewolf_game.actions import ACTIONS, Speak, InstructSpeak +from examples.werewolf_game.actions import ACTIONS, InstructSpeak, Speak, Reflect, NighttimeWhispers class BasePlayer(Role): @@ -49,7 +49,42 @@ class BasePlayer(Role): async def _act(self): """每个角色要改写此函数以实现该角色的动作""" - raise NotImplementedError + # raise NotImplementedError + + # todo为_think时确定的,有两种情况,Speak或Protect + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {str(todo)}") + + # 可以用这个函数获取该角色的全部记忆和最新的instruction + memories = self.get_all_memories() + latest_instruction = self.get_latest_instruction() + # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + reflection = await Reflect().run( + profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction + ) + + # 根据自己定义的角色Action,对应地去run,run的入参可能不同 + if isinstance(todo, Speak): + rsp = await todo.run( + profile=self.profile, name=self.name, context=memories, + latest_instruction=latest_instruction, reflection=reflection + ) + restricted_to = "" + + elif isinstance(todo, NighttimeWhispers): + rsp = await todo.run(profile=self.profile, name=self.name, context=memories, reflection=reflection) + restricted_to = f"Moderator,{self.profile}" # 给Moderator发送使用特殊技能的加密消息 + + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=type(todo), send_to="", + restricted_to=restricted_to + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg def get_all_memories(self) -> str: memories = self._rc.memory.get() diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index 580d16cd9..24cfbb7c1 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -1,7 +1,4 @@ from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak, Protect -from metagpt.schema import Message -from metagpt.logs import logger class Guard(BasePlayer): def __init__( @@ -12,33 +9,3 @@ class Guard(BasePlayer): **kwargs, ): super().__init__(name, profile, special_action_names, **kwargs) - - async def _act(self): - # todo为_think时确定的,有两种情况,Speak或Protect - todo = self._rc.todo - logger.info(f"{self._setting}: ready to {str(todo)}") - - # 可以用这个函数获取该角色的全部记忆和最新的instruction - memories = self.get_all_memories() - latest_instruction = self.get_latest_instruction() - # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) - - # 根据自己定义的角色Action,对应地去run,run的入参可能不同 - if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Speak, send_to="", restricted_to="", - ) - - elif isinstance(todo, Protect): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Protect, send_to="", - restricted_to=f"Moderator,{self.profile}", # 给Moderator发送守卫要保护的人加密消息 - ) - - logger.info(f"{self._setting}: {rsp}") - - return msg diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index 54a15689d..769713e8f 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -1,9 +1,4 @@ -from examples.werewolf_game.actions.seer_actions import Verify from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak -from metagpt.schema import Message -from metagpt.logs import logger - class Seer(BasePlayer): def __init__( @@ -14,32 +9,3 @@ class Seer(BasePlayer): **kwargs, ): super().__init__(name, profile, special_action_names, **kwargs) - - async def _act(self): - todo = self._rc.todo - logger.info(f"{self._setting}: ready to {str(todo)}") - - # 可以用这个函数获取该角色的全部记忆和最新的instruction - memories = self.get_all_memories() - latest_instruction = self.get_latest_instruction() - # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) - - # 基于todo的类型,调用不同的action - if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Speak, send_to="", restricted_to="", - ) - - elif isinstance(todo, Verify): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Verify, send_to="", - restricted_to=f"Moderator,{self.profile}", - ) - - logger.info(f"{self._setting}: {rsp}") - - return msg diff --git a/examples/werewolf_game/roles/villager.py b/examples/werewolf_game/roles/villager.py index e6e59a51e..7a39071a6 100644 --- a/examples/werewolf_game/roles/villager.py +++ b/examples/werewolf_game/roles/villager.py @@ -1,7 +1,4 @@ from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak -from metagpt.schema import Message -from metagpt.logs import logger class Villager(BasePlayer): def __init__( @@ -12,27 +9,3 @@ class Villager(BasePlayer): **kwargs, ): super().__init__(name, profile, special_action_names, **kwargs) - - async def _act(self): - - # todo为_think时确定的,在村民这里,就只有一种todo,即Speak - todo = self._rc.todo - logger.info(f"{self._setting}: ready to {todo}") - - # 可以用这个函数获取该角色的全部记忆和最新的instruction - memories = self.get_all_memories() - latest_instruction = self.get_latest_instruction() - # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) - - # 根据自己定义的角色Action,对应地去run - rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) - - # 返回消息,注意给Moderator发送的加密消息需要用restricted_to="Moderator" - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Speak, send_to="", restricted_to="", - ) - - logger.info(f"{self._setting}: {rsp}") - - return msg diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py index 786e37691..00e93c96c 100644 --- a/examples/werewolf_game/roles/werewolf.py +++ b/examples/werewolf_game/roles/werewolf.py @@ -1,7 +1,5 @@ from examples.werewolf_game.roles.base_player import BasePlayer -from examples.werewolf_game.actions import Speak, Hunt, Impersonate -from metagpt.schema import Message -from metagpt.logs import logger +from examples.werewolf_game.actions import Speak, Impersonate class Werewolf(BasePlayer): def __init__( @@ -13,33 +11,8 @@ class Werewolf(BasePlayer): ): super().__init__(name, profile, special_action_names, **kwargs) - async def _act(self): - # todo为_think时确定的,有两种情况,Speak或Hunt - todo = self._rc.todo - logger.info(f"{self._setting}: ready to {str(todo)}") - - # 可以用这个函数获取该角色的全部记忆和最新的instruction - memories = self.get_all_memories() - latest_instruction = self.get_latest_instruction() - # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) - - # 根据自己定义的角色Action,对应地去run,run的入参可能不同 - if isinstance(todo, Speak): - # rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) - rsp = await Impersonate().run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Speak, send_to="", restricted_to="", - ) - - elif isinstance(todo, Hunt): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Hunt, send_to="", - restricted_to=f"Moderator,{self.profile}", # 给Moderator及狼阵营发送要杀的人的加密消息 - ) - - logger.info(f"{self._setting}: {rsp}") - - return msg + async def _think(self): + """狼人白天发言时需要伪装,与其他角色不同,因此需要重写_think""" + await super()._think() + if isinstance(self._rc.todo, Speak): + self._rc.todo = Impersonate() diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py index a570677df..c3f0e24d0 100644 --- a/examples/werewolf_game/roles/witch.py +++ b/examples/werewolf_game/roles/witch.py @@ -1,7 +1,5 @@ from examples.werewolf_game.actions import InstructSpeak, Speak, Save, Poison from examples.werewolf_game.roles.base_player import BasePlayer -from metagpt.schema import Message -from metagpt.logs import logger class Witch(BasePlayer): def __init__( @@ -14,7 +12,7 @@ class Witch(BasePlayer): super().__init__(name, profile, special_action_names, **kwargs) async def _think(self): - # 女巫涉及两个特殊技能,因此在此需要改写_think进行路由 + """女巫涉及两个特殊技能,因此在此需要改写_think进行路由""" news = self._rc.news[0] assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 if not news.restricted_to: @@ -30,41 +28,3 @@ class Witch(BasePlayer): self._rc.todo = Poison() else: raise ValueError("Moderator's instructions must include save or poison keyword") - - async def _act(self): - # todo为_think时确定的,有三种情况,Speak或Save或Poison - todo = self._rc.todo - logger.info(f"{self._setting}: ready to {str(todo)}") - - # 可以用这个函数获取该角色的全部记忆 - memories = self.get_all_memories() - latest_instruction = self.get_latest_instruction() - # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) - - # 根据自己定义的角色Action,对应地去run,run的入参可能不同 - if isinstance(todo, Speak): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Speak, send_to="", restricted_to="", - ) - - elif isinstance(todo, Save): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Save, send_to="", - restricted_to=f"Moderator,{self.profile}", # 给Moderator发送要救的人的加密消息 - ) - - elif isinstance(todo, Poison): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories) - msg = Message( - content=rsp, role=self.profile, sent_from=self.name, - cause_by=Poison, send_to="", - restricted_to=f"Moderator,{self.profile}", # 给Moderator发送要读的人的加密消息 - ) - - logger.info(f"{self._setting}: {rsp}") - - return msg From 5026a022e1c961959d7f9461bd8779d3987d7a47 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Wed, 11 Oct 2023 16:44:56 +0800 Subject: [PATCH 35/47] minor update --- examples/werewolf_game/roles/base_player.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 734228d46..7b9ed68aa 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -48,8 +48,6 @@ class BasePlayer(Role): self._rc.todo = self.special_actions[0]() async def _act(self): - """每个角色要改写此函数以实现该角色的动作""" - # raise NotImplementedError # todo为_think时确定的,有两种情况,Speak或Protect todo = self._rc.todo From 4c0ed559cca65b446f2d01d84db0ea0b42cb78ec Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Fri, 13 Oct 2023 21:25:26 +0800 Subject: [PATCH 36/47] introduce experience --- .../werewolf_game/actions/common_actions.py | 70 ++++++++++------- .../werewolf_game/actions/werewolf_actions.py | 4 +- .../werewolf_game/actions/witch_actions.py | 12 +-- examples/werewolf_game/roles/base_player.py | 40 ++++++++-- examples/werewolf_game/roles/moderator.py | 18 ++++- examples/werewolf_game/schema.py | 13 ++++ examples/werewolf_game/start_game.py | 27 +++++-- .../actions/test_experience_operation.py | 76 +++++++++++++++++++ 8 files changed, 212 insertions(+), 48 deletions(-) create mode 100644 examples/werewolf_game/schema.py create mode 100644 examples/werewolf_game/tests/actions/test_experience_operation.py diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index 2b9600260..cefdf4126 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -8,11 +8,12 @@ class Speak(Action): PROMPT_TEMPLATE = """ { - "BACKGROUND": "It's a Werewolf game, you are __profile__, say whatever possible to increase your chance of win" + "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__." ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"ATTENTION": "You can NOT VOTE a player who is NOT ALIVE now!" ,"REFLECTION": "__reflection__" ,"STRATEGY": __strategy__ + ,"PAST_EXPERIENCES": "__experiences__" ,"MODERATOR_INSTRUCTION": __latest_instruction__, ,"RULE": "Please follow the moderator's latest instruction, figure out if you need to speak your opinion or directly to vote: 1. If the instruction is to SPEAK, speak in 200 words. Remember the goal of your role and try to achieve it using your speech; @@ -21,8 +22,10 @@ class Speak(Action): { "ROLE": "Your role, in this case, __profile__" ,"PLAYER_NAME": "Your name, in this case, __name__" - ,"LIVING_PLAYERS": "List living players based on MODERATOR_INSTRUCTION. Return a LIST datatype." - ,"THOUGHTS": "Based on `MODERATOR_INSTRUCTION` and `RULE`, carefully think about what to say or vote so that your chance of win as __profile__ maximizes. Give your step-by-step thought process, you should think no more than 3 steps. For example: My step-by-step thought process:..." + ,"LIVING_PLAYERS": "List living players based on MODERATOR_INSTRUCTION. Return a json LIST datatype." + ,"THOUGHTS": "Based on `MODERATOR_INSTRUCTION` and `RULE`, carefully think about what to say or vote so that your chance of win as __profile__ maximizes. + If you find similar situation in `PAST_EXPERIENCES`, you may draw lessons from them to refine your strategy, take better vote action, or improve your speech. + Give your step-by-step thought process, you should think no more than 3 steps. For example: My step-by-step thought process:..." ,"RESPONSE": "Based on `MODERATOR_INSTRUCTION`, `RULE`, and the 'THOUGHTS' you had, express your opinion or cast a vote." } } @@ -36,21 +39,19 @@ class Speak(Action): super().__init__(name, context, llm) @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) - async def run(self, profile: str, name: str, context: str, latest_instruction: str, reflection: str = ""): + async def run(self, profile: str, name: str, context: str, latest_instruction: str, reflection: str = "", experiences: str = ""): prompt = ( self.PROMPT_TEMPLATE.replace("__context__", context).replace("__profile__", profile) .replace("__name__", name).replace("__latest_instruction__", latest_instruction) .replace("__strategy__", self.STRATEGY).replace("__reflection__", reflection) + .replace("__experiences__", experiences) ) rsp = await self._aask(prompt) rsp = rsp.replace("\n", " ") rsp_json = json.loads(rsp) - with open(WORKSPACE_ROOT / 'speak.txt', 'a') as f: - f.write(rsp) - return rsp_json['RESPONSE'] class NighttimeWhispers(Action): @@ -94,18 +95,19 @@ class NighttimeWhispers(Action): PROMPT_TEMPLATE = """ { - "ROLE": "__profile__" + "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__." + ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"ACTION": "Choose one living player to __action__." ,"ATTENTION": "1. You can only __action__ a player who is alive this night! And you can not __action__ a player who is dead this night! 2. `HISTORY` is all the information you observed, DONT hallucinate other player actions!" - ,"BACKGROUND": "It's a werewolf game and you are a __profile__. Here's the game history: __context__." ,"REFLECTION": "__reflection__" ,"STRATEGY": "__strategy__" + ,"PAST_EXPERIENCES": "__experiences__" ,"OUTPUT_FORMAT": { "ROLE": "Your role, in this case, __profile__" ,"PLAYER_NAME": "Your name, in this case, __name__" - ,"LIVING_PLAYERS": "List the players who is alive based on moderator's latest instruction. Return a LIST datatype." - ,"THOUGHTS": "Choose one living player from `LIVING_PLAYERS` to __action__ this night. Return the reason why you choose to __action__ this player. If you observe nothing at first night, DONT imagine unexisting player actions! Give your step-by-step thought process, you should think no more than 3 steps. For example: My step-by-step thought process:..." + ,"LIVING_PLAYERS": "List the players who is alive based on moderator's latest instruction. Return a json LIST datatype." + ,"THOUGHTS": "Choose one living player from `LIVING_PLAYERS` to __action__ this night. Return the reason why you choose to __action__ this player. If you observe nothing at first night, DONT imagine unexisting player actions! If you find similar situation in `PAST_EXPERIENCES`, you may draw lessons from them to refine your strategy and take better actions. Give your step-by-step thought process, you should think no more than 3 steps. For example: My step-by-step thought process:..." ,"RESPONSE": "As a __profile__, you should choose one living player from `LIVING_PLAYERS` to __action__ this night according to the THOUGHTS you have just now. Return the player name ONLY." } } @@ -117,7 +119,7 @@ class NighttimeWhispers(Action): def __init__(self, name="NightTimeWhispers", context=None, llm=None): super().__init__(name, context, llm) - def _construct_prompt_json(self, role_profile: str, role_name: str, context: str, reflection: str, **kwargs): + def _construct_prompt_json(self, role_profile: str, role_name: str, context: str, reflection: str, experiences: str, **kwargs): prompt_template = self.PROMPT_TEMPLATE def replace_string(prompt_json: dict): @@ -132,6 +134,7 @@ class NighttimeWhispers(Action): prompt_json[k] = prompt_json[k].replace("__action__", self.name) prompt_json[k] = prompt_json[k].replace("__strategy__", self.STRATEGY) prompt_json[k] = prompt_json[k].replace("__reflection__", reflection) + prompt_json[k] = prompt_json[k].replace("__experiences__", experiences) return prompt_json @@ -139,48 +142,57 @@ class NighttimeWhispers(Action): prompt_json = replace_string(prompt_json) - prompt_json: dict = self._update_prompt_json(prompt_json, role_profile, role_name, context, reflection, **kwargs) + prompt_json: dict = self._update_prompt_json(prompt_json, role_profile, role_name, context, reflection, experiences, **kwargs) assert isinstance(prompt_json, dict) - prompt: str = json.dumps(prompt_json, indent=4, separators=(',', ': '), ensure_ascii=False) + prompt: str = json.dumps(prompt_json, indent=4, ensure_ascii=False) return prompt - def _update_prompt_json(self, prompt_json: dict, role_profile: str, role_name: str, context: str, reflection: str) -> dict: + def _update_prompt_json( + self, prompt_json: dict, role_profile: str, role_name: str, context: str, reflection: str, experiences: str + ) -> dict: # one can modify the prompt_json dictionary here return prompt_json @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) - async def run(self, context: str, profile: str, name: str, reflection: str = ""): + async def run(self, context: str, profile: str, name: str, reflection: str = "", experiences: str = ""): prompt = self._construct_prompt_json( - role_profile=profile, role_name=name, context=context, reflection=reflection + role_profile=profile, role_name=name, context=context, reflection=reflection, experiences=experiences ) rsp = await self._aask(prompt) rsp = rsp.replace("\n", " ") rsp_json = json.loads(rsp) - with open(WORKSPACE_ROOT / f'{self.name}.txt', 'a') as f: - f.write(rsp) - - return f"{self.name} " + str(rsp_json["RESPONSE"]) + return f"{self.name} " + rsp_json["RESPONSE"] class Reflect(Action): + PROMPT_TEMPLATE = """ { - "BACKGROUND": "It's a Werewolf game, you are __profile__" + "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__." ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"MODERATOR_INSTRUCTION": __latest_instruction__, - ,"OUTPUT_FORMAT": + ,"OUTPUT_FORMAT" (a json): { "ROLE": "Your role, in this case, __profile__" ,"PLAYER_NAME": "Your name, in this case, __name__" - ,"LIVING_PLAYERS": "List living players based on MODERATOR_INSTRUCTION. Return a LIST datatype." - ,"REFLECTION": "You are about to follow `MODERATOR_INSTRUCTION`, but before taking any action, think about - what insights you can draw from `HISTORY` for achieving your objective? - Try to figure out the role of each player including living or dead, and summarize the game states. Give your reflection in no more than three sentences." - ,"STRATEGY": Based on your reflection, think at high level what strategy you will take, in one sentence. + "GAME_STATES": "You are about to follow `MODERATOR_INSTRUCTION`, but before taking any action, analyze each player, including the living and the dead, and summarize the game states. + For each player, your reflection should be a ONE-LINE json covering the following dimension, return a LIST of jsons (return an empty LIST for the first night): + [ + {"TARGET": "the player you will analyze, if the player is yourself or your werewolf partner, indicate it" ,"STATUS": "living or dead, if dead, how was he/she possibly killed?", "CLAIMED_ROLE": "claims a role or not, if so, what role, any contradiction to others? If there is no claim, return 'None'", "SIDE_WITH": "sides with which players? If none, return 'None'", "ACCUSE": "accuses which players? If none, return 'None'"} + ,{...} + ,... + ]" + ,"REFLECTION": "Based on the whole `GAME_STATES`, return a json (return an empty string for the first night): + { + "Player1": "the true role (werewolf / special role / villager, living or dead) you infer about him/her, and why is this role? If the player is yourself or your werewolf partner, indicate it." + ,... + ,"Player7": "the true role (werewolf / special role / villager, living or dead) you infer about him/her, and why is this role? If the player is yourself or your werewolf partner, indicate it." + ,"GAME_STATE_SUMMARIZATION": "summarize the current situation from your standpoint in one sentence, your summarization should catch the most important information from your reflection, such as conflicts, number of living werewolves, special roles, and villagers." + }" } } """ @@ -200,4 +212,4 @@ class Reflect(Action): rsp = rsp.replace("\n", " ") rsp_json = json.loads(rsp) - return rsp_json['REFLECTION'] + return json.dumps(rsp_json['REFLECTION']) diff --git a/examples/werewolf_game/actions/werewolf_actions.py b/examples/werewolf_game/actions/werewolf_actions.py index 24272c79e..b1152092c 100644 --- a/examples/werewolf_game/actions/werewolf_actions.py +++ b/examples/werewolf_game/actions/werewolf_actions.py @@ -10,9 +10,9 @@ class Impersonate(Speak): """Action: werewolf impersonating a good guy in daytime speak""" STRATEGY = """ - Try continuously impersonating a role with special ability, such as a Seer or a Witch, in order to mislead + Try continuously impersonating a role, such as Seer, Guard, Villager, etc., in order to mislead other players, make them trust you, and thus hiding your werewolf identity. However, pay attention to what your werewolf partner said, - if your werewolf partner has claimed to be a Seer or Witch, DONT claim to be the same role. Remmber NOT to reveal your real identity as a werewolf! + DONT claim the same role as your werewolf partner. Remmber NOT to reveal your real identity as a werewolf! """ def __init__(self, name="Impersonate", context=None, llm=None): diff --git a/examples/werewolf_game/actions/witch_actions.py b/examples/werewolf_game/actions/witch_actions.py index d5d8aa5a2..7b17345e6 100644 --- a/examples/werewolf_game/actions/witch_actions.py +++ b/examples/werewolf_game/actions/witch_actions.py @@ -5,11 +5,13 @@ class Save(NighttimeWhispers): def __init__(self, name="Save", context=None, llm=None): super().__init__(name, context, llm) - def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, reflection: str, **kwargs): + def _update_prompt_json( + self, prompt_json: dict, role_profile: str, role_name: str, context: str, reflection: str, experiences: str + ) -> dict: del prompt_json['ACTION'] del prompt_json['ATTENTION'] - prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST be killed at this night." + prompt_json["OUTPUT_FORMAT"]["THOUGHTS"] = "It is night time. Return the thinking steps of your decision of whether to save the player JUST killed this night." prompt_json["OUTPUT_FORMAT"]["RESPONSE"] = "Follow the Moderator's instruction, decide whether you want to save that person or not. Return SAVE or PASS." return prompt_json @@ -28,10 +30,10 @@ class Poison(NighttimeWhispers): def __init__(self, name="Poison", context=None, llm=None): super().__init__(name, context, llm) - def _update_prompt_json(self, prompt_json: dict, profile: str, name: str, context: str, reflection: str, **kwargs): - + def _update_prompt_json( + self, prompt_json: dict, role_profile: str, role_name: str, context: str, reflection: str, experiences: str + ) -> dict: prompt_json["OUTPUT_FORMAT"]["RESPONSE"] += "Or if you want to PASS, return PASS." - return prompt_json async def run(self, *args, **kwargs): diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 7b9ed68aa..88073c559 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -4,7 +4,8 @@ from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger from examples.werewolf_game.actions import ACTIONS, InstructSpeak, Speak, Reflect, NighttimeWhispers - +from examples.werewolf_game.actions.experience_operation import AddNewExperiences, RetrieveExperiences +from examples.werewolf_game.schema import RoleExperience class BasePlayer(Role): def __init__( @@ -12,6 +13,9 @@ class BasePlayer(Role): name: str = "PlayerXYZ", profile: str = "BasePlayer", special_action_names: list[str] = [], + use_reflection: bool = True, + use_experience: bool = False, + use_memory_selection: bool = False, **kwargs, ): super().__init__(name, profile, **kwargs) @@ -25,6 +29,16 @@ class BasePlayer(Role): self._init_actions(capable_actions) # 给角色赋予行动技能 self.special_actions = special_actions + self.use_reflection = use_reflection + if not self.use_reflection and use_experience: + logger.warning("You must enable use_reflection before using experience") + self.use_experience = False + else: + self.use_experience = use_experience + self.use_memory_selection = use_memory_selection + + self.experiences = [] + async def _observe(self) -> int: if self.status == 1: # 死者不再参与游戏 @@ -60,18 +74,21 @@ class BasePlayer(Role): reflection = await Reflect().run( profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction - ) + ) if self.use_reflection else "" + + experiences = RetrieveExperiences().run(query=reflection, profile=self.profile) \ + if self.use_experience else "" # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): rsp = await todo.run( profile=self.profile, name=self.name, context=memories, - latest_instruction=latest_instruction, reflection=reflection - ) + latest_instruction=latest_instruction, reflection=reflection, experiences=experiences) restricted_to = "" elif isinstance(todo, NighttimeWhispers): - rsp = await todo.run(profile=self.profile, name=self.name, context=memories, reflection=reflection) + rsp = await todo.run(profile=self.profile, name=self.name, context=memories, + reflection=reflection, experiences=experiences) restricted_to = f"Moderator,{self.profile}" # 给Moderator发送使用特殊技能的加密消息 msg = Message( @@ -80,6 +97,11 @@ class BasePlayer(Role): restricted_to=restricted_to ) + self.experiences.append( + RoleExperience(name=self.name, profile=self.profile, reflection=reflection, + instruction=latest_instruction, response=rsp) + ) + logger.info(f"{self._setting}: {rsp}") return msg @@ -97,3 +119,11 @@ class BasePlayer(Role): def set_status(self, new_status): self.status = new_status + + def record_experiences(self, round_id: str, outcome: str, game_setup: str): + experiences = [exp for exp in self.experiences if exp.reflection] + for exp in experiences: + exp.round_id = round_id + exp.outcome = outcome + exp.game_setup = game_setup + AddNewExperiences().run(experiences) diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index f4bad6c96..8bd1982b8 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -1,5 +1,6 @@ import re from collections import Counter +from datetime import datetime from metagpt.const import WORKSPACE_ROOT from metagpt.roles import Role @@ -27,6 +28,7 @@ class Moderator(Role): self.eval_step_idx = [] # game states + self.game_setup = "" self.living_players = [] self.werewolf_players = [] self.villager_players = [] @@ -44,6 +46,7 @@ class Moderator(Role): self.player_current_dead = [] def _parse_game_setup(self, game_setup: str): + self.game_setup = game_setup self.living_players = re.findall(r"Player[0-9]+", game_setup) self.werewolf_players = re.findall(r"Player[0-9]+: Werewolf", game_setup) self.werewolf_players = [p.replace(": Werewolf", "") for p in self.werewolf_players] @@ -61,6 +64,18 @@ class Moderator(Role): if player_name in role_setting: role.set_status(new_status=1) # 更新为死亡 + def _record_all_experiences(self): + roles_in_env = self._rc.env.get_roles() + timestamp = datetime.now().strftime('%Y-%m-%d-%H:%M:%S') + for _, role in roles_in_env.items(): + if role == self: + continue + if self.winner == "werewolf": + outcome = "won" if role.name in self.werewolf_players else "lost" + else: + outcome = "won" if role.name not in self.werewolf_players else "lost" + role.record_experiences(round_id=timestamp, outcome=outcome, game_setup=self.game_setup) + async def _instruct_speak(self): print("*" * 10, "STEP: ", self.step_idx, "*" * 10) step_idx = self.step_idx % len(STEP_INSTRUCTIONS) @@ -87,7 +102,6 @@ class Moderator(Role): msg_cause_by = latest_msg.cause_by if msg_cause_by == Hunt: self.player_hunted = target - # breakpoint() elif msg_cause_by == Protect: self.player_protected = target elif msg_cause_by == Verify: @@ -167,6 +181,8 @@ class Moderator(Role): elif not living_villagers or not living_special_roles: self.winner = "werewolf" self.win_reason = "villagers all dead" if not living_villagers else "special roles all dead" + if self.winner is not None: + self._record_all_experiences() def _record_game_history(self): if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: diff --git a/examples/werewolf_game/schema.py b/examples/werewolf_game/schema.py new file mode 100644 index 000000000..bfca7ad8e --- /dev/null +++ b/examples/werewolf_game/schema.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + +class RoleExperience(BaseModel): + id: str = "" + name: str = "" + profile: str + reflection: str + instruction: str = "" + response: str + outcome: str = "" + round_id: str = "" + game_setup: str = "" + version: str = "01-10" diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 12552452c..8d50898e0 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -8,7 +8,10 @@ from examples.werewolf_game.werewolf_game import WerewolfGame from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard, Seer, Witch from examples.werewolf_game.roles.human_player import prepare_human_player -def init_game_setup(shuffle=True, add_human=False): +def init_game_setup( + shuffle=True, add_human=False, + use_reflection=True, use_experience=False, use_memory_selection=False + ): roles = [ Villager, Villager, @@ -26,7 +29,12 @@ def init_game_setup(shuffle=True, add_human=False): assigned_role = roles[assigned_role_idx] roles[assigned_role_idx] = prepare_human_player(assigned_role) - players = [role(name=f"Player{i+1}") for i, role in enumerate(roles)] + players = [ + role( + name=f"Player{i+1}", + use_reflection=use_reflection, use_experience=use_experience, use_memory_selection=use_memory_selection + ) for i, role in enumerate(roles) + ] if add_human: logger.info(f"You are assigned {players[assigned_role_idx].name}({players[assigned_role_idx].profile})") @@ -36,22 +44,29 @@ def init_game_setup(shuffle=True, add_human=False): return game_setup, players -async def start_game(investment: float = 3.0, n_round: int = 5, shuffle : bool = True, add_human: bool = False): +async def start_game( + investment: float = 3.0, n_round: int = 5, shuffle : bool = True, add_human: bool = False, + use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False +): game = WerewolfGame() - game_setup, players = init_game_setup(shuffle=shuffle, add_human=add_human) + game_setup, players = init_game_setup(shuffle=shuffle, add_human=add_human, + use_reflection=use_reflection, use_experience=use_experience, use_memory_selection=use_memory_selection) players = [Moderator()] + players game.hire(players) game.invest(investment) game.start_project(game_setup) await game.run(n_round=n_round) -def main(investment: float = 3.0, n_round: int = 100, shuffle : bool = True, add_human: bool = False): +def main(investment: float = 20.0, n_round: int = 100, shuffle : bool = True, add_human: bool = False, + use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False): """ :param investment: contribute a certain dollar amount to watch the debate :param n_round: maximum rounds of the debate :return: """ - asyncio.run(start_game(investment, n_round, shuffle, add_human)) + asyncio.run( + start_game(investment, n_round, shuffle, add_human, use_reflection, use_experience, use_memory_selection) + ) if __name__ == '__main__': diff --git a/examples/werewolf_game/tests/actions/test_experience_operation.py b/examples/werewolf_game/tests/actions/test_experience_operation.py new file mode 100644 index 000000000..54db3fec7 --- /dev/null +++ b/examples/werewolf_game/tests/actions/test_experience_operation.py @@ -0,0 +1,76 @@ +import json +import os + +import pytest + +from metagpt.logs import logger +from metagpt.const import WORKSPACE_ROOT +from examples.werewolf_game.schema import RoleExperience +from examples.werewolf_game.actions.experience_operation import AddNewExperiences, RetrieveExperiences + + +class TestExperiencesOperation: + + test_round_id = "test_01" + samples_to_add = [ + RoleExperience(profile="Witch", reflection="The game is intense with two players claiming to be the Witch and one claiming to be the Seer. Player4's behavior is suspicious.", response="", outcome="", round_id=test_round_id), + RoleExperience(profile="Witch", reflection="The game is in a critical state with only three players left, and I need to make a wise decision to save Player7 or not.", response="", outcome="", round_id=test_round_id), + RoleExperience(profile="Seer", reflection="Player1, who is a werewolf, falsely claimed to be a Seer, and Player6, who might be a Witch, sided with him. I, as the real Seer, am under suspicion.", response="", outcome="", round_id=test_round_id), + ] + + @pytest.mark.asyncio + async def test_add(self): + saved_file = f"{WORKSPACE_ROOT}/werewolf_game/experiences/{self.test_round_id}.json" + if os.path.exists(saved_file): + os.remove(saved_file) + + action = AddNewExperiences(collection_name="test", delete_existing=True) + action.run(self.samples_to_add) + + # test insertion + inserted = action.collection.get() + assert len(inserted["documents"]) == len(self.samples_to_add) + + # test if we record the samples correctly to local file + # & test if we could recover a embedding db from the file + action = AddNewExperiences(collection_name="test", delete_existing=True) + action.add_from_file(saved_file) + inserted = action.collection.get() + assert len(inserted["documents"]) == len(self.samples_to_add) + + @pytest.mark.asyncio + async def test_retrieve(self): + action = RetrieveExperiences(collection_name="test") + + query = "one player claimed to be Seer and the other Witch" + results = action.run(query, "Witch") + results = json.loads(results) + + assert len(results) == 2 + assert "The game is intense with two players" in results[0] + + @pytest.mark.asyncio + async def test_check_experience_pool(self): + logger.info("check experience pool") + action = RetrieveExperiences(collection_name="role_reflection") + print(*action.collection.get()["metadatas"][-5:], sep="\n") + + @pytest.mark.asyncio + async def test_retrieve_werewolf_experience(self): + + action = RetrieveExperiences(collection_name="role_reflection") + + query = "there are conflicts" + + logger.info(f"test retrieval with {query=}") + results = action.run(query, "Werewolf") + + @pytest.mark.asyncio + async def test_retrieve_villager_experience(self): + + action = RetrieveExperiences(collection_name="role_reflection") + + query = "there are conflicts" + + logger.info(f"test retrieval with {query=}") + results = action.run(query, "Seer") From 205d1c9843a6291fb0db6b0121e8526edf1d6005 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Fri, 13 Oct 2023 22:26:59 +0800 Subject: [PATCH 37/47] experience operation --- .../actions/experience_operation.py | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 examples/werewolf_game/actions/experience_operation.py diff --git a/examples/werewolf_game/actions/experience_operation.py b/examples/werewolf_game/actions/experience_operation.py new file mode 100644 index 000000000..2ced3d0c8 --- /dev/null +++ b/examples/werewolf_game/actions/experience_operation.py @@ -0,0 +1,156 @@ +import json +import os + +import chromadb +from chromadb.utils import embedding_functions + +from metagpt.config import CONFIG +from metagpt.actions import Action +from metagpt.const import WORKSPACE_ROOT +from metagpt.logs import logger +from examples.werewolf_game.schema import RoleExperience + +DEFAULT_COLLECTION_NAME = "role_reflection" # FIXME: some hard code for now +EMB_FN = embedding_functions.OpenAIEmbeddingFunction( + api_key=CONFIG.openai_api_key, + api_base=CONFIG.openai_api_base, + api_type=CONFIG.openai_api_type, + model_name="text-embedding-ada-002", + api_version="2", +) + +class AddNewExperiences(Action): + def __init__( + self, name="AddNewExperience", context=None, llm=None, + collection_name=DEFAULT_COLLECTION_NAME, delete_existing=False, + ): + super().__init__(name, context, llm) + chroma_client = chromadb.PersistentClient(path=f"{WORKSPACE_ROOT}/werewolf_game/chroma") + if delete_existing: + try: + chroma_client.get_collection(name=collection_name) + chroma_client.delete_collection(name=collection_name) + logger.info(f"existing collection {collection_name} deleted") + except: + pass + + # emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="multi-qa-mpnet-base-cos-v1") + + self.collection = chroma_client.get_or_create_collection( + name=collection_name, + metadata={"hnsw:space": "cosine"}, + embedding_function=EMB_FN, + ) + + def run(self, experiences: list[RoleExperience]): + if not experiences: + return + for i, exp in enumerate(experiences): + exp.id = f"{exp.profile}-{exp.name}-step{i}-round_{exp.round_id}" + ids = [exp.id for exp in experiences] + documents = [exp.reflection for exp in experiences] + metadatas = [exp.dict() for exp in experiences] + + AddNewExperiences._record_experiences_local(experiences) + + self.collection.add( + documents=documents, + metadatas=metadatas, + ids=ids + ) + + def add_from_file(self, file_path): + with open(file_path, "r") as fl: + lines = fl.readlines() + experiences = [RoleExperience(**json.loads(line)) for line in lines] + + ids = [exp.id for exp in experiences] + documents = [exp.reflection for exp in experiences] + metadatas = [exp.dict() for exp in experiences] + + self.collection.add( + documents=documents, + metadatas=metadatas, + ids=ids + ) + + @staticmethod + def _record_experiences_local(experiences: list[RoleExperience]): + round_id = experiences[0].round_id + experiences = [exp.json() for exp in experiences] + experience_folder = WORKSPACE_ROOT / 'werewolf_game/experiences' + if not os.path.exists(experience_folder): + os.makedirs(experience_folder) + save_path = f"{experience_folder}/{round_id}.json" + with open(save_path, "a") as fl: + fl.write("\n".join(experiences)) + logger.info(f"experiences saved to {save_path}") + +class RetrieveExperiences(Action): + + def __init__( + self, name="RetrieveExperiences", context=None, llm=None, collection_name=DEFAULT_COLLECTION_NAME): + super().__init__(name, context, llm) + chroma_client = chromadb.PersistentClient(path=f"{WORKSPACE_ROOT}/werewolf_game/chroma") + try: + self.collection = chroma_client.get_collection( + name=collection_name, + embedding_function=EMB_FN, + ) + self.has_experiences = True + except: + logger.warning(f"No experience pool {collection_name}") + self.has_experiences = False + + def run(self, query: str, profile: str, topk: int = 5) -> str: + """_summary_ + + Args: + query (str): 用当前的reflection作为query去检索过去相似的reflection + profile (str): _description_ + topk (int, optional): _description_. Defaults to 5. + + Returns: + _type_: _description_ + """ + if not self.has_experiences: + return "" + + results = self.collection.query( + query_texts=[query], + n_results=topk, + where={"profile": profile}, + ) + + logger.info("retrieved exp") + past_experiences = [RoleExperience(**res) for res in results["metadatas"][0]] + # print(*past_experiences, sep="\n\n") + distances = results["distances"][0] + print(distances) + + template = """ + { + "Situation __i__": "__situation__" + ,"Moderator's instruction": "__instruction__" + ,"Your action or speech during that time": "__response__" + ,"Reality": "In fact, it turned out the true roles are __game_step__", + ,"Outcome": "You __outcome__ in the end" + } + """ + past_experiences = [ + (template.replace("__i__", str(i)).replace("__situation__", exp.reflection) + .replace("__instruction__", exp.instruction).replace("__response__", exp.response) + .replace("__game_step__", exp.game_setup.replace("0 | Game setup:\n", "").replace("\n", " ")) + .replace("__outcome__", exp.outcome)) + for i, exp in enumerate(past_experiences) + ] + print(*past_experiences, sep="\n") + + return json.dumps(past_experiences) + +def delete_collection(name): + chroma_client = chromadb.PersistentClient(path=f"{WORKSPACE_ROOT}/werewolf_game/chroma") + chroma_client.delete_collection(name=name) + +# if __name__ == "__main__": +# delete_collection(name="test") From 8525ec6d7bad52685fa93889296d3a5831f1233e Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Tue, 17 Oct 2023 21:27:14 +0800 Subject: [PATCH 38/47] small bug fixed for reflection record and retrieve --- .../werewolf_game/actions/common_actions.py | 6 +- .../actions/experience_operation.py | 53 +++++++++++--- examples/werewolf_game/roles/base_player.py | 11 +-- examples/werewolf_game/roles/moderator.py | 2 +- examples/werewolf_game/schema.py | 2 +- examples/werewolf_game/start_game.py | 29 ++++---- .../actions/test_experience_operation.py | 72 +++++++++++++++---- 7 files changed, 131 insertions(+), 44 deletions(-) diff --git a/examples/werewolf_game/actions/common_actions.py b/examples/werewolf_game/actions/common_actions.py index cefdf4126..d9b886743 100644 --- a/examples/werewolf_game/actions/common_actions.py +++ b/examples/werewolf_game/actions/common_actions.py @@ -8,7 +8,7 @@ class Speak(Action): PROMPT_TEMPLATE = """ { - "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__." + "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__. Note that villager, seer, guard and witch are all in villager side, they have the same objective. Werewolves can collectively hunt ONE player at night." ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"ATTENTION": "You can NOT VOTE a player who is NOT ALIVE now!" ,"REFLECTION": "__reflection__" @@ -95,7 +95,7 @@ class NighttimeWhispers(Action): PROMPT_TEMPLATE = """ { - "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__." + "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__. Note that villager, seer, guard and witch are all in villager side, they have the same objective. Werewolves can collectively hunt ONE player at night." ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"ACTION": "Choose one living player to __action__." ,"ATTENTION": "1. You can only __action__ a player who is alive this night! And you can not __action__ a player who is dead this night! 2. `HISTORY` is all the information you observed, DONT hallucinate other player actions!" @@ -172,7 +172,7 @@ class Reflect(Action): PROMPT_TEMPLATE = """ { - "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__." + "BACKGROUND": "It's a Werewolf game, in this game, we have 2 werewolves, 2 villagers, 1 guard, 1 witch, 1 seer. You are __profile__. Note that villager, seer, guard and witch are all in villager side, they have the same objective. Werewolves can collectively hunt ONE player at night." ,"HISTORY": "You have knowledge to the following conversation: __context__" ,"MODERATOR_INSTRUCTION": __latest_instruction__, ,"OUTPUT_FORMAT" (a json): diff --git a/examples/werewolf_game/actions/experience_operation.py b/examples/werewolf_game/actions/experience_operation.py index 2ced3d0c8..ea930d743 100644 --- a/examples/werewolf_game/actions/experience_operation.py +++ b/examples/werewolf_game/actions/experience_operation.py @@ -1,5 +1,6 @@ import json import os +import glob import chromadb from chromadb.utils import embedding_functions @@ -63,6 +64,7 @@ class AddNewExperiences(Action): with open(file_path, "r") as fl: lines = fl.readlines() experiences = [RoleExperience(**json.loads(line)) for line in lines] + experiences = [exp for exp in experiences if len(exp.reflection) > 2] # not "" or not '""' ids = [exp.id for exp in experiences] documents = [exp.reflection for exp in experiences] @@ -77,13 +79,16 @@ class AddNewExperiences(Action): @staticmethod def _record_experiences_local(experiences: list[RoleExperience]): round_id = experiences[0].round_id + version = experiences[0].version + version = "test" if not version else version experiences = [exp.json() for exp in experiences] - experience_folder = WORKSPACE_ROOT / 'werewolf_game/experiences' + experience_folder = WORKSPACE_ROOT / f'werewolf_game/experiences/{version}' if not os.path.exists(experience_folder): os.makedirs(experience_folder) save_path = f"{experience_folder}/{round_id}.json" with open(save_path, "a") as fl: fl.write("\n".join(experiences)) + fl.write("\n") logger.info(f"experiences saved to {save_path}") class RetrieveExperiences(Action): @@ -102,7 +107,7 @@ class RetrieveExperiences(Action): logger.warning(f"No experience pool {collection_name}") self.has_experiences = False - def run(self, query: str, profile: str, topk: int = 5) -> str: + def run(self, query: str, profile: str, topk: int = 5, excluded_version: str = "", verbose: bool = False) -> str: """_summary_ Args: @@ -113,20 +118,30 @@ class RetrieveExperiences(Action): Returns: _type_: _description_ """ - if not self.has_experiences: + if not self.has_experiences or len(query) <= 2: # not "" or not '""' return "" + + filters = {"profile": profile} + ### 消融实验逻辑 ### + if profile == "Werewolf": # 狼人作为基线,不用经验 + logger.warning("Disable werewolves' experiences") + return "" + if excluded_version: + filters = {"$and": [{"profile": profile}, {"version": {"$ne": excluded_version}}]} # 不用同一版本的经验,只用之前的 + ################# results = self.collection.query( query_texts=[query], n_results=topk, - where={"profile": profile}, + where=filters, ) logger.info("retrieved exp") past_experiences = [RoleExperience(**res) for res in results["metadatas"][0]] - # print(*past_experiences, sep="\n\n") - distances = results["distances"][0] - print(distances) + if verbose: + print(*past_experiences, sep="\n\n") + distances = results["distances"][0] + print(distances) template = """ { @@ -148,9 +163,31 @@ class RetrieveExperiences(Action): return json.dumps(past_experiences) +# FIXME: below are some utility functions, should be moved to appropriate places def delete_collection(name): chroma_client = chromadb.PersistentClient(path=f"{WORKSPACE_ROOT}/werewolf_game/chroma") chroma_client.delete_collection(name=name) +def add_file_batch(folder, **kwargs): + action = AddNewExperiences(**kwargs) + file_paths = glob.glob(str(folder) + "/*") + for fp in file_paths: + print(fp) + action.add_from_file(fp) + +def modify_collection(): + chroma_client = chromadb.PersistentClient(path=f"{WORKSPACE_ROOT}/werewolf_game/chroma") + collection = chroma_client.get_collection(name=DEFAULT_COLLECTION_NAME) + updated_name = DEFAULT_COLLECTION_NAME + "_backup" + collection.modify(name=updated_name) + try: + chroma_client.get_collection(name=DEFAULT_COLLECTION_NAME) + except: + logger.info(f"collection {DEFAULT_COLLECTION_NAME} not found") + updated_collection = chroma_client.get_collection(name=updated_name) + print(updated_collection.get()["documents"][-5:]) + # if __name__ == "__main__": -# delete_collection(name="test") + # delete_collection(name="test") + # add_file_batch(WORKSPACE_ROOT / 'werewolf_game/experiences', collection_name=DEFAULT_COLLECTION_NAME, delete_existing=True) + # modify_collection() diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 88073c559..6aa5c5e08 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -16,6 +16,7 @@ class BasePlayer(Role): use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False, + new_experience_version: str = "", **kwargs, ): super().__init__(name, profile, **kwargs) @@ -35,6 +36,7 @@ class BasePlayer(Role): self.use_experience = False else: self.use_experience = use_experience + self.new_experience_version = new_experience_version if self.use_experience else "" self.use_memory_selection = use_memory_selection self.experiences = [] @@ -76,8 +78,9 @@ class BasePlayer(Role): profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction ) if self.use_reflection else "" - experiences = RetrieveExperiences().run(query=reflection, profile=self.profile) \ - if self.use_experience else "" + experiences = RetrieveExperiences().run( + query=reflection, profile=self.profile, excluded_version=self.new_experience_version + ) if self.use_experience else "" # 根据自己定义的角色Action,对应地去run,run的入参可能不同 if isinstance(todo, Speak): @@ -99,7 +102,7 @@ class BasePlayer(Role): self.experiences.append( RoleExperience(name=self.name, profile=self.profile, reflection=reflection, - instruction=latest_instruction, response=rsp) + instruction=latest_instruction, response=rsp, version=self.new_experience_version) ) logger.info(f"{self._setting}: {rsp}") @@ -121,7 +124,7 @@ class BasePlayer(Role): self.status = new_status def record_experiences(self, round_id: str, outcome: str, game_setup: str): - experiences = [exp for exp in self.experiences if exp.reflection] + experiences = [exp for exp in self.experiences if len(exp.reflection) > 2] # not "" or not '""' for exp in experiences: exp.round_id = round_id exp.outcome = outcome diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 8bd1982b8..6bb294e91 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -166,7 +166,7 @@ class Moderator(Role): if not voted: continue voted_all.append(voted.group(0)) - self.player_current_dead = [Counter(voted_all).most_common()[0][0]] # 平票时,杀序号小的 + self.player_current_dead = [Counter(voted_all).most_common()[0][0]] # 平票时,杀最先被投的 # print("*" * 10, "dead", self.player_current_dead) self.living_players = [p for p in self.living_players if p not in self.player_current_dead] self.update_player_status(self.player_current_dead) diff --git a/examples/werewolf_game/schema.py b/examples/werewolf_game/schema.py index bfca7ad8e..311dfa30e 100644 --- a/examples/werewolf_game/schema.py +++ b/examples/werewolf_game/schema.py @@ -10,4 +10,4 @@ class RoleExperience(BaseModel): outcome: str = "" round_id: str = "" game_setup: str = "" - version: str = "01-10" + version: str = "" diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 8d50898e0..18164b65a 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -10,7 +10,8 @@ from examples.werewolf_game.roles.human_player import prepare_human_player def init_game_setup( shuffle=True, add_human=False, - use_reflection=True, use_experience=False, use_memory_selection=False + use_reflection=True, use_experience=False, use_memory_selection=False, + new_experience_version="", ): roles = [ Villager, @@ -32,7 +33,8 @@ def init_game_setup( players = [ role( name=f"Player{i+1}", - use_reflection=use_reflection, use_experience=use_experience, use_memory_selection=use_memory_selection + use_reflection=use_reflection, use_experience=use_experience, use_memory_selection=use_memory_selection, + new_experience_version=new_experience_version ) for i, role in enumerate(roles) ] @@ -46,11 +48,14 @@ def init_game_setup( async def start_game( investment: float = 3.0, n_round: int = 5, shuffle : bool = True, add_human: bool = False, - use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False + use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False, + new_experience_version: str = "", ): game = WerewolfGame() - game_setup, players = init_game_setup(shuffle=shuffle, add_human=add_human, - use_reflection=use_reflection, use_experience=use_experience, use_memory_selection=use_memory_selection) + game_setup, players = init_game_setup( + shuffle=shuffle, add_human=add_human, use_reflection=use_reflection, use_experience=use_experience, + use_memory_selection=use_memory_selection, new_experience_version=new_experience_version, + ) players = [Moderator()] + players game.hire(players) game.invest(investment) @@ -58,15 +63,11 @@ async def start_game( await game.run(n_round=n_round) def main(investment: float = 20.0, n_round: int = 100, shuffle : bool = True, add_human: bool = False, - use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False): - """ - :param investment: contribute a certain dollar amount to watch the debate - :param n_round: maximum rounds of the debate - :return: - """ - asyncio.run( - start_game(investment, n_round, shuffle, add_human, use_reflection, use_experience, use_memory_selection) - ) + use_reflection: bool = True, use_experience: bool = False, use_memory_selection: bool = False, + new_experience_version: str = ""): + + asyncio.run(start_game(investment, n_round, shuffle, add_human, + use_reflection, use_experience, use_memory_selection, new_experience_version)) if __name__ == '__main__': diff --git a/examples/werewolf_game/tests/actions/test_experience_operation.py b/examples/werewolf_game/tests/actions/test_experience_operation.py index 54db3fec7..85a63cca4 100644 --- a/examples/werewolf_game/tests/actions/test_experience_operation.py +++ b/examples/werewolf_game/tests/actions/test_experience_operation.py @@ -11,20 +11,25 @@ from examples.werewolf_game.actions.experience_operation import AddNewExperience class TestExperiencesOperation: + collection_name = "test" test_round_id = "test_01" + version = "test" samples_to_add = [ - RoleExperience(profile="Witch", reflection="The game is intense with two players claiming to be the Witch and one claiming to be the Seer. Player4's behavior is suspicious.", response="", outcome="", round_id=test_round_id), - RoleExperience(profile="Witch", reflection="The game is in a critical state with only three players left, and I need to make a wise decision to save Player7 or not.", response="", outcome="", round_id=test_round_id), - RoleExperience(profile="Seer", reflection="Player1, who is a werewolf, falsely claimed to be a Seer, and Player6, who might be a Witch, sided with him. I, as the real Seer, am under suspicion.", response="", outcome="", round_id=test_round_id), + RoleExperience(profile="Witch", reflection="The game is intense with two players claiming to be the Witch and one claiming to be the Seer. Player4's behavior is suspicious.", response="", outcome="", round_id=test_round_id, version=version), + RoleExperience(profile="Witch", reflection="The game is in a critical state with only three players left, and I need to make a wise decision to save Player7 or not.", response="", outcome="", round_id=test_round_id, version=version), + RoleExperience(profile="Seer", reflection="Player1, who is a werewolf, falsely claimed to be a Seer, and Player6, who might be a Witch, sided with him. I, as the real Seer, am under suspicion.", response="", outcome="", round_id=test_round_id, version=version), + RoleExperience(profile="TestRole", reflection="Some test reflection1", response="", outcome="", round_id=test_round_id, version=version+"_01-10"), + RoleExperience(profile="TestRole", reflection="Some test reflection2", response="", outcome="", round_id=test_round_id, version=version+"_11-20"), + RoleExperience(profile="TestRole", reflection="Some test reflection3", response="", outcome="", round_id=test_round_id, version=version+"_21-30"), ] @pytest.mark.asyncio async def test_add(self): - saved_file = f"{WORKSPACE_ROOT}/werewolf_game/experiences/{self.test_round_id}.json" + saved_file = f"{WORKSPACE_ROOT}/werewolf_game/experiences/{self.version}/{self.test_round_id}.json" if os.path.exists(saved_file): os.remove(saved_file) - action = AddNewExperiences(collection_name="test", delete_existing=True) + action = AddNewExperiences(collection_name=self.collection_name, delete_existing=True) action.run(self.samples_to_add) # test insertion @@ -33,32 +38,55 @@ class TestExperiencesOperation: # test if we record the samples correctly to local file # & test if we could recover a embedding db from the file - action = AddNewExperiences(collection_name="test", delete_existing=True) + action = AddNewExperiences(collection_name=self.collection_name, delete_existing=True) action.add_from_file(saved_file) inserted = action.collection.get() assert len(inserted["documents"]) == len(self.samples_to_add) @pytest.mark.asyncio async def test_retrieve(self): - action = RetrieveExperiences(collection_name="test") + action = RetrieveExperiences(collection_name=self.collection_name) query = "one player claimed to be Seer and the other Witch" - results = action.run(query, "Witch") + results = action.run(query, profile="Witch") results = json.loads(results) - assert len(results) == 2 + assert len(results) == 2, "Witch should have 2 experiences" assert "The game is intense with two players" in results[0] + @pytest.mark.asyncio + async def test_retrieve_filtering(self): + action = RetrieveExperiences(collection_name=self.collection_name) + + query = "some test query" + profile = "TestRole" + + excluded_version = "" + results = action.run(query, profile=profile, excluded_version=excluded_version) + results = json.loads(results) + assert len(results) == 3 + + excluded_version = self.version + "_21-30" + results = action.run(query, profile=profile, excluded_version=excluded_version) + results = json.loads(results) + assert len(results) == 2 + +class TestActualRetrieve: + + collection_name = "role_reflection" + @pytest.mark.asyncio async def test_check_experience_pool(self): logger.info("check experience pool") - action = RetrieveExperiences(collection_name="role_reflection") - print(*action.collection.get()["metadatas"][-5:], sep="\n") + action = RetrieveExperiences(collection_name=self.collection_name) + all_experiences = action.collection.get() + logger.info(f"{len(all_experiences['metadatas'])=}") + print(*["metadatas"][-5:], sep="\n") @pytest.mark.asyncio async def test_retrieve_werewolf_experience(self): - action = RetrieveExperiences(collection_name="role_reflection") + action = RetrieveExperiences(collection_name=self.collection_name) query = "there are conflicts" @@ -68,9 +96,27 @@ class TestExperiencesOperation: @pytest.mark.asyncio async def test_retrieve_villager_experience(self): - action = RetrieveExperiences(collection_name="role_reflection") + action = RetrieveExperiences(collection_name=self.collection_name) query = "there are conflicts" logger.info(f"test retrieval with {query=}") results = action.run(query, "Seer") + assert "conflict" in results # 相似局面应该需要包含conflict关键词 + + @pytest.mark.asyncio + async def test_retrieve_villager_experience_filtering(self): + + action = RetrieveExperiences(collection_name=self.collection_name) + + query = "there are conflicts" + + excluded_version = "01-10" + logger.info(f"test retrieval with {excluded_version=}") + results_01_10 = action.run(query, profile="Seer", excluded_version=excluded_version, verbose=True) + + excluded_version = "11-20" + logger.info(f"test retrieval with {excluded_version=}") + results_11_20 = action.run(query, profile="Seer", excluded_version=excluded_version, verbose=True) + + assert results_01_10 != results_11_20 From 9ad69e957f4040c149e4a2fd91c64c958416f9a4 Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:25:02 +0800 Subject: [PATCH 39/47] # feat: eval good team vote correct probability --- examples/werewolf_game/evals/eval.py | 187 ++++++++++++++++++++++++++ examples/werewolf_game/evals/utils.py | 54 +++++++- 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 examples/werewolf_game/evals/eval.py diff --git a/examples/werewolf_game/evals/eval.py b/examples/werewolf_game/evals/eval.py new file mode 100644 index 000000000..ba14c48c5 --- /dev/null +++ b/examples/werewolf_game/evals/eval.py @@ -0,0 +1,187 @@ +''' +Filename: MetaGPT/examples/werewolf_game/evals/eval.py +Created Date: Oct 18, 2023 +Updated Date: Oct 19, 2023 +Author: [Aria](https://github.com/ariafyy) +Info: eval the vote correct probability of non_werewolves +Files Tree: + evals + ├── 01-10 + │ ├── ....txt + ├── 11-20 + │ ├── ....txt + ├── 21-30 + │ ├── ....txt + ├── outputs + │ ├──# 01-10_....txt +''' + +from metagpt.const import PROJECT_ROOT +from pathlib import Path +import pandas as pd +import re +import json +import os, glob +from tqdm import tqdm +from utils import Utils + + + +class Eval: + """Evaluation""" + def __init__(self): + self.OUT_PATH = PROJECT_ROOT / "examples/werewolf_game/evals/outputs" + os.makedirs(self.OUT_PATH, exist_ok=True) + self.SUB_FOLDER_LIST = ["01-10", "11-20", "21-30"] + + def get_all_vote_fileslist(self): + files_list = [] + for SUB_FOLDER in self.SUB_FOLDER_LIST: + ROOT_PATH = PROJECT_ROOT / ("examples/werewolf_game/evals/{}/").format(SUB_FOLDER) + tmp_files_list = Utils().get_file_list(ROOT_PATH) + files_list.extend(tmp_files_list) + return files_list + + def inlogfile_to_votelog(self, files_list): + for i in tqdm(range(0, len(files_list))): + in_logfile = files_list[i] + SUB_FOLDER = (Path(in_logfile).parent).stem + out_txtfile = self.OUT_PATH / "# {0}_{1}.txt".format(SUB_FOLDER, Path(in_logfile).stem) + Utils().pick_vote_log(in_logfile, out_txtfile) + + def get_picked_vote_texts(self): + files_list = self.get_all_vote_fileslist() + self.inlogfile_to_votelog(files_list) + + @staticmethod + def parse_vote_text2chunks(text: str): + """ + parse each game vote log into text chunks + + one chunk example: + ['Player1', 'Player2', 'Player3', 'Player5', 'Player6']. Say ONLY: I vote to eliminate ... + Player1(Witch): 49 | I vote to eliminate Player5 + Player2(Villager): 49 | I vote to eliminate Player5 + Player3(Villager): 49 | I vote to eliminate Player5 + Player5(Werewolf): 49 | I vote to eliminate Player6 + Player6(Seer): 49 | I vote to eliminate Player5 + """ + pattern = re.compile(r"""\[([^\]]+)\]. Say ONLY: I vote to eliminate ...""") + chunks = {} + chunk_id = 0 + last_end = 0 + for match in pattern.finditer(text): + start = match.start() + chunk = text[last_end:start] + chunks[f'vote_{chunk_id}'] = chunk.strip() + last_end = match.end() + chunk_id += 1 + final_chunk = text[last_end:].strip() + if final_chunk: + chunks[f'vote_{chunk_id}'] = final_chunk + return chunks + + + def get_vote_probability(self, text: str) -> float: + """ + # calculate the probability of goodteam vote werewolves + :example: + + input: + ['Player1', 'Player2', 'Player3', 'Player5', 'Player6']. Say ONLY: I vote to eliminate ... + Player1(Witch): 49 | I vote to eliminate Player5 + Player2(Villager): 49 | I vote to eliminate Player5 + Player3(Villager): 49 | I vote to eliminate Player5 + Player5(Werewolf): 49 | I vote to eliminate Player6 + Player6(Seer): 49 | I vote to eliminate Player5 + + output: + werewolves: ['Player5'] + non_werewolves: ['Player1', 'Player2', 'Player3', 'Player6'] + as you can see :Player2(Villager) and Player3(Villager) vote to eliminate Player5(Werewolf) + :return goodteam vote Probability: 100.00% + """ + pattern = re.compile(r'(\w+)\(([^\)]+)\): \d+ \| I vote to eliminate (\w+)') + # find all werewolves + werewolves = [] + for match in pattern.finditer(text): + if match.group(2) == 'Werewolf': + werewolves.append(match.group(1)) + + # find all non_werewolves + non_werewolves = [] + for match in pattern.finditer(text): + if match.group(2) != 'Werewolf': + non_werewolves.append(match.group(1)) + num_non_werewolves = len(non_werewolves) + + # count players other than werewolves made the correct votes + correct_votes = 0 + for match in pattern.finditer(text): + if match.group(2) != 'Werewolf' and match.group(3) in werewolves: + correct_votes += 1 + + # cal the probability of non_werewolves + prob = correct_votes / num_non_werewolves + good_probability = round(prob, 2) + return good_probability + + def get_result_df(self, out_txtfile: str) -> pd.DataFrame: + """ + folder: sub folders for evals + file: evaluation file, each file represents one game + votes: the number of votes, eg. vote_1 represents the first vote of this game, + good_prob:the probability of a good person voting against a werewolf, + correct_votes / the total number of players other than werewolves + vote_count:the total number of votes cast + """ + with open(out_txtfile, "r") as out_file: + text = out_file.read() + chunks = Eval().parse_vote_text2chunks(text) + res = [] + for k, v in chunks.items(): + if v != "": + chunksList = list(chunks.keys()) + vote_count = len(chunksList) - 1 + good_probability = Eval().get_vote_probability(v) + folder = Utils().filename_to_folder(out_txtfile) + result = { + "folder": folder, + "file": Path(out_txtfile).stem + ".txt", + "votes": k, + "good_prob": good_probability, + "vote_count": vote_count + } + res.append(result) + df = pd.DataFrame(res) + return df + + def get_avg_prob_df(self): + """ + get avg_prob for each game + avg_prob : the good_prob/total number of votes in the game + """ + out_txtfile_list = Utils().get_file_list(self.OUT_PATH) + df_list = [] + for i in tqdm(range(0, len(out_txtfile_list))): + out_txtfile = out_txtfile_list[i] + file_df = Eval().get_result_df(out_txtfile) + df_list.append(file_df) + combined_df = pd.concat(df_list, ignore_index=True) + + # calculate the average good_prob for each file + mean_probs = combined_df.groupby('file')['good_prob'].mean() + combined_df['avg_prob'] = combined_df['file'].map(mean_probs) + combined_df['avg_prob'] = combined_df['avg_prob'].round(2) + combined_df['good_prob'] = combined_df['good_prob'].apply(lambda x: Utils()._float_to_percent(x)) + combined_df['avg_prob'] = combined_df['avg_prob'].apply(lambda x: Utils()._float_to_percent(x)) + return combined_df + + def get_result_csv(self): + Eval().get_picked_vote_texts() + combined_df = self.get_avg_prob_df() + combined_df.to_csv(self.OUT_PATH / 'goodteam_vote_probability.csv', index=False) + + +if __name__ == '__main__': + Eval().get_result_csv() diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index d788496a3..cc90d94cc 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -5,7 +5,7 @@ Author: [Aria](https://github.com/ariafyy) ''' from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT import re - +import os,glob class Utils: """Utils: utils of logs""" @@ -53,6 +53,58 @@ class Utils: else: out.write("\n") + @staticmethod + def pick_vote_log(in_logfile, out_txtfile): + """ + pick the vote log from the log file. + ready to AnnounceGameResult serves as the 'key text' which indicates the end of the game. + """ + pattern_vote = r'(Player\d+)\(([A-Za-z]+)\): (\d+) \| (I vote to eliminate Player\d+)' + key_text = r"ready to AnnounceGameResult" + pattern_moderator = r'\[([^\]]+)\]\. Say ONLY: I vote to eliminate ...' + with open(in_logfile, "r") as f, open(out_txtfile, "w") as out: + lines = f.readlines() + start_idx = -1 + # find the index of key_text + for idx, line in enumerate(lines): + if key_text in line: + start_idx = idx + break + + # if find the 'key_text' + if start_idx >= 0: + # start from 'key_text' to the end + relevant_lines = lines[start_idx:] + for line in relevant_lines: + if re.search(pattern_vote, line): + out.write(line) + if re.search(pattern_moderator, line): + out.write(line.lstrip()) + + @staticmethod + def get_file_list(path: str) -> list: + file_pattern = os.path.join(path, '*.txt') + files_list = glob.glob(file_pattern) + return files_list + + @staticmethod + def _filename_to_folder(out_txtfile: str): + """convert filename into its parent folder name""" + s = Path(out_txtfile).stem + pattern_folder = r'(.+)_' + match = re.match(pattern_folder, s) + if match: + folder = match.group(1) + return folder + + @staticmethod + def _float_to_percent(decimal: float) -> str: + """ + input: 1.00 + output: 100.00% + """ + percent = decimal * 100 + return f"{percent:.2f}%" if __name__ == '__main__': in_logfile = PROJECT_ROOT / "logs/log.txt" From b8a30671d53676764685664580fa5eb2bc21b7e0 Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:01:05 +0800 Subject: [PATCH 40/47] =?UTF-8?q?#=20refactor=EF=BC=9Aeval=20for=20good=20?= =?UTF-8?q?team=20vote;=20fix=20re=20extract=20folder=20name;=20feat=20vot?= =?UTF-8?q?e1=20prob?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/werewolf_game/evals/eval.py | 97 ++++++++++++++------------- examples/werewolf_game/evals/utils.py | 47 +++++++------ 2 files changed, 77 insertions(+), 67 deletions(-) diff --git a/examples/werewolf_game/evals/eval.py b/examples/werewolf_game/evals/eval.py index ba14c48c5..f4adee1d4 100644 --- a/examples/werewolf_game/evals/eval.py +++ b/examples/werewolf_game/evals/eval.py @@ -1,26 +1,16 @@ ''' Filename: MetaGPT/examples/werewolf_game/evals/eval.py Created Date: Oct 18, 2023 -Updated Date: Oct 19, 2023 +Revised Date: Oct 20, 2023 Author: [Aria](https://github.com/ariafyy) Info: eval the vote correct probability of non_werewolves -Files Tree: - evals - ├── 01-10 - │ ├── ....txt - ├── 11-20 - │ ├── ....txt - ├── 21-30 - │ ├── ....txt - ├── outputs - │ ├──# 01-10_....txt ''' -from metagpt.const import PROJECT_ROOT +from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT from pathlib import Path import pandas as pd +import numpy as np import re -import json import os, glob from tqdm import tqdm from utils import Utils @@ -28,30 +18,25 @@ from utils import Utils class Eval: - """Evaluation""" + """Vote Evaluation""" def __init__(self): - self.OUT_PATH = PROJECT_ROOT / "examples/werewolf_game/evals/outputs" + self.OUT_PATH = WORKSPACE_ROOT / "outputs" os.makedirs(self.OUT_PATH, exist_ok=True) self.SUB_FOLDER_LIST = ["01-10", "11-20", "21-30"] - def get_all_vote_fileslist(self): + def _get_log_fileslist(self, IN_PATH) -> list[str]: files_list = [] for SUB_FOLDER in self.SUB_FOLDER_LIST: - ROOT_PATH = PROJECT_ROOT / ("examples/werewolf_game/evals/{}/").format(SUB_FOLDER) - tmp_files_list = Utils().get_file_list(ROOT_PATH) - files_list.extend(tmp_files_list) + files_list.extend(glob.glob(str(IN_PATH / SUB_FOLDER / '*.txt'))) return files_list - def inlogfile_to_votelog(self, files_list): - for i in tqdm(range(0, len(files_list))): - in_logfile = files_list[i] + def extract_votes_from_logs(self, files_list: list): + for in_logfile in tqdm(files_list): SUB_FOLDER = (Path(in_logfile).parent).stem out_txtfile = self.OUT_PATH / "# {0}_{1}.txt".format(SUB_FOLDER, Path(in_logfile).stem) Utils().pick_vote_log(in_logfile, out_txtfile) - - def get_picked_vote_texts(self): - files_list = self.get_all_vote_fileslist() - self.inlogfile_to_votelog(files_list) + votefiles_list = Utils().get_file_list(self.OUT_PATH) + return votefiles_list @staticmethod def parse_vote_text2chunks(text: str): @@ -141,10 +126,10 @@ class Eval: res = [] for k, v in chunks.items(): if v != "": - chunksList = list(chunks.keys()) - vote_count = len(chunksList) - 1 + chunks_list = list(chunks.keys()) + vote_count = len(chunks_list) - 1 good_probability = Eval().get_vote_probability(v) - folder = Utils().filename_to_folder(out_txtfile) + folder = Utils().filename_to_foldername(out_txtfile) result = { "folder": folder, "file": Path(out_txtfile).stem + ".txt", @@ -156,32 +141,52 @@ class Eval: df = pd.DataFrame(res) return df - def get_avg_prob_df(self): + def calc_avg_prob(self, IN_PATH) -> pd.DataFrame: """ get avg_prob for each game avg_prob : the good_prob/total number of votes in the game """ - out_txtfile_list = Utils().get_file_list(self.OUT_PATH) - df_list = [] - for i in tqdm(range(0, len(out_txtfile_list))): - out_txtfile = out_txtfile_list[i] - file_df = Eval().get_result_df(out_txtfile) - df_list.append(file_df) + infiles_list = self._get_log_fileslist(IN_PATH) + votefiles_list = self.extract_votes_from_logs(infiles_list) + df_list = [self._load_df_from_file(file) for file in votefiles_list] combined_df = pd.concat(df_list, ignore_index=True) - # calculate the average good_prob for each file - mean_probs = combined_df.groupby('file')['good_prob'].mean() + mean_probs = self._calculate_mean_probs(combined_df) combined_df['avg_prob'] = combined_df['file'].map(mean_probs) - combined_df['avg_prob'] = combined_df['avg_prob'].round(2) - combined_df['good_prob'] = combined_df['good_prob'].apply(lambda x: Utils()._float_to_percent(x)) - combined_df['avg_prob'] = combined_df['avg_prob'].apply(lambda x: Utils()._float_to_percent(x)) + # calculate vote1 prob + vote1_probs = self._calc_vote1_probs(combined_df) + combined_df['vote1_prob'] = combined_df['folder'].map(vote1_probs.set_index('folder')['good_prob']) + combined_df.loc[combined_df['votes'] != 'vote_1', 'vote1_prob'] = np.nan + combined_df['vote1_prob'] = combined_df['vote1_prob'].apply(self._format_probs) + combined_df['good_prob'] = combined_df['good_prob'].apply(self._format_probs) + combined_df['avg_prob'] = combined_df['avg_prob'].apply(self._format_probs) + combined_df.sort_values(['folder'], ascending=True, inplace=True) return combined_df - def get_result_csv(self): - Eval().get_picked_vote_texts() - combined_df = self.get_avg_prob_df() - combined_df.to_csv(self.OUT_PATH / 'goodteam_vote_probability.csv', index=False) + def _calc_vote1_probs(self, df): + df_vote1 = df[df['votes'] == 'vote_1'] + vote1_probs = df_vote1.groupby('folder')['good_prob'].mean().reset_index() + return vote1_probs + + def _load_df_from_file(self, file): + return self.get_result_df(file) + + def _calculate_mean_probs(self, df): + return df.groupby('file')['good_prob'].mean() + + def _format_probs(self, s): + return Utils().float_to_percent(s) + + def get_eval_csv(self, IN_PATH, EVAL_RESULT): + """ + IN_PATH : parent folder of ["01-10", "11-20", "21-30"] + EVAL_RESULT : output csv file path + """ + combined_df = self.calc_avg_prob(IN_PATH) + combined_df.to_csv(EVAL_RESULT, index=False) if __name__ == '__main__': - Eval().get_result_csv() + IN_PATH = PROJECT_ROOT / "examples/werewolf_game/evals" + EVAL_RESULT = WORKSPACE_ROOT / "outputs" / 'goodteam_vote_probability.csv' + Eval().get_eval_csv(IN_PATH, EVAL_RESULT) diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index cc90d94cc..8f2b16db5 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -1,11 +1,13 @@ ''' Filename: MetaGPT/examples/werewolf_game/evals/utils.py Created Date: Oct 11, 2023 +Revised Date: Oct 20, 2023 Author: [Aria](https://github.com/ariafyy) ''' from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT import re import os,glob +from pathlib import Path class Utils: """Utils: utils of logs""" @@ -57,29 +59,27 @@ class Utils: def pick_vote_log(in_logfile, out_txtfile): """ pick the vote log from the log file. - ready to AnnounceGameResult serves as the 'key text' which indicates the end of the game. + ready to AnnounceGameResult serves as the 'HINT_TEXT ' which indicates the end of the game. """ pattern_vote = r'(Player\d+)\(([A-Za-z]+)\): (\d+) \| (I vote to eliminate Player\d+)' - key_text = r"ready to AnnounceGameResult" + HINT_TEXT = r"ready to AnnounceGameResult" pattern_moderator = r'\[([^\]]+)\]\. Say ONLY: I vote to eliminate ...' - with open(in_logfile, "r") as f, open(out_txtfile, "w") as out: - lines = f.readlines() - start_idx = -1 - # find the index of key_text - for idx, line in enumerate(lines): - if key_text in line: - start_idx = idx - break + with open(in_logfile, "r") as f: + lines = f.read() + split_lines = lines.split(HINT_TEXT) - # if find the 'key_text' - if start_idx >= 0: - # start from 'key_text' to the end - relevant_lines = lines[start_idx:] + if len(split_lines) < 2: + print(f"Key text :{HINT_TEXT} not found in {in_logfile}") + return + + relevant_lines = split_lines[1].split("\n") + with open(out_txtfile, "w") as out: for line in relevant_lines: if re.search(pattern_vote, line): - out.write(line) + out.write(line + "\n") if re.search(pattern_moderator, line): - out.write(line.lstrip()) + out.write(line.lstrip() + "\n") + @staticmethod def get_file_list(path: str) -> list: @@ -88,17 +88,21 @@ class Utils: return files_list @staticmethod - def _filename_to_folder(out_txtfile: str): - """convert filename into its parent folder name""" + def filename_to_foldername(out_txtfile: str): + """ + convert filename into its parent folder name + input:"....../# 01-10_10132100.txt" + output:# 01-10 + """ s = Path(out_txtfile).stem - pattern_folder = r'(.+)_' + pattern_folder = r'([^_]*)_' match = re.match(pattern_folder, s) if match: folder = match.group(1) return folder @staticmethod - def _float_to_percent(decimal: float) -> str: + def float_to_percent(decimal: float) -> str: """ input: 1.00 output: 100.00% @@ -109,4 +113,5 @@ class Utils: if __name__ == '__main__': in_logfile = PROJECT_ROOT / "logs/log.txt" out_txtfile = "input your wish path" - Utils().polish_log(in_logfile, out_txtfile) + # Utils().polish_log(in_logfile, out_txtfile) + Utils().pick_vote_log(in_logfile, out_txtfile) From 3b958fee9209dcd49090909f75881042f068630c Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Fri, 20 Oct 2023 21:53:08 +0800 Subject: [PATCH 41/47] =?UTF-8?q?#feat:calc=20votewolf=5Fdifficulty;=20ref?= =?UTF-8?q?actor=EF=BC=9Avariable=20name;fix:ignore=20reflection=20vote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/werewolf_game/evals/eval.py | 69 +++++++++++++++++---------- examples/werewolf_game/evals/utils.py | 14 ++++-- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/examples/werewolf_game/evals/eval.py b/examples/werewolf_game/evals/eval.py index f4adee1d4..f16a357e4 100644 --- a/examples/werewolf_game/evals/eval.py +++ b/examples/werewolf_game/evals/eval.py @@ -17,7 +17,7 @@ from utils import Utils -class Eval: +class Vote: """Vote Evaluation""" def __init__(self): self.OUT_PATH = WORKSPACE_ROOT / "outputs" @@ -67,9 +67,12 @@ class Eval: return chunks - def get_vote_probability(self, text: str) -> float: + def get_vote_prob_difficulity(self, text: str) -> float: """ # calculate the probability of goodteam vote werewolves + # vote_wolf_difficulty: num_voted_wolfs / num_living_players + sometimes werewolf will camouflage as a good person and vote wolf + :example: input: @@ -85,6 +88,7 @@ class Eval: non_werewolves: ['Player1', 'Player2', 'Player3', 'Player6'] as you can see :Player2(Villager) and Player3(Villager) vote to eliminate Player5(Werewolf) :return goodteam vote Probability: 100.00% + :return vote_wolf_difficulty: 4 / 5 """ pattern = re.compile(r'(\w+)\(([^\)]+)\): \d+ \| I vote to eliminate (\w+)') # find all werewolves @@ -108,71 +112,88 @@ class Eval: # cal the probability of non_werewolves prob = correct_votes / num_non_werewolves - good_probability = round(prob, 2) - return good_probability + good_vote_prob = round(prob, 2) + + # count the num of living players voting wolfs, ignore their positions + vote2eliminate_wolfs = [] + for match in pattern.finditer(text): + if match.group(2) != 'Werewolf' and match.group(3) in werewolves: + correct_votes += 1 + if match.group(3) in werewolves: + vote2eliminate_wolfs.append(match.group(3)) + num_living_players = len(werewolves) + len(non_werewolves) + num_vote2eliminate_wolfs = len(set(vote2eliminate_wolfs)) + votewolf_difficulty = "{0} / {1}".format(num_vote2eliminate_wolfs, num_living_players) + return good_vote_prob, votewolf_difficulty + def get_result_df(self, out_txtfile: str) -> pd.DataFrame: """ folder: sub folders for evals file: evaluation file, each file represents one game votes: the number of votes, eg. vote_1 represents the first vote of this game, - good_prob:the probability of a good person voting against a werewolf, + good_vote_prob:the probability of a good person voting against a werewolf, correct_votes / the total number of players other than werewolves - vote_count:the total number of votes cast + total_votes:the total number of votes cast + vote_wolf_difficulty: num_voted_wolfs / num_living_players + sometimes werewolf will camouflage as a good person and vote wolf """ with open(out_txtfile, "r") as out_file: text = out_file.read() - chunks = Eval().parse_vote_text2chunks(text) + chunks = self.parse_vote_text2chunks(text) res = [] for k, v in chunks.items(): if v != "": chunks_list = list(chunks.keys()) - vote_count = len(chunks_list) - 1 - good_probability = Eval().get_vote_probability(v) + total_votes = len(chunks_list) - 1 + good_vote_prob, votewolf_difficulty = self.get_vote_prob_difficulity(v) folder = Utils().filename_to_foldername(out_txtfile) result = { "folder": folder, "file": Path(out_txtfile).stem + ".txt", - "votes": k, - "good_prob": good_probability, - "vote_count": vote_count + "vote_round": k, + "good_vote_prob": good_vote_prob, + "total_votes": total_votes, + "votewolf_difficulty": votewolf_difficulty } res.append(result) df = pd.DataFrame(res) return df + def calc_avg_prob(self, IN_PATH) -> pd.DataFrame: """ get avg_prob for each game avg_prob : the good_prob/total number of votes in the game + vote1_prob: only check vote round1 , eval the mean of good_vote_prob """ infiles_list = self._get_log_fileslist(IN_PATH) votefiles_list = self.extract_votes_from_logs(infiles_list) df_list = [self._load_df_from_file(file) for file in votefiles_list] combined_df = pd.concat(df_list, ignore_index=True) - # calculate the average good_prob for each file + # calculate the average good_vote_prob for each file mean_probs = self._calculate_mean_probs(combined_df) - combined_df['avg_prob'] = combined_df['file'].map(mean_probs) + combined_df["avg_prob"] = combined_df["file"].map(mean_probs) # calculate vote1 prob vote1_probs = self._calc_vote1_probs(combined_df) - combined_df['vote1_prob'] = combined_df['folder'].map(vote1_probs.set_index('folder')['good_prob']) - combined_df.loc[combined_df['votes'] != 'vote_1', 'vote1_prob'] = np.nan - combined_df['vote1_prob'] = combined_df['vote1_prob'].apply(self._format_probs) - combined_df['good_prob'] = combined_df['good_prob'].apply(self._format_probs) - combined_df['avg_prob'] = combined_df['avg_prob'].apply(self._format_probs) - combined_df.sort_values(['folder'], ascending=True, inplace=True) + combined_df["vote1_prob"] = combined_df["folder"].map(vote1_probs.set_index("folder")["good_vote_prob"]) + combined_df.loc[combined_df["vote_round"] != "vote_1", "vote1_prob"] = np.nan + combined_df["vote1_prob"] = combined_df["vote1_prob"].apply(self._format_probs) + combined_df["good_vote_prob"] = combined_df["good_vote_prob"].apply(self._format_probs) + combined_df["avg_prob"] = combined_df["avg_prob"].apply(self._format_probs) + combined_df.sort_values(["folder"], ascending=True, inplace=True) return combined_df def _calc_vote1_probs(self, df): - df_vote1 = df[df['votes'] == 'vote_1'] - vote1_probs = df_vote1.groupby('folder')['good_prob'].mean().reset_index() + df_vote1 = df[df["vote_round"] == 'vote_1'] + vote1_probs = df_vote1.groupby("folder")["good_vote_prob"].mean().reset_index() return vote1_probs def _load_df_from_file(self, file): return self.get_result_df(file) def _calculate_mean_probs(self, df): - return df.groupby('file')['good_prob'].mean() + return df.groupby("file")["good_vote_prob"].mean() def _format_probs(self, s): return Utils().float_to_percent(s) @@ -189,4 +210,4 @@ class Eval: if __name__ == '__main__': IN_PATH = PROJECT_ROOT / "examples/werewolf_game/evals" EVAL_RESULT = WORKSPACE_ROOT / "outputs" / 'goodteam_vote_probability.csv' - Eval().get_eval_csv(IN_PATH, EVAL_RESULT) + Vote().get_eval_csv(IN_PATH, EVAL_RESULT) diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index 8f2b16db5..33632063f 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -60,10 +60,14 @@ class Utils: """ pick the vote log from the log file. ready to AnnounceGameResult serves as the 'HINT_TEXT ' which indicates the end of the game. + based on bservation and reflection, then discuss is not in vote session. """ pattern_vote = r'(Player\d+)\(([A-Za-z]+)\): (\d+) \| (I vote to eliminate Player\d+)' + ignore_text = """reflection""" HINT_TEXT = r"ready to AnnounceGameResult" pattern_moderator = r'\[([^\]]+)\]\. Say ONLY: I vote to eliminate ...' + in_valid_block = False + with open(in_logfile, "r") as f: lines = f.read() split_lines = lines.split(HINT_TEXT) @@ -75,12 +79,16 @@ class Utils: relevant_lines = split_lines[1].split("\n") with open(out_txtfile, "w") as out: for line in relevant_lines: - if re.search(pattern_vote, line): - out.write(line + "\n") if re.search(pattern_moderator, line): + in_valid_block = True out.write(line.lstrip() + "\n") - + elif in_valid_block and re.search(pattern_vote, line): + out.write(line + "\n") + elif ignore_text in line: + in_valid_block = False + + @staticmethod def get_file_list(path: str) -> list: file_pattern = os.path.join(path, '*.txt') From 5778b6ebfdb2b02533e7aeca12eb0747b1806af1 Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:40:54 +0800 Subject: [PATCH 42/47] fix: votewolf_difficulty bug; refactor :rename prob into rate; perf: py func structure --- examples/werewolf_game/evals/eval.py | 106 +++++++++++++------------- examples/werewolf_game/evals/utils.py | 1 - 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/examples/werewolf_game/evals/eval.py b/examples/werewolf_game/evals/eval.py index f16a357e4..7d8a0e58e 100644 --- a/examples/werewolf_game/evals/eval.py +++ b/examples/werewolf_game/evals/eval.py @@ -3,7 +3,7 @@ Filename: MetaGPT/examples/werewolf_game/evals/eval.py Created Date: Oct 18, 2023 Revised Date: Oct 20, 2023 Author: [Aria](https://github.com/ariafyy) -Info: eval the vote correct probability of non_werewolves +Info: eval the Voting Accuracy Rate of non_werewolves and Vote Difficulity ''' from metagpt.const import WORKSPACE_ROOT, PROJECT_ROOT @@ -66,13 +66,9 @@ class Vote: chunks[f'vote_{chunk_id}'] = final_chunk return chunks - - def get_vote_prob_difficulity(self, text: str) -> float: + def _vote_rate_players(self, text: str): """ - # calculate the probability of goodteam vote werewolves - # vote_wolf_difficulty: num_voted_wolfs / num_living_players - sometimes werewolf will camouflage as a good person and vote wolf - + # calculate the rateability of goodteam vote werewolves :example: input: @@ -87,13 +83,13 @@ class Vote: werewolves: ['Player5'] non_werewolves: ['Player1', 'Player2', 'Player3', 'Player6'] as you can see :Player2(Villager) and Player3(Villager) vote to eliminate Player5(Werewolf) - :return goodteam vote Probability: 100.00% - :return vote_wolf_difficulty: 4 / 5 + :return goodteam vote rateability: 100.00% """ pattern = re.compile(r'(\w+)\(([^\)]+)\): \d+ \| I vote to eliminate (\w+)') # find all werewolves werewolves = [] for match in pattern.finditer(text): + if match.group(2) == 'Werewolf': werewolves.append(match.group(1)) @@ -110,33 +106,37 @@ class Vote: if match.group(2) != 'Werewolf' and match.group(3) in werewolves: correct_votes += 1 - # cal the probability of non_werewolves - prob = correct_votes / num_non_werewolves - good_vote_prob = round(prob, 2) + # cal the rateability of non_werewolves + rate = correct_votes / num_non_werewolves + good_vote_rate = round(rate, 2) + return {"good_vote_rate": good_vote_rate, "werewolves": werewolves, "non_werewolves": non_werewolves} - # count the num of living players voting wolfs, ignore their positions - vote2eliminate_wolfs = [] - for match in pattern.finditer(text): - if match.group(2) != 'Werewolf' and match.group(3) in werewolves: - correct_votes += 1 - if match.group(3) in werewolves: - vote2eliminate_wolfs.append(match.group(3)) + def get_goodteam_vote_rate(self, text: str) -> float: + goodteam_vote_rate = self._vote_rate_players(text)["good_vote_rate"] + return goodteam_vote_rate + + def get_werewolves(self, text: str) -> list: + werewolves_list = self._vote_rate_players(text)["werewolves"] + return werewolves_list + + def get_non_werewolves(self, text: str) -> list: + non_werewolves_list = self._vote_rate_players(text)["non_werewolves"] + return non_werewolves_list + + def get_votewolf_difficulty(self, werewolves: list, non_werewolves: list) -> str: + num_living_wolfs = len(werewolves) num_living_players = len(werewolves) + len(non_werewolves) - num_vote2eliminate_wolfs = len(set(vote2eliminate_wolfs)) - votewolf_difficulty = "{0} / {1}".format(num_vote2eliminate_wolfs, num_living_players) - return good_vote_prob, votewolf_difficulty - + votewolf_difficulty = "_{0} / {1}".format(num_living_wolfs, num_living_players) + return votewolf_difficulty def get_result_df(self, out_txtfile: str) -> pd.DataFrame: """ folder: sub folders for evals file: evaluation file, each file represents one game votes: the number of votes, eg. vote_1 represents the first vote of this game, - good_vote_prob:the probability of a good person voting against a werewolf, + good_vote_rate:the rateability of a good person voting against a werewolf, correct_votes / the total number of players other than werewolves total_votes:the total number of votes cast - vote_wolf_difficulty: num_voted_wolfs / num_living_players - sometimes werewolf will camouflage as a good person and vote wolf """ with open(out_txtfile, "r") as out_file: text = out_file.read() @@ -146,56 +146,58 @@ class Vote: if v != "": chunks_list = list(chunks.keys()) total_votes = len(chunks_list) - 1 - good_vote_prob, votewolf_difficulty = self.get_vote_prob_difficulity(v) + werewolves = self.get_werewolves(v) + non_werewolves = self.get_non_werewolves(v) + good_vote_rate = self.get_goodteam_vote_rate(v) + votewolf_difficulty = self.get_votewolf_difficulty(werewolves, non_werewolves) folder = Utils().filename_to_foldername(out_txtfile) result = { "folder": folder, "file": Path(out_txtfile).stem + ".txt", "vote_round": k, - "good_vote_prob": good_vote_prob, + "good_vote_rate": good_vote_rate, "total_votes": total_votes, "votewolf_difficulty": votewolf_difficulty } res.append(result) df = pd.DataFrame(res) return df - - def calc_avg_prob(self, IN_PATH) -> pd.DataFrame: + def calc_avg_rate(self, IN_PATH) -> pd.DataFrame: """ - get avg_prob for each game - avg_prob : the good_prob/total number of votes in the game - vote1_prob: only check vote round1 , eval the mean of good_vote_prob + get avg_rate for each game + avg_rate : the good_rate/total number of votes in the game + vote1_rate: First Round Voting Accuracy Rate """ infiles_list = self._get_log_fileslist(IN_PATH) votefiles_list = self.extract_votes_from_logs(infiles_list) df_list = [self._load_df_from_file(file) for file in votefiles_list] combined_df = pd.concat(df_list, ignore_index=True) - # calculate the average good_vote_prob for each file - mean_probs = self._calculate_mean_probs(combined_df) - combined_df["avg_prob"] = combined_df["file"].map(mean_probs) - # calculate vote1 prob - vote1_probs = self._calc_vote1_probs(combined_df) - combined_df["vote1_prob"] = combined_df["folder"].map(vote1_probs.set_index("folder")["good_vote_prob"]) - combined_df.loc[combined_df["vote_round"] != "vote_1", "vote1_prob"] = np.nan - combined_df["vote1_prob"] = combined_df["vote1_prob"].apply(self._format_probs) - combined_df["good_vote_prob"] = combined_df["good_vote_prob"].apply(self._format_probs) - combined_df["avg_prob"] = combined_df["avg_prob"].apply(self._format_probs) - combined_df.sort_values(["folder"], ascending=True, inplace=True) + # calculate the average good_vote_rate for each file + mean_rates = self._calculate_mean_rates(combined_df) + combined_df["avg_rate"] = combined_df["file"].map(mean_rates) + # calculate vote1 rate + vote1_rates = self._calc_vote1_rates(combined_df) + combined_df["vote1_rate"] = combined_df["folder"].map(vote1_rates.set_index("folder")["good_vote_rate"]) + combined_df.loc[combined_df["vote_round"] != "vote_1", "vote1_rate"] = np.nan + combined_df["vote1_rate"] = combined_df["vote1_rate"].apply(self._format_rates) + combined_df["good_vote_rate"] = combined_df["good_vote_rate"].apply(self._format_rates) + combined_df["avg_rate"] = combined_df["avg_rate"].apply(self._format_rates) + combined_df.sort_values(["file"], ascending=True, inplace=True) return combined_df - def _calc_vote1_probs(self, df): + def _calc_vote1_rates(self, df): df_vote1 = df[df["vote_round"] == 'vote_1'] - vote1_probs = df_vote1.groupby("folder")["good_vote_prob"].mean().reset_index() - return vote1_probs + vote1_rates = df_vote1.groupby("folder")["good_vote_rate"].mean().reset_index() + return vote1_rates def _load_df_from_file(self, file): return self.get_result_df(file) - def _calculate_mean_probs(self, df): - return df.groupby("file")["good_vote_prob"].mean() + def _calculate_mean_rates(self, df): + return df.groupby("file")["good_vote_rate"].mean() - def _format_probs(self, s): + def _format_rates(self, s): return Utils().float_to_percent(s) def get_eval_csv(self, IN_PATH, EVAL_RESULT): @@ -203,11 +205,11 @@ class Vote: IN_PATH : parent folder of ["01-10", "11-20", "21-30"] EVAL_RESULT : output csv file path """ - combined_df = self.calc_avg_prob(IN_PATH) + combined_df = self.calc_avg_rate(IN_PATH) combined_df.to_csv(EVAL_RESULT, index=False) if __name__ == '__main__': IN_PATH = PROJECT_ROOT / "examples/werewolf_game/evals" - EVAL_RESULT = WORKSPACE_ROOT / "outputs" / 'goodteam_vote_probability.csv' + EVAL_RESULT = WORKSPACE_ROOT / "outputs" / 'goodteam_vote_rate.csv' Vote().get_eval_csv(IN_PATH, EVAL_RESULT) diff --git a/examples/werewolf_game/evals/utils.py b/examples/werewolf_game/evals/utils.py index 33632063f..a3a5c539a 100644 --- a/examples/werewolf_game/evals/utils.py +++ b/examples/werewolf_game/evals/utils.py @@ -88,7 +88,6 @@ class Utils: elif ignore_text in line: in_valid_block = False - @staticmethod def get_file_list(path: str) -> list: file_pattern = os.path.join(path, '*.txt') From c0d6afc6d7b3c50d7ecedb306d4805a2df71fa10 Mon Sep 17 00:00:00 2001 From: Aria F <51890782+ariafyy@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:51:17 +0800 Subject: [PATCH 43/47] style: fix typo --- examples/werewolf_game/evals/eval.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/werewolf_game/evals/eval.py b/examples/werewolf_game/evals/eval.py index 7d8a0e58e..8734f438d 100644 --- a/examples/werewolf_game/evals/eval.py +++ b/examples/werewolf_game/evals/eval.py @@ -1,7 +1,7 @@ ''' Filename: MetaGPT/examples/werewolf_game/evals/eval.py Created Date: Oct 18, 2023 -Revised Date: Oct 20, 2023 +Updated Date: Oct 24, 2023 Author: [Aria](https://github.com/ariafyy) Info: eval the Voting Accuracy Rate of non_werewolves and Vote Difficulity ''' @@ -68,7 +68,7 @@ class Vote: def _vote_rate_players(self, text: str): """ - # calculate the rateability of goodteam vote werewolves + # calculate the rate of goodteam vote werewolves :example: input: From 441d9b04aefdf502fe1c319d538b499e8dbd7fb1 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Mon, 30 Oct 2023 19:28:51 +0800 Subject: [PATCH 44/47] small bug fixed & rm redundant --- .../actions/experience_operation.py | 3 +- .../actions/moderator_actions.py | 33 ------- examples/werewolf_game/prompts/prompts.py | 85 ------------------- examples/werewolf_game/roles/base_player.py | 2 +- examples/werewolf_game/roles/moderator.py | 4 +- 5 files changed, 5 insertions(+), 122 deletions(-) delete mode 100644 examples/werewolf_game/prompts/prompts.py diff --git a/examples/werewolf_game/actions/experience_operation.py b/examples/werewolf_game/actions/experience_operation.py index ea930d743..85b64d0c7 100644 --- a/examples/werewolf_game/actions/experience_operation.py +++ b/examples/werewolf_game/actions/experience_operation.py @@ -136,7 +136,7 @@ class RetrieveExperiences(Action): where=filters, ) - logger.info("retrieved exp") + logger.info(f"retrieve {profile}'s experiences") past_experiences = [RoleExperience(**res) for res in results["metadatas"][0]] if verbose: print(*past_experiences, sep="\n\n") @@ -160,6 +160,7 @@ class RetrieveExperiences(Action): for i, exp in enumerate(past_experiences) ] print(*past_experiences, sep="\n") + logger.info(f"retrieval done") return json.dumps(past_experiences) diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 638c3d658..8cb9fcdea 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -112,40 +112,7 @@ class ParseSpeak(Action): async def run(self): pass -class SummarizeDay(Action): - """consider all votes at day, conclude which player dies""" - - def __init__(self, name="SummarizeDay", context=None, llm=None): - super().__init__(name, context, llm) - - async def run(self, votes): - # 假设votes是一个字典,代表白天投票的结果,key是被投票的玩家,value是得票数 - # 例如:{"Player1": 2, "Player2": 1, "Player3": 1, "Player4": 0} - # 表示Player1得到2票,Player2和Player3各得到1票,Player4得到0票 - # 若平票,则随机选一个人出局 - if not votes: - return "No votes were cast. No one was killed." - - max_votes = max(votes.values()) - players_with_max_votes = [player for player, vote_count in votes.items() if vote_count == max_votes] - - if len(players_with_max_votes) == 1: - eliminated_player = players_with_max_votes[0] - return f"{eliminated_player} was voted out and eliminated." - else: - # 若平票,则随机选一个人出局 - eliminated_player = players_with_max_votes[int(random() * len(players_with_max_votes))] - return f"There was a tie in the votes. {eliminated_player} was randomly chosen and eliminated." - - class AnnounceGameResult(Action): async def run(self, winner: str, win_reason: str): return f"Game over! {win_reason}. The winner is the {winner}" - -async def main(): - rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) - print(rst1) - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/examples/werewolf_game/prompts/prompts.py b/examples/werewolf_game/prompts/prompts.py deleted file mode 100644 index 5f618798d..000000000 --- a/examples/werewolf_game/prompts/prompts.py +++ /dev/null @@ -1,85 +0,0 @@ -# 论文中出现的提示语,利用了思维链 -# 1. 基于游戏规则和对话内容,选择5个问题 -# 2. 生成2个问题 -# 3. 生成可能的回答 -# 4. 进行反思 -# 5. 生成最终回复 - -GAME_RULE = '''You are playing a game called the Werewolf with some other players. This game is based on text conversations. Here are -the game rules: Roles: The moderator is also host, he organised this game and you need to answer his instructions correctly. -Don’t talk with the moderator. There are five roles in the game, werewolf, villager, seer, guard and witch. There are two -alternate phases in this game, daytime and dark. When it’s dark: Your talking content with moderator is confidential. You -needn’t worry about other players and moderator knowing what you say and do. No need to worry about suspicions from -others during the night. If you are werewolf, you can know what your teammates want to kill and you should vote one player -to kill based on your analysis. Player who receives the most votes after all werewolves voting will be killed. No one will be -killed if there is no consensus! If you are witch, you have a bottle of antidote that can save a player targeted by werewolves -after dark, and a bottle of poison that can poison a player after dark. Both poison and antidote can be used only once. If you -are seer, you can verify whether a player is a werewolf every night, which is a very important thing. If you are guard, you -can protect a player every night to prevent the player from being killed by werewolves, but guard cannot resist the witch’s -poison and guard cannot protect the same player on two consecutive nights. Villagers can’t do anything at night. During the -daytime: you discuss with all players including your enemies. At the end of the discussion, players vote to eliminate one -player they suspect of being a werewolf. The player with the most votes will be eliminated. The moderator will tell who is -killed, otherwise there is no one killed. Note that villager, seer, guard and witch are all in villager side, they have the same -objective. Objectives: If you are werewolf, your goal is to cooperate with other werewolves to kill all players who are not -werewolves at last. If you are not werewolf, you need to kill all werewolves with your partner once you find out that certain -players are suspicious to be werewolves. This could greatly improve your chances of winning, although it is somewhat risky.If -one player is killed, he can’t do anything anymore and will be out of the game. Tips: To complete the objective: During -night, you should analyze and use your ability correctly. During daytime, you need to reason carefully about the roles of other -players and be careful not to reveal your own role casually unless you’re cheating other players. Only give the player’s name -when making a decision/voting, and don’t generate other players’ conversation.Reasoning based on facts you have observed -and you cannot perceive information (such as acoustic info) other than text. You are Player {name}, the {profile}. -You’re playing with 6 other players. Do not pretend you are other players or the moderator. -''' - -SELECT_QUESTIONS = ''' -Now its the {t}-th {day_or_night}. Given the game rules and conversations above, assuming you are {agent_name}, the -{role}, and to complete the instructions of the moderator, you need to think about a few questions clearly first, so that you can -make an accurate decision on the next step. Choose only five that you think are the most important in the current situation -from the list of questions below: {questions_prepared_for_specific_role} Please repeat the five important questions of your -choice, separating them with ‘##’. -''' - -# 为特定的角色,准备的问题 -questions_prepared_for_specific_role_sample = ''' -1. What is my player name and what is my role? What is my final objective in this game? -2. Based on the chat history, can you guess what some players’ role might be? -3. What is the current phase, daytime or night? what should I do at this phase according to the game rules? -4. Based on the conversation and my inference, who is most likely to be an alive werewolf? -5. I want to know who the most suspicious player, and why? -6. I also want to know if any player’s behavior has changed suspiciously compared to the previous days, and if so, who and why? -7. What is the best strategy I should use right now to uncover werewolves without revealing my own role? Should I accuse someone directly, ask probing questions, or stay silent for now? -8. Have any players claimed specific roles that can be verified or disputed? -''' - -ASK_QUESTIONS = ''' -Now its the {t}-th {day_or_night}. Given the game rules and conversations above, assuming you are {agent_name}, the -{role}, and to complete the instructions of the moderator, you need to think about a few questions clearly first, so that you can -make an accurate decision on the next step. {selected_questions} Do not answer these queations. In addition to the above -questions, please make a bold guess, what else do you want to know about the current situation? Please ask two important -questions in first person, separating them with ‘##’. -''' - -GENERATE_POSSIBLE_ANSWER = ''' -Now its the {t}-th {day_or_night}. Given the game rules and conversations above, assuming you are {agent_name}, the -{role}, for question: {question} There are some possible answers: {candidate_answers} Generate the correct answer -based on the context. If there is not direct answer, you should think and generate the answer based on the context. No need to -give options. The answer should in first person using no more than 2 sentences and without any analysis and item numbers. -''' - -REFLECTION = ''' -Now its the {t}-th {day_or_night}. Assuming you are {agent_name}, the {role}, what insights can you summarize -with few sentences based on the above conversations and {At} in heart for helping continue the talking and achieving your -objective? For example: As the {role}, I observed that... I think that... But I am... So... -''' - -# 得到最终的回复,再抽取出最终的content -GENERATE_FINAL_RESPONSE = ''' -Now its the {t}-th {day_or_night}. Think about what to say based on the game rules and context, especially the just now -reflection {R}. -Give your step-by-step thought process and your derived consise talking content (no more than 2 sentences) at last, separating them with ‘##’. -For example: -## Thought process -My step-by-step thought process:... -## Content -My concise talking content: ... -''' diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 6aa5c5e08..885684db8 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -36,7 +36,7 @@ class BasePlayer(Role): self.use_experience = False else: self.use_experience = use_experience - self.new_experience_version = new_experience_version if self.use_experience else "" + self.new_experience_version = new_experience_version self.use_memory_selection = use_memory_selection self.experiences = [] diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 6bb294e91..6818a4e77 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -66,7 +66,7 @@ class Moderator(Role): def _record_all_experiences(self): roles_in_env = self._rc.env.get_roles() - timestamp = datetime.now().strftime('%Y-%m-%d-%H:%M:%S') + timestamp = datetime.now().strftime('%Y_%m_%d_%H_%M_%S') for _, role in roles_in_env.items(): if role == self: continue @@ -157,7 +157,7 @@ class Moderator(Role): self.player_poisoned = None elif step_idx == 18: # FIXME: hard code - print("*" * 10, step_idx) + # print("*" * 10, step_idx) # day ends: after all roles voted, process all votings voting_msgs = memories[-len(self.living_players):] voted_all = [] From bcf30a9c0af296342d95b3ad94c3542e8c6a10c2 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Mon, 30 Oct 2023 19:31:51 +0800 Subject: [PATCH 45/47] clean logs --- examples/werewolf_game/roles/moderator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 6818a4e77..57bcc4dee 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -77,7 +77,6 @@ class Moderator(Role): role.record_experiences(round_id=timestamp, outcome=outcome, game_setup=self.game_setup) async def _instruct_speak(self): - print("*" * 10, "STEP: ", self.step_idx, "*" * 10) step_idx = self.step_idx % len(STEP_INSTRUCTIONS) self.step_idx += 1 return await InstructSpeak().run(step_idx, @@ -157,7 +156,6 @@ class Moderator(Role): self.player_poisoned = None elif step_idx == 18: # FIXME: hard code - # print("*" * 10, step_idx) # day ends: after all roles voted, process all votings voting_msgs = memories[-len(self.living_players):] voted_all = [] From c557768ae802372cda074a175c739ee8b2a5d51d Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Mon, 30 Oct 2023 19:35:41 +0800 Subject: [PATCH 46/47] logging --- examples/werewolf_game/actions/experience_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/werewolf_game/actions/experience_operation.py b/examples/werewolf_game/actions/experience_operation.py index 85b64d0c7..4575a1574 100644 --- a/examples/werewolf_game/actions/experience_operation.py +++ b/examples/werewolf_game/actions/experience_operation.py @@ -160,7 +160,7 @@ class RetrieveExperiences(Action): for i, exp in enumerate(past_experiences) ] print(*past_experiences, sep="\n") - logger.info(f"retrieval done") + logger.info("retrieval done") return json.dumps(past_experiences) From b2ed596e6f22ef290fed212f6247474823e481cc Mon Sep 17 00:00:00 2001 From: collinzrj Date: Fri, 29 Mar 2024 13:51:50 -0400 Subject: [PATCH 47/47] fix api_version bug --- examples/werewolf_game/actions/experience_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/werewolf_game/actions/experience_operation.py b/examples/werewolf_game/actions/experience_operation.py index 4575a1574..558418665 100644 --- a/examples/werewolf_game/actions/experience_operation.py +++ b/examples/werewolf_game/actions/experience_operation.py @@ -17,7 +17,7 @@ EMB_FN = embedding_functions.OpenAIEmbeddingFunction( api_base=CONFIG.openai_api_base, api_type=CONFIG.openai_api_type, model_name="text-embedding-ada-002", - api_version="2", + # api_version="2", ) class AddNewExperiences(Action):