diff --git a/src/plugins/group_nickname/nickname_utils.py b/src/plugins/group_nickname/nickname_utils.py index 230ce8c4..2cff5a01 100644 --- a/src/plugins/group_nickname/nickname_utils.py +++ b/src/plugins/group_nickname/nickname_utils.py @@ -1,8 +1,14 @@ -# GroupNickname/nickname_utils.py import random -from typing import List, Dict, Tuple +import time +from typing import List, Dict, Tuple, Optional from src.common.logger_manager import get_logger from src.config.config import global_config +from src.plugins.person_info.relationship_manager import relationship_manager +from src.plugins.chat.chat_stream import ChatStream +from src.plugins.chat.message import MessageRecv +from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat +from .nickname_processor import add_to_nickname_queue + logger = get_logger("nickname_utils") @@ -10,14 +16,6 @@ logger = get_logger("nickname_utils") def select_nicknames_for_prompt(all_nicknames_info: Dict[str, List[Dict[str, int]]]) -> List[Tuple[str, str, int]]: """ 从给定的绰号信息中,根据映射次数加权随机选择最多 N 个绰号。 - - Args: - all_nicknames_info: 包含用户及其绰号信息的字典,格式为 - { "用户名1": [{"绰号A": 次数}, {"绰号B": 次数}], ... } - - Returns: - List[Tuple[str, str, int]]: 选中的绰号列表,每个元素为 (用户名, 绰号, 次数)。 - 按次数降序排序。 """ if not all_nicknames_info: return [] @@ -26,12 +24,9 @@ def select_nicknames_for_prompt(all_nicknames_info: Dict[str, List[Dict[str, int for user_name, nicknames in all_nicknames_info.items(): if nicknames: for nickname_entry in nicknames: - # nickname_entry 应该是 {"绰号": 次数} 格式 if isinstance(nickname_entry, dict) and len(nickname_entry) == 1: nickname, count = list(nickname_entry.items())[0] - # 确保次数是正整数 if isinstance(count, int) and count > 0: - # 添加平滑因子,避免概率为0,并让低频词也有机会 weight = count + global_config.NICKNAME_PROBABILITY_SMOOTHING candidates.append((user_name, nickname, count, weight)) else: @@ -44,55 +39,39 @@ def select_nicknames_for_prompt(all_nicknames_info: Dict[str, List[Dict[str, int if not candidates: return [] - # 计算总权重 total_weight = sum(c[3] for c in candidates) if total_weight <= 0: - # 如果所有权重都无效或为0,则随机选择(或按次数选择) - candidates.sort(key=lambda x: x[2], reverse=True) # 按原始次数排序 + candidates.sort(key=lambda x: x[2], reverse=True) selected = candidates[: global_config.MAX_NICKNAMES_IN_PROMPT] else: - # 计算归一化概率 probabilities = [c[3] / total_weight for c in candidates] - - # 使用概率分布进行加权随机选择(不重复) num_to_select = min(global_config.MAX_NICKNAMES_IN_PROMPT, len(candidates)) try: - # random.choices 允许重复,我们需要不重复的选择 - # 可以使用 numpy.random.choice 或手动实现不重复加权抽样 - # 这里用一个简化的方法:多次 choices 然后去重,直到达到数量或无法再选 selected_indices = set() selected = [] attempts = 0 - max_attempts = num_to_select * 5 # 防止无限循环 - + max_attempts = num_to_select * 5 while len(selected) < num_to_select and attempts < max_attempts: - # 每次只选一个,避免一次选多个时概率分布变化导致的问题 chosen_index = random.choices(range(len(candidates)), weights=probabilities, k=1)[0] if chosen_index not in selected_indices: selected_indices.add(chosen_index) selected.append(candidates[chosen_index]) attempts += 1 - - # 如果尝试多次后仍未选够,补充出现次数最多的 if len(selected) < num_to_select: remaining_candidates = [c for i, c in enumerate(candidates) if i not in selected_indices] - remaining_candidates.sort(key=lambda x: x[2], reverse=True) # 按原始次数排序 + remaining_candidates.sort(key=lambda x: x[2], reverse=True) needed = num_to_select - len(selected) selected.extend(remaining_candidates[:needed]) - except Exception as e: logger.error( f"Error during weighted random choice for nicknames: {e}. Falling back to top N.", exc_info=True ) - # 出错时回退到选择次数最多的 N 个 candidates.sort(key=lambda x: x[2], reverse=True) selected = candidates[: global_config.MAX_NICKNAMES_IN_PROMPT] - # 格式化输出并按次数排序 result = [(user, nick, count) for user, nick, count, _weight in selected] - result.sort(key=lambda x: x[2], reverse=True) # 按次数降序 - + result.sort(key=lambda x: x[2], reverse=True) logger.debug(f"Selected nicknames for prompt: {result}") return result @@ -100,27 +79,154 @@ def select_nicknames_for_prompt(all_nicknames_info: Dict[str, List[Dict[str, int def format_nickname_prompt_injection(selected_nicknames: List[Tuple[str, str, int]]) -> str: """ 将选中的绰号信息格式化为注入 Prompt 的字符串。 - - Args: - selected_nicknames: 选中的绰号列表 (用户名, 绰号, 次数)。 - - Returns: - str: 格式化后的字符串,如果列表为空则返回空字符串。 + (代码保持不变) """ if not selected_nicknames: return "" - prompt_lines = ["以下是聊天记录中一些成员在本群的绰号信息(按常用度排序):"] + prompt_lines = ["【群成员绰号信息】"] grouped_by_user: Dict[str, List[str]] = {} for user_name, nickname, _count in selected_nicknames: if user_name not in grouped_by_user: grouped_by_user[user_name] = [] - # 添加引号以区分绰号 grouped_by_user[user_name].append(f"“{nickname}”") for user_name, nicknames in grouped_by_user.items(): nicknames_str = "、".join(nicknames) - prompt_lines.append(f"{user_name},在本群有时被称为:{nicknames_str}") + prompt_lines.append(f"- {user_name},有时被称为:{nicknames_str}") - return "\n".join(prompt_lines) + "\n" # 末尾加换行符 + if len(prompt_lines) > 1: + return "\n".join(prompt_lines) + "\n" + else: + return "" + + +async def get_nickname_injection_for_prompt(chat_stream: ChatStream, message_list_before_now: List[Dict]) -> str: + """ + 获取并格式化用于 Prompt 注入的绰号信息字符串。 + """ + nickname_injection_str = "" + if global_config.ENABLE_NICKNAME_MAPPING and chat_stream and chat_stream.group_info: + try: + group_id = str(chat_stream.group_info.group_id) + user_ids_in_context = set() + if message_list_before_now: + for msg in message_list_before_now: + sender_id = msg["user_info"].get("user_id") + if sender_id: + user_ids_in_context.add(str(sender_id)) + else: + recent_speakers = chat_stream.get_recent_speakers(limit=5) + for speaker in recent_speakers: + user_ids_in_context.add(str(speaker['user_id'])) + if not user_ids_in_context: + logger.warning(f"[{chat_stream.stream_id}] No messages or recent speakers found for nickname injection.") + + if user_ids_in_context: + platform = chat_stream.platform + all_nicknames_data = await relationship_manager.get_users_group_nicknames( + platform, list(user_ids_in_context), group_id + ) + if all_nicknames_data: + selected_nicknames = select_nicknames_for_prompt(all_nicknames_data) + nickname_injection_str = format_nickname_prompt_injection(selected_nicknames) + if nickname_injection_str: + logger.debug(f"[{chat_stream.stream_id}] Generated nickname info for prompt:\n{nickname_injection_str}") + except Exception as e: + logger.error(f"[{chat_stream.stream_id}] Error getting or formatting nickname info for prompt: {e}", exc_info=True) + nickname_injection_str = "" + return nickname_injection_str + + +# --- 新增:触发绰号分析的工具函数 --- +async def trigger_nickname_analysis_if_needed( + anchor_message: MessageRecv, + bot_reply: List[str], + chat_stream: Optional[ChatStream] = None # 允许传入 chat_stream 或从 anchor_message 获取 +): + """ + 如果满足条件(群聊、功能开启),则准备数据并触发绰号分析任务。 + + Args: + anchor_message: 触发回复的原始消息对象。 + bot_reply: Bot 生成的回复内容列表。 + chat_stream: 可选的 ChatStream 对象。 + """ + # 检查功能是否开启 + if not global_config.ENABLE_NICKNAME_MAPPING: + return + + # 确定使用的 chat_stream + current_chat_stream = chat_stream or anchor_message.chat_stream + + # 检查是否是群聊且 chat_stream 有效 + if not current_chat_stream or not current_chat_stream.group_info: + logger.debug(f"[{current_chat_stream.stream_id if current_chat_stream else 'Unknown'}] Skipping nickname analysis: Not a group chat or invalid chat stream.") + return + + log_prefix = f"[{current_chat_stream.stream_id}]" # 日志前缀 + + try: + # 1. 获取历史记录 + history_limit = 30 # 可配置的历史记录条数 + history_messages = get_raw_msg_before_timestamp_with_chat( + chat_id=current_chat_stream.stream_id, + timestamp=time.time(), + limit=history_limit, + ) + + # 格式化历史记录 + chat_history_str = await build_readable_messages( + messages=history_messages, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + truncate=False, + ) + + # 2. 获取 Bot 回复字符串 + bot_reply_str = " ".join(bot_reply) if bot_reply else "" # 处理空回复列表 + + # 3. 获取群号和平台 + group_id = str(current_chat_stream.group_info.group_id) + platform = current_chat_stream.platform + + # 4. 构建用户 ID 到名称的映射 + user_ids_in_history = set() + for msg in history_messages: + sender_id = msg["user_info"].get("user_id") + if sender_id: + user_ids_in_history.add(str(sender_id)) + + user_name_map = {} + if user_ids_in_history: + try: + # 批量获取 person_name + names_data = await relationship_manager.get_person_names_batch(platform, list(user_ids_in_history)) + except Exception as e: + logger.error(f"{log_prefix} Error getting person names batch: {e}", exc_info=True) + names_data = {} + + for user_id in user_ids_in_history: + if user_id in names_data: + user_name_map[user_id] = names_data[user_id] + else: + # 回退查找 nickname (从后往前找最新的) + latest_nickname = next( + ( + m["user_info"].get("user_nickname") # 从 user_info 获取 + for m in reversed(history_messages) + if str(m["user_info"].get("user_id")) == user_id and m["user_info"].get("user_nickname") # 确保 nickname 存在 + ), + None, + ) + user_name_map[user_id] = latest_nickname or f"未知({user_id})" # 提供回退 + + # 5. 添加到处理队列 + await add_to_nickname_queue(chat_history_str, bot_reply_str, platform, group_id, user_name_map) + logger.debug(f"{log_prefix} Triggered nickname analysis for group {group_id}.") + + except Exception as e: + logger.error(f"{log_prefix} Error triggering nickname analysis: {e}", exc_info=True) \ No newline at end of file diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 3d536edf..fa180673 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1,38 +1,33 @@ import asyncio import time import traceback -import random # <--- 添加导入 -import json # <--- 确保导入 json +import random +import json from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending -from src.plugins.chat.message import Seg # Local import needed after move +from src.plugins.chat.message import Seg from src.plugins.chat.chat_stream import ChatStream from src.plugins.chat.message import UserInfo from src.plugins.chat.chat_stream import chat_manager from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config -from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move -from src.plugins.utils.timer_calculator import Timer # <--- Import Timer +from src.plugins.chat.utils_image import image_path_to_base64 +from src.plugins.utils.timer_calculator import Timer from src.plugins.emoji_system.emoji_manager import emoji_manager from src.heart_flow.sub_mind import SubMind from src.heart_flow.observation import Observation from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder import contextlib -from src.plugins.utils.chat_message_builder import ( - num_new_messages_since, - get_raw_msg_before_timestamp_with_chat, - build_readable_messages, -) +from src.plugins.utils.chat_message_builder import num_new_messages_since from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo from .heartFC_sender import HeartFCSender from src.plugins.chat.utils import process_llm_response from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager from src.individuality.individuality import Individuality -from src.plugins.person_info.relationship_manager import relationship_manager -from src.plugins.group_nickname.nickname_processor import add_to_nickname_queue # <--- 导入队列添加函数 +from src.plugins.group_nickname.nickname_utils import trigger_nickname_analysis_if_needed WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 @@ -586,10 +581,9 @@ class HeartFChatting: send_emoji=emoji_query, ) print("消息发送成功,准备进入绰号分析") - # --- [新增] 触发绰号分析 --- - # 在发送成功后(或至少尝试发送后)触发 - await self._trigger_nickname_analysis(anchor_message, reply) - # --- 结束触发 --- + + # 调用工具函数触发绰号分析 + await trigger_nickname_analysis_if_needed(anchor_message, reply, self.chat_stream) return True, thinking_id @@ -697,90 +691,6 @@ class HeartFChatting: # 发生意外错误时,可以选择是否重置计数器,这里选择不重置 return False # 表示动作未成功 - # 触发绰号分析的函数 - async def _trigger_nickname_analysis(self, anchor_message: MessageRecv, reply: List[str]): - """ - 触发绰号分析任务,将相关数据放入处理队列。 - - Args: - anchor_message: 锚点消息对象。 - reply: Bot 生成的回复内容列表。 - """ - if not global_config.ENABLE_NICKNAME_MAPPING: - return # 如果功能未开启,则直接返回 - - if not anchor_message or not anchor_message.chat_stream or not anchor_message.chat_stream.group_info: - logger.debug(f"{self.log_prefix} Skipping nickname analysis: Not a group chat or invalid anchor.") - return # 仅在群聊中进行分析 - - try: - # 1. 获取原始消息列表 - history_limit = 30 # 例如,获取最近 30 条消息 - history_messages = get_raw_msg_before_timestamp_with_chat( - chat_id=anchor_message.chat_stream.stream_id, - timestamp=time.time(), # 获取当前时间点的历史 - limit=history_limit, - ) - - # 格式化历史记录 - chat_history_str = await build_readable_messages( - messages=history_messages, - replace_bot_name=True, # 在分析时也替换机器人名字,使其与 LLM 交互一致 - merge_messages=False, # 不合并,保留原始对话流 - timestamp_mode="relative", # 使用相对时间戳 - read_mark=0.0, # 不需要已读标记 - truncate=False, # 获取完整内容进行分析 - ) - - # 2. 获取 Bot 回复字符串 - bot_reply_str = " ".join(reply) - - # 3. 获取群号 - group_id = str(anchor_message.chat_stream.group_info.group_id) # 确保是字符串 - - # 4. 获取当前上下文中涉及的用户 ID 及其已知名称 - user_ids_in_history = set() - for msg in history_messages: - sender_id = msg["user_info"].get("user_id") - if sender_id: - user_ids_in_history.add(str(sender_id)) # 确保是字符串 - - user_name_map = {} - if user_ids_in_history: - platform = anchor_message.chat_stream.platform - try: - names_data = await relationship_manager.get_person_names_batch(platform, list(user_ids_in_history)) - - except Exception as e: - logger.error(f"Error getting person names: {e}", exc_info=True) - names_data = {} # 出错时置空 - print(f"\n\nnames_data:\n{names_data}\n\n") - - for user_id in user_ids_in_history: - if user_id in names_data: - user_name_map[user_id] = names_data[user_id] - else: - # 回退查找 nickname - latest_nickname = next( - ( - m.get("sender_nickname") - for m in reversed(history_messages) - if str(m.get("sender_id")) == user_id - ), - None, - ) - if latest_nickname: - user_name_map[user_id] = latest_nickname - else: - user_name_map[user_id] = f"未知({user_id})" - - # 5. 添加到队列 - await add_to_nickname_queue(chat_history_str, bot_reply_str, platform, group_id, user_name_map) - logger.debug(f"{self.log_prefix} Triggered nickname analysis for group {group_id}.") - - except Exception as e: - logger.error(f"{self.log_prefix} Error triggering nickname analysis: {e}", exc_info=True) - async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool: """ 等待新消息 或 检测到关闭信号 diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 3980b044..43075ef8 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -1,4 +1,6 @@ import random +import time +from typing import Union, Optional, List, Dict, Any # 引入 List, Dict, Any from ...config.config import global_config from src.common.logger_manager import get_logger from ...individuality.individuality import Individuality @@ -6,15 +8,13 @@ from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.chat.utils import get_embedding -import time -from typing import Union, Optional from ...common.database import db from ..chat.utils import get_recent_group_speaker from ..moods.moods import MoodManager from ..memory_system.Hippocampus import HippocampusManager from ..schedule.schedule_generator import bot_schedule from ..knowledge.knowledge_lib import qa_manager -from src.plugins.group_nickname.nickname_utils import select_nicknames_for_prompt, format_nickname_prompt_injection +from src.plugins.group_nickname.nickname_utils import get_nickname_injection_for_prompt logger = get_logger("prompt") @@ -23,6 +23,7 @@ def init_prompt(): Prompt( """ {info_from_tools} +{nickname_info} {chat_target} {chat_talking_prompt} 现在你想要在群里发言或者回复。\n @@ -131,6 +132,7 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": {relation_prompt} {prompt_info} {schedule_prompt} +{nickname_info} {chat_target} {chat_talking_prompt} 现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言或者回复这条消息。\n @@ -214,40 +216,13 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s logger.debug("开始构建prompt") - # 注入绰号信息 - nickname_injection_str = "" - if global_config.ENABLE_NICKNAME_MAPPING and chat_stream.group_info: - try: - group_id = str(chat_stream.group_info.group_id) - user_ids_in_context = set() - if message_list_before_now: - for msg in message_list_before_now: - sender_id = msg["user_info"].get("user_id") - if sender_id: - user_ids_in_context.add(str(sender_id)) - else: - logger.warning("Variable 'message_list_before_now' not found for nickname injection in focus prompt.") - - if user_ids_in_context: - platform = chat_stream.platform - # --- 调用批量获取群组绰号的方法 --- - all_nicknames_data = await relationship_manager.get_users_group_nicknames( - platform, list(user_ids_in_context), group_id - ) - - if all_nicknames_data: - selected_nicknames = select_nicknames_for_prompt(all_nicknames_data) - nickname_injection_str = format_nickname_prompt_injection(selected_nicknames) - if nickname_injection_str: - logger.debug(f"Injecting nickname info into focus prompt:\n{nickname_injection_str}") - - except Exception as e: - logger.error(f"Error getting or formatting nickname info for focus prompt: {e}", exc_info=True) - logger.debug(f"-------------------nickname_injection_str_______________________\n{nickname_injection_str}\n\n") + # 调用新的工具函数获取绰号信息 + nickname_injection_str = await get_nickname_injection_for_prompt(chat_stream, message_list_before_now) prompt = await global_prompt_manager.format_prompt( "heart_flow_prompt", info_from_tools=structured_info_prompt, + nickname_info=nickname_injection_str, chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1") if chat_in_group else await global_prompt_manager.get_prompt_async("chat_target_private1"), @@ -299,7 +274,7 @@ class PromptBuilder: ) return None - async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: + async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str: # 返回值改为 str individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=2) @@ -430,38 +405,8 @@ class PromptBuilder: else: schedule_prompt = "" - # 注入绰号信息 - nickname_injection_str = "" - if global_config.ENABLE_NICKNAME_MAPPING and chat_stream.group_info: - try: - group_id = str(chat_stream.group_info.group_id) - user_ids_in_context = set() - if message_list_before_now: - for msg in message_list_before_now: - sender_id = msg["user_info"].get("user_id") - if sender_id: - user_ids_in_context.add(str(sender_id)) - else: - logger.warning( - "Variable 'message_list_before_now' not found for nickname injection in focus prompt." - ) - - if user_ids_in_context: - platform = chat_stream.platform - # --- 调用批量获取群组绰号的方法 --- - all_nicknames_data = await relationship_manager.get_users_group_nicknames( - platform, list(user_ids_in_context), group_id - ) - - if all_nicknames_data: - selected_nicknames = select_nicknames_for_prompt(all_nicknames_data) - nickname_injection_str = format_nickname_prompt_injection(selected_nicknames) - if nickname_injection_str: - logger.debug(f"Injecting nickname info into focus prompt:\n{nickname_injection_str}") - - except Exception as e: - logger.error(f"Error getting or formatting nickname info for focus prompt: {e}", exc_info=True) - logger.debug(f"-------------------nickname_injection_str_______________________\n{nickname_injection_str}\n\n") + # 调用新的工具函数获取绰号信息 + nickname_injection_str = await get_nickname_injection_for_prompt(chat_stream, message_list_before_now) prompt = await global_prompt_manager.format_prompt( "reasoning_prompt_main", @@ -470,6 +415,7 @@ class PromptBuilder: memory_prompt=memory_prompt, prompt_info=prompt_info, schedule_prompt=schedule_prompt, + nickname_info=nickname_injection_str, # <--- 注入绰号信息 chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1") if chat_in_group else await global_prompt_manager.get_prompt_async("chat_target_private1"), diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 9ed63c2d..76fb1746 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -19,6 +19,7 @@ from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.utils.timer_calculator import Timer +from src.plugins.group_nickname.nickname_utils import trigger_nickname_analysis_if_needed logger = get_logger("chat") @@ -286,6 +287,7 @@ class NormalChat: # 检查 first_bot_msg 是否为 None (例如思考消息已被移除的情况) if first_bot_msg: info_catcher.catch_after_response(timing_results["消息发送"], response_set, first_bot_msg) + await trigger_nickname_analysis_if_needed(message, response_set, self.chat_stream) else: logger.warning(f"[{self.stream_name}] 思考消息 {thinking_id} 在发送前丢失,无法记录 info_catcher")