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="",