From 224af2214220510f770929e5c0aaa3b7f578c309 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Mon, 10 Nov 2025 00:21:36 +0800 Subject: [PATCH] =?UTF-8?q?better=EF=BC=9A=E5=A4=A7=E5=B9=85=E4=BC=98?= =?UTF-8?q?=E5=8C=96planner=E6=B6=88=E8=80=97=20-60%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/planner_actions/planner.py | 137 ++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 28 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 5294438d..24b88057 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -57,33 +57,27 @@ no_reply 控制聊天频率,不要太过频繁的发言 {{"action":"no_reply"}} -no_reply_until_call -动作描述: -保持沉默,直到有人直接叫你的名字 -当前话题不感兴趣时使用,或有人不喜欢你的发言时使用 -当你频繁选择no_reply时使用,表示话题暂时与你无关 -{{"action":"no_reply_until_call"}} +{no_reply_until_call_block} {action_options_text} + **你之前的action执行和思考记录** {actions_before_now_block} 请选择**可选的**且符合使用条件的action,并说明触发action的消息id(消息id格式:m+数字) 不要回复你自己发送的消息 -先输出你的选择思考理由,再输出你选择的action,理由是一段平文本,不要分点,精简。 +先输出你的简短的选择思考理由,再输出你选择的action,理由不要分点,精简。 **动作选择要求** 请你根据聊天内容,用户的最新消息和以下标准选择合适的动作: {plan_style} {moderation_prompt} -请选择所有符合使用要求的action,动作用json格式输出,如果输出多个json,每个json都要单独用```json包裹,你可以重复使用同一个动作或不同动作: +请选择所有符合使用要求的action,动作用json格式输出,用```json包裹,如果输出多个json,每个json都要单独一行放在同一个```json代码块内,你可以重复使用同一个动作或不同动作: **示例** -// 理由文本 +// 理由文本(简短) ```json {{"action":"动作名", "target_message_id":"m123", "reason":"原因"}} -``` -```json {{"action":"动作名", "target_message_id":"m456", "reason":"原因"}} ```""", "planner_prompt", @@ -285,18 +279,73 @@ class ActionPlanner: if len(self.plan_log) > 20: self.plan_log.pop(0) - def get_plan_log_str(self) -> str: - plan_log_str = "" - for reasoning, time, content in self.plan_log: + def get_plan_log_str(self, max_action_records: int = 2, max_execution_records: int = 5) -> str: + """ + 获取计划日志字符串 + + Args: + max_action_records: 显示多少条最新的action记录,默认2 + max_execution_records: 显示多少条最新执行结果记录,默认8 + + Returns: + 格式化的日志字符串 + """ + action_records = [] + execution_records = [] + + # 从后往前遍历,收集最新的记录 + for reasoning, timestamp, content in reversed(self.plan_log): if isinstance(content, list) and all(isinstance(action, ActionPlannerInfo) for action in content): - time = datetime.fromtimestamp(time).strftime("%H:%M:%S") - plan_log_str += f"{time}:{reasoning}|你使用了{','.join([action.action_type for action in content])}\n" + # 这是action记录 + if len(action_records) < max_action_records: + action_records.append((reasoning, timestamp, content, "action")) else: - time = datetime.fromtimestamp(time).strftime("%H:%M:%S") - plan_log_str += f"{time}:{content}\n" + # 这是执行结果记录 + if len(execution_records) < max_execution_records: + execution_records.append((reasoning, timestamp, content, "execution")) + + # 合并所有记录并按时间戳排序 + all_records = action_records + execution_records + all_records.sort(key=lambda x: x[1]) # 按时间戳排序 + + plan_log_str = "" + + # 按时间顺序添加所有记录 + for reasoning, timestamp, content, record_type in all_records: + time_str = datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") + if record_type == "action": + # plan_log_str += f"{time_str}:{reasoning}|你使用了{','.join([action.action_type for action in content])}\n" + plan_log_str += f"{time_str}:{reasoning}\n" + else: + plan_log_str += f"{time_str}:你执行了action:{content}\n" return plan_log_str + def _has_consecutive_no_reply(self, min_count: int = 3) -> bool: + """ + 检查是否有连续min_count次以上的no_reply + + Args: + min_count: 需要连续的最少次数,默认3 + + Returns: + 如果有连续min_count次以上no_reply返回True,否则返回False + """ + consecutive_count = 0 + + # 从后往前遍历plan_log,检查最新的连续记录 + for _reasoning, _timestamp, content in reversed(self.plan_log): + if isinstance(content, list) and all(isinstance(action, ActionPlannerInfo) for action in content): + # 检查所有action是否都是no_reply + if all(action.action_type == "no_reply" for action in content): + consecutive_count += 1 + if consecutive_count >= min_count: + return True + else: + # 如果遇到非no_reply的action,重置计数 + break + + return False async def build_planner_prompt( self, @@ -318,6 +367,17 @@ class ActionPlanner: # 构建动作选项块 action_options_block = await self._build_action_options_block(current_available_actions) + # 检查是否有连续3次以上no_reply,如果有则添加no_reply_until_call选项 + no_reply_until_call_block = "" + if self._has_consecutive_no_reply(min_count=3): + no_reply_until_call_block = """no_reply_until_call +动作描述: +保持沉默,直到有人直接叫你的名字 +当前话题不感兴趣时使用,或有人不喜欢你的发言时使用 +当你频繁选择no_reply时使用,表示话题暂时与你无关 +{{"action":"no_reply_until_call"}} +""" + # 其他信息 moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" @@ -335,6 +395,7 @@ class ActionPlanner: chat_content_block=chat_content_block, actions_before_now_block=actions_before_now_block, action_options_text=action_options_block, + no_reply_until_call_block=no_reply_until_call_block, moderation_prompt=moderation_prompt_block, name_block=name_block, interest=interest, @@ -532,10 +593,11 @@ class ActionPlanner: # 使用正则表达式查找```json包裹的JSON内容 json_pattern = r"```json\s*(.*?)\s*```" - matches = re.findall(json_pattern, content, re.DOTALL) + markdown_matches = re.findall(json_pattern, content, re.DOTALL) # 提取JSON之前的内容作为推理文本 - if matches: + first_json_pos = len(content) + if markdown_matches: # 找到第一个```json的位置 first_json_pos = content.find("```json") if first_json_pos > 0: @@ -544,19 +606,38 @@ class ActionPlanner: reasoning_content = re.sub(r"^//\s*", "", reasoning_content, flags=re.MULTILINE) reasoning_content = reasoning_content.strip() - for match in matches: + # 处理```json包裹的JSON + for match in markdown_matches: try: # 清理可能的注释和格式问题 json_str = re.sub(r"//.*?\n", "\n", match) # 移除单行注释 json_str = re.sub(r"/\*.*?\*/", "", json_str, flags=re.DOTALL) # 移除多行注释 if json_str := json_str.strip(): - json_obj = json.loads(repair_json(json_str)) - if isinstance(json_obj, dict): - json_objects.append(json_obj) - elif isinstance(json_obj, list): - for item in json_obj: - if isinstance(item, dict): - json_objects.append(item) + # 尝试按行分割,每行可能是一个JSON对象 + lines = [line.strip() for line in json_str.split('\n') if line.strip()] + for line in lines: + try: + # 尝试解析每一行作为独立的JSON对象 + json_obj = json.loads(repair_json(line)) + if isinstance(json_obj, dict): + json_objects.append(json_obj) + elif isinstance(json_obj, list): + for item in json_obj: + if isinstance(item, dict): + json_objects.append(item) + except json.JSONDecodeError: + # 如果单行解析失败,尝试将整个块作为一个JSON对象或数组 + pass + + # 如果按行解析没有成功,尝试将整个块作为一个JSON对象或数组 + if not json_objects: + json_obj = json.loads(repair_json(json_str)) + if isinstance(json_obj, dict): + json_objects.append(json_obj) + elif isinstance(json_obj, list): + for item in json_obj: + if isinstance(item, dict): + json_objects.append(item) except Exception as e: logger.warning(f"解析JSON块失败: {e}, 块内容: {match[:100]}...") continue