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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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="",