From 1d932fffa126642f4280366b5638d6e52c4ce747 Mon Sep 17 00:00:00 2001 From: ZiTao-Li <135263265+ZiTao-Li@users.noreply.github.com> Date: Tue, 16 Jan 2024 18:46:58 -0800 Subject: [PATCH] improve linear plot flow to support DAG style (#7) --- examples/game/config/customer_config.yaml | 48 +++++++++- examples/game/config/game_config.yaml | 16 +++- examples/game/customer.py | 100 ++++++++++++--------- examples/game/main.py | 101 +++++++++++++--------- examples/game/utils.py | 55 ++++++++++-- 5 files changed, 223 insertions(+), 97 deletions(-) diff --git a/examples/game/config/customer_config.yaml b/examples/game/config/customer_config.yaml index 7c6985929..a63c5e067 100644 --- a/examples/game/config/customer_config.yaml +++ b/examples/game/config/customer_config.yaml @@ -34,9 +34,51 @@ 4.阿炳碰到喜事的时候,喜欢点贵的的菜,比如佛跳墙 "background": > 1. 阿炳负责A市一家连锁超市的零食品类的产品选择和洽谈。 - 2. 阿炳工作的连锁超市主要目标是A市的中高端消费者 - 3. 阿炳工作的连锁超市在A市非常受欢迎 - "hidden_plot": "" + 2. 阿炳工作的连锁超市主要目标是A市的中高端消费者。 + 3. 阿炳工作的连锁超市在A市非常受欢迎。 + "hidden_plot": > + 1. 最近阿炳和一个客户达成了一个大单麻辣鸭脖的生意。 + 2. 在小范围内的试销售是,这款麻辣鸭脖的反响非常好,经常是一上架就在半天内卖光。 + 3. 但是这也给阿炳带来一个问题:超市自己的物流系统已经饱和了,如果铺开销售,这批麻辣鸭脖不知道能怎么从工厂样运送到各个门店。 + 4. 阿炳他们一共有大大小小30多门店,需要每天都动态补货。 "plugin_background": - "" - "阿炳前两天谈成一个大生意,公司给他发了特别奖金。他今晚想忙里抽闲,犒劳自己。" +- + "name": "老朱" + "model": "tongyi_model" + "use_memory": true + "character_setting": + "food_preference": > + 1. 老朱来自内蒙古; + 2. 老朱最喜欢的是羊肉,比如烤羊肉串和羊肉烧卖; + 3. 老朱不习惯吃海鲜,对海鲜有轻微过敏。 + "background": > + 1. 老朱是一家物流公司的营业点的经理。 + 2. 老朱工作的营业点每天能处理5万件的包裹。 + 3. 营业点的效益不错,但是老朱也非常忙,时常要工作到很晚才能回家。 + 4. 老朱性格豪爽,喜欢喝酒,也喜欢交朋友。 + "hidden_plot": > + 1. 老朱家的孩子最近成绩下滑,其中成绩最不好的科目是数学。 + 2. 老朱因为工作繁忙,没办法经常陪孩子,和孩子的关系有点紧张。 + 3. 老朱也因为工作太忙,也没办法辅导孩子功课。 + 3. 老朱在考虑是否要为孩子请一名家教。 + "plugin_background": + - "" +- + "name": "范老师" + "model": "tongyi_model" + "use_memory": true + "character_setting": + "food_preference": > + 1. 范老师是山东人 + 2. 范老师喜欢吃鲁菜,比如九转大肠、葱烧海参 + 3. 范老师不喜欢食物有腥臊味,所以比较抗拒羊肉 + "background": > + 1. 范老师在一所中学当老师,主要负责学科是数学,这学期也兼任一个班级的班主任。 + 2. 范老师的教学水平不错,负责的班级数学成绩高于年级平均水平。 + 3. 范老师和班里的学生水平都很不错。学生们平时有什么生活烦心事或学习上的困难,都喜欢找他聊聊天。 + "hidden_plot": "" + "plugin_background": + - "" + diff --git a/examples/game/config/game_config.yaml b/examples/game/config/game_config.yaml index c3e1cf78d..16d1aa396 100644 --- a/examples/game/config/game_config.yaml +++ b/examples/game/config/game_config.yaml @@ -33,6 +33,7 @@ "hidden_main_plot_prompt": > 但是,你最近遇到了一些烦恼{hidden_plot}。 "hidden_main_plot_after_meal": > + 你对老板推荐的菜挺满意。你现在和餐馆老板在饭后闲聊。 你会觉得跟别人分享烦恼让你难堪,所以你不会一下子说出你的烦恼。 你生成的回答要简洁明了,每次的回答只能包括基本设定里的一小部分内容。 例子1: 老板,我想打听一下你有没有认识的人有本地的经销渠道? @@ -78,4 +79,17 @@ 生成故事: "plots": - - ["王老板", "阿炳"] \ No newline at end of file + - "predecessor_plots": null + "main_role": "王老板" + "supporting_roles": + - "阿炳" + - "predecessor_plots": null + "main_role": "老朱" + "supporting_roles": + - "范老师" + - "predecessor_plots": + - 0 + - 1 + "main_role": "阿炳" + "supporting_roles": + - "老朱" \ No newline at end of file diff --git a/examples/game/customer.py b/examples/game/customer.py index ef4a99bdf..39b1fa47a 100644 --- a/examples/game/customer.py +++ b/examples/game/customer.py @@ -10,8 +10,9 @@ HISTORY_WINDOW = 10 +# TODO: for debug, set the score bars to be lower MIN_BAR_RECEIVED_CONST = 4 -MIN_BAR_FRIENDSHIP_CONST = 80 +MIN_BAR_FRIENDSHIP_CONST = 30 class CustomerConv(enum.IntEnum): @@ -57,18 +58,24 @@ def __init__(self, game_config: dict, **kwargs: Any): self.plot_stage = CustomerPlot.NOT_ACTIVE def visit(self): - return ( - np.random.binomial( - n=1, - p=min(self.friendship / 100, 1.0), - ) - > 0 - ) + # TODO: for debug, set the visit prob to be 0.9 + return np.random.binomial(n=1, p=0.9,) > 0 + # return ( + # np.random.binomial( + # n=1, + # p=min(self.friendship / 100, 1.0), + # ) + # > 0 + # ) def activate_plot(self) -> None: - # Note: once activate, never deactivate - if self.friendship >= MIN_BAR_FRIENDSHIP_CONST: - self.plot_stage = CustomerPlot.ACTIVE + # when the customer is the main role in a plot, it will be activated + self.plot_stage = CustomerPlot.ACTIVE + + def deactivate_plot(self) -> None: + # when the plot in which the customer is a main role is over, the + # customer will be deactivated + self.plot_stage = CustomerPlot.NOT_ACTIVE def reply(self, x: dict = None) -> Union[dict, tuple]: # TODO: @@ -109,10 +116,6 @@ def _default_score(_: str) -> float: score_discount = score_discount if score_discount > 0 else 0 score = score * score_discount - if score > MIN_BAR_RECEIVED_CONST and self.friendship > 60: - self.cur_state = CustomerConv.AFTER_MEAL_CHAT - self.preorder_itr_count = 0 - change_in_friendship = score - MIN_BAR_RECEIVED_CONST self.friendship += change_in_friendship change_symbol = "+" if change_in_friendship >= 0 else "" @@ -121,11 +124,15 @@ def _default_score(_: str) -> float: f"当前好感度为 {self.friendship}", ) + if score > MIN_BAR_RECEIVED_CONST and self.friendship > MIN_BAR_FRIENDSHIP_CONST: + self.transition(CustomerConv.AFTER_MEAL_CHAT) + print("---", self.cur_state) + self.preorder_itr_count = 0 + return Msg(role="assistant", name=self.name, content=text, score=score) def _pre_meal_chat(self, x: dict) -> dict: if "推荐" in x["content"]: - self.transition(CustomerConv.AFTER_MEAL_CHAT) return self._recommendation_to_score(x) self.preorder_itr_count += 1 @@ -143,6 +150,7 @@ def _pre_meal_chat(self, x: dict) -> dict: system_msg, x, ) + logger.debug(system_prompt) if x is not None: self.memory.add(x) reply = self.model(messages=prompt) @@ -161,32 +169,7 @@ def _main_plot_chat(self, x: dict) -> dict: 1.2 the customer has no hidden plot (help with background) 2. Customer is not a main role in the current plot """ - - prompt = self.game_config["basic_background_prompt"].format_map( - { - "name": self.config["name"], - "character_description": self.background, - }, - ) - if self.plot_stage == CustomerPlot.ACTIVE: - # -> prompt for the main role in the current plot - prompt += self.game_config["hidden_main_plot_prompt"].format_map( - { - "hidden_plot": self.config["character_setting"][ - "hidden_plot" - ], - }, - ) - if self.cur_state == CustomerConv.AFTER_MEAL_CHAT: - prompt += self.game_config["hidden_main_plot_after_meal"] - else: - prompt += self.game_config["hidden_main_plot_discussion"] - else: - # -> prompt for the helper or irrelvant roles in the current plot - if self.cur_state == CustomerConv.AFTER_MEAL_CHAT: - prompt += self.game_config["regular_after_meal_prompt"] - else: - prompt += self.game_config["invited_chat_prompt"] + prompt = self._gen_plot_related_prompt() logger.debug(f"{self.name} system prompt: {prompt}") @@ -282,3 +265,36 @@ def generate_pov_story(self, recent_n: int = 20): print("*" * 20) logger.info(pov_story) print("*" * 20) + + def _gen_plot_related_prompt(self) -> str: + """ + generate prompot depending on the state and friendship of the customer + """ + prompt = self.game_config["basic_background_prompt"].format_map( + { + "name": self.config["name"], + "character_description": self.background, + }, + ) + if self.plot_stage == CustomerPlot.ACTIVE \ + and self.friendship > MIN_BAR_FRIENDSHIP_CONST: + # -> prompt for the main role in the current plot + prompt += self.game_config["hidden_main_plot_prompt"].format_map( + { + "hidden_plot": self.config["character_setting"][ + "hidden_plot" + ], + }, + ) + if self.cur_state == CustomerConv.AFTER_MEAL_CHAT: + prompt += self.game_config["hidden_main_plot_after_meal"] + else: + prompt += self.game_config["hidden_main_plot_discussion"] + else: + # -> prompt for the helper or irrelvant roles in the current plot + if self.cur_state == CustomerConv.AFTER_MEAL_CHAT: + prompt += self.game_config["regular_after_meal_prompt"] + else: + prompt += self.game_config["invited_chat_prompt"] + + return prompt diff --git a/examples/game/main.py b/examples/game/main.py index 4167ff8d7..77b29554a 100644 --- a/examples/game/main.py +++ b/examples/game/main.py @@ -19,12 +19,14 @@ load_game_checkpoint, save_game_checkpoint, speak_print, + check_active_plot, ) -def invited_group_chat(invited_customer, player, cur_plot): +def invited_group_chat(invited_customer, player, cur_plots_indices): + logger.debug("\n---active_plots:" + str(cur_plots_indices)) if len(invited_customer) == 0: - return cur_plot + return cur_plots_indices invited_names = [c.name for c in invited_customer] print("===== invited group chat ====") print(f"老板今天邀请了{invited_names},大家一起聊聊") @@ -50,27 +52,31 @@ def invited_group_chat(invited_customer, player, cur_plot): speak_print(msg) invited_names.sort() - correct_names = GAME_CONFIG["plots"][cur_plot] - correct_names.sort() - - # TODO: decided by multi factor: chat history of msghub, correct_names - if invited_names == correct_names: - print("===== successfully unlock a plot =======") - questions = [ - inquirer.List( - "ans", - message="【系统】:需要以哪位角色的视角生成一段完整故事吗?", - choices=invited_names + ["跳过"], - ), - ] - answer = inquirer.prompt(questions)["ans"] - for c in invited_customer: - if c.name == answer: - c.generate_pov_story() - cur_plot += 1 # move to next plot - for c in invited_customer: - c.refine_background() - return cur_plot + print(cur_plots_indices) + for idx in cur_plots_indices: + correct_names = [GAME_CONFIG["plots"][idx]["main_role"]] + \ + GAME_CONFIG["plots"][idx]["supporting_roles"] + correct_names.sort() + print("current names", correct_names) + + # TODO: decided by multi factor: chat history of msghub, correct_names + if invited_names == correct_names: + print("===== successfully unlock a plot =======") + questions = [ + inquirer.List( + "ans", + message="【系统】:需要以哪位角色的视角生成一段完整故事吗?", + choices=invited_names + ["跳过"], + ), + ] + answer = inquirer.prompt(questions)["ans"] + for c in invited_customer: + if c.name == answer: + c.generate_pov_story() + for c in invited_customer: + c.refine_background() + return idx + return None def one_on_one_loop(customers, player): @@ -115,7 +121,7 @@ def one_on_one_loop(customers, player): "ans", message="【系统】:接下来你会说些什么吗?", choices=[ - "这里是赠送的果盘,请您享用。还有什么是我能为您做的呢?", + "感谢您的今天来我们这里消费。这里是赠送的果盘,请您享用。还有什么是我能为您做的呢?", "感谢您的光顾。(结束与该顾客的当天对话)", ], ), @@ -188,33 +194,35 @@ def main() -> None: player = RuledUser(**user_configs) - invited_customers = [] - stage_per_night = StagePerNight.CASUAL_CHAT_FOR_MEAL - cur_plot = 0 - if args.load_checkpoint is not None: checkpoint = load_game_checkpoint(args.load_checkpoint) - customers = checkpoint.customers - stage_per_night = checkpoint.stage_per_night - cur_plot = (checkpoint.cur_plot,) - invited_customers = checkpoint.invited_customers - print( - "load checkpoint", - checkpoint.stage_per_night, - checkpoint.cur_plot, + logger.debug( + "load checkpoint\n" + str(checkpoint.stage_per_night) + + str(checkpoint.cur_plots), ) else: + invited_customers = [] + stage_per_night = StagePerNight.CASUAL_CHAT_FOR_MEAL + cur_plots, done_plots = [], [] checkpoint = GameCheckpoint( stage_per_night=stage_per_night, - cur_plot=cur_plot, + cur_plots=cur_plots, + done_plots=done_plots, customers=customers, invited_customers=invited_customers, ) + # set current plot and done plots # initialize main role of current plot cur_state - main_role = GAME_CONFIG["plots"][checkpoint.cur_plot][0] + plots = GAME_CONFIG["plots"] + for i in checkpoint.done_plots: + plots[i]["state"] = "done" + to_activate_customers, active_plots = check_active_plot(plots, None) + checkpoint.cur_plots = active_plots + logger.debug(str(to_activate_customers) + str(active_plots)) + to_activate_customers = set(to_activate_customers) for c in customers: - if c.name == main_role: + if c.name in to_activate_customers: c.activate_plot() while True: @@ -226,11 +234,20 @@ def main() -> None: # set customer to invited discussion cur_state c.transition(CustomerConv.INVITED_GROUP_PLOT) # initial cur_state of the - checkpoint.cur_plot = invited_group_chat( + done_plot_idx = invited_group_chat( checkpoint.invited_customers, player, - checkpoint.cur_plot, + checkpoint.cur_plots, ) + if done_plot_idx is not None: + next_active_roles, active_plots = check_active_plot(plots, done_plot_idx) + logger.debug("---active_plots:", active_plots) + checkpoint.cur_plots = active_plots + checkpoint.done_plots += [done_plot_idx] + next_active_roles = set(next_active_roles) + for c in checkpoint.customers: + if c.name in next_active_roles: + c.activate_plot() checkpoint.stage_per_night = StagePerNight.CASUAL_CHAT_FOR_MEAL elif checkpoint.stage_per_night == StagePerNight.CASUAL_CHAT_FOR_MEAL: # ========== one-on-one loop ================= @@ -276,7 +293,7 @@ def main() -> None: "api_key": os.environ.get("TONGYI_API_KEY"), } - agentscope.init(model_configs=[TONGYI_CONFIG], logger_level="INFO") + agentscope.init(model_configs=[TONGYI_CONFIG], logger_level="DEBUG") game_description = """ 这是一款模拟餐馆经营的文字冒险游戏,玩家扮演餐馆老板,通过与顾客互动来经营餐馆并解锁剧情。 游戏分为三个阶段:随意聊天,一对一互动以及邀请对话。 diff --git a/examples/game/utils.py b/examples/game/utils.py index b694c7c56..08c8ceeac 100644 --- a/examples/game/utils.py +++ b/examples/game/utils.py @@ -2,10 +2,11 @@ import enum import os import pickle +from typing import Optional from datetime import datetime from customer import Customer from agentscope.message import Msg -from colorist import BgBrightColor, Effect +from colorist import BgBrightColor class StagePerNight(enum.IntEnum): @@ -18,21 +19,23 @@ class StagePerNight(enum.IntEnum): class GameCheckpoint: def __init__( - self, - stage_per_night: StagePerNight, - customers: list[Customer], - cur_plot: int, - invited_customers: list[Customer], + self, + stage_per_night: StagePerNight, + customers: list[Customer], + cur_plots: list, + done_plots: list, + invited_customers: list[Customer], ): self.stage_per_night = stage_per_night self.customers = customers - self.cur_plot = cur_plot + self.cur_plots = cur_plots + self.done_plots = done_plots self.invited_customers = invited_customers def save_game_checkpoint( - checkpoint: GameCheckpoint, - checkpoint_prefix: str, + checkpoint: GameCheckpoint, + checkpoint_prefix: str, ) -> None: time_str = datetime.now().strftime("%Y%m%d_%H%M%S") checkpoint_path = checkpoint_prefix + time_str @@ -48,3 +51,37 @@ def load_game_checkpoint(checkpoint_path: str) -> GameCheckpoint: def speak_print(m: Msg): print(f"{BgBrightColor.BLUE}{m.name}{BgBrightColor.OFF}: {m.content}") + + +def check_active_plot( + plots: list[dict], + curr_done: Optional[int]) -> tuple[list[str], list[int]]: + # insure all plots have been added 'state' + for p in plots: + if "state" not in p: + p["state"] = "non-active" + to_be_activated = [] + active_plots = [] + + if curr_done is not None: + plots[curr_done]["state"] = "done" + + # activate those with dependencies and the dependencies are done + for idx, p in enumerate(plots): + # activate all plots has no dependency and not done yet + if p["predecessor_plots"] is None and p["state"] == "non-active": + to_be_activated.append(p["main_role"]) + p["state"] = "active" + active_plots.append(idx) + elif p["predecessor_plots"] is not None: + to_activate = all([plots[pre_p]["state"] == "done" + for pre_p in p["predecessor_plots"]]) + + if to_activate: + p["state"] = "active" + to_be_activated.append(p["main_role"]) + active_plots.append(idx) + elif p['state'] == "active": + active_plots.append(idx) + + return to_be_activated, active_plots