diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 3ac88599..e59adadb 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -124,6 +124,48 @@ PROMPT_END_DECISION = """ 注意:请严格按照 JSON 格式输出,不要包含任何其他内容。""" +# Prompt(4): 当 reply_generator 决定不发送消息后的反思决策 Prompt +PROMPT_REFLECT_AND_ACT = """ +当前时间:{current_time_str} +{persona_text} +现在你正在和{sender_name}在QQ上私聊 +你与对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +刚刚你本来想发一条新消息,但是想了想,你决定不发了。 +请根据以下【所有信息】审慎且灵活的决策下一步行动,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方: + +【当前对话目标】 +{goals_str} +【最近行动历史概要】 +{action_history_summary} +【你想起来的相关知识】 +{retrieved_knowledge_str} +【上一次行动的详细情况和结果】 +{last_action_context} +【时间和超时提示】 +{time_since_last_bot_message_info}{timeout_context} +【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) +{chat_history_text} +【你的回忆】 +{retrieved_memory_str} + +{spam_warning_info} + +------ +可选行动类型以及解释: +wait: 等待,暂时不说话。 +listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个) +rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 +end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、已经不再回复你消息、明显暗示或表达想结束聊天时,可以果断选择 +block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择 + +请以JSON格式输出你的决策: +{{ + "action": "选择的行动类型 (必须是上面列表中的一个)", + "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。)" +}} + +注意:请严格按照JSON格式输出,不要包含任何其他内容。""" class ActionPlanner: """行动规划器""" @@ -162,6 +204,7 @@ class ActionPlanner: observation_info: ObservationInfo, conversation_info: ConversationInfo, last_successful_reply_action: Optional[str], + use_reflect_prompt: bool = False # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT ) -> Tuple[str, str]: """ 规划下一步行动。 @@ -206,24 +249,38 @@ class ActionPlanner: # --- 2. 选择并格式化 Prompt --- try: - if last_successful_reply_action in ["direct_reply", "send_new_message"]: + if use_reflect_prompt: # 新增的判断 + prompt_template = PROMPT_REFLECT_AND_ACT + log_msg = "使用 PROMPT_REFLECT_AND_ACT (反思决策)" + # 对于 PROMPT_REFLECT_AND_ACT,它不包含 send_new_message 选项,所以 spam_warning_message 中的相关提示可以调整或省略 + # 但为了保持占位符填充的一致性,我们仍然计算它 + spam_warning_message = "" + if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少 + spam_warning_message = f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**" + elif conversation_info.my_message_count > 2: + spam_warning_message = f"💬【提示】**你之前已连续发送{str(conversation_info.my_message_count)}条消息。请注意保持对话平衡。**" + + elif last_successful_reply_action in ["direct_reply", "send_new_message"]: prompt_template = PROMPT_FOLLOW_UP log_msg = "使用 PROMPT_FOLLOW_UP (追问决策)" + spam_warning_message = "" + if conversation_info.my_message_count > 5: + spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" + elif conversation_info.my_message_count > 2: + spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" + else: prompt_template = PROMPT_INITIAL_REPLY log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)" + spam_warning_message = "" # 初始回复时通常不需要刷屏警告 + logger.debug(f"[私聊][{self.private_name}] {log_msg}") - current_time_value = "获取时间失败" # 默认值 + current_time_value = "获取时间失败" if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str - spam_warning_message = "" # 初始化为空字符串 - if conversation_info.my_message_count > 5: - spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" - elif conversation_info.my_message_count > 2: - spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" - if spam_warning_message: # 仅当有警告时才添加换行符 + if spam_warning_message: spam_warning_message = f"\n{spam_warning_message}\n" prompt = prompt_template.format( @@ -236,9 +293,8 @@ class ActionPlanner: chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - current_time_str=current_time_value, # 新增:传入当前时间字符串 + current_time_str=current_time_value, spam_warning_info=spam_warning_message, - ### 标记新增/修改区域 开始 ### sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str @@ -299,7 +355,7 @@ class ActionPlanner: # final_reason = initial_reason # --- 5. 验证最终行动类型 --- - valid_actions = [ + valid_actions_default = [ "direct_reply", "send_new_message", "wait", @@ -309,7 +365,19 @@ class ActionPlanner: "block_and_ignore", "say_goodbye", ] - if final_action not in valid_actions: + valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作 + "wait", + "listening", + "rethink_goal", + "end_conversation", + "block_and_ignore", + # PROMPT_REFLECT_AND_ACT 也可以 end_conversation,然后也可能触发 say_goodbye + "say_goodbye", + ] + + current_valid_actions = valid_actions_reflect if use_reflect_prompt else valid_actions_default + + if final_action not in current_valid_actions: logger.warning(f"[私聊][{self.private_name}] LLM 返回了未知的行动类型: '{final_action}',强制改为 wait") final_reason = f"(原始行动'{final_action}'无效,已强制改为wait) {final_reason}" final_action = "wait" # 遇到无效动作,默认等待 diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 6969c73c..4d2291c3 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -2,6 +2,8 @@ import time import asyncio import datetime import traceback +import json # 确保导入 json 模块 +from .pfc_utils import get_items_from_json # 导入JSON解析工具 from typing import Dict, Any, Optional, Set, List from dateutil import tz @@ -506,7 +508,7 @@ class Conversation: if not self._initialized: logger.error(f"[私聊][{self.private_name}] 尝试在未初始化状态下运行规划循环,退出。") return # 明确退出 - + force_reflect_and_act = False # 主循环,只要 should_continue 为 True 就一直运行 while self.should_continue: loop_iter_start_time = time.time() # 记录本次循环开始时间 @@ -654,10 +656,12 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 调用 ActionPlanner.plan...") # 传入当前观察信息、对话信息和上次成功回复的动作类型 action, reason = await self.action_planner.plan( - self.observation_info, - self.conversation_info, - self.conversation_info.last_successful_reply_action + self.observation_info, + self.conversation_info, # type: ignore + self.conversation_info.last_successful_reply_action, # type: ignore + use_reflect_prompt=force_reflect_and_act # 使用标志 ) + force_reflect_and_act = False planning_duration = time.time() - planning_start_time logger.debug( f"[私聊][{self.private_name}] ActionPlanner.plan 完成 (耗时: {planning_duration:.3f} 秒),初步规划动作: {action}" @@ -765,6 +769,16 @@ class Conversation: await self._handle_action(action, reason, self.observation_info, self.conversation_info) logger.debug(f"[私聊][{self.private_name}] _handle_action 完成。") + # --- 新增逻辑:检查是否因为RG决定不发送而需要反思 --- + last_action_record = ( + self.conversation_info.done_action[-1] if self.conversation_info.done_action else {} # type: ignore + ) + if last_action_record.get("action") == "send_new_message" and \ + last_action_record.get("status") == "done_no_reply": + logger.info(f"[私聊][{self.private_name}] 检测到 ReplyGenerator 决定不发送消息,将在下一轮强制使用反思Prompt。") + force_reflect_and_act = True # 设置标志,下一轮使用反思prompt + # 不需要立即 continue,让循环自然进入下一轮,下一轮的 plan 会用这个标志 + # 8. 检查是否需要结束整个对话(例如目标达成或执行了结束动作) goal_ended: bool = False # 检查最新的目标是否是“结束对话” @@ -913,68 +927,105 @@ class Conversation: check_reason: str = "未进行检查" # 存储检查结果原因 # --- [核心修复] 引入重试循环 --- + is_send_decision_from_rg = False # 标记是否由 reply_generator 决定发送 + while reply_attempt_count < max_reply_attempts and not is_suitable and not need_replan_from_checker: reply_attempt_count += 1 log_prefix = f"[私聊][{self.private_name}] 尝试生成/检查 '{action}' 回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." logger.info(log_prefix) - # --- a. 生成回复 --- - self.state = ConversationState.GENERATING # 更新对话状态 + self.state = ConversationState.GENERATING if not self.reply_generator: - # 检查依赖组件是否存在 raise RuntimeError("ReplyGenerator 未初始化") - # 调用 ReplyGenerator 生成回复内容 - generated_content = await self.reply_generator.generate( + + raw_llm_output = await self.reply_generator.generate( observation_info, conversation_info, action_type=action ) - logger.info(f"{log_prefix} 生成内容: '{generated_content}'") # 日志中截断长内容 + logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") - # 检查生成内容是否有效 - if not generated_content or generated_content.startswith("抱歉"): - # 如果生成失败或返回错误提示 - logger.warning(f"{log_prefix} 生成内容为空或为错误提示,将进行下一次尝试。") - check_reason = "生成内容无效" # 记录原因 - # 记录拒绝信息供下次生成参考 + should_send_reply = True # 默认对于 direct_reply 是要发送的 + text_to_process = raw_llm_output # 默认情况下,处理原始输出 + + if action == "send_new_message": + is_send_decision_from_rg = True # 标记 send_new_message 的决策来自RG + try: + # 使用 pfc_utils.py 中的 get_items_from_json 来解析 + # 注意:get_items_from_json 目前主要用于提取固定字段的字典。 + # reply_generator 返回的是一个顶级JSON对象。 + # 我们需要稍微调整用法或增强 get_items_from_json。 + # 简单起见,这里我们先直接用 json.loads,后续可以优化。 + + parsed_json = None + try: + parsed_json = json.loads(raw_llm_output) + except json.JSONDecodeError: + logger.error(f"{log_prefix} ReplyGenerator 返回的不是有效的JSON: {raw_llm_output}") + # 如果JSON解析失败,视为RG决定不发送,并给出原因 + conversation_info.last_reply_rejection_reason = "回复生成器未返回有效JSON" + conversation_info.last_rejected_reply_content = raw_llm_output + should_send_reply = False + text_to_process = "no" # 或者一个特定的错误标记 + + if parsed_json: + send_decision = parsed_json.get("send", "no").lower() + generated_text_from_json = parsed_json.get("txt", "no") + + if send_decision == "yes": + should_send_reply = True + text_to_process = generated_text_from_json + logger.info(f"{log_prefix} ReplyGenerator 决定发送消息。内容: '{text_to_process[:100]}...'") + else: + should_send_reply = False + text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no" + logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") + # 此时,我们应该跳出重试循环,并触发 action_planner 的反思 prompt + # 将此信息传递到循环外部进行处理 + break # 跳出 while 循环 + + except Exception as e_json: # 更广泛地捕获解析相关的错误 + logger.error(f"{log_prefix} 解析 ReplyGenerator 的JSON输出时出错: {e_json}, 输出: {raw_llm_output}") + conversation_info.last_reply_rejection_reason = f"解析回复生成器JSON输出错误: {e_json}" + conversation_info.last_rejected_reply_content = raw_llm_output + should_send_reply = False + text_to_process = "no" + + if not should_send_reply and action == "send_new_message": # 如果RG决定不发送 (send_new_message特定逻辑) + break # 直接跳出重试循环,后续逻辑会处理这种情况 + + generated_content_for_check_or_send = text_to_process + + if not generated_content_for_check_or_send or generated_content_for_check_or_send.startswith("抱歉") or (action == "send_new_message" and generated_content_for_check_or_send == "no"): + logger.warning(f"{log_prefix} 生成内容无效或为错误提示 (或send:no),将进行下一次尝试 (如果适用)。") + check_reason = "生成内容无效或选择不发送" conversation_info.last_reply_rejection_reason = check_reason - conversation_info.last_rejected_reply_content = generated_content - await asyncio.sleep(0.5) # 短暂等待后重试 - continue # 进入下一次循环尝试 + conversation_info.last_rejected_reply_content = generated_content_for_check_or_send + if action == "direct_reply": # direct_reply 失败时才继续尝试 + await asyncio.sleep(0.5) + continue + else: # send_new_message 如果是 no,不应该继续尝试,上面已经break了 + pass # 理论上不会到这里如果上面break了 - # --- b. 检查回复 --- - self.state = ConversationState.CHECKING # 更新状态为检查中 + self.state = ConversationState.CHECKING if not self.reply_checker: raise RuntimeError("ReplyChecker 未初始化") - # 准备检查所需的上下文信息 - current_goal_str: str = "" # 当前对话目标字符串 + current_goal_str = "" if conversation_info.goal_list: - # 通常检查最新的目标 goal_item = conversation_info.goal_list[-1] if isinstance(goal_item, dict): current_goal_str = goal_item.get("goal", "") elif isinstance(goal_item, str): current_goal_str = goal_item - # 获取用于检查的聊天记录 (列表和字符串形式) - chat_history_for_check: List[Dict[str, Any]] = getattr(observation_info, "chat_history", []) - chat_history_text_for_check: str = getattr(observation_info, "chat_history_str", "") - # 当前重试次数 (传递给 checker,可能有用) - # retry_count for checker starts from 0 - current_retry_for_checker = reply_attempt_count - 1 - + chat_history_for_check = getattr(observation_info, "chat_history", []) + chat_history_text_for_check = getattr(observation_info, "chat_history_str", "") + current_retry_for_checker = reply_attempt_count - 1 + current_time_value_for_check = observation_info.current_time_str or "获取时间失败" - - current_time_value_for_check = "获取时间失败" - if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: - current_time_value_for_check = observation_info.current_time_str - - - logger.debug(f"{log_prefix} 调用 ReplyChecker 检查...") - # --- 根据配置决定是否执行检查 --- - if global_config.enable_pfc_reply_checker: # <--- 使用配置项 + if global_config.enable_pfc_reply_checker: logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") is_suitable, check_reason, need_replan_from_checker = await self.reply_checker.check( - reply=generated_content, + reply=generated_content_for_check_or_send, goal=current_goal_str, chat_history=chat_history_for_check, chat_history_text=chat_history_text_for_check, @@ -985,67 +1036,96 @@ class Conversation: f"{log_prefix} ReplyChecker 结果: 合适={is_suitable}, 原因='{check_reason}', 需重规划={need_replan_from_checker}" ) else: - # 如果配置为关闭,则默认通过检查 is_suitable = True check_reason = "ReplyChecker 已通过配置关闭" need_replan_from_checker = False - logger.info(f"[配置关闭] ReplyChecker 已跳过,默认回复为合适。") - # 如果不合适,记录原因并准备下一次尝试(如果还有次数) + logger.info(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") + if not is_suitable: - # 记录拒绝原因和内容,供下次生成时参考 conversation_info.last_reply_rejection_reason = check_reason - conversation_info.last_rejected_reply_content = generated_content - # 如果不需要重规划且还有尝试次数 + conversation_info.last_rejected_reply_content = generated_content_for_check_or_send if not need_replan_from_checker and reply_attempt_count < max_reply_attempts: logger.warning(f"{log_prefix} 回复不合适,原因: {check_reason}。将进行下一次尝试。") - await asyncio.sleep(0.5) # 等待后重试 + await asyncio.sleep(0.5) # 如果需要重规划或达到最大次数,循环会在下次判断时自动结束 + + # --- 循环结束后处理 --- + if action == "send_new_message" and not should_send_reply and is_send_decision_from_rg: + # 这是 reply_generator 决定不发送的情况 + logger.info(f"[私聊][{self.private_name}] 动作 '{action}': ReplyGenerator 决定不发送消息。将调用 ActionPlanner 进行反思。") + final_status = "done_no_reply" # 一个新的状态,表示动作完成但无回复 + final_reason = "回复生成器决定不发送消息" + action_successful = True # 动作本身(决策)是成功的 - # --- 循环结束后,处理最终结果 --- - if is_suitable: - # 如果找到了合适的回复 + # 清除追问状态,因为没有实际发送 + conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 # 重置连续发言计数 + + # !!! 触发 ActionPlanner 使用 PROMPT_REFLECT_AND_ACT !!! + if not self.action_planner: + raise RuntimeError("ActionPlanner 未初始化") + + logger.info(f"[私聊][{self.private_name}] {self.name} 本来想发一条新消息,但是想想还是算了。现在重新规划...") + # 调用 action_planner.plan 并传入 use_reflect_prompt=True + new_action, new_reason = await self.action_planner.plan( + observation_info, + conversation_info, + last_successful_reply_action=None, # 因为没发送,所以没有成功的回复动作 + use_reflect_prompt=True + ) + # 记录这次特殊的“反思”动作 + reflect_action_record = { + "action": f"reflect_after_no_send ({new_action})", # 记录原始意图和新规划 + "plan_reason": f"RG决定不发送后,AP规划: {new_reason}", + "status": "delegated", # 标记为委托给新的规划 + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + conversation_info.done_action.append(reflect_action_record) + + logger.info(f"[私聊][{self.private_name}] 反思后的新规划动作: {new_action}, 原因: {new_reason}") + # **暂定方案:** + # _handle_action 在这种情况下返回一个特殊标记。 + # 为了不立即修改返回类型,我们暂时在这里记录日志,并在 _plan_and_action_loop 中添加逻辑。 + # _handle_action 会将 action_successful 设置为 True,final_status 为 "done_no_reply"。 + # _plan_and_action_loop 之后会检查这个状态。 + + # (这里的 final_status, final_reason, action_successful 已在上面设置) + + elif is_suitable: # 适用于 direct_reply 或 send_new_message (RG决定发送且检查通过) logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 找到合适的回复,准备发送。") - # 清除上次的拒绝信息 (因为本次成功了) conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None - - # --- c. 发送回复 --- - self.generated_reply = generated_content # 使用最后一次检查通过的内容 - timestamp_before_sending = time.time() # 记录发送前时间戳 + self.generated_reply = generated_content_for_check_or_send + timestamp_before_sending = time.time() logger.debug( f"[私聊][{self.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" ) - self.state = ConversationState.SENDING # 更新状态为发送中 - # 调用内部发送方法 - send_success = await self._send_reply() - send_end_time = time.time() # 记录发送结束时间 + self.state = ConversationState.SENDING + send_success = await self._send_reply() # _send_reply 内部会更新 my_message_count + send_end_time = time.time() if send_success: - # 如果发送成功 - action_successful = True # 标记动作成功 - # final_status 和 final_reason 会在 finally 中设置 + action_successful = True logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 成功发送回复.") - # 更新空闲计时器 if self.idle_conversation_starter: await self.idle_conversation_starter.update_last_message_time(send_end_time) - # --- d. 清理已处理消息 --- current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", []) message_ids_to_clear: Set[str] = set() - # 遍历所有未处理消息 for msg in current_unprocessed_messages: msg_time = msg.get("time") msg_id = msg.get("message_id") - sender_id = msg.get("user_info", {}).get("user_id") - # 规则:只清理【发送前】收到的、【来自他人】的消息 + sender_id_info = msg.get("user_info", {}) + sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None + if ( msg_id and msg_time - and sender_id != self.bot_qq_str - and msg_time < timestamp_before_sending + and sender_id != self.bot_qq_str # 确保是对方的消息 + and msg_time < timestamp_before_sending # 只清理发送前的 ): message_ids_to_clear.add(msg_id) - # 如果有需要清理的消息,调用清理方法 + if message_ids_to_clear: logger.debug( f"[私聊][{self.private_name}] 准备清理 {len(message_ids_to_clear)} 条发送前(他人)消息: {message_ids_to_clear}" @@ -1054,73 +1134,66 @@ class Conversation: else: logger.debug(f"[私聊][{self.private_name}] 没有需要清理的发送前(他人)消息。") - # --- e. 决定下一轮规划类型 --- - # 从 conversation_info 获取【规划期间】收到的【他人】新消息数量 other_new_msg_count_during_planning = getattr( conversation_info, "other_new_messages_during_planning_count", 0 ) - # 规则:如果规划期间收到他人新消息 (0 < count <= 2),则下一轮强制初始回复 - if other_new_msg_count_during_planning > 0: + if other_new_msg_count_during_planning > 0 and action == "direct_reply": logger.info( f"[私聊][{self.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" ) - conversation_info.last_successful_reply_action = None # 强制初始回复 - conversation_info.my_message_count = 0 # 自身发言数量清零 - else: - # 规则:如果规划期间【没有】收到他人新消息,则允许追问 + conversation_info.last_successful_reply_action = None + # conversation_info.my_message_count = 0 # 不在这里重置,因为刚发了一条 + elif action == "direct_reply" or action == "send_new_message": # 成功发送后 logger.info( - f"[私聊][{self.private_name}] 规划期间无他人新消息,下一轮【允许】使用追问逻辑 (基于 '{action}')。" - ) - conversation_info.last_successful_reply_action = action # 允许追问 - - if conversation_info: # 确保 conversation_info 存在 - conversation_info.current_instance_message_count += 1 - logger.debug(f"[私聊][{self.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") - - if self.relationship_updater: - await self.relationship_updater.update_relationship_incremental( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer + f"[私聊][{self.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。" ) + conversation_info.last_successful_reply_action = action - sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" - event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" - if self.emotion_updater: - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - else: - # 如果发送失败 + if conversation_info: + conversation_info.current_instance_message_count += 1 + logger.debug(f"[私聊][{self.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") + + if self.relationship_updater: + await self.relationship_updater.update_relationship_incremental( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer + ) + + sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" + event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" + if self.emotion_updater: + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) + else: # 发送失败 logger.error(f"[私聊][{self.private_name}] 动作 '{action}': 发送回复失败。") - final_status = "recall" # 发送失败,标记为 recall + final_status = "recall" final_reason = "发送回复时失败" - # 重置追问状态 conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 # 发送失败,重置计数 elif need_replan_from_checker: - # 如果 Checker 要求重新规划 logger.warning( f"[私聊][{self.private_name}] 动作 '{action}' 因 ReplyChecker 要求而被取消,将重新规划。原因: {check_reason}" ) - final_status = "recall" # 标记为 recall + final_status = "recall" final_reason = f"回复检查要求重新规划: {check_reason}" - # # 重置追问状态 - # conversation_info.last_successful_reply_action = None + conversation_info.last_successful_reply_action = None + # my_message_count 保持不变,因为没有成功发送 - else: - # 达到最大尝试次数仍未找到合适回复 + else: # 达到最大尝试次数仍未找到合适回复 logger.warning( f"[私聊][{self.private_name}] 动作 '{action}': 达到最大尝试次数 ({max_reply_attempts}),未能生成/检查通过合适的回复。最终原因: {check_reason}" ) - final_status = "recall" # 标记为 recall + final_status = "recall" final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}" - # # 重置追问状态 - # conversation_info.last_successful_reply_action = None + conversation_info.last_successful_reply_action = None + # my_message_count 保持不变 # 2. 处理发送告别语动作 (保持简单,不加重试) elif action == "say_goodbye": diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 84dcea5f..eb8c619c 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -64,7 +64,7 @@ PROMPT_SEND_NEW_MESSAGE = """ 你正在和{sender_name}在QQ上私聊,**并且刚刚你已经发送了一条或多条消息** 你与对方的关系是:{relationship_text} 你现在的心情是:{current_emotion_text} -现在请根据以下信息再发一条新消息: +现在请根据以下信息判断你是否要继续发一条新消息,当然,如果你决定继续发消息不合适,也可以不发: 当前对话目标:{goals_str} @@ -79,7 +79,9 @@ PROMPT_SEND_NEW_MESSAGE = """ {last_rejection_info} -请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: +{spam_warning_info} + +请根据上述信息,判断你是否要继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。如果你觉得要发送,该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) @@ -88,10 +90,14 @@ PROMPT_SEND_NEW_MESSAGE = """ 请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 -请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出消息内容。 -不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 +如果你决定继续发消息不合适,也可以不发送。 -请直接输出回复内容,不需要任何额外格式。""" +请严格按照以下JSON格式输出你的选择和消息内容,不要包含任何其他说明或非JSON文本: +{{ + "send": "yes/no", + "txt": "如果选择发送,这里是具体的消息文本。如果选择不发送,这里也填写 'no'。" +}} +""" # Prompt for say_goodbye (告别语生成) PROMPT_FAREWELL = """ @@ -125,7 +131,7 @@ class ReplyGenerator: self.llm = LLMRequest( model=global_config.llm_PFC_chat, temperature=global_config.llm_PFC_chat["temp"], - max_tokens=300, + max_tokens=300, # 对于JSON输出,这个可能需要适当调整,但一般回复短,JSON结构也简单 request_type="reply_generation", ) self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) @@ -143,20 +149,18 @@ class ReplyGenerator: Args: observation_info: 观察信息 conversation_info: 对话信息 - action_type: 当前执行的动作类型 ('direct_reply' 或 'send_new_message') + action_type: 当前执行的动作类型 ('direct_reply', 'send_new_message', 'say_goodbye') Returns: - str: 生成的回复 + str: 生成的回复。 + 对于 'direct_reply' 和 'say_goodbye',返回纯文本回复。 + 对于 'send_new_message',返回包含决策和文本的JSON字符串。 """ - # 构建提示词 logger.debug( f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}" ) # --- 构建通用 Prompt 参数 --- - # (这部分逻辑基本不变) - - # 构建对话目标 (goals_str) goals_str = "" if conversation_info.goal_list: for goal_reason in conversation_info.goal_list: @@ -171,9 +175,8 @@ class ReplyGenerator: reasoning = str(reasoning) if reasoning is not None else "没有明确原因" goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n" else: - goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况 + goals_str = "- 目前没有明确对话目标\n" - # 获取聊天历史记录 (chat_history_text) chat_history_text = observation_info.chat_history_str if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages: new_messages_list = observation_info.unprocessed_messages @@ -192,17 +195,14 @@ class ReplyGenerator: f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" ) - # 获取 sender_name, relationship_text, current_emotion_text sender_name_str = getattr(observation_info, 'sender_name', '对方') if not sender_name_str: sender_name_str = '对方' relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') - # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" - retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text - # 调用共享函数进行检索 + retrieval_context = chat_history_text retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( retrieval_context, self.private_name ) @@ -210,9 +210,7 @@ class ReplyGenerator: f"[私聊][{self.private_name}] (ReplyGenerator) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str else '无'}" ) - # --- 修改:构建上次回复失败原因和内容提示 --- last_rejection_info_str = "" - # 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None last_reason = getattr(conversation_info, "last_reply_rejection_reason", None) last_content = getattr(conversation_info, "last_rejected_reply_content", None) @@ -220,7 +218,7 @@ class ReplyGenerator: last_rejection_info_str = ( f"\n------\n" f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n" - f"上次试图发送的消息内容: “{last_content}”\n" # <-- 显示上次内容 + f"上次试图发送的消息内容: “{last_content}”\n" f"失败原因: “{last_reason}”\n" f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" f"------\n" @@ -230,42 +228,67 @@ class ReplyGenerator: f" 内容: {last_content}\n" f" 原因: {last_reason}" ) + + # 新增:构建刷屏警告信息 for PROMPT_SEND_NEW_MESSAGE + spam_warning_message = "" + if action_type == "send_new_message": # 只在 send_new_message 时构建刷屏警告 + if conversation_info.my_message_count > 5: + spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎考虑是否继续发送!以免刷屏对造成对方困扰!**" + elif conversation_info.my_message_count > 2: + spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免连续发送,以免给对方造成困扰。**" + if spam_warning_message: + spam_warning_message = f"\n{spam_warning_message}\n" + # --- 选择 Prompt --- if action_type == "send_new_message": prompt_template = PROMPT_SEND_NEW_MESSAGE - logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问生成)") - elif action_type == "say_goodbye": # 处理告别动作 + logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问/补充生成, 期望JSON输出)") + elif action_type == "say_goodbye": prompt_template = PROMPT_FAREWELL logger.info(f"[私聊][{self.private_name}]使用 PROMPT_FAREWELL (告别语生成)") - else: # 默认使用 direct_reply 的 prompt (包括 'direct_reply' 或其他未明确处理的类型) + else: prompt_template = PROMPT_DIRECT_REPLY logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)") # --- 格式化最终的 Prompt --- - try: # <--- 增加 try-except 块处理可能的 format 错误 - current_time_value = "获取时间失败" # 默认值 + try: + current_time_value = "获取时间失败" if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str + if action_type == "say_goodbye": prompt = prompt_template.format( persona_text=persona_text, chat_history_text=chat_history_text, - current_time_str=current_time_value, # 添加时间 + current_time_str=current_time_value, sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str ) - - else: + elif action_type == "send_new_message": # PROMPT_SEND_NEW_MESSAGE 增加了 spam_warning_info prompt = prompt_template.format( persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text, retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因 - current_time_str=current_time_value, # 新增:传入当前时间字符串 + last_rejection_info=last_rejection_info_str, + current_time_str=current_time_value, + sender_name=sender_name_str, + relationship_text=relationship_text_str, + current_emotion_text=current_emotion_text_str, + spam_warning_info=spam_warning_message # 添加 spam_warning_info + ) + else: # PROMPT_DIRECT_REPLY (没有 spam_warning_info) + prompt = prompt_template.format( + persona_text=persona_text, + goals_str=goals_str, + chat_history_text=chat_history_text, + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", + retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + last_rejection_info=last_rejection_info_str, + current_time_str=current_time_value, sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str @@ -274,8 +297,7 @@ class ReplyGenerator: logger.error( f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" ) - # 返回错误信息或默认回复 - return "抱歉,准备回复时出了点问题,请检查一下我的代码..." + return "抱歉,准备回复时出了点问题,请检查一下我的代码..." # 对于JSON期望的场景,这里可能也需要返回一个固定的错误JSON except Exception as fmt_err: logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}") return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..." @@ -284,19 +306,30 @@ class ReplyGenerator: logger.debug(f"[私聊][{self.private_name}]发送到LLM的生成提示词:\n------\n{prompt}\n------") try: content, _ = await self.llm.generate_response_async(prompt) - logger.debug(f"[私聊][{self.private_name}]生成的回复: {content}") - # 移除旧的检查新消息逻辑,这应该由 conversation 控制流处理 + # 对于 PROMPT_SEND_NEW_MESSAGE,我们期望 content 是一个 JSON 字符串 + # 对于其他 prompts,content 是纯文本回复 + # 该方法现在直接返回 LLM 的原始输出,由调用者 (conversation._handle_action) 负责解析 + logger.debug(f"[私聊][{self.private_name}]LLM原始生成内容: {content}") return content except Exception as e: logger.error(f"[私聊][{self.private_name}]生成回复时出错: {e}") - return "抱歉,我现在有点混乱,让我重新思考一下..." + # 根据 action_type 返回不同的错误指示 + if action_type == "send_new_message": + # 返回一个表示错误的JSON,让调用方知道出错了但仍能解析 + return """{{ + "send": "no", + "txt": "LLM生成回复时出错" + }}""".strip() + else: + return "抱歉,我现在有点混乱,让我重新思考一下..." # check_reply 方法保持不变 async def check_reply( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0 + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, current_time_str: str, retry_count: int = 0 ) -> Tuple[bool, str, bool]: """检查回复是否合适 - (此方法逻辑保持不变) + (此方法逻辑保持不变, 但注意 current_time_str 参数的传递) """ - return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) + # 确保 current_time_str 被正确传递给 reply_checker.check + return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, current_time_str, retry_count) \ No newline at end of file