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