diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 8470c24e9..6b2bdaac3 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -25,18 +25,20 @@ def init_prompt(): prompt += "{extra_info}\n" prompt += "{prompt_personality}\n" prompt += "{last_loop_prompt}\n" - prompt += "-----------------------------------\n" prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" - prompt += "现在请你,阅读群里正在进行的聊天内容,思考群里的正在进行的话题,分析群里成员与你的关系。" - prompt += "请你思考,生成你的内心想法,包括你的思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" - prompt += "回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题\n" - prompt += "如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。" - prompt += "如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。" - prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言\n" - prompt += "现在请你先输出想法,{hf_do_next},不要分点输出,文字不要浮夸" - prompt += "在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。" - prompt += "如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" + prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,思考你要不要回复。" + prompt += "思考并输出你的内心想法\n" + prompt += "输出要求:\n" + prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n" + prompt += "2. 不要分点、不要使用表情符号\n" + prompt += "3. 避免多余符号(冒号、引号、括号等)\n" + prompt += "4. 语言简洁自然,不要浮夸\n" + prompt += "5. 如果你刚发言,并且没有人回复你,不要回复\n" + prompt += "工具使用说明:\n" + prompt += "1. 输出想法后考虑是否需要使用工具\n" + prompt += "2. 工具可获取信息或执行操作\n" + prompt += "3. 如需处理消息或回复,请使用工具\n" Prompt(prompt, "sub_heartflow_prompt_before") @@ -65,7 +67,7 @@ class SubMind: self.past_mind = [] self.structured_info = {} - async def do_thinking_before_reply(self, last_cycle: CycleInfo): + async def do_thinking_before_reply(self, last_cycle: CycleInfo = None): """ 在回复前进行思考,生成内心想法并收集工具调用结果 @@ -123,14 +125,14 @@ class SubMind: # 思考指导选项和权重 hf_options = [ - ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,但是不要纠结于同一个话题", 0.6), - ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1), - ("生成你在这个聊天中的想法,不要太深入", 0.2), - ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), + ("可以参考之前的想法,在原来想法的基础上继续思考", 0.2), + ("可以参考之前的想法,在原来的想法上尝试新的话题", 0.4), + ("不要太深入", 0.2), + ("进行深入思考", 0.2), ] #上一次决策信息 - if last_cycle.action_type: + if last_cycle != None: last_action = last_cycle.action_type last_reasoning = last_cycle.reasoning is_replan = last_cycle.replanned @@ -143,11 +145,13 @@ class SubMind: last_reasoning = "" is_replan = False if_replan_prompt = "" - - last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( - current_thinking_info=current_thinking_info, - if_replan_prompt=if_replan_prompt - ) + if current_thinking_info: + last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( + current_thinking_info=current_thinking_info, + if_replan_prompt=if_replan_prompt + ) + else: + last_loop_prompt = "" # 加权随机选择思考指导 hf_do_next = local_random.choices( diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py index d0ab56042..b3166f300 100644 --- a/src/plugins/chat/message_buffer.py +++ b/src/plugins/chat/message_buffer.py @@ -128,58 +128,55 @@ class MessageBuffer: if result: async with self.lock: # 再次加锁 # 清理所有早于当前消息的已处理消息, 收集所有早于当前消息的F消息的processed_plain_text - keep_msgs = OrderedDict() - combined_text = [] - found = False - type = "seglist" - is_update = True - for msg_id, msg in self.buffer_pool[person_id_].items(): + keep_msgs = OrderedDict() # 用于存放 T 消息之后的消息 + collected_texts = [] # 用于收集 T 消息及之前 F 消息的文本 + process_target_found = False + + # 遍历当前用户的所有缓冲消息 + for msg_id, cache_msg in self.buffer_pool[person_id_].items(): + # 如果找到了目标处理消息 (T 状态) if msg_id == message.message_info.message_id: - found = True - if msg.message.message_segment.type != "seglist": - type = msg.message.message_segment.type - else: - if ( - isinstance(msg.message.message_segment.data, list) - and all(isinstance(x, Seg) for x in msg.message.message_segment.data) - and len(msg.message.message_segment.data) == 1 - ): - type = msg.message.message_segment.data[0].type - combined_text.append(msg.message.processed_plain_text) - continue - if found: - keep_msgs[msg_id] = msg - elif msg.result == "F": - # 收集F消息的文本内容 - f_type = "seglist" - if msg.message.message_segment.type != "seglist": - f_type = msg.message.message_segment.type - else: - if ( - isinstance(msg.message.message_segment.data, list) - and all(isinstance(x, Seg) for x in msg.message.message_segment.data) - and len(msg.message.message_segment.data) == 1 - ): - f_type = msg.message.message_segment.data[0].type - if hasattr(msg.message, "processed_plain_text") and msg.message.processed_plain_text: - if f_type == "text": - combined_text.append(msg.message.processed_plain_text) - elif f_type != "text": - is_update = False - elif msg.result == "U": - logger.debug(f"异常未处理信息id: {msg.message.message_info.message_id}") + process_target_found = True + # 收集这条 T 消息的文本 (如果有) + if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + collected_texts.append(cache_msg.message.processed_plain_text) + # 不立即放入 keep_msgs,因为它之前的 F 消息也处理完了 - # 更新当前消息的processed_plain_text - if combined_text and combined_text[0] != message.processed_plain_text and is_update: - if type == "text": - message.processed_plain_text = ",".join(combined_text) - logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容到当前消息") - elif type == "emoji": - combined_text.pop() - message.processed_plain_text = ",".join(combined_text) - message.is_emoji = False - logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容,覆盖当前emoji消息") + # 如果已经找到了目标 T 消息,之后的消息需要保留 + elif process_target_found: + keep_msgs[msg_id] = cache_msg + # 如果还没找到目标 T 消息,说明是之前的消息 (F 或 U) + else: + if cache_msg.result == "F": + # 收集这条 F 消息的文本 (如果有) + if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + collected_texts.append(cache_msg.message.processed_plain_text) + elif cache_msg.result == "U": + # 理论上不应该在 T 消息之前还有 U 消息,记录日志 + logger.warning(f"异常状态:在目标 T 消息 {message.message_info.message_id} 之前发现未处理的 U 消息 {cache_msg.message.message_info.message_id}") + # 也可以选择收集其文本 + if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + collected_texts.append(cache_msg.message.processed_plain_text) + + + # 更新当前消息 (message) 的 processed_plain_text + # 只有在收集到的文本多于一条,或者只有一条但与原始文本不同时才合并 + if collected_texts: + # 使用 OrderedDict 去重,同时保留原始顺序 + unique_texts = list(OrderedDict.fromkeys(collected_texts)) + merged_text = ",".join(unique_texts) + + # 只有在合并后的文本与原始文本不同时才更新 + # 并且确保不是空合并 + if merged_text and merged_text != message.processed_plain_text: + message.processed_plain_text = merged_text + # 如果合并了文本,原消息不再视为纯 emoji + if hasattr(message, 'is_emoji'): + message.is_emoji = False + logger.debug(f"合并了 {len(unique_texts)} 条消息的文本内容到当前消息 {message.message_info.message_id}") + + # 更新缓冲池,只保留 T 消息之后的消息 self.buffer_pool[person_id_] = keep_msgs return result except asyncio.TimeoutError: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index c11674fe2..0135cfb75 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -405,7 +405,7 @@ class HeartFChatting: return False, "" # execute:执行 - with Timer("执行", cycle_timers): + with Timer("执行动作", cycle_timers): return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) except PlannerError as e: @@ -490,7 +490,7 @@ class HeartFChatting: try: # 生成回复 - with Timer("Replier", cycle_timers): + with Timer("生成回复", cycle_timers): reply = await self._replier_work( anchor_message=anchor_message, thinking_id=thinking_id, @@ -501,13 +501,13 @@ class HeartFChatting: raise ReplierError("回复生成失败") # 发送消息 - with Timer("Sender", cycle_timers): - await self._sender( - thinking_id=thinking_id, - anchor_message=anchor_message, - response_set=reply, - send_emoji=emoji_query, - ) + + await self._sender( + thinking_id=thinking_id, + anchor_message=anchor_message, + response_set=reply, + send_emoji=emoji_query, + ) return True, thinking_id @@ -675,7 +675,7 @@ class HeartFChatting: # 获取观察信息 observation = self.observations[0] if is_re_planned: - observation.observe() + await observation.observe() observed_messages = observation.talking_message observed_messages_str = observation.talking_message_str @@ -687,40 +687,40 @@ class HeartFChatting: try: # 构建提示词 - with Timer("构建提示词", cycle_timers): - if is_re_planned: - replan_prompt = await self._build_replan_prompt( - self._current_cycle.action, self._current_cycle.reasoning - ) - prompt = replan_prompt - else: - replan_prompt = "" - prompt = await self._build_planner_prompt( - observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt + + if is_re_planned: + replan_prompt = await self._build_replan_prompt( + self._current_cycle.action_type, self._current_cycle.reasoning ) - payload = { - "model": global_config.llm_plan["name"], - "messages": [{"role": "user", "content": prompt}], - "tools": self.action_manager.get_planner_tool_definition(), - "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, - } + prompt = replan_prompt + else: + replan_prompt = "" + prompt = await self._build_planner_prompt( + observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt + ) + payload = { + "model": global_config.llm_plan["name"], + "messages": [{"role": "user", "content": prompt}], + "tools": self.action_manager.get_planner_tool_definition(), + "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, + } # 执行LLM请求 - with Timer("LLM回复", cycle_timers): - try: - response = await self.planner_llm._execute_request( - endpoint="/chat/completions", payload=payload, prompt=prompt - ) - except Exception as req_e: - logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") - return { - "action": "error", - "reasoning": f"LLM请求执行失败: {req_e}", - "emoji_query": "", - "current_mind": current_mind, - "observed_messages": observed_messages, - "llm_error": True, - } + + try: + response = await self.planner_llm._execute_request( + endpoint="/chat/completions", payload=payload, prompt=prompt + ) + except Exception as req_e: + logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") + return { + "action": "error", + "reasoning": f"LLM请求执行失败: {req_e}", + "emoji_query": "", + "current_mind": current_mind, + "observed_messages": observed_messages, + "llm_error": True, + } # 处理LLM响应 with Timer("使用工具", cycle_timers): @@ -883,7 +883,7 @@ class HeartFChatting: # 准备聊天内容块 chat_content_block = "" if observed_messages_str: - chat_content_block = "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n" + chat_content_block = "观察到的最新聊天内容如下:\n---\n" chat_content_block += observed_messages_str chat_content_block += "\n---" else: @@ -892,9 +892,9 @@ class HeartFChatting: # 准备当前思维块 current_mind_block = "" if current_mind: - current_mind_block = f"\n---\n{current_mind}\n---\n\n" + current_mind_block = f"{current_mind}" else: - current_mind_block = " [没有特别的想法] \n\n" + current_mind_block = "[没有特别的想法]" # 获取提示词模板并填充数据 prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index ec12e2adf..9dd6e4d3a 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -44,28 +44,49 @@ def init_prompt(): 以上的消息是你获取到的消息,或许可以帮助你更好地回复。 """, "info_from_tools") - # Planner提示词 + # Planner提示词 - 优化版 Prompt( - """你的名字是 {bot_name}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。 + """你的名字是{bot_name},一名大二女生,正在QQ群聊中。需要基于以下信息决定如何参与对话: {structured_info_block} {chat_content_block} -看了以上内容,你产生的内心想法是: +你的内心想法: {current_mind_block} {replan} -请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。 -注意你必须参考以下决策依据来选择工具: -1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。 -2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (每个标签用一个词组表示,格式例如:幽默的讽刺,单纯的开心,愤怒的抗议)。 -3. 如果聊天内容或你的内心想法适合用一个表情来回应,选择 'emoji_reply' 并提供表情主题 'emoji_query'。 -4. 如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。 -5. 如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;。 -6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。 -7. 不要回复你自己的话,不要把自己的话当做别人说的。 -必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。""", + +请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意: + +【回复原则】 +1. 不回复(no_reply)适用: +- 话题无关/无聊/不感兴趣 +- 最后一条消息是你自己发的且无人回应你 +- 讨论你不懂的专业话题 +- 讨论你不想参与的话题 +- 你发送了太多消息 + +2. 文字回复(text_reply)适用: +- 有实质性内容需要表达 +- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃") +- 不要追加太多表情 + +3. 纯表情回复(emoji_reply)适用: +- 适合用表情回应的场景 +- 需提供明确的emoji_query + +4. 自我对话处理: +- 如果是自己发的消息想继续,需自然衔接 +- 避免重复或评价自己的发言 +- 不要和自己聊天 + +【必须遵守】 +- 必须调用工具并包含action和reasoning +- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) +- 选择text_reply或emoji_reply时必须提供emoji_query +- 保持回复自然,符合日常聊天习惯""", "planner_prompt", ) - Prompt("你原本打算{action},因为:{reasoning},但是你看到了新的消息,你决定重新决定行动。", "replan_prompt") + Prompt('''你原本打算{action},因为:{reasoning} +但是你看到了新的消息,你决定重新决定行动。''', "replan_prompt") Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index e4f4004e8..f5ec6d8f3 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -53,7 +53,7 @@ person_info_default = { # "impression" : None, # "gender" : Unkown, "konw_time": 0, - "msg_interval": 3000, + "msg_interval": 2000, "msg_interval_list": [], } # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项 @@ -384,18 +384,21 @@ class PersonInfoManager: if delta > 0: time_interval.append(delta) - time_interval = [t for t in time_interval if 500 <= t <= 8000] - if len(time_interval) >= 30: + time_interval = [t for t in time_interval if 200 <= t <= 8000] + # --- 修改后的逻辑 --- + # 数据量检查 (至少需要 30 条有效间隔,并且足够进行头尾截断) + if len(time_interval) >= 30 + 10: # 至少30条有效+头尾各5条 time_interval.sort() - # 画图(log) + # 画图(log) - 这部分保留 msg_interval_map = True log_dir = Path("logs/person_info") log_dir.mkdir(parents=True, exist_ok=True) plt.figure(figsize=(10, 6)) - time_series = pd.Series(time_interval) - plt.hist(time_series, bins=50, density=True, alpha=0.4, color="pink", label="Histogram") - time_series.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density") + # 使用截断前的数据画图,更能反映原始分布 + time_series_original = pd.Series(time_interval) + plt.hist(time_series_original, bins=50, density=True, alpha=0.4, color="pink", label="Histogram (Original Filtered)") + time_series_original.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density (Original Filtered)") plt.grid(True, alpha=0.2) plt.xlim(0, 8000) plt.title(f"Message Interval Distribution (User: {person_id[:8]}...)") @@ -405,15 +408,22 @@ class PersonInfoManager: img_path = log_dir / f"interval_distribution_{person_id[:8]}.png" plt.savefig(img_path) plt.close() - # 画图 + # 画图结束 - q25, q75 = np.percentile(time_interval, [25, 75]) - iqr = q75 - q25 - filtered = [x for x in time_interval if (q25 - 1.5 * iqr) <= x <= (q75 + 1.5 * iqr)] + # 去掉头尾各 5 个数据点 + trimmed_interval = time_interval[5:-5] - msg_interval = int(round(np.percentile(filtered, 80))) - await self.update_one_field(person_id, "msg_interval", msg_interval) - logger.trace(f"用户{person_id}的msg_interval已经被更新为{msg_interval}") + # 计算截断后数据的 37% 分位数 + if trimmed_interval: # 确保截断后列表不为空 + msg_interval = int(round(np.percentile(trimmed_interval, 37))) + # 更新数据库 + await self.update_one_field(person_id, "msg_interval", msg_interval) + logger.trace(f"用户{person_id}的msg_interval通过头尾截断和37分位数更新为{msg_interval}") + else: + logger.trace(f"用户{person_id}截断后数据为空,无法计算msg_interval") + else: + logger.trace(f"用户{person_id}有效消息间隔数量 ({len(time_interval)}) 不足进行推断 (需要至少 {30+10} 条)") + # --- 修改结束 --- except Exception as e: logger.trace(f"用户{person_id}消息间隔计算失败: {type(e).__name__}: {str(e)}") continue diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index edd60c05a..8ba49d9cd 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -168,7 +168,10 @@ async def _build_readable_messages_internal( user_info = msg.get("user_info", {}) platform = user_info.get("platform") user_id = user_info.get("user_id") - user_nickname = user_info.get("nickname") + + user_nickname = user_info.get("user_nickname") + user_cardname = user_info.get("user_cardname") + timestamp = msg.get("time") content = msg.get("processed_plain_text", "") # 默认空字符串 @@ -186,7 +189,12 @@ async def _build_readable_messages_internal( # 如果 person_name 未设置,则使用消息中的 nickname 或默认名称 if not person_name: - person_name = user_nickname + if user_cardname: + person_name = f"昵称:{user_cardname}" + elif user_nickname: + person_name = f"{user_nickname}" + else: + person_name = "某人" message_details.append((timestamp, person_name, content)) @@ -304,7 +312,7 @@ async def build_readable_messages( readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode) read_mark_line = ( - f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" + f"\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" ) # 组合结果,确保空部分不引入多余的标记或换行