From 46cb0278d7fdf4f020638c8bcef31bfda2eaf24e Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 10 Mar 2026 01:03:31 +0800 Subject: [PATCH] =?UTF-8?q?HFC=E5=9F=BA=E6=9C=AC=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E5=92=8CTODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/brain_chat/brain_chat.py | 2 +- src/chat/heart_flow/heartFC_chat.py | 182 +++++++++++------- src/chat/heart_flow/heartFC_chat_old.py | 4 +- src/chat/heart_flow/heartFC_utils.py | 31 +++ .../{hfc_utils.py => hfc_utils_old.py} | 55 ------ src/common/utils/utils_config.py | 133 +++++++++++++ 6 files changed, 281 insertions(+), 126 deletions(-) create mode 100644 src/chat/heart_flow/heartFC_utils.py rename src/chat/heart_flow/{hfc_utils.py => hfc_utils_old.py} (53%) create mode 100644 src/common/utils/utils_config.py diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index ce329d87..1c34570d 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -15,7 +15,7 @@ from src.chat.utils.timer_calculator import Timer from src.chat.brain_chat.brain_planner import BrainPlanner 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_old import CycleDetail from src.bw_learner.expression_learner_old import expression_learner_manager from src.bw_learner.message_recorder_old import extract_and_distribute_messages from src.person_info.person_info import Person diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 309ec47a..23211925 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -1,5 +1,5 @@ from rich.traceback import install -from typing import Optional, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING, Tuple, Dict import asyncio import time @@ -7,7 +7,7 @@ import traceback import random from src.common.logger import get_logger -from src.common.utils.utils_session import SessionUtils +from src.common.utils.utils_config import ExpressionConfigUtils, ChatConfigUtils from src.config.config import global_config from src.config.file_watcher import FileChange from src.chat.message_receive.chat_manager import chat_manager @@ -15,6 +15,8 @@ from src.bw_learner.expression_reflector import ExpressionReflector from src.bw_learner.expression_learner import ExpressionLearner from src.bw_learner.jargon_miner import JargonMiner +from .heartFC_utils import CycleDetail, CycleActionInfo, CyclePlanInfo + if TYPE_CHECKING: from src.chat.message_receive.message import SessionMessage @@ -40,6 +42,7 @@ class HeartFChatting: self.session_id = session_id session_name = chat_manager.get_session_name(session_id) or session_id self.log_prefix = f"[{session_name}]" + self.session_name = session_name # 系统运行状态 self._running: bool = False @@ -57,12 +60,23 @@ class HeartFChatting: self._cycle_event = asyncio.Event() # 表达方式相关内容 + self._min_messages_for_extraction = 30 # 最少提取消息数 + self._min_extraction_interval = 60 # 最小提取时间间隔,单位为秒 + self._last_extraction_time: float = 0.0 # 上次提取的时间戳 + expr_use, jargon_learn, expr_learn = ExpressionConfigUtils.get_expression_config_for_chat(session_id) + self._enable_expression_use = expr_use # 允许使用表达方式,但不一定启用学习 + self._enable_expression_learning = expr_learn # 允许学习表达方式 + self._enable_jargon_learning = jargon_learn # 允许学习黑话 # 反思器 - self._reflector: Optional[ExpressionReflector] = None + self._reflector: ExpressionReflector = ExpressionReflector(session_id) # 表达学习器 - self._expression_learner: Optional[ExpressionLearner] = None + self._expression_learner: ExpressionLearner = ExpressionLearner(session_id) # 黑话挖掘器 - self._jargon_miner: Optional[JargonMiner] = None + self._jargon_miner: JargonMiner = JargonMiner(session_id, session_name=session_name) + + # TODO: ChatSummarizer 聊天总结器重构 + + # ====== 公开方法 ====== async def start(self): """启动 HeartFChatting 的主循环""" @@ -149,10 +163,18 @@ class HeartFChatting: await self.stop() # 确保状态正确 await asyncio.sleep(3) await self.start() # 尝试重新启动 - - async def _config_callback(self, file_change: FileChange): - + async def _config_callback(self, file_change: Optional[FileChange] = None): + """配置文件变更回调函数""" + # TODO: 根据配置文件变动重新计算相关参数: + """ + 需要计算的参数: + self._enable_expression_use = expr_use # 允许使用表达方式,但不一定启用学习 + self._enable_expression_learning = expr_learn # 允许学习表达方式 + self._enable_jargon_learning = jargon_learn # 允许学习黑话 + """ + + # ====== 心流聊天核心逻辑 ====== async def _hfc_func(self, mentioned_message: Optional["SessionMessage"] = None): """心流聊天的主循环逻辑""" if self._consecutive_no_reply_count >= 5: @@ -166,7 +188,9 @@ class HeartFChatting: await asyncio.sleep(0.2) return True - talk_value_threshold = random.random() * self._get_talk_value(self.session_id) * self._talk_frequency_adjust + talk_value_threshold = ( + random.random() * ChatConfigUtils.get_talk_value(self.session_id) * self._talk_frequency_adjust + ) if mentioned_message and global_config.chat.mentioned_bot_reply: await self._judge_and_response(mentioned_message) elif random.random() < talk_value_threshold: @@ -175,14 +199,23 @@ class HeartFChatting: async def _judge_and_response(self, mentioned_message: Optional["SessionMessage"] = None): """判定和生成回复""" - if self._reflector: - await self._reflector.check_and_ask() - if self._reflector.reflect_tracker.tracking and await self._reflector.reflect_tracker.trigger_tracker(): - logger.info(f"{self.log_prefix} 追踪检查已解决,结束追踪器") - self._reflector.reflect_tracker.reset_tracker() # 结束当前追踪器 - + await self._trigger_reflector() + asyncio.create_task(self._trigger_expression_learning(self.message_cache)) # TODO: 完成反思器之后的逻辑 start_time = time.time() + current_cycle_detail = self._start_cycle() + logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") + + # TODO: 动作检查逻辑 + # TODO: Planner逻辑 + # TODO: 动作执行逻辑 + + cycle_detail = self._end_cycle(current_cycle_detail) + if wait_time := global_config.chat.planner_smooth - (time.time() - start_time) > 0: + await asyncio.sleep(wait_time) + else: + await asyncio.sleep(0.1) # 最小等待时间,避免过快循环 + return True def _handle_loop_completion(self, task: asyncio.Task): """当 _hfc_func 任务完成时执行的回调。""" @@ -195,59 +228,72 @@ class HeartFChatting: except asyncio.CancelledError: logger.info(f"{self.log_prefix} HeartFChatting: 结束了聊天") - def _get_talk_value(self, session_id: Optional[str]) -> float: - result = global_config.chat.talk_value or 0.0 - if not global_config.chat.enable_talk_value_rules or not global_config.chat.talk_value_rules: - return result - local_time = time.localtime() - now_min = local_time.tm_hour * 60 + local_time.tm_min + # ====== 反思器和学习器触发逻辑 ====== + async def _trigger_reflector(self): + await self._reflector.check_and_ask() + if self._reflector.reflect_tracker.tracking and await self._reflector.reflect_tracker.trigger_tracker(): + logger.info(f"{self.log_prefix} 追踪检查已解决,结束追踪器") + self._reflector.reflect_tracker.reset_tracker() # 结束当前追踪器 - # 优先匹配会话相关的规则 - if session_id: - for rule in global_config.chat.talk_value_rules: - if not rule.platform and not rule.item_id: - continue # 一起留空表示全局 - if rule.rule_type == "group": - rule_session_id = SessionUtils.calculate_session_id(rule.platform, group_id=str(rule.item_id)) - else: - rule_session_id = SessionUtils.calculate_session_id(rule.platform, user_id=str(rule.item_id)) - if rule_session_id != session_id: - continue # 不匹配的会话ID,跳过 - parsed_range = self._parse_range(rule.time) - if not parsed_range: - continue # 无法解析的时间范围,跳过 - start_min, end_min = parsed_range - in_range: bool = False - if start_min <= end_min: - in_range = start_min <= now_min <= end_min - else: # 跨天的时间范围 - in_range = now_min >= start_min or now_min <= end_min - if in_range: - return rule.value or 0.0 # 如果规则生效但没有设置值,返回0.0 + async def _trigger_expression_learning(self, messages: List["SessionMessage"]): + self._expression_learner.add_messages(messages) + if time.time() - self._last_extraction_time < self._min_extraction_interval: + return + if self._expression_learner.get_cache_size() < self._min_messages_for_extraction: + return + extraction_end_time = time.time() + logger.info( + f"聊天流 {self.session_name} 提取到 {len(messages)} 条消息," + f"时间窗口: {self._last_extraction_time:.2f} - {extraction_end_time:.2f}" + ) + self._last_extraction_time = extraction_end_time + if self._enable_expression_learning: + asyncio.create_task(self._expression_learning()) - # 没有匹配到会话相关的规则,继续匹配全局规则 - for rule in global_config.chat.talk_value_rules: - if rule.platform or rule.item_id: - continue # 只匹配全局规则 - parsed_range = self._parse_range(rule.time) - if not parsed_range: - continue # 无法解析的时间范围,跳过 - start_min, end_min = parsed_range - in_range: bool = False - if start_min <= end_min: - in_range = start_min <= now_min <= end_min - else: # 跨天的时间范围 - in_range = now_min >= start_min or now_min <= end_min - if in_range: - return rule.value or 0.0 # 如果规则生效但没有设置值,返回0.0 - return result # 如果没有任何规则生效,返回默认值 - - def _parse_range(self, range_str: str) -> Optional[tuple[int, int]]: - """解析 "HH:MM-HH:MM" 到 (start_min, end_min)。""" + async def _expression_learning(self): try: - start_str, end_str = [s.strip() for s in range_str.split("-")] - sh, sm = [int(x) for x in start_str.split(":")] - eh, em = [int(x) for x in end_str.split(":")] - return sh * 60 + sm, eh * 60 + em - except Exception: - return None + learnt_style = await self._expression_learner.learn() + if learnt_style: + logger.info(f"{self.log_prefix} 表达学习完成") + else: + logger.debug(f"{self.log_prefix} 表达学习未获得有效结果") + except Exception as e: + logger.error(f"{self.log_prefix} 表达学习失败: {e}", exc_info=True) + + # ====== 记录循环执行信息相关逻辑 ====== + def _start_cycle(self) -> CycleDetail: + self._cycle_counter += 1 + current_cycle_detail = CycleDetail(cycle_id=self._cycle_counter) + current_cycle_detail.thinking_id = f"tid{str(round(time.time(), 2))}" + return current_cycle_detail + + def _end_cycle(self, cycle_detail: CycleDetail, only_long_execution: bool = True): + cycle_detail.end_time = time.time() + timer_strings: List[str] = [ + f"{name}: {duration:.2f}s" + for name, duration in cycle_detail.time_records.items() + if not only_long_execution or duration >= 0.1 + ] + logger.info( + f"{self.log_prefix} 第 {cycle_detail.cycle_id} 个心流循环完成" + f"耗时: {cycle_detail.end_time - cycle_detail.start_time:.2f}秒\n" + f"详细计时: {', '.join(timer_strings) if timer_strings else '无'}" + ) + + return cycle_detail + + # ====== Action相关逻辑 ====== + async def _execute_action(self, *args, **kwargs): + """原ExecuteAction""" + raise NotImplementedError("执行动作的逻辑尚未实现") # TODO: 实现动作执行的逻辑,替换掉*args, **kwargs*占位符 + + async def _execute_other_actions(self, *args, **kwargs): + """原HandleAction""" + raise NotImplementedError( + "执行其他动作的逻辑尚未实现" + ) # TODO: 实现其他动作执行的逻辑, 替换掉*args, **kwargs*占位符 + + # ====== 响应发送相关方法 ====== + async def _send_response(self, *args, **kwargs): + raise NotImplementedError("发送回复的逻辑尚未实现") # TODO: 实现发送回复的逻辑,替换掉*args, **kwargs*占位符 + # 传入的消息至少应该是个MessageSequence实例,最好是SessionMessage实例,随后可直接转化为MessageSending实例 diff --git a/src/chat/heart_flow/heartFC_chat_old.py b/src/chat/heart_flow/heartFC_chat_old.py index 2842758f..a9fabf2c 100644 --- a/src/chat/heart_flow/heartFC_chat_old.py +++ b/src/chat/heart_flow/heartFC_chat_old.py @@ -15,7 +15,7 @@ from src.chat.utils.timer_calculator import Timer from src.chat.planner_actions.planner import ActionPlanner 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_old import CycleDetail from src.bw_learner.expression_learner_old import expression_learner_manager from src.chat.heart_flow.frequency_control import frequency_control_manager from src.bw_learner.reflect_tracker import reflect_tracker_manager @@ -155,7 +155,7 @@ class HeartFChatting: def start_cycle(self) -> Tuple[Dict[str, float], str]: self._cycle_counter += 1 - self._current_cycle_detail = CycleDetail(self._cycle_counter) + self._current_cycle_detail = CycleDetail(cycle_id=self._cycle_counter) self._current_cycle_detail.thinking_id = f"tid{str(round(time.time(), 2))}" cycle_timers = {} return cycle_timers, self._current_cycle_detail.thinking_id diff --git a/src/chat/heart_flow/heartFC_utils.py b/src/chat/heart_flow/heartFC_utils.py new file mode 100644 index 00000000..f1fd12bd --- /dev/null +++ b/src/chat/heart_flow/heartFC_utils.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import Optional, Dict, TypedDict + +import time + + +@dataclass +class CyclePlanInfo(TypedDict): ... # TODO: 根据实际需要补充字段 + + +@dataclass +class CycleActionInfo(TypedDict): ... # TODO: 根据实际需要补充字段 + + +@dataclass +class CycleDetail: + """循环信息记录类""" + + cycle_id: int + thinking_id: str = "" + """思考ID""" + start_time: float = time.time() + """开始时间,单位为秒""" + end_time: Optional[float] = None + """结束时间,单位为秒,None表示未结束""" + time_records: Dict[str, float] = {} + """计时器记录,key为计时器名称,value为用时,单位为秒""" + loop_plan_info: Optional[CyclePlanInfo] = None + """循环计划记录""" + loop_action_info: Optional[CycleActionInfo] = None + """循环Action调用记录""" diff --git a/src/chat/heart_flow/hfc_utils.py b/src/chat/heart_flow/hfc_utils_old.py similarity index 53% rename from src/chat/heart_flow/hfc_utils.py rename to src/chat/heart_flow/hfc_utils_old.py index 20843bf6..43db6a5d 100644 --- a/src/chat/heart_flow/hfc_utils.py +++ b/src/chat/heart_flow/hfc_utils_old.py @@ -31,61 +31,6 @@ class CycleDetail: self.end_time: Optional[float] = None self.timers: Dict[str, float] = {} - self.loop_plan_info: CyclePlanInfo = CyclePlanInfo() - self.loop_action_info: CycleActionInfo = CycleActionInfo() - - def to_dict(self) -> Dict[str, Any]: - """将循环信息转换为字典格式""" - - def convert_to_serializable(obj, depth=0, seen=None): - if seen is None: - seen = set() - - # 防止递归过深 - if depth > 5: # 降低递归深度限制 - return str(obj) - - # 防止循环引用 - obj_id = id(obj) - if obj_id in seen: - return str(obj) - seen.add(obj_id) - - try: - if hasattr(obj, "to_dict"): - # 对于有to_dict方法的对象,直接调用其to_dict方法 - return obj.to_dict() - elif isinstance(obj, dict): - # 对于字典,只保留基本类型和可序列化的值 - return { - k: convert_to_serializable(v, depth + 1, seen) - for k, v in obj.items() - if isinstance(k, (str, int, float, bool)) - } - elif isinstance(obj, (list, tuple)): - # 对于列表和元组,只保留可序列化的元素 - return [ - convert_to_serializable(item, depth + 1, seen) - for item in obj - if not isinstance(item, (dict, list, tuple)) - or isinstance(item, (str, int, float, bool, type(None))) - ] - elif isinstance(obj, (str, int, float, bool, type(None))): - return obj - else: - return str(obj) - finally: - seen.remove(obj_id) - - return { - "cycle_id": self.cycle_id, - "start_time": self.start_time, - "end_time": self.end_time, - "timers": self.timers, - "thinking_id": self.thinking_id, - "loop_plan_info": convert_to_serializable(self.loop_plan_info), - "loop_action_info": convert_to_serializable(self.loop_action_info), - } def set_loop_info(self, loop_info: Dict[str, Any]): """设置循环信息""" diff --git a/src/common/utils/utils_config.py b/src/common/utils/utils_config.py new file mode 100644 index 00000000..bd571629 --- /dev/null +++ b/src/common/utils/utils_config.py @@ -0,0 +1,133 @@ +from typing import Optional + +import time + +from src.common.logger import get_logger +from src.config.config import global_config + +logger = get_logger("config_utils") + + +class ExpressionConfigUtils: + @staticmethod + def get_expression_config_for_chat(session_id: Optional[str] = None) -> tuple[bool, bool, bool]: + # sourcery skip: use-next + """ + 根据聊天会话ID获取表达配置 + + Args: + session_id: 聊天会话ID,格式为哈希值 + + Returns: + tuple: (是否使用表达, 是否学习表达, 是否启用jargon学习) + """ + if not global_config.expression.learning_list: + return True, True, True + + if session_id: + for config_item in global_config.expression.learning_list: + if not config_item.platform and not config_item.item_id: + continue # 这是全局的 + stream_id = ExpressionConfigUtils._get_stream_id( + config_item.platform, + str(config_item.item_id), + (config_item.rule_type == "group"), + ) + if stream_id is None: + continue + if stream_id == session_id: + continue + return config_item.use_expression, config_item.enable_learning, config_item.enable_jargon_learning + for config_item in global_config.expression.learning_list: + if not config_item.platform and not config_item.item_id: + return config_item.use_expression, config_item.enable_learning, config_item.enable_jargon_learning + + return True, True, True + + @staticmethod + def _get_stream_id(platform: str, id_str: str, is_group: bool = False) -> Optional[str]: + # sourcery skip: remove-unnecessary-cast + """ + 根据平台、ID字符串和是否为群聊生成聊天流ID + + Args: + platform: 平台名称 + id_str: 用户或群组的原始ID字符串 + is_group: 是否为群聊 + + Returns: + str: 生成的聊天流ID(哈希值) + """ + try: + from src.common.utils.utils_session import SessionUtils + + if is_group: + return SessionUtils.calculate_session_id(platform, group_id=str(id_str)) + else: + return SessionUtils.calculate_session_id(platform, user_id=str(id_str)) + except Exception as e: + logger.error(f"生成聊天流ID失败: {e}") + return None + + +class ChatConfigUtils: + @staticmethod + def get_talk_value(session_id: Optional[str]) -> float: + result = global_config.chat.talk_value or 0.0 + if not global_config.chat.enable_talk_value_rules or not global_config.chat.talk_value_rules: + return result + local_time = time.localtime() + now_min = local_time.tm_hour * 60 + local_time.tm_min + + # 优先匹配会话相关的规则 + if session_id: + from src.common.utils.utils_session import SessionUtils + + for rule in global_config.chat.talk_value_rules: + if not rule.platform and not rule.item_id: + continue # 一起留空表示全局 + if rule.rule_type == "group": + rule_session_id = SessionUtils.calculate_session_id(rule.platform, group_id=str(rule.item_id)) + else: + rule_session_id = SessionUtils.calculate_session_id(rule.platform, user_id=str(rule.item_id)) + if rule_session_id != session_id: + continue # 不匹配的会话ID,跳过 + parsed_range = ChatConfigUtils.parse_range(rule.time) + if not parsed_range: + continue # 无法解析的时间范围,跳过 + start_min, end_min = parsed_range + in_range: bool = False + if start_min <= end_min: + in_range = start_min <= now_min <= end_min + else: # 跨天的时间范围 + in_range = now_min >= start_min or now_min <= end_min + if in_range: + return rule.value or 0.0 # 如果规则生效但没有设置值,返回0.0 + + # 没有匹配到会话相关的规则,继续匹配全局规则 + for rule in global_config.chat.talk_value_rules: + if rule.platform or rule.item_id: + continue # 只匹配全局规则 + parsed_range = ChatConfigUtils.parse_range(rule.time) + if not parsed_range: + continue # 无法解析的时间范围,跳过 + start_min, end_min = parsed_range + in_range: bool = False + if start_min <= end_min: + in_range = start_min <= now_min <= end_min + else: # 跨天的时间范围 + in_range = now_min >= start_min or now_min <= end_min + if in_range: + return rule.value or 0.0 # 如果规则生效但没有设置值,返回0.0 + return result # 如果没有任何规则生效,返回默认值 + + @staticmethod + def parse_range(range_str: str) -> Optional[tuple[int, int]]: + """解析 "HH:MM-HH:MM" 到 (start_min, end_min)。""" + try: + start_str, end_str = [s.strip() for s in range_str.split("-")] + sh, sm = [int(x) for x in start_str.split(":")] + eh, em = [int(x) for x in end_str.split(":")] + return sh * 60 + sm, eh * 60 + em + except Exception: + return None