diff --git a/README.md b/README.md index 9b057508..403b42e0 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ > - QQ 机器人存在被限制风险,请自行了解,谨慎使用。 > - 由于程序处于开发中,可能消耗较多 token。 +## 麦麦MC项目(早期开发) +[让麦麦玩MC](https://github.com/MaiM-with-u/Maicraft) + +交流群:1058573197 + ## 💬 讨论 **技术交流群:** diff --git a/bot.py b/bot.py index 631abd61..ea5244f2 100644 --- a/bot.py +++ b/bot.py @@ -66,7 +66,7 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression from src.plugin_system.core.events_manager import events_manager from src.plugin_system.base.component_types import EventType # 触发 ON_STOP 事件 - _ = await events_manager.handle_mai_events(event_type=EventType.ON_STOP) + await events_manager.handle_mai_events(event_type=EventType.ON_STOP) # 停止所有异步任务 await async_task_manager.stop_and_wait_all_tasks() diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 99fcd682..baa45d45 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,10 +1,25 @@ # Changelog + +## [0.10.2] - 2025-8-31 +### 🌟 主要功能更改 +- 精简了人格相关配置,提供更清晰,有效的自定义 +- 大幅优化了聊天逻辑,更易配置,动态控制 +- 现在支持提及100%回复 + +### 细节功能更改 +- 更好的event系统 +- 记忆系统优化 +- 为空回复添加重试机制 +- 修复tts插件可能的复读问题 + + ## [0.10.1] - 2025-8-24 ### 🌟 主要功能更改 - planner现在改为大小核结构,移除激活阶段,提高回复速度和动作调用精准度 - 优化关系的表现的效率 +### 细节功能更改 - 优化识图的表现 - 为planner添加单独控制的提示词 - 修复激活值计算异常的BUG diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index f9855481..c4e6d72c 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -11,7 +11,7 @@ from src.plugin_system import ( BaseEventHandler, EventType, MaiMessages, - ToolParamType + ToolParamType, ) @@ -136,12 +136,12 @@ class PrintMessage(BaseEventHandler): handler_name = "print_message_handler" handler_description = "打印接收到的消息" - async def execute(self, message: MaiMessages) -> Tuple[bool, bool, str | None]: + async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, str | None, None]: """执行打印消息事件处理""" # 打印接收到的消息 if self.get_config("print_message.enabled", False): - print(f"接收到消息: {message.raw_message}") - return True, True, "消息已打印" + print(f"接收到消息: {message.raw_message if message else '无效消息'}") + return True, True, "消息已打印", None # ===== 插件注册 ===== diff --git a/src/chat/frequency_control/focus_value_control.py b/src/chat/frequency_control/focus_value_control.py index 290dcc9e..be820760 100644 --- a/src/chat/frequency_control/focus_value_control.py +++ b/src/chat/frequency_control/focus_value_control.py @@ -3,26 +3,7 @@ from src.config.config import global_config from src.chat.frequency_control.utils import parse_stream_config_to_chat_id -class FocusValueControl: - def __init__(self, chat_id: str): - self.chat_id = chat_id - self.focus_value_adjust: float = 1 - - def get_current_focus_value(self) -> float: - return get_current_focus_value(self.chat_id) * self.focus_value_adjust - - -class FocusValueControlManager: - def __init__(self): - self.focus_value_controls: dict[str, FocusValueControl] = {} - - def get_focus_value_control(self, chat_id: str) -> FocusValueControl: - if chat_id not in self.focus_value_controls: - self.focus_value_controls[chat_id] = FocusValueControl(chat_id) - return self.focus_value_controls[chat_id] - - -def get_current_focus_value(chat_id: Optional[str] = None) -> float: +def get_config_base_focus_value(chat_id: Optional[str] = None) -> float: """ 根据当前时间和聊天流获取对应的 focus_value """ @@ -139,5 +120,3 @@ def get_global_focus_value() -> Optional[float]: return None - -focus_value_control = FocusValueControlManager() diff --git a/src/chat/frequency_control/frequency_control.py b/src/chat/frequency_control/frequency_control.py new file mode 100644 index 00000000..33c7e712 --- /dev/null +++ b/src/chat/frequency_control/frequency_control.py @@ -0,0 +1,501 @@ +import time +from typing import Optional, Dict, List +from src.plugin_system.apis import message_api +from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager +from src.common.logger import get_logger +from src.config.config import global_config +from src.chat.frequency_control.talk_frequency_control import get_config_base_talk_frequency +from src.chat.frequency_control.focus_value_control import get_config_base_focus_value + +logger = get_logger("frequency_control") + + +class FrequencyControl: + """ + 频率控制类,可以根据最近时间段的发言数量和发言人数动态调整频率 + + 特点: + - 发言频率调整:基于最近10分钟的数据,评估单位为"消息数/10分钟" + - 专注度调整:基于最近10分钟的数据,评估单位为"消息数/10分钟" + - 历史基准值:基于最近一周的数据,按小时统计,每小时都有独立的基准值(需要至少50条历史消息) + - 统一标准:两个调整都使用10分钟窗口,确保逻辑一致性和响应速度 + - 双向调整:根据活跃度高低,既能提高也能降低频率和专注度 + - 数据充足性检查:当历史数据不足50条时,不更新基准值;当基准值为默认值时,不进行动态调整 + - 基准值更新:直接使用新计算的周均值,无平滑更新 + """ + + def __init__(self, chat_id: str): + self.chat_id = chat_id + self.chat_stream: ChatStream = get_chat_manager().get_stream(self.chat_id) + if not self.chat_stream: + raise ValueError(f"无法找到聊天流: {chat_id}") + self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]" + # 发言频率调整值 + self.talk_frequency_adjust: float = 1.0 + self.talk_frequency_external_adjust: float = 1.0 + # 专注度调整值 + self.focus_value_adjust: float = 1.0 + self.focus_value_external_adjust: float = 1.0 + + # 动态调整相关参数 + self.last_update_time = time.time() + self.update_interval = 60 # 每60秒更新一次 + + # 历史数据缓存 + self._message_count_cache = 0 + self._user_count_cache = 0 + self._last_cache_time = 0 + self._cache_duration = 30 # 缓存30秒 + + # 调整参数 + self.min_adjust = 0.3 # 最小调整值 + self.max_adjust = 2.0 # 最大调整值 + + # 动态基准值(将根据历史数据计算) + self.base_message_count = 5 # 默认基准消息数量,将被动态更新 + self.base_user_count = 3 # 默认基准用户数量,将被动态更新 + + # 平滑因子 + self.smoothing_factor = 0.3 + + # 历史数据相关参数 + self._last_historical_update = 0 + self._historical_update_interval = 600 # 每十分钟更新一次历史基准值 + self._historical_days = 7 # 使用最近7天的数据计算基准值 + + # 按小时统计的历史基准值 + self._hourly_baseline = { + 'messages': {}, # {0-23: 平均消息数} + 'users': {} # {0-23: 平均用户数} + } + + # 初始化24小时的默认基准值 + for hour in range(24): + self._hourly_baseline['messages'][hour] = 0.0 + self._hourly_baseline['users'][hour] = 0.0 + + def _update_historical_baseline(self): + """ + 更新基于历史数据的基准值 + 使用最近一周的数据,按小时统计平均消息数量和用户数量 + """ + current_time = time.time() + + # 检查是否需要更新历史基准值 + if current_time - self._last_historical_update < self._historical_update_interval: + return + + try: + # 计算一周前的时间戳 + week_ago = current_time - (self._historical_days * 24 * 3600) + + # 获取最近一周的消息数据 + historical_messages = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_stream.stream_id, + start_time=week_ago, + end_time=current_time, + filter_mai=True, + filter_command=True + ) + + if historical_messages and len(historical_messages) >= 50: + # 按小时统计消息数和用户数 + hourly_stats = {hour: {'messages': [], 'users': set()} for hour in range(24)} + + for msg in historical_messages: + # 获取消息的小时(UTC时间) + msg_time = time.localtime(msg.time) + msg_hour = msg_time.tm_hour + + # 统计消息数 + hourly_stats[msg_hour]['messages'].append(msg) + + # 统计用户数 + if msg.user_info and msg.user_info.user_id: + hourly_stats[msg_hour]['users'].add(msg.user_info.user_id) + + # 计算每个小时的平均值(基于一周的数据) + for hour in range(24): + # 计算该小时的平均消息数(一周内该小时的总消息数 / 7天) + total_messages = len(hourly_stats[hour]['messages']) + total_users = len(hourly_stats[hour]['users']) + + # 只计算有消息的时段,没有消息的时段设为0 + if total_messages > 0: + avg_messages = total_messages / self._historical_days + avg_users = total_users / self._historical_days + self._hourly_baseline['messages'][hour] = avg_messages + self._hourly_baseline['users'][hour] = avg_users + else: + # 没有消息的时段设为0,表示该时段不活跃 + self._hourly_baseline['messages'][hour] = 0.0 + self._hourly_baseline['users'][hour] = 0.0 + + # 更新整体基准值(用于兼容性)- 基于原始数据计算,不受max(1.0)限制影响 + overall_avg_messages = sum(len(hourly_stats[hour]['messages']) for hour in range(24)) / (24 * self._historical_days) + overall_avg_users = sum(len(hourly_stats[hour]['users']) for hour in range(24)) / (24 * self._historical_days) + + self.base_message_count = overall_avg_messages + self.base_user_count = overall_avg_users + + logger.info( + f"{self.log_prefix} 历史基准值更新完成: " + f"整体平均消息数={overall_avg_messages:.2f}, 整体平均用户数={overall_avg_users:.2f}" + ) + + # 记录几个关键时段的基准值 + key_hours = [8, 12, 18, 22] # 早、中、晚、夜 + for hour in key_hours: + # 计算该小时平均每10分钟的消息数和用户数 + hourly_10min_messages = self._hourly_baseline['messages'][hour] / 6 # 1小时 = 6个10分钟 + hourly_10min_users = self._hourly_baseline['users'][hour] / 6 + logger.info( + f"{self.log_prefix} {hour}时基准值: " + f"消息数={self._hourly_baseline['messages'][hour]:.2f}/小时 " + f"({hourly_10min_messages:.2f}/10分钟), " + f"用户数={self._hourly_baseline['users'][hour]:.2f}/小时 " + f"({hourly_10min_users:.2f}/10分钟)" + ) + + elif historical_messages and len(historical_messages) < 50: + # 历史数据不足50条,不更新基准值 + logger.info(f"{self.log_prefix} 历史数据不足50条({len(historical_messages)}条),不更新基准值") + else: + # 如果没有历史数据,不更新基准值 + logger.info(f"{self.log_prefix} 无历史数据,不更新基准值") + + except Exception as e: + logger.error(f"{self.log_prefix} 更新历史基准值时出错: {e}") + # 出错时保持原有基准值不变 + + self._last_historical_update = current_time + + def _get_current_hour_baseline(self) -> tuple[float, float]: + """ + 获取当前小时的基准值 + + Returns: + tuple: (基准消息数, 基准用户数) + """ + current_hour = time.localtime().tm_hour + return ( + self._hourly_baseline['messages'][current_hour], + self._hourly_baseline['users'][current_hour] + ) + + def get_dynamic_talk_frequency_adjust(self) -> float: + """ + 获取纯动态调整值(不包含配置文件基础值) + + Returns: + float: 动态调整值 + """ + self._update_talk_frequency_adjust() + return self.talk_frequency_adjust + + def get_dynamic_focus_value_adjust(self) -> float: + """ + 获取纯动态调整值(不包含配置文件基础值) + + Returns: + float: 动态调整值 + """ + self._update_focus_value_adjust() + return self.focus_value_adjust + + def _update_talk_frequency_adjust(self): + """ + 更新发言频率调整值 + 适合人少话多的时候:人少但消息多,提高回复频率 + """ + current_time = time.time() + + # 检查是否需要更新 + if current_time - self.last_update_time < self.update_interval: + return + + # 先更新历史基准值 + self._update_historical_baseline() + + try: + # 获取最近10分钟的数据(发言频率更敏感) + recent_messages = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_stream.stream_id, + start_time=current_time - 600, # 10分钟前 + end_time=current_time, + filter_mai=True, + filter_command=True + ) + + # 计算消息数量和用户数量 + message_count = len(recent_messages) + user_ids = set() + for msg in recent_messages: + if msg.user_info and msg.user_info.user_id: + user_ids.add(msg.user_info.user_id) + user_count = len(user_ids) + + # 获取当前小时的基准值 + current_hour_base_messages, current_hour_base_users = self._get_current_hour_baseline() + + # 计算当前小时平均每10分钟的基准值 + current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 + current_hour_10min_users = current_hour_base_users / 6 + + # 发言频率调整逻辑:根据活跃度双向调整 + # 检查是否有足够的数据进行分析 + if user_count > 0 and message_count >= 2: # 至少需要2条消息才能进行有意义的分析 + # 检查历史基准值是否有效(该时段有活跃度) + if current_hour_base_messages > 0.0 and current_hour_base_users > 0.0: + # 计算人均消息数(10分钟窗口) + messages_per_user = message_count / user_count + # 使用当前小时每10分钟的基准人均消息数 + base_messages_per_user = current_hour_10min_messages / current_hour_10min_users if current_hour_10min_users > 0 else 1.0 + + # 双向调整逻辑 + if messages_per_user > base_messages_per_user * 1.2: + # 活跃度很高:提高回复频率 + target_talk_adjust = min(self.max_adjust, messages_per_user / base_messages_per_user) + elif messages_per_user < base_messages_per_user * 0.8: + # 活跃度很低:降低回复频率 + target_talk_adjust = max(self.min_adjust, messages_per_user / base_messages_per_user) + else: + # 活跃度正常:保持正常 + target_talk_adjust = 1.0 + else: + # 历史基准值不足,不调整 + target_talk_adjust = 1.0 + else: + # 数据不足:不调整 + target_talk_adjust = 1.0 + + # 限制调整范围 + target_talk_adjust = max(self.min_adjust, min(self.max_adjust, target_talk_adjust)) + + # 记录调整前的值 + old_adjust = self.talk_frequency_adjust + + # 平滑调整 + self.talk_frequency_adjust = ( + self.talk_frequency_adjust * (1 - self.smoothing_factor) + + target_talk_adjust * self.smoothing_factor + ) + + # 判断调整方向 + if target_talk_adjust > 1.0: + adjust_direction = "提高" + elif target_talk_adjust < 1.0: + adjust_direction = "降低" + else: + if current_hour_base_messages <= 0.0 or current_hour_base_users <= 0.0: + adjust_direction = "不调整(该时段无活跃度)" + else: + adjust_direction = "保持" + + # 计算实际变化方向 + actual_change = "" + if self.talk_frequency_adjust > old_adjust: + actual_change = f"(实际提高: {old_adjust:.2f}→{self.talk_frequency_adjust:.2f})" + elif self.talk_frequency_adjust < old_adjust: + actual_change = f"(实际降低: {old_adjust:.2f}→{self.talk_frequency_adjust:.2f})" + else: + actual_change = f"(无变化: {self.talk_frequency_adjust:.2f})" + + logger.info( + f"{self.log_prefix} 发言频率调整: " + f"当前: {message_count}消息/{user_count}用户, 人均: {message_count/user_count if user_count > 0 else 0:.2f}消息|" + f"基准: {current_hour_10min_messages:.2f}消息/{current_hour_10min_users:.2f}用户,人均:{current_hour_10min_messages/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}消息|" + f"目标调整: {adjust_direction}到{target_talk_adjust:.2f}, 实际结果: {self.talk_frequency_adjust:.2f} {actual_change}" + ) + + except Exception as e: + logger.error(f"{self.log_prefix} 更新发言频率调整值时出错: {e}") + + def _update_focus_value_adjust(self): + """ + 更新专注度调整值 + 适合人多话多的时候:人多且消息多,提高专注度(LLM消耗更多,但回复更精准) + """ + current_time = time.time() + + # 检查是否需要更新 + if current_time - self.last_update_time < self.update_interval: + return + + try: + # 获取最近10分钟的数据(与发言频率保持一致) + recent_messages = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_stream.stream_id, + start_time=current_time - 600, # 10分钟前 + end_time=current_time, + filter_mai=True, + filter_command=True + ) + + # 计算消息数量和用户数量 + message_count = len(recent_messages) + user_ids = set() + for msg in recent_messages: + if msg.user_info and msg.user_info.user_id: + user_ids.add(msg.user_info.user_id) + user_count = len(user_ids) + + # 获取当前小时的基准值 + current_hour_base_messages, current_hour_base_users = self._get_current_hour_baseline() + + # 计算当前小时平均每10分钟的基准值 + current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 + current_hour_10min_users = current_hour_base_users / 6 + + # 专注度调整逻辑:根据活跃度双向调整 + # 检查是否有足够的数据进行分析 + if user_count > 0 and current_hour_10min_users > 0 and message_count >= 2: + # 检查历史基准值是否有效(该时段有活跃度) + if current_hour_base_messages > 0.0 and current_hour_base_users > 0.0: + # 计算用户活跃度比率(基于10分钟数据) + user_ratio = user_count / current_hour_10min_users + # 计算消息活跃度比率(基于10分钟数据) + message_ratio = message_count / current_hour_10min_messages if current_hour_10min_messages > 0 else 1.0 + + # 双向调整逻辑 + if user_ratio > 1.3 and message_ratio > 1.3: + # 活跃度很高:提高专注度,消耗更多LLM资源但回复更精准 + target_focus_adjust = min(self.max_adjust, (user_ratio + message_ratio) / 2) + elif user_ratio > 1.1 and message_ratio > 1.1: + # 活跃度较高:适度提高专注度 + target_focus_adjust = min(self.max_adjust, 1.0 + (user_ratio + message_ratio - 2.0) * 0.2) + elif user_ratio < 0.7 or message_ratio < 0.7: + # 活跃度很低:降低专注度,节省LLM资源 + target_focus_adjust = max(self.min_adjust, min(user_ratio, message_ratio)) + else: + # 正常情况:保持默认专注度 + target_focus_adjust = 1.0 + else: + # 历史基准值不足,不调整 + target_focus_adjust = 1.0 + else: + # 数据不足:不调整 + target_focus_adjust = 1.0 + + # 限制调整范围 + target_focus_adjust = max(self.min_adjust, min(self.max_adjust, target_focus_adjust)) + + # 记录调整前的值 + old_focus_adjust = self.focus_value_adjust + + # 平滑调整 + self.focus_value_adjust = ( + self.focus_value_adjust * (1 - self.smoothing_factor) + + target_focus_adjust * self.smoothing_factor + ) + + # 计算当前小时平均每10分钟的基准值 + current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 + current_hour_10min_users = current_hour_base_users / 6 + + # 判断调整方向 + if target_focus_adjust > 1.0: + adjust_direction = "提高" + elif target_focus_adjust < 1.0: + adjust_direction = "降低" + else: + if current_hour_base_messages <= 0.0 or current_hour_base_users <= 0.0: + adjust_direction = "不调整(该时段无活跃度)" + else: + adjust_direction = "保持" + + # 计算实际变化方向 + actual_change = "" + if self.focus_value_adjust > old_focus_adjust: + actual_change = f"(实际提高: {old_focus_adjust:.2f}→{self.focus_value_adjust:.2f})" + elif self.focus_value_adjust < old_focus_adjust: + actual_change = f"(实际降低: {old_focus_adjust:.2f}→{self.focus_value_adjust:.2f})" + else: + actual_change = f"(无变化: {self.focus_value_adjust:.2f})" + + logger.info( + f"{self.log_prefix} 专注度调整(10分钟): " + f"当前: {message_count}消息/{user_count}用户,人均:{message_count/user_count if user_count > 0 else 0:.2f}消息|" + f"基准: {current_hour_10min_messages:.2f}消息/{current_hour_10min_users:.2f}用户,人均:{current_hour_10min_messages/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}消息|" + f"比率: 用户{user_count/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}x, 消息{message_count/current_hour_10min_messages if current_hour_10min_messages > 0 else 0:.2f}x, " + f"目标调整: {adjust_direction}到{target_focus_adjust:.2f}, 实际结果: {self.focus_value_adjust:.2f} {actual_change}" + ) + + except Exception as e: + logger.error(f"{self.log_prefix} 更新专注度调整值时出错: {e}") + + def get_final_talk_frequency(self) -> float: + return get_config_base_talk_frequency(self.chat_stream.stream_id) * self.get_dynamic_talk_frequency_adjust() * self.talk_frequency_external_adjust + + def get_final_focus_value(self) -> float: + return get_config_base_focus_value(self.chat_stream.stream_id) * self.get_dynamic_focus_value_adjust() * self.focus_value_external_adjust + + + def set_adjustment_parameters( + self, + min_adjust: Optional[float] = None, + max_adjust: Optional[float] = None, + base_message_count: Optional[int] = None, + base_user_count: Optional[int] = None, + smoothing_factor: Optional[float] = None, + update_interval: Optional[int] = None, + historical_update_interval: Optional[int] = None, + historical_days: Optional[int] = None + ): + """ + 设置调整参数 + + Args: + min_adjust: 最小调整值 + max_adjust: 最大调整值 + base_message_count: 基准消息数量 + base_user_count: 基准用户数量 + smoothing_factor: 平滑因子 + update_interval: 更新间隔(秒) + """ + if min_adjust is not None: + self.min_adjust = max(0.1, min_adjust) + if max_adjust is not None: + self.max_adjust = max(1.0, max_adjust) + if base_message_count is not None: + self.base_message_count = max(1, base_message_count) + if base_user_count is not None: + self.base_user_count = max(1, base_user_count) + if smoothing_factor is not None: + self.smoothing_factor = max(0.0, min(1.0, smoothing_factor)) + if update_interval is not None: + self.update_interval = max(10, update_interval) + if historical_update_interval is not None: + self._historical_update_interval = max(300, historical_update_interval) # 最少5分钟 + if historical_days is not None: + self._historical_days = max(1, min(30, historical_days)) # 1-30天之间 + + +class FrequencyControlManager: + """ + 频率控制管理器,管理多个聊天流的频率控制实例 + """ + + def __init__(self): + self.frequency_control_dict: Dict[str, FrequencyControl] = {} + + def get_or_create_frequency_control(self, chat_id: str) -> FrequencyControl: + """ + 获取或创建指定聊天流的频率控制实例 + + Args: + chat_id: 聊天流ID + + Returns: + FrequencyControl: 频率控制实例 + """ + if chat_id not in self.frequency_control_dict: + self.frequency_control_dict[chat_id] = FrequencyControl(chat_id) + return self.frequency_control_dict[chat_id] + +# 创建全局实例 +frequency_control_manager = FrequencyControlManager() + + + + diff --git a/src/chat/frequency_control/talk_frequency_control.py b/src/chat/frequency_control/talk_frequency_control.py index ad81fbd8..11728e26 100644 --- a/src/chat/frequency_control/talk_frequency_control.py +++ b/src/chat/frequency_control/talk_frequency_control.py @@ -3,26 +3,7 @@ from src.config.config import global_config from src.chat.frequency_control.utils import parse_stream_config_to_chat_id -class TalkFrequencyControl: - def __init__(self, chat_id: str): - self.chat_id = chat_id - self.talk_frequency_adjust: float = 1 - - def get_current_talk_frequency(self) -> float: - return get_current_talk_frequency(self.chat_id) * self.talk_frequency_adjust - - -class TalkFrequencyControlManager: - def __init__(self): - self.talk_frequency_controls = {} - - def get_talk_frequency_control(self, chat_id: str) -> TalkFrequencyControl: - if chat_id not in self.talk_frequency_controls: - self.talk_frequency_controls[chat_id] = TalkFrequencyControl(chat_id) - return self.talk_frequency_controls[chat_id] - - -def get_current_talk_frequency(chat_id: Optional[str] = None) -> float: +def get_config_base_talk_frequency(chat_id: Optional[str] = None) -> float: """ 根据当前时间和聊天流获取对应的 talk_frequency @@ -145,4 +126,3 @@ def get_global_frequency() -> Optional[float]: return None -talk_frequency_control = TalkFrequencyControlManager() diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 8680392a..289752d0 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -18,8 +18,7 @@ from src.chat.planner_actions.action_modifier import ActionModifier from src.chat.planner_actions.action_manager import ActionManager from src.chat.heart_flow.hfc_utils import CycleDetail from src.chat.heart_flow.hfc_utils import send_typing, stop_typing -from src.chat.frequency_control.talk_frequency_control import talk_frequency_control -from src.chat.frequency_control.focus_value_control import focus_value_control +from src.chat.frequency_control.frequency_control import frequency_control_manager from src.chat.express.expression_learner import expression_learner_manager from src.person_info.person_info import Person from src.plugin_system.base.component_types import ChatMode, EventType, ActionInfo @@ -85,8 +84,7 @@ class HeartFChatting: self.expression_learner = expression_learner_manager.get_expression_learner(self.stream_id) - self.talk_frequency_control = talk_frequency_control.get_talk_frequency_control(self.stream_id) - self.focus_value_control = focus_value_control.get_focus_value_control(self.stream_id) + self.frequency_control = frequency_control_manager.get_or_create_frequency_control(self.stream_id) self.action_manager = ActionManager() self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager) @@ -101,15 +99,8 @@ class HeartFChatting: self._cycle_counter = 0 self._current_cycle_detail: CycleDetail = None # type: ignore - self.reply_timeout_count = 0 - self.plan_timeout_count = 0 - self.last_read_time = time.time() - 10 - self.focus_energy = 1 - self.no_action_consecutive = 0 - # 最近三次no_action的新消息兴趣度记录 - self.recent_interest_records: deque = deque(maxlen=3) async def start(self): """检查是否需要启动主循环,如果未激活则启动。""" @@ -187,87 +178,14 @@ class HeartFChatting: f"耗时: {self._current_cycle_detail.end_time - self._current_cycle_detail.start_time:.1f}秒, " # type: ignore f"选择动作: {action_type}" + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") ) - - def _determine_form_type(self) -> None: - """判断使用哪种形式的no_action""" - # 如果连续no_action次数少于3次,使用waiting形式 - if self.no_action_consecutive <= 3: - self.focus_energy = 1 - else: - # 计算最近三次记录的兴趣度总和 - total_recent_interest = sum(self.recent_interest_records) - - # 计算调整后的阈值 - adjusted_threshold = 1 / self.talk_frequency_control.get_current_talk_frequency() - - logger.info( - f"{self.log_prefix} 最近三次兴趣度总和: {total_recent_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}" - ) - - # 如果兴趣度总和小于阈值,进入breaking形式 - if total_recent_interest < adjusted_threshold: - logger.info(f"{self.log_prefix} 兴趣度不足,进入休息") - self.focus_energy = random.randint(3, 6) - else: - logger.info(f"{self.log_prefix} 兴趣度充足,等待新消息") - self.focus_energy = 1 - - async def _should_process_messages(self, new_message: List["DatabaseMessages"]) -> tuple[bool, float]: - """ - 判断是否应该处理消息 - - Args: - new_message: 新消息列表 - mode: 当前聊天模式 - - Returns: - bool: 是否应该处理消息 - """ - new_message_count = len(new_message) - talk_frequency = self.talk_frequency_control.get_current_talk_frequency() - - modified_exit_count_threshold = self.focus_energy * 0.5 / talk_frequency - modified_exit_interest_threshold = 1.5 / talk_frequency + + async def caculate_interest_value(self, recent_messages_list: List["DatabaseMessages"]) -> float: total_interest = 0.0 - for msg in new_message: + for msg in recent_messages_list: interest_value = msg.interest_value if interest_value is not None and msg.processed_plain_text: total_interest += float(interest_value) - - if new_message_count >= modified_exit_count_threshold: - self.recent_interest_records.append(total_interest) - logger.info( - f"{self.log_prefix} 累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold:.1f}),结束等待" - ) - # logger.info(self.last_read_time) - # logger.info(new_message) - return True, total_interest / new_message_count if new_message_count > 0 else 0.0 - - # 检查累计兴趣值 - if new_message_count > 0: - # 只在兴趣值变化时输出log - if not hasattr(self, "_last_accumulated_interest") or total_interest != self._last_accumulated_interest: - logger.info( - f"{self.log_prefix} 休息中,新消息:{new_message_count}条,累计兴趣值: {total_interest:.2f}, 活跃度: {talk_frequency:.1f}" - ) - self._last_accumulated_interest = total_interest - - if total_interest >= modified_exit_interest_threshold: - # 记录兴趣度到列表 - self.recent_interest_records.append(total_interest) - logger.info( - f"{self.log_prefix} 累计兴趣值达到{total_interest:.2f}(>{modified_exit_interest_threshold:.1f}),结束等待" - ) - return True, total_interest / new_message_count if new_message_count > 0 else 0.0 - - # 每10秒输出一次等待状态 - if int(time.time() - self.last_read_time) > 0 and int(time.time() - self.last_read_time) % 15 == 0: - logger.debug( - f"{self.log_prefix} 已等待{time.time() - self.last_read_time:.0f}秒,累计{new_message_count}条消息,累计兴趣{total_interest:.1f},继续等待..." - ) - await asyncio.sleep(0.5) - - return False, 0.0 + return total_interest / len(recent_messages_list) async def _loopbody(self): recent_messages_list = message_api.get_messages_by_time_in_chat( @@ -279,16 +197,13 @@ class HeartFChatting: filter_mai=True, filter_command=True, ) - # 统一的消息处理逻辑 - should_process, interest_value = await self._should_process_messages(recent_messages_list) - - if should_process: + + if recent_messages_list: self.last_read_time = time.time() - await self._observe(interest_value=interest_value) - + await self._observe(interest_value=await self.caculate_interest_value(recent_messages_list),recent_messages_list=recent_messages_list) else: # Normal模式:消息数量不足,等待 - await asyncio.sleep(0.5) + await asyncio.sleep(0.2) return True return True @@ -342,8 +257,7 @@ class HeartFChatting: return loop_info, reply_text, cycle_timers - async def _observe(self, interest_value: float = 0.0) -> bool: - action_type = "no_action" + async def _observe(self, interest_value: float = 0.0,recent_messages_list: List["DatabaseMessages"] = []) -> bool: reply_text = "" # 初始化reply_text变量,避免UnboundLocalError # 使用sigmoid函数将interest_value转换为概率 @@ -362,22 +276,28 @@ class HeartFChatting: normal_mode_probability = ( calculate_normal_mode_probability(interest_value) * 2 - * self.talk_frequency_control.get_current_talk_frequency() + * self.frequency_control.get_final_talk_frequency() ) + + #对呼唤名字进行增幅 + for msg in recent_messages_list: + if msg.reply_probability_boost is not None and msg.reply_probability_boost > 0.0: + normal_mode_probability += msg.reply_probability_boost + if global_config.chat.mentioned_bot_reply and msg.is_mentioned: + normal_mode_probability += global_config.chat.mentioned_bot_reply + if global_config.chat.at_bot_inevitable_reply and msg.is_at: + normal_mode_probability += global_config.chat.at_bot_inevitable_reply + - # 根据概率决定使用哪种模式 + # 根据概率决定使用直接回复 + interest_triggerd = False + focus_triggerd = False + if random.random() < normal_mode_probability: - mode = ChatMode.NORMAL + interest_triggerd = True logger.info( - f"{self.log_prefix} 有兴趣({interest_value:.2f}),在{normal_mode_probability * 100:.0f}%概率下选择回复" + f"{self.log_prefix} 有新消息,在{normal_mode_probability * 100:.0f}%概率下选择回复" ) - else: - mode = ChatMode.FOCUS - - # 创建新的循环信息 - cycle_timers, thinking_id = self.start_cycle() - - logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") if s4u_config.enable_s4u: await send_typing() @@ -385,30 +305,28 @@ class HeartFChatting: async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): await self.expression_learner.trigger_learning_for_chat() - # # 记忆构建:为当前chat_id构建记忆 - # try: - # await hippocampus_manager.build_memory_for_chat(self.stream_id) - # except Exception as e: - # logger.error(f"{self.log_prefix} 记忆构建失败: {e}") - available_actions: Dict[str, ActionInfo] = {} - if random.random() > self.focus_value_control.get_current_focus_value() and mode == ChatMode.FOCUS: - # 如果激活度没有激活,并且聊天活跃度低,有可能不进行plan,相当于不在电脑前,不进行认真思考 - action_to_use_info = [ - ActionPlannerInfo( - action_type="no_action", - reasoning="专注不足", - action_data={}, - ) - ] - else: + + #如果兴趣度不足以激活 + if not interest_triggerd: + #看看专注值够不够 + if random.random() < self.frequency_control.get_final_focus_value(): + #专注值足够,仍然进入正式思考 + focus_triggerd = True #都没触发,路边 + + + # 任意一种触发都行 + if interest_triggerd or focus_triggerd: + # 进入正式思考模式 + cycle_timers, thinking_id = self.start_cycle() + logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") + # 第一步:动作检查 - with Timer("动作检查", cycle_timers): - try: - await self.action_modifier.modify_actions() - available_actions = self.action_manager.get_using_actions() - except Exception as e: - logger.error(f"{self.log_prefix} 动作修改失败: {e}") + try: + await self.action_modifier.modify_actions() + available_actions = self.action_manager.get_using_actions() + except Exception as e: + logger.error(f"{self.log_prefix} 动作修改失败: {e}") # 执行planner is_group_chat, chat_target_info, _ = self.action_planner.get_necessary_info() @@ -439,103 +357,93 @@ class HeartFChatting: ): return False with Timer("规划器", cycle_timers): + # 根据不同触发,进入不同plan + if focus_triggerd: + mode = ChatMode.FOCUS + else: + mode = ChatMode.NORMAL + action_to_use_info, _ = await self.action_planner.plan( mode=mode, loop_start_time=self.last_read_time, available_actions=available_actions, ) - for action in action_to_use_info: - print(action.action_type) + # 3. 并行执行所有动作 + action_tasks = [ + asyncio.create_task( + self._execute_action(action, action_to_use_info, thinking_id, available_actions, cycle_timers) + ) + for action in action_to_use_info + ] - # 3. 并行执行所有动作 - action_tasks = [ - asyncio.create_task( - self._execute_action(action, action_to_use_info, thinking_id, available_actions, cycle_timers) - ) - for action in action_to_use_info - ] + # 并行执行所有任务 + results = await asyncio.gather(*action_tasks, return_exceptions=True) - # 并行执行所有任务 - results = await asyncio.gather(*action_tasks, return_exceptions=True) + # 处理执行结果 + reply_loop_info = None + reply_text_from_reply = "" + action_success = False + action_reply_text = "" + action_command = "" - # 处理执行结果 - reply_loop_info = None - reply_text_from_reply = "" - action_success = False - action_reply_text = "" - action_command = "" + for i, result in enumerate(results): + if isinstance(result, BaseException): + logger.error(f"{self.log_prefix} 动作执行异常: {result}") + continue - for i, result in enumerate(results): - if isinstance(result, BaseException): - logger.error(f"{self.log_prefix} 动作执行异常: {result}") - continue + _cur_action = action_to_use_info[i] + if result["action_type"] != "reply": + action_success = result["success"] + action_reply_text = result["reply_text"] + action_command = result.get("command", "") + elif result["action_type"] == "reply": + if result["success"]: + reply_loop_info = result["loop_info"] + reply_text_from_reply = result["reply_text"] + else: + logger.warning(f"{self.log_prefix} 回复动作执行失败") - _cur_action = action_to_use_info[i] - if result["action_type"] != "reply": - action_success = result["success"] - action_reply_text = result["reply_text"] - action_command = result.get("command", "") - elif result["action_type"] == "reply": - if result["success"]: - reply_loop_info = result["loop_info"] - reply_text_from_reply = result["reply_text"] - else: - logger.warning(f"{self.log_prefix} 回复动作执行失败") - - # 构建最终的循环信息 - if reply_loop_info: - # 如果有回复信息,使用回复的loop_info作为基础 - loop_info = reply_loop_info - # 更新动作执行信息 - loop_info["loop_action_info"].update( - { - "action_taken": action_success, - "command": action_command, - "taken_time": time.time(), + # 构建最终的循环信息 + if reply_loop_info: + # 如果有回复信息,使用回复的loop_info作为基础 + loop_info = reply_loop_info + # 更新动作执行信息 + loop_info["loop_action_info"].update( + { + "action_taken": action_success, + "command": action_command, + "taken_time": time.time(), + } + ) + reply_text = reply_text_from_reply + else: + # 没有回复信息,构建纯动作的loop_info + loop_info = { + "loop_plan_info": { + "action_result": action_to_use_info, + }, + "loop_action_info": { + "action_taken": action_success, + "reply_text": action_reply_text, + "command": action_command, + "taken_time": time.time(), + }, } - ) - reply_text = reply_text_from_reply - else: - # 没有回复信息,构建纯动作的loop_info - loop_info = { - "loop_plan_info": { - "action_result": action_to_use_info, - }, - "loop_action_info": { - "action_taken": action_success, - "reply_text": action_reply_text, - "command": action_command, - "taken_time": time.time(), - }, - } - reply_text = action_reply_text + reply_text = action_reply_text + + + self.end_cycle(loop_info, cycle_timers) + self.print_cycle_info(cycle_timers) - if s4u_config.enable_s4u: - await stop_typing() - await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text) + """S4U内容,暂时保留""" + if s4u_config.enable_s4u: + await stop_typing() + await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text) + """S4U内容,暂时保留""" - self.end_cycle(loop_info, cycle_timers) - self.print_cycle_info(cycle_timers) - - # await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", "")) - - action_type = action_to_use_info[0].action_type if action_to_use_info else "no_action" - - # 管理no_action计数器:当执行了非no_action动作时,重置计数器 - if action_type != "no_action": - # no_action逻辑已集成到heartFC_chat.py中,直接重置计数器 - self.recent_interest_records.clear() - self.no_action_consecutive = 0 - logger.debug(f"{self.log_prefix} 执行了{action_type}动作,重置no_action计数器") return True - if action_type == "no_action": - self.no_action_consecutive += 1 - self._determine_form_type() - - return True - async def _main_chat_loop(self): """主循环,持续进行计划并可能回复消息,直到被外部取消。""" try: diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index b1dccdaf..ac424c66 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -32,10 +32,10 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]: Returns: Tuple[float, bool, list[str]]: (兴趣度, 是否被提及, 关键词) """ - if message.is_picid: + if message.is_picid or message.is_emoji: return 0.0, [] - is_mentioned, _ = is_mentioned_bot_in_message(message) + is_mentioned,is_at,reply_probability_boost = is_mentioned_bot_in_message(message) interested_rate = 0.0 with Timer("记忆激活"): @@ -79,17 +79,13 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]: # 确保在范围内 base_interest = min(max(base_interest, 0.01), 0.3) - interested_rate += base_interest - - if is_mentioned: - interest_increase_on_mention = 2 - interested_rate += interest_increase_on_mention - - message.interest_value = interested_rate + message.interest_value = base_interest message.is_mentioned = is_mentioned - - return interested_rate, keywords + message.is_at = is_at + message.reply_probability_boost = reply_probability_boost + + return base_interest, keywords class HeartFCMessageReceiver: diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index 1b15d717..82901a91 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -18,6 +18,7 @@ from src.config.config import global_config, model_config from src.common.data_models.database_data_model import DatabaseMessages from src.common.database.database_model import GraphNodes, GraphEdges # Peewee Models导入 from src.common.logger import get_logger +from src.chat.utils.utils import cut_key_words from src.chat.utils.chat_message_builder import ( build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive, @@ -98,19 +99,23 @@ class MemoryGraph: current_weight = self.G.nodes[concept].get("weight", 0.0) self.G.nodes[concept]["weight"] = current_weight + 1.0 logger.debug(f"节点 {concept} 记忆整合成功,权重增加到 {current_weight + 1.0}") + logger.info(f"节点 {concept} 记忆内容已更新:{integrated_memory}") except Exception as e: logger.error(f"LLM整合记忆失败: {e}") # 降级到简单连接 new_memory_str = f"{existing_memory} | {memory}" self.G.nodes[concept]["memory_items"] = new_memory_str + logger.info(f"节点 {concept} 记忆内容已简单拼接并更新:{new_memory_str}") else: new_memory_str = str(memory) self.G.nodes[concept]["memory_items"] = new_memory_str + logger.info(f"节点 {concept} 记忆内容已直接更新:{new_memory_str}") else: self.G.nodes[concept]["memory_items"] = str(memory) # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time if "created_time" not in self.G.nodes[concept]: self.G.nodes[concept]["created_time"] = current_time + logger.info(f"节点 {concept} 创建新记忆:{str(memory)}") # 更新最后修改时间 self.G.nodes[concept]["last_modified"] = current_time else: @@ -122,6 +127,7 @@ class MemoryGraph: created_time=current_time, # 添加创建时间 last_modified=current_time, ) # 添加最后修改时间 + logger.info(f"新节点 {concept} 已添加,记忆内容已写入:{str(memory)}") def get_dot(self, concept): # 检查节点是否存在于图中 @@ -402,9 +408,7 @@ class Hippocampus: text_length = len(text) topic_num: int | list[int] = 0 - words = jieba.cut(text) - keywords_lite = [word for word in words if len(word) > 1] - keywords_lite = list(set(keywords_lite)) + keywords_lite = cut_key_words(text) if keywords_lite: logger.debug(f"提取关键词极简版: {keywords_lite}") @@ -1113,6 +1117,7 @@ class ParahippocampalGyrus: # 4. 创建所有话题的摘要生成任务 tasks: List[Tuple[str, Coroutine[Any, Any, Tuple[str, Tuple[str, str, List | None]]]]] = [] + topic_what_prompt: str = "" for topic in filtered_topics: # 调用修改后的 topic_what,不再需要 time_info topic_what_prompt = self.hippocampus.topic_what(input_text, topic) @@ -1159,6 +1164,131 @@ class ParahippocampalGyrus: return compressed_memory, similar_topics_dict + def get_similar_topics_from_keywords( + self, + keywords: list[str] | str, + top_k: int = 3, + threshold: float = 0.7, + ) -> dict[str, list[tuple[str, float]]]: + """基于输入的关键词,返回每个关键词对应的相似主题列表。 + + Args: + keywords: 关键词列表或以逗号/空格/顿号分隔的字符串。 + top_k: 每个关键词返回的相似主题数量上限。 + threshold: 相似度阈值,低于该值的主题将被过滤。 + + Returns: + dict[str, list[tuple[str, float]]]: {keyword: [(topic, similarity), ...]} + """ + # 规范化输入为列表[str] + if isinstance(keywords, str): + # 支持中英文逗号、顿号、空格分隔 + parts = ( + keywords.replace(",", ",").replace("、", ",").replace(" ", ",").strip(", ") + ) + keyword_list = [p.strip() for p in parts.split(",") if p.strip()] + else: + keyword_list = [k.strip() for k in keywords if isinstance(k, str) and k.strip()] + + if not keyword_list: + return {} + + existing_topics = list(self.memory_graph.G.nodes()) + result: dict[str, list[tuple[str, float]]] = {} + + for kw in keyword_list: + kw_words = set(jieba.cut(kw)) + similar_topics: list[tuple[str, float]] = [] + + for topic in existing_topics: + topic_words = set(jieba.cut(topic)) + all_words = kw_words | topic_words + if not all_words: + continue + v1 = [1 if w in kw_words else 0 for w in all_words] + v2 = [1 if w in topic_words else 0 for w in all_words] + sim = cosine_similarity(v1, v2) + if sim >= threshold: + similar_topics.append((topic, sim)) + + similar_topics.sort(key=lambda x: x[1], reverse=True) + result[kw] = similar_topics[:top_k] + + return result + + async def add_memory_with_similar( + self, + memory_item: str, + similar_topics_dict: dict[str, list[tuple[str, float]]], + ) -> bool: + """将单条记忆内容与相似主题写入记忆网络并同步数据库。 + + 按 build_memory_for_chat 的方式:为 similar_topics_dict 的每个键作为主题添加节点内容, + 并与其相似主题建立连接,连接强度为 int(similarity * 10)。 + + Args: + memory_item: 记忆内容字符串,将作为每个主题节点的 memory_items。 + similar_topics_dict: {topic: [(similar_topic, similarity), ...]} + + Returns: + bool: 是否成功执行添加与同步。 + """ + try: + if not memory_item or not isinstance(memory_item, str): + return False + + if not similar_topics_dict or not isinstance(similar_topics_dict, dict): + return False + + current_time = time.time() + + # 为每个主题写入节点 + for topic, similar_list in similar_topics_dict.items(): + if not topic or not isinstance(topic, str): + continue + + await self.hippocampus.memory_graph.add_dot(topic, memory_item, self.hippocampus) + + # 连接相似主题 + if isinstance(similar_list, list): + for item in similar_list: + try: + similar_topic, similarity = item + except Exception: + continue + if not isinstance(similar_topic, str): + continue + if topic == similar_topic: + continue + # 强度按 build_memory_for_chat 的规则 + strength = int(max(0.0, float(similarity)) * 10) if similarity is not None else 0 + if strength <= 0: + continue + # 确保相似主题节点存在(如果没有,也可以只建立边,networkx会创建节点,但需初始化属性) + if similar_topic not in self.memory_graph.G: + # 创建一个空的相似主题节点,避免悬空边,memory_items 为空字符串 + self.memory_graph.G.add_node( + similar_topic, + memory_items="", + weight=1.0, + created_time=current_time, + last_modified=current_time, + ) + self.memory_graph.G.add_edge( + topic, + similar_topic, + strength=strength, + created_time=current_time, + last_modified=current_time, + ) + + # 同步数据库 + await self.hippocampus.entorhinal_cortex.sync_memory_to_db() + return True + except Exception as e: + logger.error(f"添加记忆节点失败: {e}") + return False + async def operation_forget_topic(self, percentage=0.005): start_time = time.time() logger.info("[遗忘] 开始检查数据库...") @@ -1325,7 +1455,6 @@ class HippocampusManager: logger.info(f""" -------------------------------- 记忆系统参数配置: - 构建频率: {global_config.memory.memory_build_frequency}秒|压缩率: {global_config.memory.memory_compress_rate} 遗忘间隔: {global_config.memory.forget_memory_interval}秒|遗忘比例: {global_config.memory.memory_forget_percentage}|遗忘: {global_config.memory.memory_forget_time}小时之后 记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count} --------------------------------""") # noqa: E501 @@ -1343,61 +1472,6 @@ class HippocampusManager: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) - async def build_memory_for_chat(self, chat_id: str): - """为指定chat_id构建记忆(在heartFC_chat.py中调用)""" - if not self._initialized: - raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") - - try: - # 检查是否需要构建记忆 - logger.info(f"为 {chat_id} 构建记忆") - if memory_segment_manager.check_and_build_memory_for_chat(chat_id): - logger.info(f"为 {chat_id} 构建记忆,需要构建记忆") - messages = memory_segment_manager.get_messages_for_memory_build(chat_id, 50) - - build_probability = 0.3 * global_config.memory.memory_build_frequency - - if messages and random.random() < build_probability: - logger.info(f"为 {chat_id} 构建记忆,消息数量: {len(messages)}") - - # 调用记忆压缩和构建 - ( - compressed_memory, - similar_topics_dict, - ) = await self._hippocampus.parahippocampal_gyrus.memory_compress( - messages, global_config.memory.memory_compress_rate - ) - - # 添加记忆节点 - current_time = time.time() - for topic, memory in compressed_memory: - await self._hippocampus.memory_graph.add_dot(topic, memory, self._hippocampus) - - # 连接相似主题 - if topic in similar_topics_dict: - similar_topics = similar_topics_dict[topic] - for similar_topic, similarity in similar_topics: - if topic != similar_topic: - strength = int(similarity * 10) - self._hippocampus.memory_graph.G.add_edge( - topic, - similar_topic, - strength=strength, - created_time=current_time, - last_modified=current_time, - ) - - # 同步到数据库 - await self._hippocampus.entorhinal_cortex.sync_memory_to_db() - logger.info(f"为 {chat_id} 构建记忆完成") - return True - - except Exception as e: - logger.error(f"为 {chat_id} 构建记忆失败: {e}") - return False - - return False - async def get_memory_from_topic( self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3 ) -> list: @@ -1441,89 +1515,3 @@ class HippocampusManager: # 创建全局实例 hippocampus_manager = HippocampusManager() - - -# 在Hippocampus类中添加新的记忆构建管理器 -class MemoryBuilder: - """记忆构建器 - - 为每个chat_id维护消息缓存和触发机制,类似ExpressionLearner - """ - - def __init__(self, chat_id: str): - self.chat_id = chat_id - self.last_update_time: float = time.time() - self.last_processed_time: float = 0.0 - - def should_trigger_memory_build(self) -> bool: - # sourcery skip: assign-if-exp, boolean-if-exp-identity, reintroduce-else - """检查是否应该触发记忆构建""" - current_time = time.time() - - # 检查时间间隔 - time_diff = current_time - self.last_update_time - if time_diff < 600 / global_config.memory.memory_build_frequency: - return False - - # 检查消息数量 - - recent_messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_update_time, - timestamp_end=current_time, - ) - - logger.info(f"最近消息数量: {len(recent_messages)},间隔时间: {time_diff}") - - if not recent_messages or len(recent_messages) < 30 / global_config.memory.memory_build_frequency: - return False - - return True - - def get_messages_for_memory_build(self, threshold: int = 25) -> List[DatabaseMessages]: - """获取用于记忆构建的消息""" - current_time = time.time() - - messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_update_time, - timestamp_end=current_time, - limit=threshold, - ) - if messages: - # 更新最后处理时间 - self.last_processed_time = current_time - self.last_update_time = current_time - - return messages or [] - - -class MemorySegmentManager: - """记忆段管理器 - - 管理所有chat_id的MemoryBuilder实例,自动检查和触发记忆构建 - """ - - def __init__(self): - self.builders: Dict[str, MemoryBuilder] = {} - - def get_or_create_builder(self, chat_id: str) -> MemoryBuilder: - """获取或创建指定chat_id的MemoryBuilder""" - if chat_id not in self.builders: - self.builders[chat_id] = MemoryBuilder(chat_id) - return self.builders[chat_id] - - def check_and_build_memory_for_chat(self, chat_id: str) -> bool: - """检查指定chat_id是否需要构建记忆,如果需要则返回True""" - builder = self.get_or_create_builder(chat_id) - return builder.should_trigger_memory_build() - - def get_messages_for_memory_build(self, chat_id: str, threshold: int = 25) -> List[DatabaseMessages]: - """获取指定chat_id用于记忆构建的消息""" - if chat_id not in self.builders: - return [] - return self.builders[chat_id].get_messages_for_memory_build(threshold) - - -# 创建全局实例 -memory_segment_manager = MemorySegmentManager() diff --git a/src/chat/memory_system/instant_memory.py b/src/chat/memory_system/instant_memory.py deleted file mode 100644 index f8e91b5c..00000000 --- a/src/chat/memory_system/instant_memory.py +++ /dev/null @@ -1,254 +0,0 @@ -# -*- coding: utf-8 -*- -import time -import re -import json -import ast -import traceback - -from json_repair import repair_json -from datetime import datetime, timedelta - -from src.llm_models.utils_model import LLMRequest -from src.common.logger import get_logger -from src.common.database.database_model import Memory # Peewee Models导入 -from src.config.config import model_config, global_config - - -logger = get_logger(__name__) - - -class MemoryItem: - def __init__(self, memory_id: str, chat_id: str, memory_text: str, keywords: list[str]): - self.memory_id = memory_id - self.chat_id = chat_id - self.memory_text: str = memory_text - self.keywords: list[str] = keywords - self.create_time: float = time.time() - self.last_view_time: float = time.time() - - -class MemoryManager: - def __init__(self): - # self.memory_items:list[MemoryItem] = [] - pass - - -class InstantMemory: - def __init__(self, chat_id): - self.chat_id = chat_id - self.last_view_time = time.time() - self.summary_model = LLMRequest( - model_set=model_config.model_task_config.utils, - request_type="memory.summary", - ) - - async def if_need_build(self, text: str): - prompt = f""" -请判断以下内容中是否有值得记忆的信息,如果有,请输出1,否则输出0 -{text} -请只输出1或0就好 - """ - - try: - response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5) - if global_config.debug.show_prompt: - print(prompt) - print(response) - - return "1" in response - except Exception as e: - logger.error(f"判断是否需要记忆出现错误:{str(e)} {traceback.format_exc()}") - return False - - async def build_memory(self, text): - prompt = f""" - 以下内容中存在值得记忆的信息,请你从中总结出一段值得记忆的信息,并输出 - {text} - 请以json格式输出一段概括的记忆内容和关键词 - {{ - "memory_text": "记忆内容", - "keywords": "关键词,用/划分" - }} - """ - try: - response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5) - # print(prompt) - # print(response) - if not response: - return None - try: - repaired = repair_json(response) - result = json.loads(repaired) - memory_text = result.get("memory_text", "") - keywords = result.get("keywords", "") - if isinstance(keywords, str): - keywords_list = [k.strip() for k in keywords.split("/") if k.strip()] - elif isinstance(keywords, list): - keywords_list = keywords - else: - keywords_list = [] - return {"memory_text": memory_text, "keywords": keywords_list} - except Exception as parse_e: - logger.error(f"解析记忆json失败:{str(parse_e)} {traceback.format_exc()}") - return None - except Exception as e: - logger.error(f"构建记忆出现错误:{str(e)} {traceback.format_exc()}") - return None - - async def create_and_store_memory(self, text: str): - if_need = await self.if_need_build(text) - if if_need: - logger.info(f"需要记忆:{text}") - memory = await self.build_memory(text) - if memory and memory.get("memory_text"): - memory_id = f"{self.chat_id}_{time.time()}" - memory_item = MemoryItem( - memory_id=memory_id, - chat_id=self.chat_id, - memory_text=memory["memory_text"], - keywords=memory.get("keywords", []), - ) - await self.store_memory(memory_item) - else: - logger.info(f"不需要记忆:{text}") - - async def store_memory(self, memory_item: MemoryItem): - memory = Memory( - memory_id=memory_item.memory_id, - chat_id=memory_item.chat_id, - memory_text=memory_item.memory_text, - keywords=memory_item.keywords, - create_time=memory_item.create_time, - last_view_time=memory_item.last_view_time, - ) - memory.save() - - async def get_memory(self, target: str): - from json_repair import repair_json - - prompt = f""" -请根据以下发言内容,判断是否需要提取记忆 -{target} -请用json格式输出,包含以下字段: -其中,time的要求是: -可以选择具体日期时间,格式为YYYY-MM-DD HH:MM:SS,或者大致时间,格式为YYYY-MM-DD -可以选择相对时间,例如:今天,昨天,前天,5天前,1个月前 -可以选择留空进行模糊搜索 -{{ - "need_memory": 1, - "keywords": "希望获取的记忆关键词,用/划分", - "time": "希望获取的记忆大致时间" -}} -请只输出json格式,不要输出其他多余内容 -""" - try: - response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5) - if global_config.debug.show_prompt: - print(prompt) - print(response) - if not response: - return None - try: - repaired = repair_json(response) - result = json.loads(repaired) - # 解析keywords - keywords = result.get("keywords", "") - if isinstance(keywords, str): - keywords_list = [k.strip() for k in keywords.split("/") if k.strip()] - elif isinstance(keywords, list): - keywords_list = keywords - else: - keywords_list = [] - # 解析time为时间段 - time_str = result.get("time", "").strip() - start_time, end_time = self._parse_time_range(time_str) - logger.info(f"start_time: {start_time}, end_time: {end_time}") - # 检索包含关键词的记忆 - memories_set = set() - if start_time and end_time: - start_ts = start_time.timestamp() - end_ts = end_time.timestamp() - query = Memory.select().where( - (Memory.chat_id == self.chat_id) - & (Memory.create_time >= start_ts) # type: ignore - & (Memory.create_time < end_ts) # type: ignore - ) - else: - query = Memory.select().where(Memory.chat_id == self.chat_id) - - for mem in query: - # 对每条记忆 - mem_keywords = mem.keywords or "" - parsed = ast.literal_eval(mem_keywords) - if isinstance(parsed, list): - mem_keywords = [str(k).strip() for k in parsed if str(k).strip()] - else: - mem_keywords = [] - # logger.info(f"mem_keywords: {mem_keywords}") - # logger.info(f"keywords_list: {keywords_list}") - for kw in keywords_list: - # logger.info(f"kw: {kw}") - # logger.info(f"kw in mem_keywords: {kw in mem_keywords}") - if kw in mem_keywords: - # logger.info(f"mem.memory_text: {mem.memory_text}") - memories_set.add(mem.memory_text) - break - return list(memories_set) - except Exception as parse_e: - logger.error(f"解析记忆json失败:{str(parse_e)} {traceback.format_exc()}") - return None - except Exception as e: - logger.error(f"获取记忆出现错误:{str(e)} {traceback.format_exc()}") - return None - - def _parse_time_range(self, time_str): - # sourcery skip: extract-duplicate-method, use-contextlib-suppress - """ - 支持解析如下格式: - - 具体日期时间:YYYY-MM-DD HH:MM:SS - - 具体日期:YYYY-MM-DD - - 相对时间:今天,昨天,前天,N天前,N个月前 - - 空字符串:返回(None, None) - """ - now = datetime.now() - if not time_str: - return 0, now - time_str = time_str.strip() - # 具体日期时间 - try: - dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") - return dt, dt + timedelta(hours=1) - except Exception: - pass - # 具体日期 - try: - dt = datetime.strptime(time_str, "%Y-%m-%d") - return dt, dt + timedelta(days=1) - except Exception: - pass - # 相对时间 - if time_str == "今天": - start = now.replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if time_str == "昨天": - start = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if time_str == "前天": - start = (now - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if m := re.match(r"(\d+)天前", time_str): - days = int(m.group(1)) - start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if m := re.match(r"(\d+)个月前", time_str): - months = int(m.group(1)) - # 近似每月30天 - start = (now - timedelta(days=months * 30)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - # 其他无法解析 - return 0, now diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 66a1c029..8af56605 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -84,7 +84,7 @@ class Message(MessageBase): return await self._process_single_segment(segment) # type: ignore @abstractmethod - async def _process_single_segment(self, segment): + async def _process_single_segment(self, segment) -> str: pass @@ -108,6 +108,8 @@ class MessageRecv(Message): self.has_picid = False self.is_voice = False self.is_mentioned = None + self.is_at = False + self.reply_probability_boost = 0.0 self.is_notify = False self.is_command = False @@ -353,44 +355,44 @@ class MessageProcessBase(Message): self.thinking_time = round(time.time() - self.thinking_start_time, 2) return self.thinking_time - async def _process_single_segment(self, seg: Seg) -> str | None: + async def _process_single_segment(self, segment: Seg) -> str: """处理单个消息段 Args: - seg: 要处理的消息段 + segment: 要处理的消息段 Returns: str: 处理后的文本 """ try: - if seg.type == "text": - return seg.data # type: ignore - elif seg.type == "image": + if segment.type == "text": + return segment.data # type: ignore + elif segment.type == "image": # 如果是base64图片数据 - if isinstance(seg.data, str): - return await get_image_manager().get_image_description(seg.data) + if isinstance(segment.data, str): + return await get_image_manager().get_image_description(segment.data) return "[图片,网卡了加载不出来]" - elif seg.type == "emoji": - if isinstance(seg.data, str): - return await get_image_manager().get_emoji_tag(seg.data) + elif segment.type == "emoji": + if isinstance(segment.data, str): + return await get_image_manager().get_emoji_tag(segment.data) return "[表情,网卡了加载不出来]" - elif seg.type == "voice": - if isinstance(seg.data, str): - return await get_voice_text(seg.data) + elif segment.type == "voice": + if isinstance(segment.data, str): + return await get_voice_text(segment.data) return "[发了一段语音,网卡了加载不出来]" - elif seg.type == "at": - return f"[@{seg.data}]" - elif seg.type == "reply": + elif segment.type == "at": + return f"[@{segment.data}]" + elif segment.type == "reply": if self.reply and hasattr(self.reply, "processed_plain_text"): # print(f"self.reply.processed_plain_text: {self.reply.processed_plain_text}") # print(f"reply: {self.reply}") return f"[回复<{self.reply.message_info.user_info.user_nickname}:{self.reply.message_info.user_info.user_id}> 的消息:{self.reply.processed_plain_text}]" # type: ignore - return None + return "" else: - return f"[{seg.type}:{str(seg.data)}]" + return f"[{segment.type}:{str(segment.data)}]" except Exception as e: - logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") - return f"[处理失败的{seg.type}消息]" + logger.error(f"处理消息段失败: {str(e)}, 类型: {segment.type}, 数据: {segment.data}") + return f"[处理失败的{segment.type}消息]" def _generate_detailed_text(self) -> str: """生成详细文本,包含时间和用户信息""" diff --git a/src/chat/message_receive/storage.py b/src/chat/message_receive/storage.py index c9de76ec..3d84f270 100644 --- a/src/chat/message_receive/storage.py +++ b/src/chat/message_receive/storage.py @@ -56,6 +56,8 @@ class MessageStorage: filtered_display_message = "" interest_value = 0 is_mentioned = False + is_at = False + reply_probability_boost = 0.0 reply_to = message.reply_to priority_mode = "" priority_info = {} @@ -70,6 +72,8 @@ class MessageStorage: filtered_display_message = "" interest_value = message.interest_value is_mentioned = message.is_mentioned + is_at = message.is_at + reply_probability_boost = message.reply_probability_boost reply_to = "" priority_mode = message.priority_mode priority_info = message.priority_info @@ -100,6 +104,8 @@ class MessageStorage: # Flattened chat_info reply_to=reply_to, is_mentioned=is_mentioned, + is_at=is_at, + reply_probability_boost=reply_probability_boost, chat_info_stream_id=chat_info_dict.get("stream_id"), chat_info_platform=chat_info_dict.get("platform"), chat_info_user_platform=user_info_from_chat.get("platform"), diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index b4587474..1de033bf 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -84,7 +84,7 @@ class ActionManager: log_prefix=log_prefix, shutting_down=shutting_down, plugin_config=plugin_config, - action_message=action_message.flatten() if action_message else None, + action_message=action_message, ) logger.debug(f"创建Action实例成功: {action_name}") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 3a11c5c4..a4de0419 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -40,41 +40,39 @@ def init_prompt(): """ {time_block} {name_block} -你现在需要根据聊天内容,选择的合适的action来参与聊天。 -请你根据以下行事风格来决定action: -{plan_style} - {chat_context_description},以下是具体的聊天内容 +**聊天内容** {chat_content_block} -{moderation_prompt} - -现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息: +**动作记录** {actions_before_now_block} -动作:no_action -动作描述:不进行动作,等待合适的时机 -- 当你刚刚发送了消息,没有人回复时,选择no_action -- 当你一次发送了太多消息,为了避免过于烦人,可以不回复 +**回复标准** +请你根据聊天内容和用户的最新消息选择合适回复或者沉默: +1.你可以选择呼叫了你的名字,但是你没有做出回应的消息进行回复 +2.你可以自然的顺着正在进行的聊天内容进行回复或自然的提出一个问题 +3.你的兴趣是:{interest} +4.如果你刚刚进行了回复,不要对同一个话题重复回应 +5.请控制你的发言频率,不要太过频繁的发言,当你刚刚发送了消息,没有人回复时,选择no_action +6.如果有人对你感到厌烦,请减少回复 +7.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对 +8.最好不要选择图片和表情包作为回复对象 +{moderation_prompt} + +**动作** +保持沉默:no_action {{ "action": "no_action", - "reason":"不动作的原因" + "reason":"不回复的原因" }} -动作:reply -动作描述:参与聊天回复,发送文本进行表达 -- 你想要闲聊或者随便附和 -- 有人提到了你,但是你还没有回应 -- {mentioned_bonus} -- 如果你刚刚进行了回复,不要对同一个话题重复回应 +进行回复:reply {{ "action": "reply", "target_message_id":"想要回复的消息id", "reason":"回复的原因" }} - 你必须从上面列出的可用action中选择一个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字 - 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: """, "planner_prompt", @@ -85,27 +83,29 @@ def init_prompt(): {time_block} {name_block} -{chat_context_description},以下是具体的聊天内容 +{chat_context_description} +**聊天内容** {chat_content_block} -{moderation_prompt} - -现在,最新的聊天消息引起了你的兴趣,你想要对其中的消息进行回复,回复标准如下: -- 你想要闲聊或者随便附和 -- 有人提到了你,但是你还没有回应 -- {mentioned_bonus} -- 如果你刚刚进行了回复,不要对同一个话题重复回应 - -你之前的动作记录: +**动作记录** {actions_before_now_block} +**回复标准** +请你选择合适的消息进行回复: +1.你可以选择呼叫了你的名字,但是你没有做出回应的消息进行回复 +2.你可以自然的顺着正在进行的聊天内容进行回复,或者自然的提出一个问题 +3.你的兴趣是{interest} +4.如果有人对你感到厌烦,请你不要太积极的提问或是表达,可以进行顺从 +5.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对 +6.最好不要选择图片和表情包作为回复对象 +7.{moderation_prompt} + 请你从新消息中选出一条需要回复的消息并输出其id,输出格式如下: {{ "action": "reply", "target_message_id":"想要回复的消息id,消息id格式:m+数字", "reason":"回复的原因" }} - 请根据示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: """, "planner_reply_prompt", @@ -129,12 +129,18 @@ def init_prompt(): """ {name_block} -{chat_context_description},{time_block},现在请你根据以下聊天内容,选择一个或多个action来参与聊天。如果没有合适的action,请选择no_action。, +{chat_context_description},{time_block},现在请你根据以下聊天内容,选择一个或多个合适的action。如果没有合适的action,请选择no_action。, {chat_content_block} -{moderation_prompt} -现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息: +**要求** +1.action必须符合使用条件,如果符合条件,就选择 +2.如果聊天内容不适合使用action,即使符合条件,也不要使用 +3.{moderation_prompt} +4.请注意如果相同的内容已经被执行,请不要重复执行 +这是你最近执行过的动作: +{actions_before_now_block} +**可用的action** no_action:不选择任何动作 {{ @@ -144,9 +150,6 @@ no_action:不选择任何动作 {action_options_text} -这是你最近执行过的动作,请注意如果相同的内容已经被执行,请不要重复执行: -{actions_before_now_block} - 请选择,并说明触发action的消息id和选择该action的原因。消息id格式:m+数字 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: """, @@ -465,7 +468,7 @@ class ActionPlanner: ) ) - logger.info(f"{self.log_prefix}副规划器返回了{len(action_planner_infos)}个action") + logger.debug(f"{self.log_prefix}副规划器返回了{len(action_planner_infos)}个action") return action_planner_infos async def plan( @@ -510,7 +513,7 @@ class ActionPlanner: ) self.last_obs_time_mark = time.time() - + all_sub_planner_results: List[ActionPlannerInfo] = [] # 防止Unbound try: sub_planner_actions: Dict[str, ActionInfo] = {} @@ -553,7 +556,7 @@ class ActionPlanner: for i, (action_name, action_info) in enumerate(action_items): sub_planner_lists[i % sub_planner_num].append((action_name, action_info)) - logger.info( + logger.debug( f"{self.log_prefix}成功将{sub_planner_actions_num}个actions分配到{sub_planner_num}个子列表中" ) for i, action_list in enumerate(sub_planner_lists): @@ -581,11 +584,10 @@ class ActionPlanner: sub_plan_results = await asyncio.gather(*sub_plan_tasks) # 收集所有结果 - all_sub_planner_results: List[ActionPlannerInfo] = [] for sub_result in sub_plan_results: all_sub_planner_results.extend(sub_result) - logger.info(f"{self.log_prefix}所有副规划器共返回了{len(all_sub_planner_results)}个action") + logger.info(f"{self.log_prefix}小脑决定执行{len(all_sub_planner_results)}个动作") # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- prompt, message_id_list = await self.build_planner_prompt( @@ -596,6 +598,7 @@ class ActionPlanner: chat_content_block=chat_content_block, # actions_before_now_block=actions_before_now_block, message_id_list=message_id_list, + interest=global_config.personality.interest, ) # --- 调用 LLM (普通文本生成) --- @@ -724,11 +727,9 @@ class ActionPlanner: ] action_str = "" - for action in actions: - action_str += f"{action.action_type} " - logger.info( - f"{self.log_prefix}大脑小脑决定执行{len(actions)}个动作: {action_str}" - ) + for action_planner_info in actions: + action_str += f"{action_planner_info.action_type} " + logger.info(f"{self.log_prefix}大脑小脑决定执行{len(actions)}个动作: {action_str}") else: # 如果为假,只返回副规划器的结果 actions = self._filter_no_actions(all_sub_planner_results) @@ -758,6 +759,7 @@ class ActionPlanner: mode: ChatMode = ChatMode.FOCUS, # actions_before_now_block :str = "", chat_content_block: str = "", + interest: str = "", ) -> tuple[str, List[Tuple[str, "DatabaseMessages"]]]: # sourcery skip: use-join """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: @@ -777,12 +779,6 @@ class ActionPlanner: else: actions_before_now_block = "" - mentioned_bonus = "" - if global_config.chat.mentioned_bot_inevitable_reply: - mentioned_bonus = "\n- 有人提到你" - if global_config.chat.at_bot_inevitable_reply: - mentioned_bonus = "\n- 有人提到你,或者at你" - chat_context_description = "你现在正在一个群聊中" chat_target_name = None if not is_group_chat and chat_target_info: @@ -838,11 +834,10 @@ class ActionPlanner: chat_context_description=chat_context_description, chat_content_block=chat_content_block, actions_before_now_block=actions_before_now_block, - mentioned_bonus=mentioned_bonus, # action_options_text=action_options_block, moderation_prompt=moderation_prompt_block, name_block=name_block, - plan_style=global_config.personality.plan_style, + interest=interest, ) else: planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_reply_prompt") @@ -850,10 +845,10 @@ class ActionPlanner: time_block=time_block, chat_context_description=chat_context_description, chat_content_block=chat_content_block, - mentioned_bonus=mentioned_bonus, moderation_prompt=moderation_prompt_block, name_block=name_block, actions_before_now_block=actions_before_now_block, + interest=interest, ) return prompt, message_id_list except Exception as e: diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 1db4efa6..fb7b903c 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -26,7 +26,6 @@ from src.chat.utils.chat_message_builder import ( ) from src.chat.express.expression_selector import expression_selector from src.chat.memory_system.memory_activator import MemoryActivator -from src.chat.memory_system.instant_memory import InstantMemory from src.mood.mood_manager import mood_manager from src.person_info.person_info import Person, is_person_known from src.plugin_system.base.component_types import ActionInfo, EventType @@ -52,11 +51,14 @@ def init_prompt(): {chat_info} {identity} -你正在{chat_target_2},{reply_target_block} -对这句话,你想表达,原句:{raw_reply},原因是:{reason}。你现在要思考怎么组织回复 你现在的心情是:{mood_state} +你正在{chat_target_2},{reply_target_block} +你想要对上述的发言进行回复,回复的具体内容(原句)是:{raw_reply} +原因是:{reason} +现在请你将这条具体内容改写成一条适合在群聊中发送的回复消息。 你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯 -{reply_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。 +{reply_style} +你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。 {keywords_reaction_prompt} {moderation_prompt} 不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,emoji,at或 @等 ),只输出一条回复就好。 @@ -67,44 +69,39 @@ def init_prompt(): # s4u 风格的 prompt 模板 Prompt( - """ -{expression_habits_block}{tool_info_block} -{knowledge_prompt}{memory_block}{relation_info_block} -{extra_info_block} -{identity} -{action_descriptions} -{time_block} -你现在的主要任务是和 {sender_name} 聊天。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。 + """{identity} +你正在群聊中聊天,你想要回复 {sender_name} 的发言。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。 +{time_block} {background_dialogue_prompt} {core_dialogue_prompt} +{expression_habits_block}{tool_info_block} +{knowledge_prompt}{memory_block}{relation_info_block} +{extra_info_block} + {reply_target_block} - - -你现在的心情是:{mood_state} +你的心情:{mood_state} {reply_style} 注意不要复读你说过的话 {keywords_reaction_prompt} 请注意不要输出多余内容(包括前后缀,冒号和引号,at或 @等 )。只输出回复内容。 {moderation_prompt} 不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,emoji,at或 @等 )。只输出一条回复就好 -现在,你说: -""", +现在,你说:""", "replyer_prompt", ) Prompt( - """ -{expression_habits_block}{tool_info_block} -{knowledge_prompt}{memory_block}{relation_info_block} -{extra_info_block} -{identity} -{action_descriptions} + """{identity} {time_block} 你现在正在一个QQ群里聊天,以下是正在进行的聊天内容: {background_dialogue_prompt} +{expression_habits_block}{tool_info_block} +{knowledge_prompt}{memory_block}{relation_info_block} +{extra_info_block} + 你现在想补充说明你刚刚自己的发言内容:{target},原因是{reason} 请你根据聊天内容,组织一条新回复。注意,{target} 是刚刚你自己的发言,你要在这基础上进一步发言,请按照你自己的角度来继续进行回复。 注意保持上下文的连贯性。 @@ -147,7 +144,6 @@ class DefaultReplyer: self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id) self.heart_fc_sender = HeartFCSender() self.memory_activator = MemoryActivator() - self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id) from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 @@ -375,20 +371,11 @@ class DefaultReplyer: instant_memory = None - # running_memories = await self.memory_activator.activate_memory_with_chat_history( - # target_message=target, chat_history=chat_history - # ) + running_memories = await self.memory_activator.activate_memory_with_chat_history( + target_message=target, chat_history=chat_history + ) running_memories = None - if global_config.memory.enable_instant_memory: - chat_history_str = build_readable_messages( - messages=chat_history, replace_bot_name=True, timestamp_mode="normal" - ) - asyncio.create_task(self.instant_memory.create_and_store_memory(chat_history_str)) - - instant_memory = await self.instant_memory.get_memory(target) - logger.info(f"即时记忆:{instant_memory}") - if not running_memories: return "" @@ -649,9 +636,12 @@ class DefaultReplyer: """构建动作提示""" action_descriptions = "" + skip_names = ["emoji","build_memory","build_relation","reply"] if available_actions: action_descriptions = "除了进行回复之外,你可以做以下这些动作,不过这些动作由另一个模型决定,:\n" for action_name, action_info in available_actions.items(): + if action_name in skip_names: + continue action_description = action_info.description action_descriptions += f"- {action_name}: {action_description}\n" action_descriptions += "\n" @@ -660,11 +650,13 @@ class DefaultReplyer: if chosen_actions_info: for action_plan_info in chosen_actions_info: action_name = action_plan_info.action_type - if action_name == "reply": + if action_name in skip_names: continue + action_description: str = "无描述" + reasoning: str = "无原因" if action := available_actions.get(action_name): - action_description = action.description or "无描述" - reasoning = action_plan_info.reasoning or "无原因" + action_description = action.description or action_description + reasoning = action_plan_info.reasoning or reasoning chosen_action_descriptions += f"- {action_name}: {action_description},原因:{reasoning}\n" @@ -682,18 +674,18 @@ class DefaultReplyer: bot_nickname = "" prompt_personality = ( - f"{global_config.personality.personality_core};{global_config.personality.personality_side}" + f"{global_config.personality.personality};" ) return f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" async def build_prompt_reply_context( self, + reply_message: DatabaseMessages, extra_info: str = "", reply_reason: str = "", available_actions: Optional[Dict[str, ActionInfo]] = None, chosen_actions: Optional[List[ActionPlannerInfo]] = None, enable_tool: bool = True, - reply_message: Optional[DatabaseMessages] = None, ) -> Tuple[str, List[int]]: """ 构建回复器上下文 @@ -716,24 +708,25 @@ class DefaultReplyer: is_group_chat = bool(chat_stream.group_info) platform = chat_stream.platform + user_id = "用户ID" + person_name = "用户" + sender = "用户" + target = "消息" + if reply_message: user_id = reply_message.user_info.user_id person = Person(platform=platform, user_id=user_id) person_name = person.person_name or user_id sender = person_name target = reply_message.processed_plain_text - else: - person_name = "用户" - sender = "用户" - target = "消息" + mood_prompt: str = "" if global_config.mood.enable_mood: chat_mood = mood_manager.get_mood_by_chat_id(chat_id) mood_prompt = chat_mood.mood_state - else: - mood_prompt = "" target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) + target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) message_list_before_now_long = get_raw_msg_before_timestamp_with_chat( chat_id=chat_id, @@ -789,14 +782,14 @@ class DefaultReplyer: for name, result, duration in task_results: results_dict[name] = result chinese_name = task_name_mapping.get(name, name) - if duration < 0.01: + if duration < 0.1: almost_zero_str += f"{chinese_name}," continue timing_logs.append(f"{chinese_name}: {duration:.1f}s") if duration > 8: logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s,请使用更快的模型") - logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.01s") + logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s") expression_habits_block, selected_expressions = results_dict["expression_habits"] expression_habits_block: str @@ -818,6 +811,11 @@ class DefaultReplyer: moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" + + + + + if sender: if is_group_chat: reply_target_block = ( @@ -888,6 +886,8 @@ class DefaultReplyer: is_group_chat = bool(chat_stream.group_info) sender, target = self._parse_reply_target(reply_to) + target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) + target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) # 添加情绪状态获取 if global_config.mood.enable_mood: @@ -950,9 +950,7 @@ class DefaultReplyer: else: chat_target_name = "对方" if self.chat_target_info: - chat_target_name = ( - self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" - ) + chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" chat_target_1 = await global_prompt_manager.format_prompt( "chat_target_private1", sender_name=chat_target_name ) @@ -1019,6 +1017,8 @@ class DefaultReplyer: # 直接使用已初始化的模型实例 logger.info(f"使用模型集生成回复: {', '.join(map(str, self.express_model.model_for_task.model_list))}") + logger.info(f"\n{prompt}\n") + if global_config.debug.show_prompt: logger.info(f"\n{prompt}\n") else: diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index 2dbb19a1..1bd72c85 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -203,18 +203,21 @@ def get_actions_by_timestamp_with_chat( query = query.order_by(ActionRecords.time.asc()) actions = list(query) - return [DatabaseActionRecords( - action_id=action.action_id, - time=action.time, - action_name=action.action_name, - action_data=action.action_data, - action_done=action.action_done, - action_build_into_prompt=action.action_build_into_prompt, - action_prompt_display=action.action_prompt_display, - chat_id=action.chat_id, - chat_info_stream_id=action.chat_info_stream_id, - chat_info_platform=action.chat_info_platform, - ) for action in actions] + return [ + DatabaseActionRecords( + action_id=action.action_id, + time=action.time, + action_name=action.action_name, + action_data=action.action_data, + action_done=action.action_done, + action_build_into_prompt=action.action_build_into_prompt, + action_prompt_display=action.action_prompt_display, + chat_id=action.chat_id, + chat_info_stream_id=action.chat_info_stream_id, + chat_info_platform=action.chat_info_platform, + ) + for action in actions + ] def get_actions_by_timestamp_with_chat_inclusive( @@ -474,7 +477,7 @@ def _build_readable_messages_internal( truncated_content = content if 0 < limit < original_len: - truncated_content = f"{content[:limit]}{replace_content}" + truncated_content = f"{content[:limit]}{replace_content}" # pyright: ignore[reportPossiblyUnboundVariable] detailed_message.append((timestamp, name, truncated_content, is_action)) else: @@ -544,7 +547,7 @@ def build_pic_mapping_info(pic_id_mapping: Dict[str, str]) -> str: return "\n".join(mapping_lines) -def build_readable_actions(actions: List[DatabaseActionRecords],mode:str="relative") -> str: +def build_readable_actions(actions: List[DatabaseActionRecords], mode: str = "relative") -> str: """ 将动作列表转换为可读的文本格式。 格式: 在()分钟前,你使用了(action_name),具体内容是:(action_prompt_display) @@ -585,6 +588,8 @@ def build_readable_actions(actions: List[DatabaseActionRecords],mode:str="relati action_time_struct = time.localtime(action_time) time_str = time.strftime("%H:%M:%S", action_time_struct) time_ago_str = f"在{time_str}" + else: + raise ValueError(f"Unsupported mode: {mode}") line = f"{time_ago_str},你使用了“{action_name}”,具体内容是:“{action_prompt_display}”" output_lines.append(line) diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index f1b1f429..1aaa9461 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -728,7 +728,7 @@ class StatisticOutputTask(AsyncTask): f"