diff --git a/README.md b/README.md index 656f536a..7eca2260 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

- Logo + Logo
@@ -34,7 +34,6 @@ · 提出新特性

-

## 新版0.6.x部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a @@ -53,7 +52,7 @@
- 麦麦演示视频 + 麦麦演示视频
👆 点击观看麦麦演示视频 👆
@@ -99,7 +98,7 @@
-

📚 文档

+

📚 文档

### (部分内容可能过时,请注意版本对应) @@ -186,7 +185,7 @@ MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献, 感谢各位大佬! - + contributors **也感谢每一位给麦麦发展提出宝贵意见与建议的用户,感谢陪伴麦麦走到现在的你们** diff --git a/src/MaiBot0.6roadmap.md b/src/MaiBot0.6roadmap.md new file mode 100644 index 00000000..54774197 --- /dev/null +++ b/src/MaiBot0.6roadmap.md @@ -0,0 +1,16 @@ +MaiCore/MaiBot 0.6路线图 draft + +0.6.3:解决0.6.x版本核心问题,改进功能 +主要功能加入 +LPMM全面替代旧知识库 +采用新的HFC回复模式,取代旧心流 +合并推理模式和心流模式,根据麦麦自己决策回复模式 +提供新的表情包系统 + +0.6.4:提升用户体验,交互优化 +加入webui +提供麦麦 API +修复prompt建构的各种问题 +修复各种bug +调整代码文件结构,重构部分落后设计 + diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 00000000..f5bc08a6 --- /dev/null +++ b/src/api/__init__.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI +from strawberry.fastapi import GraphQLRouter + +app = FastAPI() + +graphql_router = GraphQLRouter(schema=None, path="/") # Replace `None` with your actual schema + +app.include_router(graphql_router, prefix="/graphql", tags=["GraphQL"]) diff --git a/src/api/config_api.py b/src/api/config_api.py new file mode 100644 index 00000000..e3934617 --- /dev/null +++ b/src/api/config_api.py @@ -0,0 +1,155 @@ +from typing import Dict, List, Optional +import strawberry + +# from packaging.version import Version, InvalidVersion +# from packaging.specifiers import SpecifierSet, InvalidSpecifier +# from ..config.config import global_config +# import os +from packaging.version import Version + + +@strawberry.type +class BotConfig: + """机器人配置类""" + + INNER_VERSION: Version + MAI_VERSION: str # 硬编码的版本信息 + + # bot + BOT_QQ: Optional[int] + BOT_NICKNAME: Optional[str] + BOT_ALIAS_NAMES: List[str] # 别名,可以通过这个叫它 + + # group + talk_allowed_groups: set + talk_frequency_down_groups: set + ban_user_id: set + + # personality + personality_core: str # 建议20字以内,谁再写3000字小作文敲谁脑袋 + personality_sides: List[str] + # identity + identity_detail: List[str] + height: int # 身高 单位厘米 + weight: int # 体重 单位千克 + age: int # 年龄 单位岁 + gender: str # 性别 + appearance: str # 外貌特征 + + # schedule + ENABLE_SCHEDULE_GEN: bool # 是否启用日程生成 + PROMPT_SCHEDULE_GEN: str + SCHEDULE_DOING_UPDATE_INTERVAL: int # 日程表更新间隔 单位秒 + SCHEDULE_TEMPERATURE: float # 日程表温度,建议0.5-1.0 + TIME_ZONE: str # 时区 + + # message + MAX_CONTEXT_SIZE: int # 上下文最大消息数 + emoji_chance: float # 发送表情包的基础概率 + thinking_timeout: int # 思考时间 + max_response_length: int # 最大回复长度 + message_buffer: bool # 消息缓冲器 + + ban_words: set + ban_msgs_regex: set + # heartflow + # enable_heartflow: bool = False # 是否启用心流 + sub_heart_flow_update_interval: int # 子心流更新频率,间隔 单位秒 + sub_heart_flow_freeze_time: int # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒 + sub_heart_flow_stop_time: int # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒 + heart_flow_update_interval: int # 心流更新频率,间隔 单位秒 + observation_context_size: int # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩 + compressed_length: int # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5 + compress_length_limit: int # 最多压缩份数,超过该数值的压缩上下文会被删除 + + # willing + willing_mode: str # 意愿模式 + response_willing_amplifier: float # 回复意愿放大系数 + response_interested_rate_amplifier: float # 回复兴趣度放大系数 + down_frequency_rate: float # 降低回复频率的群组回复意愿降低系数 + emoji_response_penalty: float # 表情包回复惩罚 + mentioned_bot_inevitable_reply: bool # 提及 bot 必然回复 + at_bot_inevitable_reply: bool # @bot 必然回复 + + # response + response_mode: str # 回复策略 + MODEL_R1_PROBABILITY: float # R1模型概率 + MODEL_V3_PROBABILITY: float # V3模型概率 + # MODEL_R1_DISTILL_PROBABILITY: float # R1蒸馏模型概率 + + # emoji + max_emoji_num: int # 表情包最大数量 + max_reach_deletion: bool # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包 + EMOJI_CHECK_INTERVAL: int # 表情包检查间隔(分钟) + EMOJI_REGISTER_INTERVAL: int # 表情包注册间隔(分钟) + EMOJI_SAVE: bool # 偷表情包 + EMOJI_CHECK: bool # 是否开启过滤 + EMOJI_CHECK_PROMPT: str # 表情包过滤要求 + + # memory + build_memory_interval: int # 记忆构建间隔(秒) + memory_build_distribution: list # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 + build_memory_sample_num: int # 记忆构建采样数量 + build_memory_sample_length: int # 记忆构建采样长度 + memory_compress_rate: float # 记忆压缩率 + + forget_memory_interval: int # 记忆遗忘间隔(秒) + memory_forget_time: int # 记忆遗忘时间(小时) + memory_forget_percentage: float # 记忆遗忘比例 + + memory_ban_words: list # 添加新的配置项默认值 + + # mood + mood_update_interval: float # 情绪更新间隔 单位秒 + mood_decay_rate: float # 情绪衰减率 + mood_intensity_factor: float # 情绪强度因子 + + # keywords + keywords_reaction_rules: list # 关键词回复规则 + + # chinese_typo + chinese_typo_enable: bool # 是否启用中文错别字生成器 + chinese_typo_error_rate: float # 单字替换概率 + chinese_typo_min_freq: int # 最小字频阈值 + chinese_typo_tone_error_rate: float # 声调错误概率 + chinese_typo_word_replace_rate: float # 整词替换概率 + + # response_splitter + enable_response_splitter: bool # 是否启用回复分割器 + response_max_length: int # 回复允许的最大长度 + response_max_sentence_num: int # 回复允许的最大句子数 + + # remote + remote_enable: bool # 是否启用远程控制 + + # experimental + enable_friend_chat: bool # 是否启用好友聊天 + # enable_think_flow: bool # 是否启用思考流程 + enable_pfc_chatting: bool # 是否启用PFC聊天 + + # 模型配置 + llm_reasoning: Dict[str, str] # LLM推理 + # llm_reasoning_minor: Dict[str, str] + llm_normal: Dict[str, str] # LLM普通 + llm_topic_judge: Dict[str, str] # LLM话题判断 + llm_summary_by_topic: Dict[str, str] # LLM话题总结 + llm_emotion_judge: Dict[str, str] # LLM情感判断 + embedding: Dict[str, str] # 嵌入 + vlm: Dict[str, str] # VLM + moderation: Dict[str, str] # 审核 + + # 实验性 + llm_observation: Dict[str, str] # LLM观察 + llm_sub_heartflow: Dict[str, str] # LLM子心流 + llm_heartflow: Dict[str, str] # LLM心流 + + api_urls: Dict[str, str] # API URLs + + +@strawberry.type +class EnvConfig: + pass + + @strawberry.field + def get_env(self) -> str: + return "env" diff --git a/src/config/config.py b/src/config/config.py index 0dae0244..bf184a00 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -28,7 +28,7 @@ logger = get_module_logger("config", config=config_config) # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 is_test = True mai_version_main = "0.6.3" -mai_version_fix = "snapshot-2" +mai_version_fix = "snapshot-3" if mai_version_fix: if is_test: @@ -186,12 +186,18 @@ class BotConfig: ban_words = set() ban_msgs_regex = set() - # heartflow - # enable_heartflow: bool = False # 是否启用心流 - sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒 - sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒 + # [heartflow] # 启用启用heart_flowC(心流聊天)模式时生效, 需要填写token消耗量巨大的相关模型 + # 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间), 进行长时间高质量的聊天 + enable_heart_flowC: bool = True # 是否启用heart_flowC(心流聊天, HFC)模式 + reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发 + probability_decay_factor_per_second: float = 0.2 # 概率衰减因子,越大衰减越快 + default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢 + initial_duration: int = 60 # 初始持续时间,越大心流聊天持续的时间越长 + + # sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒 + # sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒 sub_heart_flow_stop_time: int = 600 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒 - heart_flow_update_interval: int = 300 # 心流更新频率,间隔 单位秒 + # heart_flow_update_interval: int = 300 # 心流更新频率,间隔 单位秒 observation_context_size: int = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩 compressed_length: int = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5 compress_length_limit: int = 5 # 最多压缩份数,超过该数值的压缩上下文会被删除 @@ -207,8 +213,8 @@ class BotConfig: # response response_mode: str = "heart_flow" # 回复策略 - MODEL_R1_PROBABILITY: float = 0.8 # R1模型概率 - MODEL_V3_PROBABILITY: float = 0.1 # V3模型概率 + model_reasoning_probability: float = 0.7 # 麦麦回答时选择推理模型(主要)模型概率 + model_normal_probability: float = 0.3 # 麦麦回答时选择一般模型(次要)模型概率 # MODEL_R1_DISTILL_PROBABILITY: float = 0.1 # R1蒸馏模型概率 # emoji @@ -401,29 +407,34 @@ class BotConfig: def response(parent: dict): response_config = parent["response"] - config.MODEL_R1_PROBABILITY = response_config.get("model_r1_probability", config.MODEL_R1_PROBABILITY) - config.MODEL_V3_PROBABILITY = response_config.get("model_v3_probability", config.MODEL_V3_PROBABILITY) - # config.MODEL_R1_DISTILL_PROBABILITY = response_config.get( - # "model_r1_distill_probability", config.MODEL_R1_DISTILL_PROBABILITY - # ) - config.max_response_length = response_config.get("max_response_length", config.max_response_length) - if config.INNER_VERSION in SpecifierSet(">=1.0.4"): - config.response_mode = response_config.get("response_mode", config.response_mode) + config.model_reasoning_probability = response_config.get( + "model_reasoning_probability", config.model_reasoning_probability + ) + config.model_normal_probability = response_config.get( + "model_normal_probability", config.model_normal_probability + ) + + # 添加 enable_heart_flowC 的加载逻辑 (假设它在 [response] 部分) + if config.INNER_VERSION in SpecifierSet(">=1.4.0"): + config.enable_heart_flowC = response_config.get("enable_heart_flowC", config.enable_heart_flowC) def heartflow(parent: dict): heartflow_config = parent["heartflow"] - config.sub_heart_flow_update_interval = heartflow_config.get( - "sub_heart_flow_update_interval", config.sub_heart_flow_update_interval - ) - config.sub_heart_flow_freeze_time = heartflow_config.get( - "sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time - ) + # 加载新增的 heartflowC 参数 + + # 加载原有的 heartflow 参数 + # config.sub_heart_flow_update_interval = heartflow_config.get( + # "sub_heart_flow_update_interval", config.sub_heart_flow_update_interval + # ) + # config.sub_heart_flow_freeze_time = heartflow_config.get( + # "sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time + # ) config.sub_heart_flow_stop_time = heartflow_config.get( "sub_heart_flow_stop_time", config.sub_heart_flow_stop_time ) - config.heart_flow_update_interval = heartflow_config.get( - "heart_flow_update_interval", config.heart_flow_update_interval - ) + # config.heart_flow_update_interval = heartflow_config.get( + # "heart_flow_update_interval", config.heart_flow_update_interval + # ) if config.INNER_VERSION in SpecifierSet(">=1.3.0"): config.observation_context_size = heartflow_config.get( "observation_context_size", config.observation_context_size @@ -432,6 +443,17 @@ class BotConfig: config.compress_length_limit = heartflow_config.get( "compress_length_limit", config.compress_length_limit ) + if config.INNER_VERSION in SpecifierSet(">=1.4.0"): + config.reply_trigger_threshold = heartflow_config.get( + "reply_trigger_threshold", config.reply_trigger_threshold + ) + config.probability_decay_factor_per_second = heartflow_config.get( + "probability_decay_factor_per_second", config.probability_decay_factor_per_second + ) + config.default_decay_rate_per_second = heartflow_config.get( + "default_decay_rate_per_second", config.default_decay_rate_per_second + ) + config.initial_duration = heartflow_config.get("initial_duration", config.initial_duration) def willing(parent: dict): willing_config = parent["willing"] diff --git a/src/heart_flow/L{QA$T9C4`IVQEAB3WZYFXL.jpg b/src/heart_flow/L{QA$T9C4`IVQEAB3WZYFXL.jpg deleted file mode 100644 index 186b34de..00000000 Binary files a/src/heart_flow/L{QA$T9C4`IVQEAB3WZYFXL.jpg and /dev/null differ diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index 5e442d8f..9b392a94 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -79,4 +79,16 @@ await heartflow.heartflow_start_working() 1. 子心流会在长时间不活跃后自动清理 2. 需要合理配置更新间隔以平衡性能和响应速度 -3. 观察系统会限制消息处理数量以避免过载 \ No newline at end of file +3. 观察系统会限制消息处理数量以避免过载 + + +更新: +把聊天控制移动到心流下吧 +首先心流要根据日程以及当前状况判定总体状态MaiStateInfo + +然后根据每个子心流的运行情况,给子心流分配聊天资源(ChatStateInfo:ABSENT CHAT 或者 FOCUS) + +子心流负责根据状态进行执行 + +1.将interest.py进行拆分,class InterestChatting 将会在 sub_heartflow中声明,每个sub_heartflow都会所属一个InterestChatting +class InterestManager 将会在heartflow中声明,成为heartflow的一个组件,伴随heartflow产生 diff --git a/src/heart_flow/SKG`8J~]3I~E8WEB%Y85I`M.jpg b/src/heart_flow/SKG`8J~]3I~E8WEB%Y85I`M.jpg deleted file mode 100644 index dc86382f..00000000 Binary files a/src/heart_flow/SKG`8J~]3I~E8WEB%Y85I`M.jpg and /dev/null differ diff --git a/src/heart_flow/Update.md b/src/heart_flow/Update.md new file mode 100644 index 00000000..45a45723 --- /dev/null +++ b/src/heart_flow/Update.md @@ -0,0 +1,11 @@ + +更新: +把聊天控制移动到心流下吧 +首先心流要根据日程以及当前状况判定总体状态MaiStateInfo + +然后根据每个子心流的运行情况,给子心流分配聊天资源(ChatStateInfo:ABSENT CHAT 或者 FOCUS) + +子心流负责根据状态进行执行 + +1.将interest.py进行拆分,class InterestChatting 将会在 sub_heartflow中声明,每个sub_heartflow都会所属一个InterestChatting +class InterestManager 将会在heartflow中声明,成为heartflow的一个组件,伴随heartflow产生 diff --git a/src/heart_flow/ZX65~ALHC_7{Q9FKE$X}TQC.jpg b/src/heart_flow/ZX65~ALHC_7{Q9FKE$X}TQC.jpg deleted file mode 100644 index a2490075..00000000 Binary files a/src/heart_flow/ZX65~ALHC_7{Q9FKE$X}TQC.jpg and /dev/null differ diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py index 793f406f..f30621b0 100644 --- a/src/heart_flow/heartflow.py +++ b/src/heart_flow/heartflow.py @@ -1,16 +1,20 @@ -from .sub_heartflow import SubHeartflow -from .observation import ChattingObservation +from .sub_heartflow import SubHeartflow, ChattingObservation from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config from src.plugins.schedule.schedule_generator import bot_schedule from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager import asyncio -from src.common.logger import get_module_logger, LogConfig, HEARTFLOW_STYLE_CONFIG # noqa: E402 +from src.common.logger import get_module_logger, LogConfig, HEARTFLOW_STYLE_CONFIG # 修改 from src.individuality.individuality import Individuality import time import random -from typing import Dict, Any +from typing import Dict, Any, Optional +import traceback +import enum +import os # 新增 +import json # 新增 +from src.plugins.chat.chat_stream import chat_manager # 新增 heartflow_config = LogConfig( # 使用海马体专用样式 @@ -41,76 +45,269 @@ def init_prompt(): Prompt(prompt, "mind_summary_prompt") -class CurrentState: +# --- 新增:从 interest.py 移动过来的常量 --- +LOG_DIRECTORY = "logs/interest" +HISTORY_LOG_FILENAME = "interest_history.log" +CLEANUP_INTERVAL_SECONDS = 1200 # 清理任务运行间隔 (例如:20分钟) - 保持与 interest.py 一致 +INACTIVE_THRESHOLD_SECONDS = 1200 # 不活跃时间阈值 (例如:20分钟) - 保持与 interest.py 一致 +LOG_INTERVAL_SECONDS = 3 # 日志记录间隔 (例如:3秒) - 保持与 interest.py 一致 +# --- 结束新增常量 --- + + +# 新增 ChatStatus 枚举 +class MaiState(enum.Enum): + """ + 聊天状态: + OFFLINE: 不在线:回复概率极低,不会进行任何聊天 + PEEKING: 看一眼手机:回复概率较低,会进行一些普通聊天 + NORMAL_CHAT: 正常聊天:回复概率较高,会进行一些普通聊天和少量的专注聊天 + FOCUSED_CHAT: 专注聊天:回复概率极高,会进行专注聊天和少量的普通聊天 + """ + + OFFLINE = "不在线" + PEEKING = "看一眼手机" + NORMAL_CHAT = "正常聊天" + FOCUSED_CHAT = "专注聊天" + + def get_normal_chat_max_num(self): + if self == MaiState.OFFLINE: + return 0 + elif self == MaiState.PEEKING: + return 1 + elif self == MaiState.NORMAL_CHAT: + return 3 + elif self == MaiState.FOCUSED_CHAT: + return 2 + + def get_focused_chat_max_num(self): + if self == MaiState.OFFLINE: + return 0 + elif self == MaiState.PEEKING: + return 0 + elif self == MaiState.NORMAL_CHAT: + return 1 + elif self == MaiState.FOCUSED_CHAT: + return 2 + + +class MaiStateInfo: def __init__(self): self.current_state_info = "" + # 使用枚举类型初始化状态,默认为不在线 + self.mai_status: MaiState = MaiState.OFFLINE + + self.normal_chatting = [] + self.focused_chatting = [] + self.mood_manager = MoodManager() self.mood = self.mood_manager.get_prompt() - self.attendance_factor = 0 - self.engagement_factor = 0 - def update_current_state_info(self): self.current_state_info = self.mood_manager.get_current_mood() + # 新增更新聊天状态的方法 + def update_mai_status(self, new_status: MaiState): + """更新聊天状态""" + if isinstance(new_status, MaiState): + self.mai_status = new_status + logger.info(f"麦麦状态更新为: {self.mai_status.value}") + else: + logger.warning(f"尝试设置无效的麦麦状态: {new_status}") + class Heartflow: def __init__(self): self.current_mind = "你什么也没想" self.past_mind = [] - self.current_state: CurrentState = CurrentState() + self.current_state: MaiStateInfo = MaiStateInfo() self.llm_model = LLMRequest( model=global_config.llm_heartflow, temperature=0.6, max_tokens=1000, request_type="heart_flow" ) self._subheartflows: Dict[Any, SubHeartflow] = {} - async def _cleanup_inactive_subheartflows(self): - """定期清理不活跃的子心流""" + # --- 新增:日志和清理相关属性 (从 InterestManager 移动) --- + self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME) + self._ensure_log_directory() # 初始化时确保目录存在 + self._cleanup_task: Optional[asyncio.Task] = None + self._logging_task: Optional[asyncio.Task] = None + # 注意:衰减任务 (_decay_task) 不再需要,衰减在 SubHeartflow 的 InterestChatting 内部处理 + # --- 结束新增属性 --- + + def _ensure_log_directory(self): # 新增方法 (从 InterestManager 移动) + """确保日志目录存在""" + # 移除 try-except 块,根据用户要求 + os.makedirs(LOG_DIRECTORY, exist_ok=True) + logger.info(f"Log directory '{LOG_DIRECTORY}' ensured.") + # except OSError as e: + # logger.error(f"Error creating log directory '{LOG_DIRECTORY}': {e}") + + async def _periodic_cleanup_task( + self, interval_seconds: int, max_age_seconds: int + ): # 新增方法 (从 InterestManager 移动和修改) + """后台清理任务的异步函数""" while True: - current_time = time.time() - inactive_subheartflows = [] + await asyncio.sleep(interval_seconds) + logger.info(f"[Heartflow] 运行定期清理 (间隔: {interval_seconds}秒)...") + self.cleanup_inactive_subheartflows(max_age_seconds=max_age_seconds) # 调用 Heartflow 自己的清理方法 - # 检查所有子心流 - for subheartflow_id, subheartflow in self._subheartflows.items(): - if ( - current_time - subheartflow.last_active_time > global_config.sub_heart_flow_stop_time - ): # 10分钟 = 600秒 - inactive_subheartflows.append(subheartflow_id) - logger.info(f"发现不活跃的子心流: {subheartflow_id}") + async def _periodic_log_task(self, interval_seconds: int): # 新增方法 (从 InterestManager 移动和修改) + """后台日志记录任务的异步函数 (记录所有子心流的兴趣历史数据)""" + while True: + await asyncio.sleep(interval_seconds) + try: + current_timestamp = time.time() + all_interest_states = self.get_all_interest_states() # 获取所有子心流的兴趣状态 - # 清理不活跃的子心流 - for subheartflow_id in inactive_subheartflows: - del self._subheartflows[subheartflow_id] - logger.info(f"已清理不活跃的子心流: {subheartflow_id}") + # 以追加模式打开历史日志文件 + # 移除 try-except IO 块,根据用户要求 + with open(self._history_log_file_path, "a", encoding="utf-8") as f: + count = 0 + # 创建 items 快照以安全迭代 + items_snapshot = list(all_interest_states.items()) + for stream_id, state in items_snapshot: + # 从 chat_manager 获取 group_name + group_name = stream_id # 默认值 + try: + chat_stream = chat_manager.get_stream(stream_id) + if chat_stream and chat_stream.group_info: + group_name = chat_stream.group_info.group_name + elif chat_stream and not chat_stream.group_info: # 处理私聊 + group_name = ( + f"私聊_{chat_stream.user_info.user_nickname}" + if chat_stream.user_info + else stream_id + ) + except Exception: + # 不记录警告,避免刷屏,使用默认 stream_id 即可 + # logger.warning(f"Could not get group name for stream_id {stream_id}: {e}") + pass # 静默处理 - await asyncio.sleep(30) # 每分钟检查一次 + log_entry = { + "timestamp": round(current_timestamp, 2), + "stream_id": stream_id, + "interest_level": state.get("interest_level", 0.0), # 使用 get 获取,提供默认值 + "group_name": group_name, + "reply_probability": state.get("current_reply_probability", 0.0), # 使用 get 获取 + "is_above_threshold": state.get("is_above_threshold", False), # 使用 get 获取 + } + # 将每个条目作为单独的 JSON 行写入 + f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") + count += 1 + # logger.debug(f"[Heartflow] Successfully appended {count} interest history entries to {self._history_log_file_path}") - async def _sub_heartflow_update(self): + # except IOError as e: + # logger.error(f"[Heartflow] Error writing interest history log to {self._history_log_file_path}: {e}") + except Exception as e: # 保留对其他异常的捕获 + logger.error(f"[Heartflow] Unexpected error during periodic history logging: {e}") + logger.error(traceback.format_exc()) # 记录 traceback + + def get_all_interest_states(self) -> Dict[str, Dict]: # 新增方法 + """获取所有活跃子心流的当前兴趣状态""" + states = {} + # 创建副本以避免在迭代时修改字典 + items_snapshot = list(self._subheartflows.items()) + for stream_id, subheartflow in items_snapshot: + try: + # 从 SubHeartflow 获取其 InterestChatting 的状态 + states[stream_id] = subheartflow.get_interest_state() + except Exception as e: + logger.warning(f"[Heartflow] Error getting interest state for subheartflow {stream_id}: {e}") + return states + + def cleanup_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): # 修改此方法以使用兴趣时间 + """ + 清理长时间不活跃的子心流记录 (基于兴趣交互时间) + max_age_seconds: 超过此时间未通过兴趣系统交互的将被清理 + """ + current_time = time.time() + keys_to_remove = [] + _initial_count = len(self._subheartflows) + + # 创建副本以避免在迭代时修改字典 + items_snapshot = list(self._subheartflows.items()) + + for subheartflow_id, subheartflow in items_snapshot: + should_remove = False + reason = "" + # 检查 InterestChatting 的最后交互时间 + last_interaction = subheartflow.interest_chatting.last_interaction_time + if max_age_seconds is not None and (current_time - last_interaction) > max_age_seconds: + should_remove = True + reason = ( + f"interest inactive time ({current_time - last_interaction:.0f}s) > max age ({max_age_seconds}s)" + ) + + if should_remove: + keys_to_remove.append(subheartflow_id) + stream_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id # 获取流名称 + logger.debug(f"[Heartflow] Marking stream {stream_name} for removal. Reason: {reason}") + + # 标记子心流让其后台任务停止 (如果其后台任务还在运行) + subheartflow.should_stop = True + + if keys_to_remove: + logger.info(f"[Heartflow] 清理识别到 {len(keys_to_remove)} 个不活跃的流。") + for key in keys_to_remove: + if key in self._subheartflows: + # 尝试取消子心流的后台任务 + task_to_cancel = self._subheartflows[key].task + if task_to_cancel and not task_to_cancel.done(): + task_to_cancel.cancel() + logger.debug(f"[Heartflow] Cancelled background task for subheartflow {key}") + # 从字典中删除 + del self._subheartflows[key] + stream_name = chat_manager.get_stream_name(key) or key # 获取流名称 + logger.debug(f"[Heartflow] 移除了流: {stream_name}") + final_count = len(self._subheartflows) # 直接获取当前长度 + logger.info(f"[Heartflow] 清理完成。移除了 {len(keys_to_remove)} 个流。当前数量: {final_count}") + else: + # logger.info(f"[Heartflow] 清理完成。没有流符合移除条件。当前数量: {initial_count}") # 减少日志噪音 + pass + + async def _sub_heartflow_update(self): # 这个任务目前作用不大,可以考虑移除或赋予新职责 while True: # 检查是否存在子心流 if not self._subheartflows: # logger.info("当前没有子心流,等待新的子心流创建...") - await asyncio.sleep(30) # 每分钟检查一次是否有新的子心流 + await asyncio.sleep(30) # 短暂休眠 continue - await self.do_a_thinking() - await asyncio.sleep(global_config.heart_flow_update_interval) # 5分钟思考一次 + # 当前无实际操作,只是等待 + await asyncio.sleep(300) async def heartflow_start_working(self): - # 启动清理任务 - asyncio.create_task(self._cleanup_inactive_subheartflows()) + # 启动清理任务 (使用新的 periodic_cleanup_task) + if self._cleanup_task is None or self._cleanup_task.done(): + self._cleanup_task = asyncio.create_task( + self._periodic_cleanup_task( + interval_seconds=CLEANUP_INTERVAL_SECONDS, + max_age_seconds=INACTIVE_THRESHOLD_SECONDS, + ) + ) + logger.info( + f"[Heartflow] 已创建定期清理任务。间隔: {CLEANUP_INTERVAL_SECONDS}s, 不活跃阈值: {INACTIVE_THRESHOLD_SECONDS}s" + ) + else: + logger.warning("[Heartflow] 跳过创建清理任务: 任务已在运行或存在。") - # 启动子心流更新任务 - asyncio.create_task(self._sub_heartflow_update()) + # 启动日志任务 (使用新的 periodic_log_task) + if self._logging_task is None or self._logging_task.done(): + self._logging_task = asyncio.create_task(self._periodic_log_task(interval_seconds=LOG_INTERVAL_SECONDS)) + logger.info(f"[Heartflow] 已创建定期日志任务。间隔: {LOG_INTERVAL_SECONDS}s") + else: + logger.warning("[Heartflow] 跳过创建日志任务: 任务已在运行或存在。") + + # (可选) 启动旧的子心流更新任务,如果它还有用的话 + # asyncio.create_task(self._sub_heartflow_update()) @staticmethod async def _update_current_state(): print("TODO") async def do_a_thinking(self): - logger.debug("麦麦大脑袋转起来了") + # logger.debug("麦麦大脑袋转起来了") self.current_state.update_current_state_info() # 开始构建prompt @@ -122,127 +319,152 @@ class Heartflow: prompt_personality += personality_core personality_sides = individuality.personality.personality_sides - random.shuffle(personality_sides) - prompt_personality += f",{personality_sides[0]}" + # 检查列表是否为空 + if personality_sides: + random.shuffle(personality_sides) + prompt_personality += f",{personality_sides[0]}" identity_detail = individuality.identity.identity_detail - random.shuffle(identity_detail) - prompt_personality += f",{identity_detail[0]}" + # 检查列表是否为空 + if identity_detail: + random.shuffle(identity_detail) + prompt_personality += f",{identity_detail[0]}" personality_info = prompt_personality current_thinking_info = self.current_mind mood_info = self.current_state.mood - related_memory_info = "memory" + related_memory_info = "memory" # TODO: 替换为实际的记忆获取逻辑 try: - sub_flows_info = await self.get_all_subheartflows_minds() + sub_flows_info = await self.get_all_subheartflows_minds_summary() # 修改为调用汇总方法 except Exception as e: - logger.error(f"获取子心流的想法失败: {e}") - return + logger.error(f"[Heartflow] 获取子心流想法汇总失败: {e}") + logger.error(traceback.format_exc()) + sub_flows_info = "(获取子心流想法时出错)" # 提供默认值 schedule_info = bot_schedule.get_current_num_task(num=4, time_info=True) - # prompt = "" - # prompt += f"你刚刚在做的事情是:{schedule_info}\n" - # prompt += f"{personality_info}\n" - # prompt += f"你想起来{related_memory_info}。" - # prompt += f"刚刚你的主要想法是{current_thinking_info}。" - # prompt += f"你还有一些小想法,因为你在参加不同的群聊天,这是你正在做的事情:{sub_flows_info}\n" - # prompt += f"你现在{mood_info}。" - # prompt += "现在你接下去继续思考,产生新的想法,但是要基于原有的主要想法,不要分点输出," - # prompt += "输出连贯的内心独白,不要太长,但是记得结合上述的消息,关注新内容:" prompt = (await global_prompt_manager.get_prompt_async("thinking_prompt")).format( - schedule_info, personality_info, related_memory_info, current_thinking_info, sub_flows_info, mood_info + schedule_info=schedule_info, # 使用关键字参数确保正确格式化 + personality_info=personality_info, + related_memory_info=related_memory_info, + current_thinking_info=current_thinking_info, + sub_flows_info=sub_flows_info, + mood_info=mood_info, ) try: response, reasoning_content = await self.llm_model.generate_response_async(prompt) + if not response: + logger.warning("[Heartflow] 内心独白 LLM 返回空结果。") + response = "(暂时没什么想法...)" # 提供默认想法 + + self.update_current_mind(response) # 更新主心流想法 + logger.info(f"麦麦的总体脑内状态:{self.current_mind}") + + # 更新所有子心流的主心流信息 + items_snapshot = list(self._subheartflows.items()) # 创建快照 + for _, subheartflow in items_snapshot: + subheartflow.main_heartflow_info = response + except Exception as e: - logger.error(f"内心独白获取失败: {e}") - return - self.update_current_mind(response) - - self.current_mind = response - logger.info(f"麦麦的总体脑内状态:{self.current_mind}") - # logger.info("麦麦想了想,当前活动:") - # await bot_schedule.move_doing(self.current_mind) - - for _, subheartflow in self._subheartflows.items(): - subheartflow.main_heartflow_info = response + logger.error(f"[Heartflow] 内心独白获取失败: {e}") + logger.error(traceback.format_exc()) + # 此处不返回,允许程序继续执行,但主心流想法未更新 def update_current_mind(self, response): self.past_mind.append(self.current_mind) self.current_mind = response - async def get_all_subheartflows_minds(self): - sub_minds = "" - for _, subheartflow in self._subheartflows.items(): - sub_minds += subheartflow.current_mind + async def get_all_subheartflows_minds_summary(self): # 重命名并修改 + """获取所有子心流的当前想法,并进行汇总""" + sub_minds_list = [] + # 创建快照 + items_snapshot = list(self._subheartflows.items()) + for _, subheartflow in items_snapshot: + sub_minds_list.append(subheartflow.current_mind) - return await self.minds_summary(sub_minds) + if not sub_minds_list: + return "(当前没有活跃的子心流想法)" + + minds_str = "\n".join([f"- {mind}" for mind in sub_minds_list]) # 格式化为列表 + + # 调用 LLM 进行汇总 + return await self.minds_summary(minds_str) async def minds_summary(self, minds_str): + """使用 LLM 汇总子心流的想法字符串""" # 开始构建prompt prompt_personality = "你" - # person individuality = Individuality.get_instance() - - personality_core = individuality.personality.personality_core - prompt_personality += personality_core - - personality_sides = individuality.personality.personality_sides - random.shuffle(personality_sides) - prompt_personality += f",{personality_sides[0]}" - - identity_detail = individuality.identity.identity_detail - random.shuffle(identity_detail) - prompt_personality += f",{identity_detail[0]}" + prompt_personality += individuality.personality.personality_core + if individuality.personality.personality_sides: + prompt_personality += f",{random.choice(individuality.personality.personality_sides)}" # 随机选一个 + if individuality.identity.identity_detail: + prompt_personality += f",{random.choice(individuality.identity.identity_detail)}" # 随机选一个 personality_info = prompt_personality mood_info = self.current_state.mood + bot_name = global_config.BOT_NICKNAME # 使用全局配置中的机器人昵称 - # prompt = "" - # prompt += f"{personality_info}\n" - # prompt += f"现在{global_config.BOT_NICKNAME}的想法是:{self.current_mind}\n" - # prompt += f"现在{global_config.BOT_NICKNAME}在qq群里进行聊天,聊天的话题如下:{minds_str}\n" - # prompt += f"你现在{mood_info}\n" - # prompt += """现在请你总结这些聊天内容,注意关注聊天内容对原有的想法的影响,输出连贯的内心独白 - # 不要太长,但是记得结合上述的消息,要记得你的人设,关注新内容:""" prompt = (await global_prompt_manager.get_prompt_async("mind_summary_prompt")).format( - personality_info, global_config.BOT_NICKNAME, self.current_mind, minds_str, mood_info + personality_info=personality_info, # 使用关键字参数 + bot_name=bot_name, + current_mind=self.current_mind, + minds_str=minds_str, + mood_info=mood_info, ) - response, reasoning_content = await self.llm_model.generate_response_async(prompt) - - return response - - async def create_subheartflow(self, subheartflow_id): - """ - 创建一个新的SubHeartflow实例 - 添加一个SubHeartflow实例到self._subheartflows字典中 - 并根据subheartflow_id为子心流创建一个观察对象 - """ - try: - if subheartflow_id not in self._subheartflows: - subheartflow = SubHeartflow(subheartflow_id) - # 创建一个观察对象,目前只可以用chat_id创建观察对象 - logger.debug(f"创建 observation: {subheartflow_id}") - observation = ChattingObservation(subheartflow_id) - await observation.initialize() - subheartflow.add_observation(observation) - logger.debug("添加 observation 成功") - # 创建异步任务 - asyncio.create_task(subheartflow.subheartflow_start_working()) - logger.debug("创建异步任务 成功") - self._subheartflows[subheartflow_id] = subheartflow - logger.info("添加 subheartflow 成功") - return self._subheartflows[subheartflow_id] + response, reasoning_content = await self.llm_model.generate_response_async(prompt) + if not response: + logger.warning("[Heartflow] 想法汇总 LLM 返回空结果。") + return "(想法汇总失败...)" + return response except Exception as e: - logger.error(f"创建 subheartflow 失败: {e}") + logger.error(f"[Heartflow] 想法汇总失败: {e}") + logger.error(traceback.format_exc()) + return "(想法汇总时发生错误...)" + + async def create_subheartflow(self, subheartflow_id: Any) -> Optional[SubHeartflow]: + """ + 获取或创建一个新的SubHeartflow实例。 + (主要逻辑不变,InterestChatting 现在在 SubHeartflow 内部创建) + """ + existing_subheartflow = self._subheartflows.get(subheartflow_id) + if existing_subheartflow: + # 如果已存在,确保其 last_active_time 更新 (如果需要的话) + # existing_subheartflow.last_active_time = time.time() # 移除,活跃时间由实际操作更新 + # logger.debug(f"[Heartflow] 返回已存在的 subheartflow: {subheartflow_id}") + return existing_subheartflow + + logger.info(f"[Heartflow] 尝试创建新的 subheartflow: {subheartflow_id}") + try: + # 创建 SubHeartflow,它内部会创建 InterestChatting + subheartflow = SubHeartflow(subheartflow_id) + + # 创建并初始化观察对象 + logger.debug(f"[Heartflow] 为 {subheartflow_id} 创建 observation") + observation = ChattingObservation(subheartflow_id) + await observation.initialize() + subheartflow.add_observation(observation) + logger.debug(f"[Heartflow] 为 {subheartflow_id} 添加 observation 成功") + + # 创建并存储后台任务 (SubHeartflow 自己的后台任务) + subheartflow.task = asyncio.create_task(subheartflow.subheartflow_start_working()) + logger.debug(f"[Heartflow] 为 {subheartflow_id} 创建后台任务成功") + + # 添加到管理字典 + self._subheartflows[subheartflow_id] = subheartflow + logger.info(f"[Heartflow] 添加 subheartflow {subheartflow_id} 成功") + return subheartflow + + except Exception as e: + logger.error(f"[Heartflow] 创建 subheartflow {subheartflow_id} 失败: {e}") + logger.error(traceback.format_exc()) return None - def get_subheartflow(self, observe_chat_id) -> SubHeartflow: + def get_subheartflow(self, observe_chat_id: Any) -> Optional[SubHeartflow]: """获取指定ID的SubHeartflow实例""" return self._subheartflows.get(observe_chat_id) diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 9903b184..49efe7eb 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -139,7 +139,7 @@ class ChattingObservation(Observation): # traceback.print_exc() # 记录详细堆栈 # print(f"处理后self.talking_message:{self.talking_message}") - self.talking_message_str = await build_readable_messages(self.talking_message) + self.talking_message_str = await build_readable_messages(messages=self.talking_message, timestamp_mode="normal") logger.trace( f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}" diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 439b2a3f..584d24f2 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -4,22 +4,20 @@ from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config import time -from typing import Optional -from datetime import datetime +from typing import Optional, List, Dict import traceback from src.plugins.chat.utils import parse_text_timestamps - -# from src.plugins.schedule.schedule_generator import bot_schedule -# from src.plugins.memory_system.Hippocampus import HippocampusManager +import enum from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 - -# from src.plugins.chat.utils import get_embedding -# from src.common.database import db -# from typing import Union from src.individuality.individuality import Individuality import random from src.plugins.person_info.relationship_manager import relationship_manager from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager +from src.plugins.chat.message import MessageRecv +import math + +# 定义常量 (从 interest.py 移动过来) +MAX_INTEREST = 15.0 subheartflow_config = LogConfig( # 使用海马体专用样式 @@ -28,6 +26,12 @@ subheartflow_config = LogConfig( ) logger = get_module_logger("subheartflow", config=subheartflow_config) +interest_log_config = LogConfig( + console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"], + file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"], +) +interest_logger = get_module_logger("InterestChatting", config=interest_log_config) + def init_prompt(): prompt = "" @@ -49,25 +53,178 @@ def init_prompt(): Prompt(prompt, "sub_heartflow_prompt_before") -class CurrentState: +class ChatState(enum.Enum): + ABSENT = "不参与" + CHAT = "闲聊" + FOCUSED = "专注" + + +class ChatStateInfo: def __init__(self): self.willing = 0 - self.current_state_info = "" + + self.chat_status: ChatState = ChatState.ABSENT self.mood_manager = MoodManager() self.mood = self.mood_manager.get_prompt() - def update_current_state_info(self): - self.current_state_info = self.mood_manager.get_current_mood() + def update_chat_state_info(self): + self.chat_state_info = self.mood_manager.get_current_mood() + + +base_reply_probability = 0.05 +probability_increase_rate_per_second = 0.08 +max_reply_probability = 1 + + +class InterestChatting: + def __init__( + self, + decay_rate=global_config.default_decay_rate_per_second, + max_interest=MAX_INTEREST, + trigger_threshold=global_config.reply_trigger_threshold, + base_reply_probability=base_reply_probability, + increase_rate=probability_increase_rate_per_second, + decay_factor=global_config.probability_decay_factor_per_second, + max_probability=max_reply_probability, + ): + self.interest_level: float = 0.0 + self.last_update_time: float = time.time() + self.decay_rate_per_second: float = decay_rate + self.max_interest: float = max_interest + self.last_interaction_time: float = self.last_update_time + + self.trigger_threshold: float = trigger_threshold + self.base_reply_probability: float = base_reply_probability + self.probability_increase_rate: float = increase_rate + self.probability_decay_factor: float = decay_factor + self.max_reply_probability: float = max_probability + self.current_reply_probability: float = 0.0 + self.is_above_threshold: bool = False + + self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {} + + def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool): + self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) + self.last_interaction_time = time.time() + + def _calculate_decay(self, current_time: float): + time_delta = current_time - self.last_update_time + if time_delta > 0: + old_interest = self.interest_level + if self.interest_level < 1e-9: + self.interest_level = 0.0 + else: + if self.decay_rate_per_second <= 0: + interest_logger.warning( + f"InterestChatting encountered non-positive decay rate: {self.decay_rate_per_second}. Setting interest to 0." + ) + self.interest_level = 0.0 + elif self.interest_level < 0: + interest_logger.warning( + f"InterestChatting encountered negative interest level: {self.interest_level}. Setting interest to 0." + ) + self.interest_level = 0.0 + else: + try: + decay_factor = math.pow(self.decay_rate_per_second, time_delta) + self.interest_level *= decay_factor + except ValueError as e: + interest_logger.error( + f"Math error during decay calculation: {e}. Rate: {self.decay_rate_per_second}, Delta: {time_delta}, Level: {self.interest_level}. Setting interest to 0." + ) + self.interest_level = 0.0 + + if old_interest != self.interest_level: + self.last_update_time = current_time + + def _update_reply_probability(self, current_time: float): + time_delta = current_time - self.last_update_time + if time_delta <= 0: + return + + currently_above = self.interest_level >= self.trigger_threshold + + if currently_above: + if not self.is_above_threshold: + self.current_reply_probability = self.base_reply_probability + interest_logger.debug( + f"兴趣跨过阈值 ({self.trigger_threshold}). 概率重置为基础值: {self.base_reply_probability:.4f}" + ) + else: + increase_amount = self.probability_increase_rate * time_delta + self.current_reply_probability += increase_amount + + self.current_reply_probability = min(self.current_reply_probability, self.max_reply_probability) + + else: + if 0 < self.probability_decay_factor < 1: + decay_multiplier = math.pow(self.probability_decay_factor, time_delta) + self.current_reply_probability *= decay_multiplier + if self.current_reply_probability < 1e-6: + self.current_reply_probability = 0.0 + elif self.probability_decay_factor <= 0: + if self.current_reply_probability > 0: + interest_logger.warning(f"无效的衰减因子 ({self.probability_decay_factor}). 设置概率为0.") + self.current_reply_probability = 0.0 + + self.current_reply_probability = max(self.current_reply_probability, 0.0) + + self.is_above_threshold = currently_above + + def increase_interest(self, current_time: float, value: float): + self._update_reply_probability(current_time) + self._calculate_decay(current_time) + self.interest_level += value + self.interest_level = min(self.interest_level, self.max_interest) + self.last_update_time = current_time + self.last_interaction_time = current_time + + def decrease_interest(self, current_time: float, value: float): + self._update_reply_probability(current_time) + self.interest_level -= value + self.interest_level = max(self.interest_level, 0.0) + self.last_update_time = current_time + self.last_interaction_time = current_time + + def get_interest(self) -> float: + current_time = time.time() + self._update_reply_probability(current_time) + self._calculate_decay(current_time) + self.last_update_time = current_time + return self.interest_level + + def get_state(self) -> dict: + interest = self.get_interest() + return { + "interest_level": round(interest, 2), + "last_update_time": self.last_update_time, + "current_reply_probability": round(self.current_reply_probability, 4), + "is_above_threshold": self.is_above_threshold, + "last_interaction_time": self.last_interaction_time, + } + + def should_evaluate_reply(self) -> bool: + current_time = time.time() + self._update_reply_probability(current_time) + + if self.current_reply_probability > 0: + trigger = random.random() < self.current_reply_probability + return trigger + else: + return False class SubHeartflow: def __init__(self, subheartflow_id): self.subheartflow_id = subheartflow_id - self.current_mind = "" + self.current_mind = "你什么也没想" self.past_mind = [] - self.current_state: CurrentState = CurrentState() + self.chat_state: ChatStateInfo = ChatStateInfo() + + self.interest_chatting = InterestChatting() + self.llm_model = LLMRequest( model=global_config.llm_sub_heartflow, temperature=global_config.llm_sub_heartflow["temp"], @@ -77,15 +234,13 @@ class SubHeartflow: self.main_heartflow_info = "" - self.last_reply_time = time.time() self.last_active_time = time.time() # 添加最后激活时间 - - if not self.current_mind: - self.current_mind = "你什么也没想" + self.should_stop = False # 添加停止标志 + self.task: Optional[asyncio.Task] = None # 添加 task 属性 self.is_active = False - self.observations: list[ChattingObservation] = [] + self.observations: List[ChattingObservation] = [] # 使用 List 类型提示 self.running_knowledges = [] @@ -93,19 +248,13 @@ class SubHeartflow: async def subheartflow_start_working(self): while True: - current_time = time.time() # --- 调整后台任务逻辑 --- # # 这个后台循环现在主要负责检查是否需要自我销毁 # 不再主动进行思考或状态更新,这些由 HeartFC_Chat 驱动 - # 检查是否超过指定时间没有激活 (例如,没有被调用进行思考) - if current_time - self.last_active_time > global_config.sub_heart_flow_stop_time: # 例如 5 分钟 - logger.info( - f"子心流 {self.subheartflow_id} 超过 {global_config.sub_heart_flow_stop_time} 秒没有激活,正在销毁..." - f" (Last active: {datetime.fromtimestamp(self.last_active_time).strftime('%Y-%m-%d %H:%M:%S')})" - ) - # 在这里添加实际的销毁逻辑,例如从主 Heartflow 管理器中移除自身 - # heartflow.remove_subheartflow(self.subheartflow_id) # 假设有这样的方法 + # 检查是否被主心流标记为停止 + if self.should_stop: + logger.info(f"子心流 {self.subheartflow_id} 被标记为停止,正在退出后台任务...") break # 退出循环以停止任务 await asyncio.sleep(global_config.sub_heart_flow_update_interval) # 定期检查销毁条件 @@ -132,7 +281,7 @@ class SubHeartflow: self.last_active_time = time.time() # 更新最后激活时间戳 current_thinking_info = self.current_mind - mood_info = self.current_state.mood + mood_info = self.chat_state.mood observation = self._get_primary_observation() # --- 获取观察信息 --- # @@ -264,6 +413,26 @@ class SubHeartflow: logger.warning(f"SubHeartflow {self.subheartflow_id} 没有找到有效的 ChattingObservation") return None + def get_interest_state(self) -> dict: + """获取当前兴趣状态""" + return self.interest_chatting.get_state() + + def get_interest_level(self) -> float: + """获取当前兴趣等级""" + return self.interest_chatting.get_interest() + + def should_evaluate_reply(self) -> bool: + """判断是否应该评估回复""" + return self.interest_chatting.should_evaluate_reply() + + def add_interest_dict_entry(self, message: MessageRecv, interest_value: float, is_mentioned: bool): + """添加兴趣字典条目""" + self.interest_chatting.add_interest_dict(message, interest_value, is_mentioned) + + def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]: + """获取兴趣字典""" + return self.interest_chatting.interest_dict + init_prompt() # subheartflow = SubHeartflow() diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index e7616ec2..2a489338 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -105,3 +105,4 @@ class Individuality: return self.personality.agreeableness elif factor == "neuroticism": return self.personality.neuroticism + return None diff --git a/src/main.py b/src/main.py index aad08b90..8e4d966c 100644 --- a/src/main.py +++ b/src/main.py @@ -17,8 +17,7 @@ from .common.logger import get_module_logger from .plugins.remote import heartbeat_thread # noqa: F401 from .individuality.individuality import Individuality from .common.server import global_server -from .plugins.chat_module.heartFC_chat.interest import InterestManager -from .plugins.chat_module.heartFC_chat.heartFC_controler import HeartFC_Controller +from .plugins.chat_module.heartFC_chat.heartFC_controler import HeartFCController logger = get_module_logger("main") @@ -112,14 +111,9 @@ class MainSystem: asyncio.create_task(heartflow.heartflow_start_working()) logger.success("心流系统启动成功") - # 启动 InterestManager 的后台任务 - interest_manager = InterestManager() # 获取单例 - await interest_manager.start_background_tasks() - logger.success("兴趣管理器后台任务启动成功") - - # 初始化并独立启动 HeartFC_Chat - HeartFC_Controller() - heartfc_chat_instance = HeartFC_Controller.get_instance() + # 初始化并独立启动 HeartFCController + HeartFCController() + heartfc_chat_instance = HeartFCController.get_instance() if heartfc_chat_instance: await heartfc_chat_instance.start() logger.success("HeartFC_Chat 模块独立启动成功") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 598468e8..9502b755 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -180,6 +180,7 @@ class Conversation: "time": datetime.datetime.now().strftime("%H:%M:%S"), } ) + return None elif action == "fetch_knowledge": self.waiter.wait_accumulated_time = 0 @@ -193,28 +194,35 @@ class Conversation: if knowledge: if topic not in self.conversation_info.knowledge_list: self.conversation_info.knowledge_list.append({"topic": topic, "knowledge": knowledge}) + return None else: self.conversation_info.knowledge_list[topic] += knowledge + return None + return None elif action == "rethink_goal": self.waiter.wait_accumulated_time = 0 self.state = ConversationState.RETHINKING await self.goal_analyzer.analyze_goal(conversation_info, observation_info) + return None elif action == "listening": self.state = ConversationState.LISTENING logger.info("倾听对方发言...") await self.waiter.wait_listening(conversation_info) + return None elif action == "end_conversation": self.should_continue = False logger.info("决定结束对话...") + return None else: # wait self.state = ConversationState.WAITING logger.info("等待更多信息...") await self.waiter.wait(self.conversation_info) + return None async def _send_timeout_message(self): """发送超时结束消息""" diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index bb471900..a27abecd 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -151,7 +151,7 @@ class ReplyGenerator: return content except Exception as e: - logger.error(f"生成回复时出错: {e}") + logger.error(f"生成回复时出错: {str(e)}") return "抱歉,我现在有点混乱,让我重新思考一下..." async def check_reply(self, reply: str, goal: str, retry_count: int = 0) -> Tuple[bool, str, bool]: diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 314d20ff..05a0bcff 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -7,7 +7,7 @@ from ..chat_module.only_process.only_message_process import MessageProcessor from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from ..chat_module.reasoning_chat.reasoning_chat import ReasoningChat -from ..chat_module.heartFC_chat.heartFC_processor import HeartFC_Processor +from ..chat_module.heartFC_chat.heartFC_processor import HeartFCProcessor from ..utils.prompt_builder import Prompt, global_prompt_manager import traceback @@ -27,9 +27,8 @@ class ChatBot: self.bot = None # bot 实例引用 self._started = False self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例 - self.mood_manager.start_mood_update() # 启动情绪更新 self.reasoning_chat = ReasoningChat() - self.heartFC_processor = HeartFC_Processor() # 新增 + self.heartFC_processor = HeartFCProcessor() # 新增 # 创建初始化PFC管理器的任务,会在_ensure_started时执行 self.only_process_chat = MessageProcessor() @@ -105,53 +104,24 @@ class ChatBot: template_group_name = None async def preprocess(): - if global_config.enable_pfc_chatting: - try: - if groupinfo is None: - if global_config.enable_friend_chat: - userinfo = message.message_info.user_info - messageinfo = message.message_info - # 创建聊天流 - chat = await chat_manager.get_or_create_stream( - platform=messageinfo.platform, - user_info=userinfo, - group_info=groupinfo, - ) - message.update_chat_stream(chat) - await self.only_process_chat.process_message(message) - await self._create_pfc_chat(message) + if groupinfo is None: + if global_config.enable_friend_chat: + if global_config.enable_pfc_chatting: + userinfo = message.message_info.user_info + messageinfo = message.message_info + # 创建聊天流 + chat = await chat_manager.get_or_create_stream( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo, + ) + message.update_chat_stream(chat) + await self.only_process_chat.process_message(message) + await self._create_pfc_chat(message) else: - if groupinfo.group_id in global_config.talk_allowed_groups: - # logger.debug(f"开始群聊模式{str(message_data)[:50]}...") - if global_config.response_mode == "heart_flow": - # logger.info(f"启动最新最好的思维流FC模式{str(message_data)[:50]}...") - await self.heartFC_processor.process_message(message_data) - elif global_config.response_mode == "reasoning": - # logger.debug(f"开始推理模式{str(message_data)[:50]}...") - await self.reasoning_chat.process_message(message_data) - else: - logger.error(f"未知的回复模式,请检查配置文件!!: {global_config.response_mode}") - except Exception as e: - logger.error(f"处理PFC消息失败: {e}") + await self.heartFC_processor.process_message(message_data) else: - if groupinfo is None: - if global_config.enable_friend_chat: - # 私聊处理流程 - # await self._handle_private_chat(message) - if global_config.response_mode == "heart_flow": - await self.heartFC_processor.process_message(message_data) - elif global_config.response_mode == "reasoning": - await self.reasoning_chat.process_message(message_data) - else: - logger.error(f"未知的回复模式,请检查配置文件!!: {global_config.response_mode}") - else: # 群聊处理 - if groupinfo.group_id in global_config.talk_allowed_groups: - if global_config.response_mode == "heart_flow": - await self.heartFC_processor.process_message(message_data) - elif global_config.response_mode == "reasoning": - await self.reasoning_chat.process_message(message_data) - else: - logger.error(f"未知的回复模式,请检查配置文件!!: {global_config.response_mode}") + await self.heartFC_processor.process_message(message_data) if template_group_name: async with global_prompt_manager.async_message_scope(template_group_name): diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index cbea1fd9..b7afa817 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -1,14 +1,13 @@ import time from dataclasses import dataclass -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import urllib3 -from .utils_image import image_manager - -from ..message.message_base import Seg, UserInfo, BaseMessageInfo, MessageBase -from .chat_stream import ChatStream from src.common.logger import get_module_logger +from .chat_stream import ChatStream +from .utils_image import image_manager +from ..message.message_base import Seg, UserInfo, BaseMessageInfo, MessageBase logger = get_module_logger("chat_message") @@ -207,7 +206,7 @@ class MessageProcessBase(Message): # 处理单个消息段 return await self._process_single_segment(segment) - async def _process_single_segment(self, seg: Seg) -> str: + async def _process_single_segment(self, seg: Seg) -> Union[str, None]: """处理单个消息段 Args: @@ -233,6 +232,7 @@ class MessageProcessBase(Message): elif seg.type == "reply": if self.reply and hasattr(self.reply, "processed_plain_text"): return f"[回复:{self.reply.processed_plain_text}]" + return None else: return f"[{seg.type}:{str(seg.data)}]" except Exception as e: @@ -309,10 +309,7 @@ class MessageSending(MessageProcessBase): def set_reply(self, reply: Optional["MessageRecv"] = None) -> None: """设置回复消息""" - if ( - self.message_info.format_info.accept_format is not None - and "reply" in self.message_info.format_info.accept_format - ): + if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: if reply: self.reply = reply if self.reply: diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 9c98a16a..271386ff 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -2,7 +2,7 @@ import random import time import re from collections import Counter -from typing import Dict, List +from typing import Dict, List, Optional import jieba import numpy as np @@ -76,18 +76,20 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: else: if not is_mentioned: # 判断是否被回复 - if re.match("回复[\s\S]*?\((\d+)\)的消息,说:", message.processed_plain_text): + if re.match( + f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?\],说:", message.processed_plain_text + ): is_mentioned = True - - # 判断内容中是否被提及 - message_content = re.sub(r"@[\s\S]*?((\d+))", "", message.processed_plain_text) - message_content = re.sub(r"回复[\s\S]*?\((\d+)\)的消息,说: ", "", message_content) - for keyword in keywords: - if keyword in message_content: - is_mentioned = True - for nickname in nicknames: - if nickname in message_content: - is_mentioned = True + else: + # 判断内容中是否被提及 + message_content = re.sub(r"@[\s\S]*?((\d+))", "", message.processed_plain_text) + message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?\],说:", "", message_content) + for keyword in keywords: + if keyword in message_content: + is_mentioned = True + for nickname in nicknames: + if nickname in message_content: + is_mentioned = True if is_mentioned and global_config.mentioned_bot_inevitable_reply: reply_probability = 1.0 logger.info("被提及,回复概率设置为100%") @@ -688,7 +690,7 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) - return 0, 0 -def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> str: +def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> Optional[str]: """将时间戳转换为人类可读的时间格式 Args: @@ -716,6 +718,7 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal" return f"{int(diff / 86400)}天前:\n" else: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" + return None def parse_text_timestamps(text: str, mode: str = "normal") -> str: diff --git a/src/plugins/chat_module/heartFC_chat/heartFC_controler.py b/src/plugins/chat_module/heartFC_chat/heartFC_controler.py index 389e030a..cd33221f 100644 --- a/src/plugins/chat_module/heartFC_chat/heartFC_controler.py +++ b/src/plugins/chat_module/heartFC_chat/heartFC_controler.py @@ -1,82 +1,108 @@ import traceback from typing import Optional, Dict import asyncio -from asyncio import Lock +import threading # 导入 threading from ...moods.moods import MoodManager from ...chat.emoji_manager import emoji_manager from .heartFC_generator import ResponseGenerator from .messagesender import MessageManager from src.heart_flow.heartflow import heartflow +from src.heart_flow.sub_heartflow import SubHeartflow, ChatState from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from src.plugins.person_info.relationship_manager import relationship_manager from src.do_tool.tool_use import ToolUser -from .interest import InterestManager from src.plugins.chat.chat_stream import chat_manager from .pf_chatting import PFChatting + # 定义日志配置 chat_config = LogConfig( console_format=CHAT_STYLE_CONFIG["console_format"], file_format=CHAT_STYLE_CONFIG["file_format"], ) -logger = get_module_logger("HeartFC_Controller", config=chat_config) +logger = get_module_logger("HeartFCController", config=chat_config) # 检测群聊兴趣的间隔时间 INTEREST_MONITOR_INTERVAL_SECONDS = 1 -class HeartFC_Controller: - _instance = None # For potential singleton access if needed by MessageManager +# 合并后的版本:使用 __new__ + threading.Lock 实现线程安全单例,类名为 HeartFCController +class HeartFCController: + _instance = None + _lock = threading.Lock() # 使用 threading.Lock 保证 __new__ 线程安全 + _initialized = False - def __init__(self): - # --- Updated Init --- - if HeartFC_Controller._instance is not None: - # Prevent re-initialization if used as a singleton - return - self.gpt = ResponseGenerator() - self.mood_manager = MoodManager.get_instance() - self.mood_manager.start_mood_update() - self.tool_user = ToolUser() - self.interest_manager = InterestManager() - self._interest_monitor_task: Optional[asyncio.Task] = None - # --- New PFChatting Management --- - self.pf_chatting_instances: Dict[str, PFChatting] = {} - self._pf_chatting_lock = Lock() - # --- End New PFChatting Management --- - HeartFC_Controller._instance = self # Register instance - # --- End Updated Init --- - # --- Make dependencies accessible for PFChatting --- - # These are accessed via the passed instance in PFChatting - self.emoji_manager = emoji_manager - self.relationship_manager = relationship_manager - self.MessageManager = MessageManager # Pass the class/singleton access - # --- End dependencies --- - - # --- Added Class Method for Singleton Access --- - @classmethod - def get_instance(cls): + def __new__(cls, *args, **kwargs): if cls._instance is None: - # This might indicate an issue if called before initialization - logger.warning("HeartFC_Controller get_instance called before initialization.") - # Optionally, initialize here if a strict singleton pattern is desired - # cls._instance = cls() + with cls._lock: + # Double-checked locking + if cls._instance is None: + logger.debug("创建 HeartFCController 单例实例...") + cls._instance = super().__new__(cls) return cls._instance - # --- End Added Class Method --- + def __init__(self): + # 使用 _initialized 标志确保 __init__ 只执行一次 + if self._initialized: + return + + self.gpt = ResponseGenerator() + self.mood_manager = MoodManager.get_instance() + self.tool_user = ToolUser() + self._interest_monitor_task: Optional[asyncio.Task] = None + + self.heartflow = heartflow + + self.pf_chatting_instances: Dict[str, PFChatting] = {} + self._pf_chatting_lock = asyncio.Lock() # 这个是 asyncio.Lock,用于异步上下文 + self.emoji_manager = emoji_manager # 假设是全局或已初始化的实例 + self.relationship_manager = relationship_manager # 假设是全局或已初始化的实例 + + self.MessageManager = MessageManager + self._initialized = True + logger.info("HeartFCController 单例初始化完成。") + + @classmethod + def get_instance(cls): + """获取 HeartFCController 的单例实例。""" + # 如果实例尚未创建,调用构造函数(这将触发 __new__ 和 __init__) + if cls._instance is None: + # 在首次调用 get_instance 时创建实例。 + # __new__ 中的锁会确保线程安全。 + cls() + # 添加日志记录,说明实例是在 get_instance 调用时创建的 + logger.info("HeartFCController 实例在首次 get_instance 时创建。") + elif not cls._initialized: + # 实例已创建但可能未初始化完成(理论上不太可能发生,除非 __init__ 异常) + logger.warning("HeartFCController 实例存在但尚未完成初始化。") + return cls._instance + + # --- 新增:检查 PFChatting 状态的方法 --- # + def is_pf_chatting_active(self, stream_id: str) -> bool: + """检查指定 stream_id 的 PFChatting 循环是否处于活动状态。""" + # 注意:这里直接访问字典,不加锁,因为读取通常是安全的, + # 并且 PFChatting 实例的 _loop_active 状态由其自身的异步循环管理。 + # 如果需要更强的保证,可以在访问 pf_instance 前获取 _pf_chatting_lock + pf_instance = self.pf_chatting_instances.get(stream_id) + if pf_instance and pf_instance._loop_active: # 直接检查 PFChatting 实例的 _loop_active 属性 + return True + return False + + # --- 结束新增 --- # async def start(self): """启动异步任务,如回复启动器""" - logger.debug("HeartFC_Controller 正在启动异步任务...") + logger.debug("HeartFCController 正在启动异步任务...") self._initialize_monitor_task() - logger.info("HeartFC_Controller 异步任务启动完成") + logger.info("HeartFCController 异步任务启动完成") def _initialize_monitor_task(self): """启动后台兴趣监控任务,可以检查兴趣是否足以开启心流对话""" if self._interest_monitor_task is None or self._interest_monitor_task.done(): try: loop = asyncio.get_running_loop() - self._interest_monitor_task = loop.create_task(self._interest_monitor_loop()) + self._interest_monitor_task = loop.create_task(self._response_control_loop()) except RuntimeError: logger.error("创建兴趣监控任务失败:没有运行中的事件循环。") raise @@ -89,7 +115,7 @@ class HeartFC_Controller: async with self._pf_chatting_lock: if stream_id not in self.pf_chatting_instances: logger.info(f"为流 {stream_id} 创建新的PFChatting实例") - # 传递 self (HeartFC_Controller 实例) 进行依赖注入 + # 传递 self (HeartFCController 实例) 进行依赖注入 instance = PFChatting(stream_id, self) # 执行异步初始化 if not await instance._initialize(): @@ -100,41 +126,41 @@ class HeartFC_Controller: # --- End Added PFChatting Instance Manager --- - async def _interest_monitor_loop(self): + # async def update_mai_Status(self): + # """后台任务,定期检查更新麦麦状态""" + # logger.info("麦麦状态更新循环开始...") + # while True: + # await asyncio.sleep(0) + # self.heartflow.update_chat_status() + + async def _response_control_loop(self): """后台任务,定期检查兴趣度变化并触发回复""" logger.info("兴趣监控循环开始...") while True: await asyncio.sleep(INTEREST_MONITOR_INTERVAL_SECONDS) + try: # 从心流中获取活跃流 - active_stream_ids = list(heartflow.get_all_subheartflows_streams_ids()) + active_stream_ids = list(self.heartflow.get_all_subheartflows_streams_ids()) for stream_id in active_stream_ids: stream_name = chat_manager.get_stream_name(stream_id) or stream_id # 获取流名称 - sub_hf = heartflow.get_subheartflow(stream_id) + sub_hf = self.heartflow.get_subheartflow(stream_id) if not sub_hf: logger.warning(f"监控循环: 无法获取活跃流 {stream_name} 的 sub_hf") continue - should_trigger = False + should_trigger_hfc = False try: - interest_chatting = self.interest_manager.get_interest_chatting(stream_id) - if interest_chatting: - should_trigger = interest_chatting.should_evaluate_reply() - else: - logger.trace( - f"[{stream_name}] 没有找到对应的 InterestChatting 实例,跳过基于兴趣的触发检查。" - ) + interest_chatting = sub_hf.interest_chatting + should_trigger_hfc = interest_chatting.should_evaluate_reply() + except Exception as e: logger.error(f"检查兴趣触发器时出错 流 {stream_name}: {e}") logger.error(traceback.format_exc()) - if should_trigger: + if should_trigger_hfc: # 启动一次麦麦聊天 - pf_instance = await self._get_or_create_pf_chatting(stream_id) - if pf_instance: - asyncio.create_task(pf_instance.add_time()) - else: - logger.error(f"[{stream_name}] 无法获取或创建PFChatting实例。跳过触发。") + await self._trigger_hfc(sub_hf) except asyncio.CancelledError: logger.info("兴趣监控循环已取消。") @@ -143,3 +169,17 @@ class HeartFC_Controller: logger.error(f"兴趣监控循环错误: {e}") logger.error(traceback.format_exc()) await asyncio.sleep(5) # 发生错误时等待 + + async def _trigger_hfc(self, sub_hf: SubHeartflow): + chat_state = sub_hf.chat_state + if chat_state == ChatState.ABSENT: + chat_state = ChatState.CHAT + elif chat_state == ChatState.CHAT: + chat_state = ChatState.FOCUSED + + # 从 sub_hf 获取 stream_id + if chat_state == ChatState.FOCUSED: + stream_id = sub_hf.subheartflow_id + pf_instance = await self._get_or_create_pf_chatting(stream_id) + if pf_instance: # 确保实例成功获取或创建 + asyncio.create_task(pf_instance.add_time()) diff --git a/src/plugins/chat_module/heartFC_chat/heartFC_processor.py b/src/plugins/chat_module/heartFC_chat/heartFC_processor.py index 37708a94..00a9a024 100644 --- a/src/plugins/chat_module/heartFC_chat/heartFC_processor.py +++ b/src/plugins/chat_module/heartFC_chat/heartFC_processor.py @@ -11,8 +11,8 @@ from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from ...chat.chat_stream import chat_manager from ...chat.message_buffer import message_buffer from ...utils.timer_calculater import Timer -from .interest import InterestManager from src.plugins.person_info.relationship_manager import relationship_manager +from .reasoning_chat import ReasoningChat # 定义日志配置 processor_config = LogConfig( @@ -21,15 +21,11 @@ processor_config = LogConfig( ) logger = get_module_logger("heartFC_processor", config=processor_config) -# # 定义兴趣度增加触发回复的阈值 (移至 InterestManager) -# INTEREST_INCREASE_THRESHOLD = 0.5 - -class HeartFC_Processor: +class HeartFCProcessor: def __init__(self): self.storage = MessageStorage() - self.interest_manager = InterestManager() - # self.chat_instance = chat_instance # 持有 HeartFC_Chat 实例 + self.reasoning_chat = ReasoningChat.get_instance() async def process_message(self, message_data: str) -> None: """处理接收到的原始消息数据,完成消息解析、缓冲、过滤、存储、兴趣度计算与更新等核心流程。 @@ -72,12 +68,18 @@ class HeartFC_Processor: user_info=userinfo, group_info=groupinfo, ) - if not chat: - logger.error( - f"无法为消息创建或获取聊天流: user {userinfo.user_id}, group {groupinfo.group_id if groupinfo else 'None'}" - ) + + # --- 确保 SubHeartflow 存在 --- + subheartflow = await heartflow.create_subheartflow(chat.stream_id) + if not subheartflow: + logger.error(f"无法为 stream_id {chat.stream_id} 创建或获取 SubHeartflow,中止处理") return + # --- 添加兴趣追踪启动 (现在移动到这里,确保 subheartflow 存在后启动) --- + # 在获取到 chat 对象和确认 subheartflow 后,启动对该聊天流的兴趣监控 + await self.reasoning_chat.start_monitoring_interest(chat) # start_monitoring_interest 内部需要修改以适应 + # --- 结束添加 --- + message.update_chat_stream(chat) await heartflow.create_subheartflow(chat.stream_id) @@ -90,28 +92,27 @@ class HeartFC_Processor: message.raw_message, chat, userinfo ): return - logger.trace(f"过滤词/正则表达式过滤成功: {message.processed_plain_text}") # 查询缓冲器结果 buffer_result = await message_buffer.query_buffer_result(message) # 处理缓冲器结果 (Bombing logic) if not buffer_result: - F_type = "seglist" + f_type = "seglist" if message.message_segment.type != "seglist": - F_type = message.message_segment.type + f_type = message.message_segment.type else: if ( isinstance(message.message_segment.data, list) and all(isinstance(x, Seg) for x in message.message_segment.data) and len(message.message_segment.data) == 1 ): - F_type = message.message_segment.data[0].type - if F_type == "text": + f_type = message.message_segment.data[0].type + if f_type == "text": logger.debug(f"触发缓冲,消息:{message.processed_plain_text}") - elif F_type == "image": + elif f_type == "image": logger.debug("触发缓冲,表情包/图片等待中") - elif F_type == "seglist": + elif f_type == "seglist": logger.debug("触发缓冲,消息列表等待中") return # 被缓冲器拦截,不生成回复 @@ -141,21 +142,35 @@ class HeartFC_Processor: logger.error(f"计算记忆激活率失败: {e}") logger.error(traceback.format_exc()) + # --- 修改:兴趣度更新逻辑 --- # if is_mentioned: - interested_rate += 0.8 + interest_increase_on_mention = 2 + mentioned_boost = interest_increase_on_mention # 从配置获取提及增加值 + interested_rate += mentioned_boost + logger.trace(f"消息提及机器人,额外增加兴趣 {mentioned_boost:.2f}") - # 更新兴趣度 + # 更新兴趣度 (调用 SubHeartflow 的方法) + current_interest = 0.0 # 初始化 try: - self.interest_manager.increase_interest(chat.stream_id, value=interested_rate) - current_interest = self.interest_manager.get_interest(chat.stream_id) # 获取更新后的值用于日志 + # 获取当前时间,传递给 increase_interest + current_time = time.time() + subheartflow.interest_chatting.increase_interest(current_time, value=interested_rate) + current_interest = subheartflow.get_interest_level() # 获取更新后的值 + logger.trace( - f"使用激活率 {interested_rate:.2f} 更新后 (通过缓冲后),当前兴趣度: {current_interest:.2f}" + f"使用激活率 {interested_rate:.2f} 更新后 (通过缓冲后),当前兴趣度: {current_interest:.2f} (Stream: {chat.stream_id})" + ) + + # 添加到 SubHeartflow 的 interest_dict + subheartflow.add_interest_dict_entry(message, interested_rate, is_mentioned) + logger.trace( + f"Message {message.message_info.message_id} added to interest dict for stream {chat.stream_id}" ) except Exception as e: - logger.error(f"更新兴趣度失败: {e}") # 调整日志消息 + logger.error(f"更新兴趣度失败 (Stream: {chat.stream_id}): {e}") logger.error(traceback.format_exc()) - # ---- 兴趣度计算和更新结束 ---- + # --- 结束修改 --- # # 打印消息接收和处理信息 mes_name = chat.group_info.group_name if chat.group_info else "私聊" diff --git a/src/plugins/chat_module/heartFC_chat/interest.py b/src/plugins/chat_module/heartFC_chat/interest.py deleted file mode 100644 index 5a961e91..00000000 --- a/src/plugins/chat_module/heartFC_chat/interest.py +++ /dev/null @@ -1,491 +0,0 @@ -import time -import math -import asyncio -import threading -import json # 引入 json -import os # 引入 os -from typing import Optional # <--- 添加导入 -import random # <--- 添加导入 random -from src.common.logger import get_module_logger, LogConfig, DEFAULT_CONFIG # 引入 DEFAULT_CONFIG -from src.plugins.chat.chat_stream import chat_manager # *** Import ChatManager *** - -# 定义日志配置 (使用 loguru 格式) -interest_log_config = LogConfig( - console_format=DEFAULT_CONFIG["console_format"], # 使用默认控制台格式 - file_format=DEFAULT_CONFIG["file_format"], # 使用默认文件格式 -) -logger = get_module_logger("InterestManager", config=interest_log_config) - - -# 定义常量 -DEFAULT_DECAY_RATE_PER_SECOND = 0.98 # 每秒衰减率 (兴趣保留 99%) -MAX_INTEREST = 15.0 # 最大兴趣值 -# MIN_INTEREST_THRESHOLD = 0.1 # 低于此值可能被清理 (可选) -CLEANUP_INTERVAL_SECONDS = 1200 # 清理任务运行间隔 (例如:20分钟) -INACTIVE_THRESHOLD_SECONDS = 1200 # 不活跃时间阈值 (例如:20分钟) -LOG_INTERVAL_SECONDS = 3 # 日志记录间隔 (例如:30秒) -LOG_DIRECTORY = "logs/interest" # 日志目录 -# LOG_FILENAME = "interest_log.json" # 快照日志文件名 (保留,以防其他地方用到) -HISTORY_LOG_FILENAME = "interest_history.log" # 新的历史日志文件名 -# 移除阈值,将移至 HeartFC_Chat -# INTEREST_INCREASE_THRESHOLD = 0.5 - -# --- 新增:概率回复相关常量 --- -REPLY_TRIGGER_THRESHOLD = 3.0 # 触发概率回复的兴趣阈值 (示例值) -BASE_REPLY_PROBABILITY = 0.1 # 首次超过阈值时的基础回复概率 (示例值) -PROBABILITY_INCREASE_RATE_PER_SECOND = 0.02 # 高于阈值时,每秒概率增加量 (线性增长, 示例值) -PROBABILITY_DECAY_FACTOR_PER_SECOND = 0.2 # 低于阈值时,每秒概率衰减因子 (指数衰减, 示例值) -MAX_REPLY_PROBABILITY = 1 # 回复概率上限 (示例值) -# --- 结束:概率回复相关常量 --- - - -class InterestChatting: - def __init__( - self, - decay_rate=DEFAULT_DECAY_RATE_PER_SECOND, - max_interest=MAX_INTEREST, - trigger_threshold=REPLY_TRIGGER_THRESHOLD, - base_reply_probability=BASE_REPLY_PROBABILITY, - increase_rate=PROBABILITY_INCREASE_RATE_PER_SECOND, - decay_factor=PROBABILITY_DECAY_FACTOR_PER_SECOND, - max_probability=MAX_REPLY_PROBABILITY, - ): - self.interest_level: float = 0.0 - self.last_update_time: float = time.time() # 同时作为兴趣和概率的更新时间基准 - self.decay_rate_per_second: float = decay_rate - self.max_interest: float = max_interest - self.last_interaction_time: float = self.last_update_time # 新增:最后交互时间 - - # --- 新增:概率回复相关属性 --- - self.trigger_threshold: float = trigger_threshold - self.base_reply_probability: float = base_reply_probability - self.probability_increase_rate: float = increase_rate - self.probability_decay_factor: float = decay_factor - self.max_reply_probability: float = max_probability - self.current_reply_probability: float = 0.0 - self.is_above_threshold: bool = False # 标记兴趣值是否高于阈值 - # --- 结束:概率回复相关属性 --- - - def _calculate_decay(self, current_time: float): - """计算从上次更新到现在的衰减""" - time_delta = current_time - self.last_update_time - if time_delta > 0: - # 指数衰减: interest = interest * (decay_rate ^ time_delta) - # 添加处理极小兴趣值避免 math domain error - old_interest = self.interest_level - if self.interest_level < 1e-9: - self.interest_level = 0.0 - else: - # 检查 decay_rate_per_second 是否为非正数,避免 math domain error - if self.decay_rate_per_second <= 0: - logger.warning( - f"InterestChatting encountered non-positive decay rate: {self.decay_rate_per_second}. Setting interest to 0." - ) - self.interest_level = 0.0 - # 检查 interest_level 是否为负数,虽然理论上不应发生,但以防万一 - elif self.interest_level < 0: - logger.warning( - f"InterestChatting encountered negative interest level: {self.interest_level}. Setting interest to 0." - ) - self.interest_level = 0.0 - else: - try: - decay_factor = math.pow(self.decay_rate_per_second, time_delta) - self.interest_level *= decay_factor - except ValueError as e: - # 捕获潜在的 math domain error,例如对负数开非整数次方(虽然已加保护) - logger.error( - f"Math error during decay calculation: {e}. Rate: {self.decay_rate_per_second}, Delta: {time_delta}, Level: {self.interest_level}. Setting interest to 0." - ) - self.interest_level = 0.0 - - # 防止低于阈值 (如果需要) - # self.interest_level = max(self.interest_level, MIN_INTEREST_THRESHOLD) - - # 只有在兴趣值发生变化时才更新时间戳 - if old_interest != self.interest_level: - self.last_update_time = current_time - - def _update_reply_probability(self, current_time: float): - """根据当前兴趣是否超过阈值及时间差,更新回复概率""" - time_delta = current_time - self.last_update_time - if time_delta <= 0: - return # 时间未前进,无需更新 - - currently_above = self.interest_level >= self.trigger_threshold - - if currently_above: - if not self.is_above_threshold: - # 刚跨过阈值,重置为基础概率 - self.current_reply_probability = self.base_reply_probability - logger.debug( - f"兴趣跨过阈值 ({self.trigger_threshold}). 概率重置为基础值: {self.base_reply_probability:.4f}" - ) - else: - # 持续高于阈值,线性增加概率 - increase_amount = self.probability_increase_rate * time_delta - self.current_reply_probability += increase_amount - # logger.debug(f"兴趣高于阈值 ({self.trigger_threshold}) 持续 {time_delta:.2f}秒. 概率增加 {increase_amount:.4f} 到 {self.current_reply_probability:.4f}") - - # 限制概率不超过最大值 - self.current_reply_probability = min(self.current_reply_probability, self.max_reply_probability) - - else: - if 0 < self.probability_decay_factor < 1: - decay_multiplier = math.pow(self.probability_decay_factor, time_delta) - # old_prob = self.current_reply_probability - self.current_reply_probability *= decay_multiplier - # 避免因浮点数精度问题导致概率略微大于0,直接设为0 - if self.current_reply_probability < 1e-6: - self.current_reply_probability = 0.0 - # logger.debug(f"兴趣低于阈值 ({self.trigger_threshold}) 持续 {time_delta:.2f}秒. 概率从 {old_prob:.4f} 衰减到 {self.current_reply_probability:.4f} (因子: {self.probability_decay_factor})") - elif self.probability_decay_factor <= 0: - # 如果衰减因子无效或为0,直接清零 - if self.current_reply_probability > 0: - logger.warning(f"无效的衰减因子 ({self.probability_decay_factor}). 设置概率为0.") - self.current_reply_probability = 0.0 - # else: decay_factor >= 1, probability will not decay or increase, which might be intended in some cases. - - # 确保概率不低于0 - self.current_reply_probability = max(self.current_reply_probability, 0.0) - - # 更新状态标记 - self.is_above_threshold = currently_above - # 更新时间戳放在调用者处,确保 interest 和 probability 基于同一点更新 - - def increase_interest(self, current_time: float, value: float): - """根据传入的值增加兴趣值,并记录增加量""" - # 先更新概率和计算衰减(基于上次更新时间) - self._update_reply_probability(current_time) - self._calculate_decay(current_time) - # 应用增加 - self.interest_level += value - self.interest_level = min(self.interest_level, self.max_interest) # 不超过最大值 - self.last_update_time = current_time # 更新时间戳 - self.last_interaction_time = current_time # 更新最后交互时间 - - def decrease_interest(self, current_time: float, value: float): - """降低兴趣值并更新时间 (确保不低于0)""" - # 先更新概率(基于上次更新时间) - self._update_reply_probability(current_time) - # 注意:降低兴趣度是否需要先衰减?取决于具体逻辑,这里假设不衰减直接减 - self.interest_level -= value - self.interest_level = max(self.interest_level, 0.0) # 确保不低于0 - self.last_update_time = current_time # 降低也更新时间戳 - self.last_interaction_time = current_time # 更新最后交互时间 - - def get_interest(self) -> float: - """获取当前兴趣值 (计算衰减后)""" - # 注意:这个方法现在会触发概率和兴趣的更新 - current_time = time.time() - self._update_reply_probability(current_time) - self._calculate_decay(current_time) - self.last_update_time = current_time # 更新时间戳 - return self.interest_level - - def get_state(self) -> dict: - """获取当前状态字典""" - # 调用 get_interest 来确保状态已更新 - interest = self.get_interest() - return { - "interest_level": round(interest, 2), - "last_update_time": self.last_update_time, - "current_reply_probability": round(self.current_reply_probability, 4), # 添加概率到状态 - "is_above_threshold": self.is_above_threshold, # 添加阈值状态 - "last_interaction_time": self.last_interaction_time, # 新增:添加最后交互时间到状态 - # 可以选择性地暴露 last_increase_amount 给状态,方便调试 - # "last_increase_amount": round(self.last_increase_amount, 2) - } - - def should_evaluate_reply(self) -> bool: - """ - 判断是否应该触发一次回复评估。 - 首先更新概率状态,然后根据当前概率进行随机判断。 - """ - current_time = time.time() - # 确保概率是基于最新兴趣值计算的 - self._update_reply_probability(current_time) - # 更新兴趣衰减(如果需要,取决于逻辑,这里保持和 get_interest 一致) - # self._calculate_decay(current_time) - # self.last_update_time = current_time # 更新时间戳 - - if self.current_reply_probability > 0: - # 只有在阈值之上且概率大于0时才有可能触发 - trigger = random.random() < self.current_reply_probability - # if trigger: - # logger.info(f"回复概率评估触发! 概率: {self.current_reply_probability:.4f}, 阈值: {self.trigger_threshold}, 兴趣: {self.interest_level:.2f}") - # # 可选:触发后是否重置/降低概率?根据需要决定 - # # self.current_reply_probability = self.base_reply_probability # 例如,触发后降回基础概率 - # # self.current_reply_probability *= 0.5 # 例如,触发后概率减半 - # else: - # logger.debug(f"回复概率评估未触发。概率: {self.current_reply_probability:.4f}") - return trigger - else: - # logger.debug(f"Reply evaluation check: Below threshold or zero probability. Probability: {self.current_reply_probability:.4f}") - return False - - -class InterestManager: - _instance = None - _lock = threading.Lock() - _initialized = False - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - with cls._lock: - # Double-check locking - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self): - if not self._initialized: - with self._lock: - # 确保初始化也只执行一次 - if not self._initialized: - logger.info("Initializing InterestManager singleton...") - # key: stream_id (str), value: InterestChatting instance - self.interest_dict: dict[str, InterestChatting] = {} - # 保留旧的快照文件路径变量,尽管此任务不再写入 - # self._snapshot_log_file_path = os.path.join(LOG_DIRECTORY, LOG_FILENAME) - # 定义新的历史日志文件路径 - self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME) - self._ensure_log_directory() - self._cleanup_task = None - self._logging_task = None # 添加日志任务变量 - self._initialized = True - logger.info("InterestManager initialized.") # 修改日志消息 - self._decay_task = None # 新增:衰减任务变量 - - def _ensure_log_directory(self): - """确保日志目录存在""" - try: - os.makedirs(LOG_DIRECTORY, exist_ok=True) - logger.info(f"Log directory '{LOG_DIRECTORY}' ensured.") - except OSError as e: - logger.error(f"Error creating log directory '{LOG_DIRECTORY}': {e}") - - async def _periodic_cleanup_task(self, interval_seconds: int, max_age_seconds: int): - """后台清理任务的异步函数""" - while True: - await asyncio.sleep(interval_seconds) - logger.info(f"运行定期清理 (间隔: {interval_seconds}秒)...") - self.cleanup_inactive_chats(max_age_seconds=max_age_seconds) - - async def _periodic_log_task(self, interval_seconds: int): - """后台日志记录任务的异步函数 (记录历史数据,包含 group_name)""" - while True: - await asyncio.sleep(interval_seconds) - # logger.debug(f"运行定期历史记录 (间隔: {interval_seconds}秒)...") - try: - current_timestamp = time.time() - all_states = self.get_all_interest_states() # 获取当前所有状态 - - # 以追加模式打开历史日志文件 - with open(self._history_log_file_path, "a", encoding="utf-8") as f: - count = 0 - for stream_id, state in all_states.items(): - # *** Get group name from ChatManager *** - group_name = stream_id # Default to stream_id - try: - # Use the imported chat_manager instance - chat_stream = chat_manager.get_stream(stream_id) - if chat_stream and chat_stream.group_info: - group_name = chat_stream.group_info.group_name - elif chat_stream and not chat_stream.group_info: - # Handle private chats - maybe use user nickname? - group_name = ( - f"私聊_{chat_stream.user_info.user_nickname}" - if chat_stream.user_info - else stream_id - ) - except Exception as e: - logger.warning(f"Could not get group name for stream_id {stream_id}: {e}") - # Fallback to stream_id is already handled by default value - - log_entry = { - "timestamp": round(current_timestamp, 2), - "stream_id": stream_id, - "interest_level": state.get("interest_level", 0.0), # 确保有默认值 - "group_name": group_name, # *** Add group_name *** - # --- 新增:记录概率相关信息 --- - "reply_probability": state.get("current_reply_probability", 0.0), - "is_above_threshold": state.get("is_above_threshold", False), - # --- 结束新增 --- - } - # 将每个条目作为单独的 JSON 行写入 - f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") - count += 1 - # logger.debug(f"Successfully appended {count} interest history entries to {self._history_log_file_path}") - - # 注意:不再写入快照文件 interest_log.json - # 如果需要快照文件,可以在这里单独写入 self._snapshot_log_file_path - # 例如: - # with open(self._snapshot_log_file_path, 'w', encoding='utf-8') as snap_f: - # json.dump(all_states, snap_f, indent=4, ensure_ascii=False) - # logger.debug(f"Successfully wrote snapshot to {self._snapshot_log_file_path}") - - except IOError as e: - logger.error(f"Error writing interest history log to {self._history_log_file_path}: {e}") - except Exception as e: - logger.error(f"Unexpected error during periodic history logging: {e}") - - async def _periodic_decay_task(self): - """后台衰减任务的异步函数,每秒更新一次所有实例的衰减""" - while True: - await asyncio.sleep(1) # 每秒运行一次 - current_time = time.time() - # logger.debug("Running periodic decay calculation...") # 调试日志,可能过于频繁 - - # 创建字典项的快照进行迭代,避免在迭代时修改字典的问题 - items_snapshot = list(self.interest_dict.items()) - count = 0 - for stream_id, chatting in items_snapshot: - try: - # 调用 InterestChatting 实例的衰减方法 - chatting._calculate_decay(current_time) - count += 1 - except Exception as e: - logger.error(f"Error calculating decay for stream_id {stream_id}: {e}") - # if count > 0: # 仅在实际处理了项目时记录日志,避免空闲时刷屏 - # logger.debug(f"Applied decay to {count} streams.") - - async def start_background_tasks(self): - """启动清理,启动衰减,启动记录,启动启动启动启动启动""" - if self._cleanup_task is None or self._cleanup_task.done(): - self._cleanup_task = asyncio.create_task( - self._periodic_cleanup_task( - interval_seconds=CLEANUP_INTERVAL_SECONDS, max_age_seconds=INACTIVE_THRESHOLD_SECONDS - ) - ) - logger.info( - f"已创建定期清理任务。间隔时间: {CLEANUP_INTERVAL_SECONDS}秒, 不活跃阈值: {INACTIVE_THRESHOLD_SECONDS}秒" - ) - else: - logger.warning("跳过创建清理任务:任务已在运行或存在。") - - if self._logging_task is None or self._logging_task.done(): - self._logging_task = asyncio.create_task(self._periodic_log_task(interval_seconds=LOG_INTERVAL_SECONDS)) - logger.info(f"已创建定期日志任务。间隔时间: {LOG_INTERVAL_SECONDS}秒") - else: - logger.warning("跳过创建日志任务:任务已在运行或存在。") - - # 启动新的衰减任务 - if self._decay_task is None or self._decay_task.done(): - self._decay_task = asyncio.create_task(self._periodic_decay_task()) - logger.info("已创建定期衰减任务。间隔时间: 1秒") - else: - logger.warning("跳过创建衰减任务:任务已在运行或存在。") - - def get_all_interest_states(self) -> dict[str, dict]: - """获取所有聊天流的当前兴趣状态""" - # 不再需要 current_time, 因为 get_state 现在不接收它 - states = {} - # 创建副本以避免在迭代时修改字典 - items_snapshot = list(self.interest_dict.items()) - for stream_id, chatting in items_snapshot: - try: - # 直接调用 get_state,它会使用内部的 get_interest 获取已更新的值 - states[stream_id] = chatting.get_state() - except Exception as e: - logger.warning(f"Error getting state for stream_id {stream_id}: {e}") - return states - - def get_interest_chatting(self, stream_id: str) -> Optional[InterestChatting]: - """获取指定流的 InterestChatting 实例,如果不存在则返回 None""" - return self.interest_dict.get(stream_id) - - def _get_or_create_interest_chatting(self, stream_id: str) -> InterestChatting: - """获取或创建指定流的 InterestChatting 实例 (线程安全)""" - if stream_id not in self.interest_dict: - logger.debug(f"创建兴趣流: {stream_id}") - # --- 修改:创建时传入概率相关参数 (如果需要定制化,否则使用默认值) --- - self.interest_dict[stream_id] = InterestChatting( - # decay_rate=..., max_interest=..., # 可以从配置读取 - trigger_threshold=REPLY_TRIGGER_THRESHOLD, # 使用全局常量 - base_reply_probability=BASE_REPLY_PROBABILITY, - increase_rate=PROBABILITY_INCREASE_RATE_PER_SECOND, - decay_factor=PROBABILITY_DECAY_FACTOR_PER_SECOND, - max_probability=MAX_REPLY_PROBABILITY, - ) - # --- 结束修改 --- - # 首次创建时兴趣为 0,由第一次消息的 activate rate 决定初始值 - return self.interest_dict[stream_id] - - def get_interest(self, stream_id: str) -> float: - """获取指定聊天流当前的兴趣度 (值由后台任务更新)""" - # current_time = time.time() # 不再需要获取当前时间 - interest_chatting = self._get_or_create_interest_chatting(stream_id) - # 直接调用修改后的 get_interest,不传入时间 - return interest_chatting.get_interest() - - def increase_interest(self, stream_id: str, value: float): - """当收到消息时,增加指定聊天流的兴趣度""" - current_time = time.time() - interest_chatting = self._get_or_create_interest_chatting(stream_id) - # 调用修改后的 increase_interest,不再传入 message - interest_chatting.increase_interest(current_time, value) - stream_name = chat_manager.get_stream_name(stream_id) or stream_id # 获取流名称 - logger.debug( - f"增加了聊天流 {stream_name} 的兴趣度 {value:.2f},当前值为 {interest_chatting.interest_level:.2f}" - ) # 更新日志 - - def decrease_interest(self, stream_id: str, value: float): - """降低指定聊天流的兴趣度""" - current_time = time.time() - # 尝试获取,如果不存在则不做任何事 - interest_chatting = self.get_interest_chatting(stream_id) - if interest_chatting: - interest_chatting.decrease_interest(current_time, value) - stream_name = chat_manager.get_stream_name(stream_id) or stream_id # 获取流名称 - logger.debug( - f"降低了聊天流 {stream_name} 的兴趣度 {value:.2f},当前值为 {interest_chatting.interest_level:.2f}" - ) - else: - stream_name = chat_manager.get_stream_name(stream_id) or stream_id # 获取流名称 - logger.warning(f"尝试降低不存在的聊天流 {stream_name} 的兴趣度") - - def cleanup_inactive_chats(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): - """ - 清理长时间不活跃的聊天流记录 - max_age_seconds: 超过此时间未更新的将被清理 - """ - current_time = time.time() - keys_to_remove = [] - initial_count = len(self.interest_dict) - # with self._lock: # 如果需要锁整个迭代过程 - # 创建副本以避免在迭代时修改字典 - items_snapshot = list(self.interest_dict.items()) - - for stream_id, chatting in items_snapshot: - # 先计算当前兴趣,确保是最新的 - # 加锁保护 chatting 对象状态的读取和可能的修改 - # with self._lock: # 如果 InterestChatting 内部操作不是原子的 - last_interaction = chatting.last_interaction_time # 使用最后交互时间 - should_remove = False - reason = "" - # 只有设置了 max_age_seconds 才检查时间 - if ( - max_age_seconds is not None and (current_time - last_interaction) > max_age_seconds - ): # 使用 last_interaction - should_remove = True - reason = f"inactive time ({current_time - last_interaction:.0f}s) > max age ({max_age_seconds}s)" # 更新日志信息 - - if should_remove: - keys_to_remove.append(stream_id) - stream_name = chat_manager.get_stream_name(stream_id) or stream_id # 获取流名称 - logger.debug(f"Marking stream {stream_name} for removal. Reason: {reason}") - - if keys_to_remove: - logger.info(f"清理识别到 {len(keys_to_remove)} 个不活跃/低兴趣的流。") - # with self._lock: # 确保删除操作的原子性 - for key in keys_to_remove: - # 再次检查 key 是否存在,以防万一在迭代和删除之间状态改变 - if key in self.interest_dict: - del self.interest_dict[key] - stream_name = chat_manager.get_stream_name(key) or key # 获取流名称 - logger.debug(f"移除了流: {stream_name}") - final_count = initial_count - len(keys_to_remove) - logger.info(f"清理完成。移除了 {len(keys_to_remove)} 个流。当前数量: {final_count}") - else: - logger.info(f"清理完成。没有流符合移除条件。当前数量: {initial_count}") diff --git a/src/plugins/chat_module/heartFC_chat/messagesender.py b/src/plugins/chat_module/heartFC_chat/messagesender.py index fb295bed..897bc45f 100644 --- a/src/plugins/chat_module/heartFC_chat/messagesender.py +++ b/src/plugins/chat_module/heartFC_chat/messagesender.py @@ -220,9 +220,8 @@ class MessageManager: await asyncio.sleep(typing_time) logger.debug(f"\n{message_earliest.processed_plain_text},{typing_time},等待输入时间结束\n") - await self.storage.store_message(message_earliest, message_earliest.chat_stream) - await MessageSender().send_message(message_earliest) + await self.storage.store_message(message_earliest, message_earliest.chat_stream) container.remove_message(message_earliest) diff --git a/src/plugins/chat_module/heartFC_chat/pf_chatting.py b/src/plugins/chat_module/heartFC_chat/pf_chatting.py index 59472fd1..12a0e8ec 100644 --- a/src/plugins/chat_module/heartFC_chat/pf_chatting.py +++ b/src/plugins/chat_module/heartFC_chat/pf_chatting.py @@ -15,6 +15,9 @@ from src.config.config import global_config from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move from src.plugins.utils.timer_calculater import Timer # <--- Import Timer +INITIAL_DURATION = 60.0 + + # 定义日志配置 (使用 loguru 格式) interest_log_config = LogConfig( console_format=PFC_STYLE_CONFIG["console_format"], # 使用默认控制台格式 @@ -25,7 +28,7 @@ logger = get_module_logger("PFCLoop", config=interest_log_config) # Logger Name # Forward declaration for type hinting if TYPE_CHECKING: - from .heartFC_controler import HeartFC_Controller + from .heartFC_controler import HeartFCController PLANNER_TOOL_DEFINITION = [ { @@ -61,13 +64,13 @@ class PFChatting: 只要计时器>0,循环就会继续。 """ - def __init__(self, chat_id: str, heartfc_controller_instance: "HeartFC_Controller"): + def __init__(self, chat_id: str, heartfc_controller_instance: "HeartFCController"): """ 初始化PFChatting实例。 Args: chat_id: The identifier for the chat stream (e.g., stream_id). - heartfc_controller_instance: 访问共享资源和方法的主HeartFC_Controller实例。 + heartfc_controller_instance: 访问共享资源和方法的主HeartFCController实例。 """ self.heartfc_controller = heartfc_controller_instance # Store the controller instance self.stream_id: str = chat_id @@ -91,7 +94,7 @@ class PFChatting: self._loop_active: bool = False # Is the loop currently running? self._loop_task: Optional[asyncio.Task] = None # Stores the main loop task self._trigger_count_this_activation: int = 0 # Counts triggers within an active period - self._initial_duration: float = 60.0 # 首次触发增加的时间 + self._initial_duration: float = INITIAL_DURATION # 首次触发增加的时间 self._last_added_duration: float = self._initial_duration # <--- 新增:存储上次增加的时间 def _get_log_prefix(self) -> str: @@ -374,6 +377,22 @@ class PFChatting: ) action_taken_this_cycle = False + # --- Print Timer Results --- # + if cycle_timers: # 先检查cycle_timers是否非空 + timer_strings = [] + for name, elapsed in cycle_timers.items(): + # 直接格式化存储在字典中的浮点数 elapsed + formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" + timer_strings.append(f"{name}: {formatted_time}") + + if timer_strings: # 如果有有效计时器数据才打印 + logger.debug( + f"{log_prefix} test testtesttesttesttesttesttesttesttesttest Cycle Timers: {'; '.join(timer_strings)}" + ) + + # --- Timer Decrement --- # + cycle_duration = time.monotonic() - loop_cycle_start_time + except Exception as e_cycle: logger.error(f"{log_prefix} 循环周期执行时发生错误: {e_cycle}") logger.error(traceback.format_exc()) @@ -387,21 +406,6 @@ class PFChatting: self._processing_lock.release() logger.trace(f"{log_prefix} 循环释放了处理锁.") - # --- Print Timer Results --- # - if cycle_timers: # 先检查cycle_timers是否非空 - timer_strings = [] - for name, elapsed in cycle_timers.items(): - # 直接格式化存储在字典中的浮点数 elapsed - formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" - timer_strings.append(f"{name}: {formatted_time}") - - if timer_strings: # 如果有有效计时器数据才打印 - logger.debug( - f"{log_prefix} test testtesttesttesttesttesttesttesttesttest Cycle Timers: {'; '.join(timer_strings)}" - ) - - # --- Timer Decrement --- # - cycle_duration = time.monotonic() - loop_cycle_start_time async with self._timer_lock: self._loop_timer -= cycle_duration # Log timer decrement less aggressively @@ -749,7 +753,7 @@ class PFChatting: # --- Generate Response with LLM --- # # Access gpt instance via controller gpt_instance = self.heartfc_controller.gpt - logger.debug(f"{log_prefix}[Replier-{thinking_id}] Calling LLM to generate response...") + # logger.debug(f"{log_prefix}[Replier-{thinking_id}] Calling LLM to generate response...") # Ensure generate_response has access to current_mind if it's crucial context response_set = await gpt_instance.generate_response( @@ -771,7 +775,7 @@ class PFChatting: logger.error(traceback.format_exc()) return None - # --- Methods moved from HeartFC_Controller start --- + # --- Methods moved from HeartFCController start --- async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]: """创建思考消息 (尝试锚定到 anchor_message)""" if not anchor_message or not anchor_message.chat_stream: diff --git a/src/plugins/chat_module/heartFC_chat/reasoning_chat.py b/src/plugins/chat_module/heartFC_chat/reasoning_chat.py new file mode 100644 index 00000000..addcd53d --- /dev/null +++ b/src/plugins/chat_module/heartFC_chat/reasoning_chat.py @@ -0,0 +1,425 @@ +import time +import threading # 导入 threading +from random import random +import traceback +import asyncio +from typing import List, Dict +from ...moods.moods import MoodManager +from ....config.config import global_config +from ...chat.emoji_manager import emoji_manager +from .reasoning_generator import ResponseGenerator +from ...chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet +from ...chat.messagesender import message_manager +from ...storage.storage import MessageStorage +from ...chat.utils import is_mentioned_bot_in_message +from ...chat.utils_image import image_path_to_base64 +from ...willing.willing_manager import willing_manager +from ...message import UserInfo, Seg +from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig +from src.plugins.chat.chat_stream import ChatStream +from src.plugins.person_info.relationship_manager import relationship_manager +from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager +from src.plugins.utils.timer_calculater import Timer +from src.heart_flow.heartflow import heartflow +from .heartFC_controler import HeartFCController + +# 定义日志配置 +chat_config = LogConfig( + console_format=CHAT_STYLE_CONFIG["console_format"], + file_format=CHAT_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("reasoning_chat", config=chat_config) + + +class ReasoningChat: + _instance = None + _lock = threading.Lock() + _initialized = False + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + with cls._lock: + # Double-check locking + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + # 防止重复初始化 + if self._initialized: + return + with self.__class__._lock: # 使用类锁确保线程安全 + if self._initialized: + return + logger.info("正在初始化 ReasoningChat 单例...") # 添加日志 + self.storage = MessageStorage() + self.gpt = ResponseGenerator() + self.mood_manager = MoodManager.get_instance() + # 用于存储每个 chat stream 的兴趣监控任务 + self._interest_monitoring_tasks: Dict[str, asyncio.Task] = {} + self._initialized = True + logger.info("ReasoningChat 单例初始化完成。") # 添加日志 + + @classmethod + def get_instance(cls): + """获取 ReasoningChat 的单例实例。""" + if cls._instance is None: + # 如果实例还未创建(理论上应该在 main 中初始化,但作为备用) + logger.warning("ReasoningChat 实例在首次 get_instance 时创建。") + cls() # 调用构造函数来创建实例 + return cls._instance + + @staticmethod + async def _create_thinking_message(message, chat, userinfo, messageinfo): + """创建思考消息""" + bot_user_info = UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=messageinfo.platform, + ) + + thinking_time_point = round(time.time(), 2) + thinking_id = "mt" + str(thinking_time_point) + thinking_message = MessageThinking( + message_id=thinking_id, + chat_stream=chat, + bot_user_info=bot_user_info, + reply=message, + thinking_start_time=thinking_time_point, + ) + + message_manager.add_message(thinking_message) + + return thinking_id + + @staticmethod + async def _send_response_messages(message, chat, response_set: List[str], thinking_id) -> MessageSending: + """发送回复消息""" + container = message_manager.get_container(chat.stream_id) + thinking_message = None + + for msg in container.messages: + if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: + thinking_message = msg + container.messages.remove(msg) + break + + if not thinking_message: + logger.warning("未找到对应的思考消息,可能已超时被移除") + return + + thinking_start_time = thinking_message.thinking_start_time + message_set = MessageSet(chat, thinking_id) + + mark_head = False + first_bot_msg = None + for msg in response_set: + message_segment = Seg(type="text", data=msg) + bot_message = MessageSending( + message_id=thinking_id, + chat_stream=chat, + bot_user_info=UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=message.message_info.platform, + ), + sender_info=message.message_info.user_info, + message_segment=message_segment, + reply=message, + is_head=not mark_head, + is_emoji=False, + thinking_start_time=thinking_start_time, + ) + if not mark_head: + mark_head = True + first_bot_msg = bot_message + message_set.add_message(bot_message) + message_manager.add_message(message_set) + + return first_bot_msg + + @staticmethod + async def _handle_emoji(message, chat, response): + """处理表情包""" + if random() < global_config.emoji_chance: + emoji_raw = await emoji_manager.get_emoji_for_text(response) + if emoji_raw: + emoji_path, description = emoji_raw + emoji_cq = image_path_to_base64(emoji_path) + + thinking_time_point = round(message.message_info.time, 2) + + message_segment = Seg(type="emoji", data=emoji_cq) + bot_message = MessageSending( + message_id="mt" + str(thinking_time_point), + chat_stream=chat, + bot_user_info=UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=message.message_info.platform, + ), + sender_info=message.message_info.user_info, + message_segment=message_segment, + reply=message, + is_head=False, + is_emoji=True, + ) + message_manager.add_message(bot_message) + + async def _update_relationship(self, message: MessageRecv, response_set): + """更新关系情绪""" + ori_response = ",".join(response_set) + stance, emotion = await self.gpt._get_emotion_tags(ori_response, message.processed_plain_text) + await relationship_manager.calculate_update_relationship_value( + chat_stream=message.chat_stream, label=emotion, stance=stance + ) + self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor) + + async def _find_interested_message(self, chat: ChatStream) -> None: + # 此函数设计为后台任务,轮询指定 chat 的兴趣消息。 + # 它通常由外部代码在 chat 流活跃时启动。 + controller = HeartFCController.get_instance() # 获取控制器实例 + stream_id = chat.stream_id # 获取 stream_id + + if not controller: + logger.error(f"无法获取 HeartFCController 实例,无法检查 PFChatting 状态。stream: {stream_id}") + # 在没有控制器的情况下可能需要决定是继续处理还是完全停止?这里暂时假设继续 + pass # 或者 return? + + logger.info(f"[{stream_id}] 兴趣消息监控任务启动。") # 增加启动日志 + while True: + await asyncio.sleep(1) # 每秒检查一次 + + # --- 修改:通过 heartflow 获取 subheartflow 和 interest_dict --- # + subheartflow = heartflow.get_subheartflow(stream_id) + + # 检查 subheartflow 是否存在以及是否被标记停止 + if not subheartflow or subheartflow.should_stop: + logger.info(f"[{stream_id}] SubHeartflow 不存在或已停止,兴趣消息监控任务退出。") + break # 退出循环,任务结束 + + # 从 subheartflow 获取 interest_dict + interest_dict = subheartflow.get_interest_dict() + # --- 结束修改 --- # + + # 创建 items 快照进行迭代,避免在迭代时修改字典 + items_to_process = list(interest_dict.items()) + + if not items_to_process: + continue # 没有需要处理的消息,继续等待 + + # logger.debug(f"[{stream_id}] 发现 {len(items_to_process)} 条待处理兴趣消息。") # 调试日志 + + for msg_id, (message, interest_value, is_mentioned) in items_to_process: + # --- 检查 PFChatting 是否活跃 --- # + pf_active = False + if controller: + pf_active = controller.is_pf_chatting_active(stream_id) + + if pf_active: + # 如果 PFChatting 活跃,则跳过处理,直接移除消息 + removed_item = interest_dict.pop(msg_id, None) + if removed_item: + logger.debug(f"[{stream_id}] PFChatting 活跃,已跳过并移除兴趣消息 {msg_id}") + continue # 处理下一条消息 + # --- 结束检查 --- # + + # 只有当 PFChatting 不活跃时才执行以下处理逻辑 + try: + # logger.debug(f"[{stream_id}] 正在处理兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})" ) + await self.normal_reasoning_chat( + message=message, + chat=chat, # chat 对象仍然有效 + is_mentioned=is_mentioned, + interested_rate=interest_value, # 使用从字典获取的原始兴趣值 + ) + # logger.debug(f"[{stream_id}] 处理完成消息 {msg_id}") + except Exception as e: + logger.error(f"[{stream_id}] 处理兴趣消息 {msg_id} 时出错: {e}\n{traceback.format_exc()}") + finally: + # 无论处理成功与否(且PFChatting不活跃),都尝试从原始字典中移除该消息 + # 使用 pop(key, None) 避免 Key Error + removed_item = interest_dict.pop(msg_id, None) + if removed_item: + logger.debug(f"[{stream_id}] 已从兴趣字典中移除消息 {msg_id}") + + async def normal_reasoning_chat( + self, message: MessageRecv, chat: ChatStream, is_mentioned: bool, interested_rate: float + ) -> None: + timing_results = {} + userinfo = message.message_info.user_info + messageinfo = message.message_info + + is_mentioned, reply_probability = is_mentioned_bot_in_message(message) + # 意愿管理器:设置当前message信息 + willing_manager.setup(message, chat, is_mentioned, interested_rate) + + # 获取回复概率 + is_willing = False + if reply_probability != 1: + is_willing = True + reply_probability = await willing_manager.get_reply_probability(message.message_info.message_id) + + if message.message_info.additional_config: + if "maimcore_reply_probability_gain" in message.message_info.additional_config.keys(): + reply_probability += message.message_info.additional_config["maimcore_reply_probability_gain"] + + # 打印消息信息 + mes_name = chat.group_info.group_name if chat.group_info else "私聊" + current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time)) + willing_log = f"[回复意愿:{await willing_manager.get_willing(chat.stream_id):.2f}]" if is_willing else "" + logger.info( + f"[{current_time}][{mes_name}]" + f"{chat.user_info.user_nickname}:" + f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]" + ) + do_reply = False + if random() < reply_probability: + do_reply = True + + # 回复前处理 + await willing_manager.before_generate_reply_handle(message.message_info.message_id) + + # 创建思考消息 + with Timer("创建思考消息", timing_results): + thinking_id = await self._create_thinking_message(message, chat, userinfo, messageinfo) + + logger.debug(f"创建捕捉器,thinking_id:{thinking_id}") + + info_catcher = info_catcher_manager.get_info_catcher(thinking_id) + info_catcher.catch_decide_to_response(message) + + # 生成回复 + try: + with Timer("生成回复", timing_results): + response_set = await self.gpt.generate_response( + message=message, + thinking_id=thinking_id, + ) + + info_catcher.catch_after_generate_response(timing_results["生成回复"]) + except Exception as e: + logger.error(f"回复生成出现错误:{str(e)} {traceback.format_exc()}") + response_set = None + + if not response_set: + logger.info(f"[{chat.stream_id}] 模型未生成回复内容") + # 如果模型未生成回复,移除思考消息 + container = message_manager.get_container(chat.stream_id) + # thinking_message = None + for msg in container.messages[:]: # Iterate over a copy + if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: + # thinking_message = msg + container.messages.remove(msg) + logger.debug(f"[{chat.stream_id}] 已移除未产生回复的思考消息 {thinking_id}") + break + return # 不发送回复 + + logger.info(f"[{chat.stream_id}] 回复内容: {response_set}") + + # 发送回复 + with Timer("消息发送", timing_results): + first_bot_msg = await self._send_response_messages(message, chat, response_set, thinking_id) + + info_catcher.catch_after_response(timing_results["消息发送"], response_set, first_bot_msg) + + info_catcher.done_catch() + + # 处理表情包 + with Timer("处理表情包", timing_results): + await self._handle_emoji(message, chat, response_set[0]) + + # 更新关系情绪 + with Timer("关系更新", timing_results): + await self._update_relationship(message, response_set) + + # 回复后处理 + await willing_manager.after_generate_reply_handle(message.message_info.message_id) + + # 输出性能计时结果 + if do_reply: + timing_str = " | ".join([f"{step}: {duration:.2f}秒" for step, duration in timing_results.items()]) + trigger_msg = message.processed_plain_text + response_msg = " ".join(response_set) if response_set else "无回复" + logger.info(f"触发消息: {trigger_msg[:20]}... | 推理消息: {response_msg[:20]}... | 性能计时: {timing_str}") + else: + # 不回复处理 + await willing_manager.not_reply_handle(message.message_info.message_id) + + # 意愿管理器:注销当前message信息 + willing_manager.delete(message.message_info.message_id) + + @staticmethod + def _check_ban_words(text: str, chat, userinfo) -> bool: + """检查消息中是否包含过滤词""" + for word in global_config.ban_words: + if word in text: + logger.info( + f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" + ) + logger.info(f"[过滤词识别]消息中含有{word},filtered") + return True + return False + + @staticmethod + def _check_ban_regex(text: str, chat, userinfo) -> bool: + """检查消息是否匹配过滤正则表达式""" + for pattern in global_config.ban_msgs_regex: + if pattern.search(text): + logger.info( + f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" + ) + logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") + return True + return False + + async def start_monitoring_interest(self, chat: ChatStream): + """为指定的 ChatStream 启动兴趣消息监控任务(如果尚未运行)。""" + stream_id = chat.stream_id + if stream_id not in self._interest_monitoring_tasks or self._interest_monitoring_tasks[stream_id].done(): + logger.info(f"为聊天流 {stream_id} 启动兴趣消息监控任务...") + # 创建新任务 + task = asyncio.create_task(self._find_interested_message(chat)) + # 添加完成回调 + task.add_done_callback(lambda t: self._handle_task_completion(stream_id, t)) + self._interest_monitoring_tasks[stream_id] = task + # else: + # logger.debug(f"聊天流 {stream_id} 的兴趣消息监控任务已在运行。") + + def _handle_task_completion(self, stream_id: str, task: asyncio.Task): + """兴趣监控任务完成时的回调函数。""" + try: + # 检查任务是否因异常而结束 + exception = task.exception() + if exception: + logger.error(f"聊天流 {stream_id} 的兴趣监控任务因异常结束: {exception}") + logger.error(traceback.format_exc()) # 记录完整的 traceback + else: + logger.info(f"聊天流 {stream_id} 的兴趣监控任务正常结束。") + except asyncio.CancelledError: + logger.info(f"聊天流 {stream_id} 的兴趣监控任务被取消。") + except Exception as e: + logger.error(f"处理聊天流 {stream_id} 任务完成回调时出错: {e}") + finally: + # 从字典中移除已完成或取消的任务 + if stream_id in self._interest_monitoring_tasks: + del self._interest_monitoring_tasks[stream_id] + logger.debug(f"已从监控任务字典中移除 {stream_id}") + + async def stop_monitoring_interest(self, stream_id: str): + """停止指定聊天流的兴趣监控任务。""" + if stream_id in self._interest_monitoring_tasks: + task = self._interest_monitoring_tasks[stream_id] + if task and not task.done(): + task.cancel() # 尝试取消任务 + logger.info(f"尝试取消聊天流 {stream_id} 的兴趣监控任务。") + try: + await task # 等待任务响应取消 + except asyncio.CancelledError: + logger.info(f"聊天流 {stream_id} 的兴趣监控任务已成功取消。") + except Exception as e: + logger.error(f"等待聊天流 {stream_id} 监控任务取消时出现异常: {e}") + # 在回调函数 _handle_task_completion 中移除任务 + # else: + # logger.debug(f"聊天流 {stream_id} 没有正在运行的兴趣监控任务可停止。") diff --git a/src/plugins/chat_module/heartFC_chat/reasoning_generator.py b/src/plugins/chat_module/heartFC_chat/reasoning_generator.py new file mode 100644 index 00000000..2f4ba06e --- /dev/null +++ b/src/plugins/chat_module/heartFC_chat/reasoning_generator.py @@ -0,0 +1,199 @@ +from typing import List, Optional, Tuple, Union +import random + +from ...models.utils_model import LLMRequest +from ....config.config import global_config +from ...chat.message import MessageThinking +from .reasoning_prompt_builder import prompt_builder +from ...chat.utils import process_llm_response +from ...utils.timer_calculater import Timer +from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG +from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager + +# 定义日志配置 +llm_config = LogConfig( + # 使用消息发送专用样式 + console_format=LLM_STYLE_CONFIG["console_format"], + file_format=LLM_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("llm_generator", config=llm_config) + + +class ResponseGenerator: + def __init__(self): + self.model_reasoning = LLMRequest( + model=global_config.llm_reasoning, + temperature=0.7, + max_tokens=3000, + request_type="response_reasoning", + ) + self.model_normal = LLMRequest( + model=global_config.llm_normal, + temperature=global_config.llm_normal["temp"], + max_tokens=256, + request_type="response_reasoning", + ) + + self.model_sum = LLMRequest( + model=global_config.llm_summary_by_topic, temperature=0.7, max_tokens=3000, request_type="relation" + ) + self.current_model_type = "r1" # 默认使用 R1 + self.current_model_name = "unknown model" + + async def generate_response(self, message: MessageThinking, thinking_id: str) -> Optional[Union[str, List[str]]]: + """根据当前模型类型选择对应的生成函数""" + # 从global_config中获取模型概率值并选择模型 + if random.random() < global_config.model_reasoning_probability: + self.current_model_type = "深深地" + current_model = self.model_reasoning + else: + self.current_model_type = "浅浅的" + current_model = self.model_normal + + logger.info( + f"{self.current_model_type}思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}" + ) # noqa: E501 + + model_response = await self._generate_response_with_model(message, current_model, thinking_id) + + # print(f"raw_content: {model_response}") + + if model_response: + logger.info(f"{global_config.BOT_NICKNAME}的回复是:{model_response}") + model_response = await self._process_response(model_response) + + return model_response + else: + logger.info(f"{self.current_model_type}思考,失败") + return None + + async def _generate_response_with_model(self, message: MessageThinking, model: LLMRequest, thinking_id: str): + info_catcher = info_catcher_manager.get_info_catcher(thinking_id) + + if message.chat_stream.user_info.user_cardname and message.chat_stream.user_info.user_nickname: + sender_name = ( + f"[({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}]" + f"{message.chat_stream.user_info.user_cardname}" + ) + elif message.chat_stream.user_info.user_nickname: + sender_name = f"({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}" + else: + sender_name = f"用户({message.chat_stream.user_info.user_id})" + + logger.debug("开始使用生成回复-2") + # 构建prompt + with Timer() as t_build_prompt: + prompt = await prompt_builder._build_prompt( + message.chat_stream, + message_txt=message.processed_plain_text, + sender_name=sender_name, + stream_id=message.chat_stream.stream_id, + ) + logger.info(f"构建prompt时间: {t_build_prompt.human_readable}") + + try: + content, reasoning_content, self.current_model_name = await model.generate_response(prompt) + + info_catcher.catch_after_llm_generated( + prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name + ) + + except Exception: + logger.exception("生成回复时出错") + return None + + # 保存到数据库 + # self._save_to_db( + # message=message, + # sender_name=sender_name, + # prompt=prompt, + # content=content, + # reasoning_content=reasoning_content, + # # reasoning_content_check=reasoning_content_check if global_config.enable_kuuki_read else "" + # ) + + return content + + # def _save_to_db( + # self, + # message: MessageRecv, + # sender_name: str, + # prompt: str, + # content: str, + # reasoning_content: str, + # ): + # """保存对话记录到数据库""" + # db.reasoning_logs.insert_one( + # { + # "time": time.time(), + # "chat_id": message.chat_stream.stream_id, + # "user": sender_name, + # "message": message.processed_plain_text, + # "model": self.current_model_name, + # "reasoning": reasoning_content, + # "response": content, + # "prompt": prompt, + # } + # ) + + async def _get_emotion_tags(self, content: str, processed_plain_text: str): + """提取情感标签,结合立场和情绪""" + try: + # 构建提示词,结合回复内容、被回复的内容以及立场分析 + prompt = f""" + 请严格根据以下对话内容,完成以下任务: + 1. 判断回复者对被回复者观点的直接立场: + - "支持":明确同意或强化被回复者观点 + - "反对":明确反驳或否定被回复者观点 + - "中立":不表达明确立场或无关回应 + 2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签 + 3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒" + 4. 考虑回复者的人格设定为{global_config.personality_core} + + 对话示例: + 被回复:「A就是笨」 + 回复:「A明明很聪明」 → 反对-愤怒 + + 当前对话: + 被回复:「{processed_plain_text}」 + 回复:「{content}」 + + 输出要求: + - 只需输出"立场-情绪"结果,不要解释 + - 严格基于文字直接表达的对立关系判断 + """ + + # 调用模型生成结果 + result, _, _ = await self.model_sum.generate_response(prompt) + result = result.strip() + + # 解析模型输出的结果 + if "-" in result: + stance, emotion = result.split("-", 1) + valid_stances = ["支持", "反对", "中立"] + valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"] + if stance in valid_stances and emotion in valid_emotions: + return stance, emotion # 返回有效的立场-情绪组合 + else: + logger.debug(f"无效立场-情感组合:{result}") + return "中立", "平静" # 默认返回中立-平静 + else: + logger.debug(f"立场-情感格式错误:{result}") + return "中立", "平静" # 格式错误时返回默认值 + + except Exception as e: + logger.debug(f"获取情感标签时出错: {e}") + return "中立", "平静" # 出错时返回默认值 + + @staticmethod + async def _process_response(content: str) -> Tuple[List[str], List[str]]: + """处理响应内容,返回处理后的内容和情感标签""" + if not content: + return None, [] + + processed_response = process_llm_response(content) + + # print(f"得到了处理后的llm返回{processed_response}") + + return processed_response diff --git a/src/plugins/chat_module/heartFC_chat/reasoning_prompt_builder.py b/src/plugins/chat_module/heartFC_chat/reasoning_prompt_builder.py new file mode 100644 index 00000000..d37d6545 --- /dev/null +++ b/src/plugins/chat_module/heartFC_chat/reasoning_prompt_builder.py @@ -0,0 +1,445 @@ +import random +import time +from typing import Optional, Union + +from ....common.database import db +from ...chat.utils import get_embedding, get_recent_group_detailed_plain_text, get_recent_group_speaker +from ...chat.chat_stream import chat_manager +from ...moods.moods import MoodManager +from ....individuality.individuality import Individuality +from ...memory_system.Hippocampus import HippocampusManager +from ...schedule.schedule_generator import bot_schedule +from ....config.config import global_config +from ...person_info.relationship_manager import relationship_manager +from src.common.logger import get_module_logger +from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager + +logger = get_module_logger("prompt") + + +def init_prompt(): + Prompt( + """ +{relation_prompt_all} +{memory_prompt} +{prompt_info} +{schedule_prompt} +{chat_target} +{chat_talking_prompt} +现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n +你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 +你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些, +尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} +请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 +请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 +{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", + "reasoning_prompt_main", + ) + Prompt( + "{relation_prompt}关系等级越大,关系越好,请分析聊天记录,根据你和说话者{sender_name}的关系和态度进行回复,明确你的立场和情感。", + "relationship_prompt", + ) + Prompt( + "你想起你之前见过的事情:{related_memory_info}。\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n", + "memory_prompt", + ) + Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt") + Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") + + +class PromptBuilder: + def __init__(self): + self.prompt_built = "" + self.activate_messages = "" + + async def _build_prompt( + self, chat_stream, message_txt: str, sender_name: str = "某人", stream_id: Optional[int] = None + ) -> tuple[str, str]: + # 开始构建prompt + prompt_personality = "你" + # person + individuality = Individuality.get_instance() + + personality_core = individuality.personality.personality_core + prompt_personality += personality_core + + personality_sides = individuality.personality.personality_sides + random.shuffle(personality_sides) + prompt_personality += f",{personality_sides[0]}" + + identity_detail = individuality.identity.identity_detail + random.shuffle(identity_detail) + prompt_personality += f",{identity_detail[0]}" + + # 关系 + who_chat_in_group = [ + (chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname) + ] + who_chat_in_group += get_recent_group_speaker( + stream_id, + (chat_stream.user_info.platform, chat_stream.user_info.user_id), + limit=global_config.MAX_CONTEXT_SIZE, + ) + + relation_prompt = "" + for person in who_chat_in_group: + relation_prompt += await relationship_manager.build_relationship_info(person) + + # relation_prompt_all = ( + # f"{relation_prompt}关系等级越大,关系越好,请分析聊天记录," + # f"根据你和说话者{sender_name}的关系和态度进行回复,明确你的立场和情感。" + # ) + + # 心情 + mood_manager = MoodManager.get_instance() + mood_prompt = mood_manager.get_prompt() + + # logger.info(f"心情prompt: {mood_prompt}") + + # 调取记忆 + memory_prompt = "" + related_memory = await HippocampusManager.get_instance().get_memory_from_text( + text=message_txt, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False + ) + related_memory_info = "" + if related_memory: + for memory in related_memory: + related_memory_info += memory[1] + # memory_prompt = f"你想起你之前见过的事情:{related_memory_info}。\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n" + memory_prompt = await global_prompt_manager.format_prompt( + "memory_prompt", related_memory_info=related_memory_info + ) + + # print(f"相关记忆:{related_memory_info}") + + # 日程构建 + # schedule_prompt = f"""你现在正在做的事情是:{bot_schedule.get_current_num_task(num=1, time_info=False)}""" + + # 获取聊天上下文 + chat_in_group = True + chat_talking_prompt = "" + if stream_id: + chat_talking_prompt = get_recent_group_detailed_plain_text( + stream_id, limit=global_config.MAX_CONTEXT_SIZE, combine=True + ) + chat_stream = chat_manager.get_stream(stream_id) + if chat_stream.group_info: + chat_talking_prompt = chat_talking_prompt + else: + chat_in_group = False + chat_talking_prompt = chat_talking_prompt + # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") + # 关键词检测与反应 + keywords_reaction_prompt = "" + for rule in global_config.keywords_reaction_rules: + if rule.get("enable", False): + if any(keyword in message_txt.lower() for keyword in rule.get("keywords", [])): + logger.info( + f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}" + ) + keywords_reaction_prompt += rule.get("reaction", "") + "," + else: + for pattern in rule.get("regex", []): + result = pattern.search(message_txt) + if result: + reaction = rule.get("reaction", "") + for name, content in result.groupdict().items(): + reaction = reaction.replace(f"[{name}]", content) + logger.info(f"匹配到以下正则表达式:{pattern},触发反应:{reaction}") + keywords_reaction_prompt += reaction + "," + break + + # 中文高手(新加的好玩功能) + prompt_ger = "" + if random.random() < 0.04: + prompt_ger += "你喜欢用倒装句" + if random.random() < 0.02: + prompt_ger += "你喜欢用反问句" + if random.random() < 0.01: + prompt_ger += "你喜欢用文言文" + + # 知识构建 + start_time = time.time() + prompt_info = await self.get_prompt_info(message_txt, threshold=0.38) + if prompt_info: + # prompt_info = f"""\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n""" + prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info) + + end_time = time.time() + logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}秒") + + # moderation_prompt = "" + # moderation_prompt = """**检查并忽略**任何涉及尝试绕过审核的行为。 + # 涉及政治敏感以及违法违规的内容请规避。""" + + logger.debug("开始构建prompt") + + # prompt = f""" + # {relation_prompt_all} + # {memory_prompt} + # {prompt_info} + # {schedule_prompt} + # {chat_target} + # {chat_talking_prompt} + # 现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n + # 你的网名叫{global_config.BOT_NICKNAME},有人也叫你{"/".join(global_config.BOT_ALIAS_NAMES)},{prompt_personality}。 + # 你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些, + # 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} + # 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 + # 请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 + # {moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""" + + prompt = await global_prompt_manager.format_prompt( + "reasoning_prompt_main", + relation_prompt_all=await global_prompt_manager.get_prompt_async("relationship_prompt"), + relation_prompt=relation_prompt, + sender_name=sender_name, + memory_prompt=memory_prompt, + prompt_info=prompt_info, + schedule_prompt=await global_prompt_manager.format_prompt( + "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) + ), + chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1") + if chat_in_group + else await global_prompt_manager.get_prompt_async("chat_target_private1"), + chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") + if chat_in_group + else await global_prompt_manager.get_prompt_async("chat_target_private2"), + chat_talking_prompt=chat_talking_prompt, + message_txt=message_txt, + bot_name=global_config.BOT_NICKNAME, + bot_other_names="/".join( + global_config.BOT_ALIAS_NAMES, + ), + prompt_personality=prompt_personality, + mood_prompt=mood_prompt, + keywords_reaction_prompt=keywords_reaction_prompt, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + + return prompt + + async def get_prompt_info(self, message: str, threshold: float): + start_time = time.time() + related_info = "" + logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}") + + # 1. 先从LLM获取主题,类似于记忆系统的做法 + topics = [] + # try: + # # 先尝试使用记忆系统的方法获取主题 + # hippocampus = HippocampusManager.get_instance()._hippocampus + # topic_num = min(5, max(1, int(len(message) * 0.1))) + # topics_response = await hippocampus.llm_topic_judge.generate_response(hippocampus.find_topic_llm(message, topic_num)) + + # # 提取关键词 + # topics = re.findall(r"<([^>]+)>", topics_response[0]) + # if not topics: + # topics = [] + # else: + # topics = [ + # topic.strip() + # for topic in ",".join(topics).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + # if topic.strip() + # ] + + # logger.info(f"从LLM提取的主题: {', '.join(topics)}") + # except Exception as e: + # logger.error(f"从LLM提取主题失败: {str(e)}") + # # 如果LLM提取失败,使用jieba分词提取关键词作为备选 + # words = jieba.cut(message) + # topics = [word for word in words if len(word) > 1][:5] + # logger.info(f"使用jieba提取的主题: {', '.join(topics)}") + + # 如果无法提取到主题,直接使用整个消息 + if not topics: + logger.info("未能提取到任何主题,使用整个消息进行查询") + embedding = await get_embedding(message, request_type="prompt_build") + if not embedding: + logger.error("获取消息嵌入向量失败") + return "" + + related_info = self.get_info_from_db(embedding, limit=3, threshold=threshold) + logger.info(f"知识库检索完成,总耗时: {time.time() - start_time:.3f}秒") + return related_info + + # 2. 对每个主题进行知识库查询 + logger.info(f"开始处理{len(topics)}个主题的知识库查询") + + # 优化:批量获取嵌入向量,减少API调用 + embeddings = {} + topics_batch = [topic for topic in topics if len(topic) > 0] + if message: # 确保消息非空 + topics_batch.append(message) + + # 批量获取嵌入向量 + embed_start_time = time.time() + for text in topics_batch: + if not text or len(text.strip()) == 0: + continue + + try: + embedding = await get_embedding(text, request_type="prompt_build") + if embedding: + embeddings[text] = embedding + else: + logger.warning(f"获取'{text}'的嵌入向量失败") + except Exception as e: + logger.error(f"获取'{text}'的嵌入向量时发生错误: {str(e)}") + + logger.info(f"批量获取嵌入向量完成,耗时: {time.time() - embed_start_time:.3f}秒") + + if not embeddings: + logger.error("所有嵌入向量获取失败") + return "" + + # 3. 对每个主题进行知识库查询 + all_results = [] + query_start_time = time.time() + + # 首先添加原始消息的查询结果 + if message in embeddings: + original_results = self.get_info_from_db(embeddings[message], limit=3, threshold=threshold, return_raw=True) + if original_results: + for result in original_results: + result["topic"] = "原始消息" + all_results.extend(original_results) + logger.info(f"原始消息查询到{len(original_results)}条结果") + + # 然后添加每个主题的查询结果 + for topic in topics: + if not topic or topic not in embeddings: + continue + + try: + topic_results = self.get_info_from_db(embeddings[topic], limit=3, threshold=threshold, return_raw=True) + if topic_results: + # 添加主题标记 + for result in topic_results: + result["topic"] = topic + all_results.extend(topic_results) + logger.info(f"主题'{topic}'查询到{len(topic_results)}条结果") + except Exception as e: + logger.error(f"查询主题'{topic}'时发生错误: {str(e)}") + + logger.info(f"知识库查询完成,耗时: {time.time() - query_start_time:.3f}秒,共获取{len(all_results)}条结果") + + # 4. 去重和过滤 + process_start_time = time.time() + unique_contents = set() + filtered_results = [] + for result in all_results: + content = result["content"] + if content not in unique_contents: + unique_contents.add(content) + filtered_results.append(result) + + # 5. 按相似度排序 + filtered_results.sort(key=lambda x: x["similarity"], reverse=True) + + # 6. 限制总数量(最多10条) + filtered_results = filtered_results[:10] + logger.info( + f"结果处理完成,耗时: {time.time() - process_start_time:.3f}秒,过滤后剩余{len(filtered_results)}条结果" + ) + + # 7. 格式化输出 + if filtered_results: + format_start_time = time.time() + grouped_results = {} + for result in filtered_results: + topic = result["topic"] + if topic not in grouped_results: + grouped_results[topic] = [] + grouped_results[topic].append(result) + + # 按主题组织输出 + for topic, results in grouped_results.items(): + related_info += f"【主题: {topic}】\n" + for _i, result in enumerate(results, 1): + _similarity = result["similarity"] + content = result["content"].strip() + # 调试:为内容添加序号和相似度信息 + # related_info += f"{i}. [{similarity:.2f}] {content}\n" + related_info += f"{content}\n" + related_info += "\n" + + logger.info(f"格式化输出完成,耗时: {time.time() - format_start_time:.3f}秒") + + logger.info(f"知识库检索总耗时: {time.time() - start_time:.3f}秒") + return related_info + + @staticmethod + def get_info_from_db( + query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False + ) -> Union[str, list]: + if not query_embedding: + return "" if not return_raw else [] + # 使用余弦相似度计算 + pipeline = [ + { + "$addFields": { + "dotProduct": { + "$reduce": { + "input": {"$range": [0, {"$size": "$embedding"}]}, + "initialValue": 0, + "in": { + "$add": [ + "$$value", + { + "$multiply": [ + {"$arrayElemAt": ["$embedding", "$$this"]}, + {"$arrayElemAt": [query_embedding, "$$this"]}, + ] + }, + ] + }, + } + }, + "magnitude1": { + "$sqrt": { + "$reduce": { + "input": "$embedding", + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + "magnitude2": { + "$sqrt": { + "$reduce": { + "input": query_embedding, + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + } + }, + {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$multiply": ["$magnitude1", "$magnitude2"]}]}}}, + { + "$match": { + "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 + } + }, + {"$sort": {"similarity": -1}}, + {"$limit": limit}, + {"$project": {"content": 1, "similarity": 1}}, + ] + + results = list(db.knowledges.aggregate(pipeline)) + logger.debug(f"知识库查询结果数量: {len(results)}") + + if not results: + return "" if not return_raw else [] + + if return_raw: + return results + else: + # 返回所有找到的内容,用换行分隔 + return "\n".join(str(result["content"]) for result in results) + + +init_prompt() +prompt_builder = PromptBuilder() diff --git a/src/plugins/chat_module/reasoning_chat/reasoning_chat.py b/src/plugins/chat_module/reasoning_chat/reasoning_chat.py index d149f68b..5455aed6 100644 --- a/src/plugins/chat_module/reasoning_chat/reasoning_chat.py +++ b/src/plugins/chat_module/reasoning_chat/reasoning_chat.py @@ -1,25 +1,26 @@ import time -from random import random import traceback -from typing import List -from ...memory_system.Hippocampus import HippocampusManager -from ...moods.moods import MoodManager -from ....config.config import global_config -from ...chat.emoji_manager import emoji_manager +from random import random +from typing import List, Optional + +from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig +from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from .reasoning_generator import ResponseGenerator +from ...chat.chat_stream import chat_manager +from ...chat.emoji_manager import emoji_manager from ...chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet +from ...chat.message_buffer import message_buffer from ...chat.messagesender import message_manager -from ...storage.storage import MessageStorage from ...chat.utils import is_mentioned_bot_in_message from ...chat.utils_image import image_path_to_base64 -from ...willing.willing_manager import willing_manager +from ...memory_system.Hippocampus import HippocampusManager from ...message import UserInfo, Seg -from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig -from ...chat.chat_stream import chat_manager +from ...moods.moods import MoodManager from ...person_info.relationship_manager import relationship_manager -from ...chat.message_buffer import message_buffer -from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager +from ...storage.storage import MessageStorage from ...utils.timer_calculater import Timer +from ...willing.willing_manager import willing_manager +from ....config.config import global_config # 定义日志配置 chat_config = LogConfig( @@ -35,7 +36,6 @@ class ReasoningChat: self.storage = MessageStorage() self.gpt = ResponseGenerator() self.mood_manager = MoodManager.get_instance() - self.mood_manager.start_mood_update() @staticmethod async def _create_thinking_message(message, chat, userinfo, messageinfo): @@ -61,7 +61,7 @@ class ReasoningChat: return thinking_id @staticmethod - async def _send_response_messages(message, chat, response_set: List[str], thinking_id) -> MessageSending: + async def _send_response_messages(message, chat, response_set: List[str], thinking_id) -> Optional[MessageSending]: """发送回复消息""" container = message_manager.get_container(chat.stream_id) thinking_message = None @@ -74,7 +74,7 @@ class ReasoningChat: if not thinking_message: logger.warning("未找到对应的思考消息,可能已超时被移除") - return + return None thinking_start_time = thinking_message.thinking_start_time message_set = MessageSet(chat, thinking_id) @@ -156,17 +156,17 @@ class ReasoningChat: # 消息加入缓冲池 await message_buffer.start_caching_messages(message) - # logger.info("使用推理聊天模式") - # 创建聊天流 chat = await chat_manager.get_or_create_stream( platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo, ) + message.update_chat_stream(chat) await message.process() + logger.trace(f"消息处理成功: {message.processed_plain_text}") # 过滤词/正则表达式过滤 if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( @@ -174,27 +174,13 @@ class ReasoningChat: ): return - await self.storage.store_message(message, chat) - - # 记忆激活 - with Timer("记忆激活", timing_results): - interested_rate = await HippocampusManager.get_instance().get_activate_from_text( - message.processed_plain_text, fast_retrieval=True - ) - # 查询缓冲器结果,会整合前面跳过的消息,改变processed_plain_text buffer_result = await message_buffer.query_buffer_result(message) - # 处理提及 - is_mentioned, reply_probability = is_mentioned_bot_in_message(message) - - # 意愿管理器:设置当前message信息 - willing_manager.setup(message, chat, is_mentioned, interested_rate) - # 处理缓冲器结果 if not buffer_result: - await willing_manager.bombing_buffer_message_handle(message.message_info.message_id) - willing_manager.delete(message.message_info.message_id) + # await willing_manager.bombing_buffer_message_handle(message.message_info.message_id) + # willing_manager.delete(message.message_info.message_id) f_type = "seglist" if message.message_segment.type != "seglist": f_type = message.message_segment.type @@ -213,6 +199,27 @@ class ReasoningChat: logger.info("触发缓冲,已炸飞消息列") return + try: + await self.storage.store_message(message, chat) + logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}") + except Exception as e: + logger.error(f"存储消息失败: {e}") + logger.error(traceback.format_exc()) + # 存储失败可能仍需考虑是否继续,暂时返回 + return + + is_mentioned, reply_probability = is_mentioned_bot_in_message(message) + # 记忆激活 + with Timer("记忆激活", timing_results): + interested_rate = await HippocampusManager.get_instance().get_activate_from_text( + message.processed_plain_text, fast_retrieval=True + ) + + # 处理提及 + + # 意愿管理器:设置当前message信息 + willing_manager.setup(message, chat, is_mentioned, interested_rate) + # 获取回复概率 is_willing = False if reply_probability != 1: diff --git a/src/plugins/chat_module/reasoning_chat/reasoning_generator.py b/src/plugins/chat_module/reasoning_chat/reasoning_generator.py index dda4e7c7..2f4ba06e 100644 --- a/src/plugins/chat_module/reasoning_chat/reasoning_generator.py +++ b/src/plugins/chat_module/reasoning_chat/reasoning_generator.py @@ -44,7 +44,7 @@ class ResponseGenerator: async def generate_response(self, message: MessageThinking, thinking_id: str) -> Optional[Union[str, List[str]]]: """根据当前模型类型选择对应的生成函数""" # 从global_config中获取模型概率值并选择模型 - if random.random() < global_config.MODEL_R1_PROBABILITY: + if random.random() < global_config.model_reasoning_probability: self.current_model_type = "深深地" current_model = self.model_reasoning else: diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 557b42f2..738e47c4 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -342,720 +342,6 @@ class Hippocampus: memories.sort(key=lambda x: x[2], reverse=True) return memories - async def get_memory_from_text( - self, - text: str, - max_memory_num: int = 3, - max_memory_length: int = 2, - max_depth: int = 3, - fast_retrieval: bool = False, - ) -> list: - """从文本中提取关键词并获取相关记忆。 - - Args: - text (str): 输入文本 - max_memory_num (int, optional): 记忆数量限制。默认为3。 - max_memory_length (int, optional): 记忆长度限制。默认为2。 - max_depth (int, optional): 记忆检索深度。默认为2。 - fast_retrieval (bool, optional): 是否使用快速检索。默认为False。 - 如果为True,使用jieba分词和TF-IDF提取关键词,速度更快但可能不够准确。 - 如果为False,使用LLM提取关键词,速度较慢但更准确。 - - Returns: - list: 记忆列表,每个元素是一个元组 (topic, memory_items, similarity) - - topic: str, 记忆主题 - - memory_items: list, 该主题下的记忆项列表 - - similarity: float, 与文本的相似度 - """ - if not text: - return [] - - if fast_retrieval: - # 使用jieba分词提取关键词 - words = jieba.cut(text) - # 过滤掉停用词和单字词 - keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - keywords = keywords[:5] - else: - # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 - # logger.info(f"提取关键词数量: {topic_num}") - topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(text, topic_num)) - - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response[0]) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - # logger.info(f"提取的关键词: {', '.join(keywords)}") - - # 过滤掉不存在于记忆图中的关键词 - valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] - if not valid_keywords: - # logger.info("没有找到有效的关键词节点") - return [] - - logger.info(f"有效的关键词: {', '.join(valid_keywords)}") - - # 从每个关键词获取记忆 - all_memories = [] - activate_map = {} # 存储每个词的累计激活值 - - # 对每个关键词进行扩散式检索 - for keyword in valid_keywords: - logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") - # 初始化激活值 - activation_values = {keyword: 1.0} - # 记录已访问的节点 - visited_nodes = {keyword} - # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) - nodes_to_process = [(keyword, 1.0, 0)] - - while nodes_to_process: - current_node, current_activation, current_depth = nodes_to_process.pop(0) - - # 如果激活值小于0或超过最大深度,停止扩散 - if current_activation <= 0 or current_depth >= max_depth: - continue - - # 获取当前节点的所有邻居 - neighbors = list(self.memory_graph.G.neighbors(current_node)) - - for neighbor in neighbors: - if neighbor in visited_nodes: - continue - - # 获取连接强度 - edge_data = self.memory_graph.G[current_node][neighbor] - strength = edge_data.get("strength", 1) - - # 计算新的激活值 - new_activation = current_activation - (1 / strength) - - if new_activation > 0: - activation_values[neighbor] = new_activation - visited_nodes.add(neighbor) - nodes_to_process.append((neighbor, new_activation, current_depth + 1)) - logger.trace( - f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})" - ) # noqa: E501 - - # 更新激活映射 - for node, activation_value in activation_values.items(): - if activation_value > 0: - if node in activate_map: - activate_map[node] += activation_value - else: - activate_map[node] = activation_value - - # 输出激活映射 - # logger.info("激活映射统计:") - # for node, total_activation in sorted(activate_map.items(), key=lambda x: x[1], reverse=True): - # logger.info(f"节点 '{node}': 累计激活值 = {total_activation:.2f}") - - # 基于激活值平方的独立概率选择 - remember_map = {} - # logger.info("基于激活值平方的归一化选择:") - - # 计算所有激活值的平方和 - total_squared_activation = sum(activation**2 for activation in activate_map.values()) - if total_squared_activation > 0: - # 计算归一化的激活值 - normalized_activations = { - node: (activation**2) / total_squared_activation for node, activation in activate_map.items() - } - - # 按归一化激活值排序并选择前max_memory_num个 - sorted_nodes = sorted(normalized_activations.items(), key=lambda x: x[1], reverse=True)[:max_memory_num] - - # 将选中的节点添加到remember_map - for node, normalized_activation in sorted_nodes: - remember_map[node] = activate_map[node] # 使用原始激活值 - logger.debug( - f"节点 '{node}' (归一化激活值: {normalized_activation:.2f}, 激活值: {activate_map[node]:.2f})" - ) - else: - logger.info("没有有效的激活值") - - # 从选中的节点中提取记忆 - all_memories = [] - # logger.info("开始从选中的节点中提取记忆:") - for node, activation in remember_map.items(): - logger.debug(f"处理节点 '{node}' (激活值: {activation:.2f}):") - node_data = self.memory_graph.G.nodes[node] - memory_items = node_data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - if memory_items: - logger.debug(f"节点包含 {len(memory_items)} 条记忆") - # 计算每条记忆与输入文本的相似度 - memory_similarities = [] - for memory in memory_items: - # 计算与输入文本的相似度 - memory_words = set(jieba.cut(memory)) - text_words = set(jieba.cut(text)) - all_words = memory_words | text_words - v1 = [1 if word in memory_words else 0 for word in all_words] - v2 = [1 if word in text_words else 0 for word in all_words] - similarity = cosine_similarity(v1, v2) - memory_similarities.append((memory, similarity)) - - # 按相似度排序 - memory_similarities.sort(key=lambda x: x[1], reverse=True) - # 获取最匹配的记忆 - top_memories = memory_similarities[:max_memory_length] - - # 添加到结果中 - for memory, similarity in top_memories: - all_memories.append((node, [memory], similarity)) - # logger.info(f"选中记忆: {memory} (相似度: {similarity:.2f})") - else: - logger.info("节点没有记忆") - - # 去重(基于记忆内容) - logger.debug("开始记忆去重:") - seen_memories = set() - unique_memories = [] - for topic, memory_items, activation_value in all_memories: - memory = memory_items[0] # 因为每个topic只有一条记忆 - if memory not in seen_memories: - seen_memories.add(memory) - unique_memories.append((topic, memory_items, activation_value)) - logger.debug(f"保留记忆: {memory} (来自节点: {topic}, 激活值: {activation_value:.2f})") - else: - logger.debug(f"跳过重复记忆: {memory} (来自节点: {topic})") - - # 转换为(关键词, 记忆)格式 - result = [] - for topic, memory_items, _ in unique_memories: - memory = memory_items[0] # 因为每个topic只有一条记忆 - result.append((topic, memory)) - logger.info(f"选中记忆: {memory} (来自节点: {topic})") - - return result - - async def get_activate_from_text(self, text: str, max_depth: int = 3, fast_retrieval: bool = False) -> float: - """从文本中提取关键词并获取相关记忆。 - - Args: - text (str): 输入文本 - max_depth (int, optional): 记忆检索深度。默认为2。 - fast_retrieval (bool, optional): 是否使用快速检索。默认为False。 - 如果为True,使用jieba分词和TF-IDF提取关键词,速度更快但可能不够准确。 - 如果为False,使用LLM提取关键词,速度较慢但更准确。 - - Returns: - float: 激活节点数与总节点数的比值 - """ - if not text: - return 0 - - if fast_retrieval: - # 使用jieba分词提取关键词 - words = jieba.cut(text) - # 过滤掉停用词和单字词 - keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - keywords = keywords[:5] - else: - # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 - # logger.info(f"提取关键词数量: {topic_num}") - topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(text, topic_num)) - - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response[0]) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - # logger.info(f"提取的关键词: {', '.join(keywords)}") - - # 过滤掉不存在于记忆图中的关键词 - valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] - if not valid_keywords: - # logger.info("没有找到有效的关键词节点") - return 0 - - logger.info(f"有效的关键词: {', '.join(valid_keywords)}") - - # 从每个关键词获取记忆 - activate_map = {} # 存储每个词的累计激活值 - - # 对每个关键词进行扩散式检索 - for keyword in valid_keywords: - logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") - # 初始化激活值 - activation_values = {keyword: 1.0} - # 记录已访问的节点 - visited_nodes = {keyword} - # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) - nodes_to_process = [(keyword, 1.0, 0)] - - while nodes_to_process: - current_node, current_activation, current_depth = nodes_to_process.pop(0) - - # 如果激活值小于0或超过最大深度,停止扩散 - if current_activation <= 0 or current_depth >= max_depth: - continue - - # 获取当前节点的所有邻居 - neighbors = list(self.memory_graph.G.neighbors(current_node)) - - for neighbor in neighbors: - if neighbor in visited_nodes: - continue - - # 获取连接强度 - edge_data = self.memory_graph.G[current_node][neighbor] - strength = edge_data.get("strength", 1) - - # 计算新的激活值 - new_activation = current_activation - (1 / strength) - - if new_activation > 0: - activation_values[neighbor] = new_activation - visited_nodes.add(neighbor) - nodes_to_process.append((neighbor, new_activation, current_depth + 1)) - # logger.debug( - # f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") # noqa: E501 - - # 更新激活映射 - for node, activation_value in activation_values.items(): - if activation_value > 0: - if node in activate_map: - activate_map[node] += activation_value - else: - activate_map[node] = activation_value - - # 输出激活映射 - # logger.info("激活映射统计:") - # for node, total_activation in sorted(activate_map.items(), key=lambda x: x[1], reverse=True): - # logger.info(f"节点 '{node}': 累计激活值 = {total_activation:.2f}") - - # 计算激活节点数与总节点数的比值 - total_activation = sum(activate_map.values()) - logger.info(f"总激活值: {total_activation:.2f}") - total_nodes = len(self.memory_graph.G.nodes()) - # activated_nodes = len(activate_map) - activation_ratio = total_activation / total_nodes if total_nodes > 0 else 0 - activation_ratio = activation_ratio * 60 - logger.info(f"总激活值: {total_activation:.2f}, 总节点数: {total_nodes}, 激活: {activation_ratio}") - - return activation_ratio - - -# 负责海马体与其他部分的交互 -class EntorhinalCortex: - def __init__(self, hippocampus: Hippocampus): - self.hippocampus = hippocampus - self.memory_graph = hippocampus.memory_graph - self.config = hippocampus.config - - def get_memory_sample(self): - """从数据库获取记忆样本""" - # 硬编码:每条消息最大记忆次数 - max_memorized_time_per_msg = 3 - - # 创建双峰分布的记忆调度器 - sample_scheduler = MemoryBuildScheduler( - n_hours1=self.config.memory_build_distribution[0], - std_hours1=self.config.memory_build_distribution[1], - weight1=self.config.memory_build_distribution[2], - n_hours2=self.config.memory_build_distribution[3], - std_hours2=self.config.memory_build_distribution[4], - weight2=self.config.memory_build_distribution[5], - total_samples=self.config.build_memory_sample_num, - ) - - timestamps = sample_scheduler.get_timestamp_array() - logger.info(f"回忆往事: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") - chat_samples = [] - for timestamp in timestamps: - messages = self.random_get_msg_snippet( - timestamp, self.config.build_memory_sample_length, max_memorized_time_per_msg - ) - if messages: - time_diff = (datetime.datetime.now().timestamp() - timestamp) / 3600 - logger.debug(f"成功抽取 {time_diff:.1f} 小时前的消息样本,共{len(messages)}条") - chat_samples.append(messages) - else: - logger.debug(f"时间戳 {timestamp} 的消息样本抽取失败") - - return chat_samples - - @staticmethod - def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: - """从数据库中随机获取指定时间戳附近的消息片段""" - try_count = 0 - while try_count < 3: - messages = get_closest_chat_from_db(length=chat_size, timestamp=target_timestamp) - if messages: - for message in messages: - if message["memorized_times"] >= max_memorized_time_per_msg: - messages = None - break - if messages: - for message in messages: - db.messages.update_one( - {"_id": message["_id"]}, {"$set": {"memorized_times": message["memorized_times"] + 1}} - ) - return messages - try_count += 1 - return None - - async def sync_memory_to_db(self): - """将记忆图同步到数据库""" - # 获取数据库中所有节点和内存中所有节点 - db_nodes = list(db.graph_data.nodes.find()) - memory_nodes = list(self.memory_graph.G.nodes(data=True)) - - # 转换数据库节点为字典格式,方便查找 - db_nodes_dict = {node["concept"]: node for node in db_nodes} - - # 检查并更新节点 - for concept, data in memory_nodes: - memory_items = data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 计算内存中节点的特征值 - memory_hash = self.hippocampus.calculate_node_hash(concept, memory_items) - - # 获取时间信息 - created_time = data.get("created_time", datetime.datetime.now().timestamp()) - last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) - - if concept not in db_nodes_dict: - # 数据库中缺少的节点,添加 - node_data = { - "concept": concept, - "memory_items": memory_items, - "hash": memory_hash, - "created_time": created_time, - "last_modified": last_modified, - } - db.graph_data.nodes.insert_one(node_data) - else: - # 获取数据库中节点的特征值 - db_node = db_nodes_dict[concept] - db_hash = db_node.get("hash", None) - - # 如果特征值不同,则更新节点 - if db_hash != memory_hash: - db.graph_data.nodes.update_one( - {"concept": concept}, - { - "$set": { - "memory_items": memory_items, - "hash": memory_hash, - "created_time": created_time, - "last_modified": last_modified, - } - }, - ) - - # 处理边的信息 - db_edges = list(db.graph_data.edges.find()) - memory_edges = list(self.memory_graph.G.edges(data=True)) - - # 创建边的哈希值字典 - db_edge_dict = {} - for edge in db_edges: - edge_hash = self.hippocampus.calculate_edge_hash(edge["source"], edge["target"]) - db_edge_dict[(edge["source"], edge["target"])] = {"hash": edge_hash, "strength": edge.get("strength", 1)} - - # 检查并更新边 - for source, target, data in memory_edges: - edge_hash = self.hippocampus.calculate_edge_hash(source, target) - edge_key = (source, target) - strength = data.get("strength", 1) - - # 获取边的时间信息 - created_time = data.get("created_time", datetime.datetime.now().timestamp()) - last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) - - if edge_key not in db_edge_dict: - # 添加新边 - edge_data = { - "source": source, - "target": target, - "strength": strength, - "hash": edge_hash, - "created_time": created_time, - "last_modified": last_modified, - } - db.graph_data.edges.insert_one(edge_data) - else: - # 检查边的特征值是否变化 - if db_edge_dict[edge_key]["hash"] != edge_hash: - db.graph_data.edges.update_one( - {"source": source, "target": target}, - { - "$set": { - "hash": edge_hash, - "strength": strength, - "created_time": created_time, - "last_modified": last_modified, - } - }, - ) - - def sync_memory_from_db(self): - """从数据库同步数据到内存中的图结构""" - current_time = datetime.datetime.now().timestamp() - need_update = False - - # 清空当前图 - self.memory_graph.G.clear() - - # 从数据库加载所有节点 - nodes = list(db.graph_data.nodes.find()) - for node in nodes: - concept = node["concept"] - memory_items = node.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 检查时间字段是否存在 - if "created_time" not in node or "last_modified" not in node: - need_update = True - # 更新数据库中的节点 - update_data = {} - if "created_time" not in node: - update_data["created_time"] = current_time - if "last_modified" not in node: - update_data["last_modified"] = current_time - - db.graph_data.nodes.update_one({"concept": concept}, {"$set": update_data}) - logger.info(f"[时间更新] 节点 {concept} 添加缺失的时间字段") - - # 获取时间信息(如果不存在则使用当前时间) - created_time = node.get("created_time", current_time) - last_modified = node.get("last_modified", current_time) - - # 添加节点到图中 - self.memory_graph.G.add_node( - concept, memory_items=memory_items, created_time=created_time, last_modified=last_modified - ) - - # 从数据库加载所有边 - edges = list(db.graph_data.edges.find()) - for edge in edges: - source = edge["source"] - target = edge["target"] - strength = edge.get("strength", 1) - - # 检查时间字段是否存在 - if "created_time" not in edge or "last_modified" not in edge: - need_update = True - # 更新数据库中的边 - update_data = {} - if "created_time" not in edge: - update_data["created_time"] = current_time - if "last_modified" not in edge: - update_data["last_modified"] = current_time - - db.graph_data.edges.update_one({"source": source, "target": target}, {"$set": update_data}) - logger.info(f"[时间更新] 边 {source} - {target} 添加缺失的时间字段") - - # 获取时间信息(如果不存在则使用当前时间) - created_time = edge.get("created_time", current_time) - last_modified = edge.get("last_modified", current_time) - - # 只有当源节点和目标节点都存在时才添加边 - if source in self.memory_graph.G and target in self.memory_graph.G: - self.memory_graph.G.add_edge( - source, target, strength=strength, created_time=created_time, last_modified=last_modified - ) - - if need_update: - logger.success("[数据库] 已为缺失的时间字段进行补充") - - async def resync_memory_to_db(self): - """清空数据库并重新同步所有记忆数据""" - start_time = time.time() - logger.info("[数据库] 开始重新同步所有记忆数据...") - - # 清空数据库 - clear_start = time.time() - db.graph_data.nodes.delete_many({}) - db.graph_data.edges.delete_many({}) - clear_end = time.time() - logger.info(f"[数据库] 清空数据库耗时: {clear_end - clear_start:.2f}秒") - - # 获取所有节点和边 - memory_nodes = list(self.memory_graph.G.nodes(data=True)) - memory_edges = list(self.memory_graph.G.edges(data=True)) - - # 重新写入节点 - node_start = time.time() - for concept, data in memory_nodes: - memory_items = data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - node_data = { - "concept": concept, - "memory_items": memory_items, - "hash": self.hippocampus.calculate_node_hash(concept, memory_items), - "created_time": data.get("created_time", datetime.datetime.now().timestamp()), - "last_modified": data.get("last_modified", datetime.datetime.now().timestamp()), - } - db.graph_data.nodes.insert_one(node_data) - node_end = time.time() - logger.info(f"[数据库] 写入 {len(memory_nodes)} 个节点耗时: {node_end - node_start:.2f}秒") - - # 重新写入边 - edge_start = time.time() - for source, target, data in memory_edges: - edge_data = { - "source": source, - "target": target, - "strength": data.get("strength", 1), - "hash": self.hippocampus.calculate_edge_hash(source, target), - "created_time": data.get("created_time", datetime.datetime.now().timestamp()), - "last_modified": data.get("last_modified", datetime.datetime.now().timestamp()), - } - db.graph_data.edges.insert_one(edge_data) - edge_end = time.time() - logger.info(f"[数据库] 写入 {len(memory_edges)} 条边耗时: {edge_end - edge_start:.2f}秒") - - end_time = time.time() - logger.success(f"[数据库] 重新同步完成,总耗时: {end_time - start_time:.2f}秒") - logger.success(f"[数据库] 同步了 {len(memory_nodes)} 个节点和 {len(memory_edges)} 条边") - - -# 海马体 -class Hippocampus: - def __init__(self): - self.memory_graph = MemoryGraph() - self.llm_topic_judge = None - self.llm_summary_by_topic = None - self.entorhinal_cortex = None - self.parahippocampal_gyrus = None - self.config = None - - def initialize(self, global_config): - self.config = MemoryConfig.from_global_config(global_config) - # 初始化子组件 - self.entorhinal_cortex = EntorhinalCortex(self) - self.parahippocampal_gyrus = ParahippocampalGyrus(self) - # 从数据库加载记忆图 - self.entorhinal_cortex.sync_memory_from_db() - self.llm_topic_judge = LLMRequest(self.config.llm_topic_judge, request_type="memory") - self.llm_summary_by_topic = LLMRequest(self.config.llm_summary_by_topic, request_type="memory") - - def get_all_node_names(self) -> list: - """获取记忆图中所有节点的名字列表""" - return list(self.memory_graph.G.nodes()) - - @staticmethod - def calculate_node_hash(concept, memory_items) -> int: - """计算节点的特征值""" - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - sorted_items = sorted(memory_items) - content = f"{concept}:{'|'.join(sorted_items)}" - return hash(content) - - @staticmethod - def calculate_edge_hash(source, target) -> int: - """计算边的特征值""" - nodes = sorted([source, target]) - return hash(f"{nodes[0]}:{nodes[1]}") - - @staticmethod - def find_topic_llm(text, topic_num): - prompt = ( - f"这是一段文字:{text}。请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," - f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" - f"如果确定找不出主题或者没有明显主题,返回。" - ) - return prompt - - @staticmethod - def topic_what(text, topic, time_info): - prompt = ( - f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,' - f"可以包含时间和人物,以及具体的观点。只输出这句话就好" - ) - return prompt - - @staticmethod - def calculate_topic_num(text, compress_rate): - """计算文本的话题数量""" - information_content = calculate_information_content(text) - topic_by_length = text.count("\n") * compress_rate - topic_by_information_content = max(1, min(5, int((information_content - 3) * 2))) - topic_num = int((topic_by_length + topic_by_information_content) / 2) - logger.debug( - f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, " - f"topic_num: {topic_num}" - ) - return topic_num - - def get_memory_from_keyword(self, keyword: str, max_depth: int = 2) -> list: - """从关键词获取相关记忆。 - - Args: - keyword (str): 关键词 - max_depth (int, optional): 记忆检索深度,默认为2。1表示只获取直接相关的记忆,2表示获取间接相关的记忆。 - - Returns: - list: 记忆列表,每个元素是一个元组 (topic, memory_items, similarity) - - topic: str, 记忆主题 - - memory_items: list, 该主题下的记忆项列表 - - similarity: float, 与关键词的相似度 - """ - if not keyword: - return [] - - # 获取所有节点 - all_nodes = list(self.memory_graph.G.nodes()) - memories = [] - - # 计算关键词的词集合 - keyword_words = set(jieba.cut(keyword)) - - # 遍历所有节点,计算相似度 - for node in all_nodes: - node_words = set(jieba.cut(node)) - all_words = keyword_words | node_words - v1 = [1 if word in keyword_words else 0 for word in all_words] - v2 = [1 if word in node_words else 0 for word in all_words] - similarity = cosine_similarity(v1, v2) - - # 如果相似度超过阈值,获取该节点的记忆 - if similarity >= 0.3: # 可以调整这个阈值 - node_data = self.memory_graph.G.nodes[node] - memory_items = node_data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - memories.append((node, memory_items, similarity)) - - # 按相似度降序排序 - memories.sort(key=lambda x: x[2], reverse=True) - return memories - async def get_memory_from_text( self, text: str, @@ -1543,6 +829,287 @@ class Hippocampus: return activation_ratio +# 负责海马体与其他部分的交互 +class EntorhinalCortex: + def __init__(self, hippocampus: Hippocampus): + self.hippocampus = hippocampus + self.memory_graph = hippocampus.memory_graph + self.config = hippocampus.config + + def get_memory_sample(self): + """从数据库获取记忆样本""" + # 硬编码:每条消息最大记忆次数 + max_memorized_time_per_msg = 3 + + # 创建双峰分布的记忆调度器 + sample_scheduler = MemoryBuildScheduler( + n_hours1=self.config.memory_build_distribution[0], + std_hours1=self.config.memory_build_distribution[1], + weight1=self.config.memory_build_distribution[2], + n_hours2=self.config.memory_build_distribution[3], + std_hours2=self.config.memory_build_distribution[4], + weight2=self.config.memory_build_distribution[5], + total_samples=self.config.build_memory_sample_num, + ) + + timestamps = sample_scheduler.get_timestamp_array() + logger.info(f"回忆往事: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") + chat_samples = [] + for timestamp in timestamps: + messages = self.random_get_msg_snippet( + timestamp, self.config.build_memory_sample_length, max_memorized_time_per_msg + ) + if messages: + time_diff = (datetime.datetime.now().timestamp() - timestamp) / 3600 + logger.debug(f"成功抽取 {time_diff:.1f} 小时前的消息样本,共{len(messages)}条") + chat_samples.append(messages) + else: + logger.debug(f"时间戳 {timestamp} 的消息样本抽取失败") + + return chat_samples + + @staticmethod + def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: + """从数据库中随机获取指定时间戳附近的消息片段""" + try_count = 0 + while try_count < 3: + messages = get_closest_chat_from_db(length=chat_size, timestamp=target_timestamp) + if messages: + for message in messages: + if message["memorized_times"] >= max_memorized_time_per_msg: + messages = None + break + if messages: + for message in messages: + db.messages.update_one( + {"_id": message["_id"]}, {"$set": {"memorized_times": message["memorized_times"] + 1}} + ) + return messages + try_count += 1 + return None + + async def sync_memory_to_db(self): + """将记忆图同步到数据库""" + # 获取数据库中所有节点和内存中所有节点 + db_nodes = list(db.graph_data.nodes.find()) + memory_nodes = list(self.memory_graph.G.nodes(data=True)) + + # 转换数据库节点为字典格式,方便查找 + db_nodes_dict = {node["concept"]: node for node in db_nodes} + + # 检查并更新节点 + for concept, data in memory_nodes: + memory_items = data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 计算内存中节点的特征值 + memory_hash = self.hippocampus.calculate_node_hash(concept, memory_items) + + # 获取时间信息 + created_time = data.get("created_time", datetime.datetime.now().timestamp()) + last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) + + if concept not in db_nodes_dict: + # 数据库中缺少的节点,添加 + node_data = { + "concept": concept, + "memory_items": memory_items, + "hash": memory_hash, + "created_time": created_time, + "last_modified": last_modified, + } + db.graph_data.nodes.insert_one(node_data) + else: + # 获取数据库中节点的特征值 + db_node = db_nodes_dict[concept] + db_hash = db_node.get("hash", None) + + # 如果特征值不同,则更新节点 + if db_hash != memory_hash: + db.graph_data.nodes.update_one( + {"concept": concept}, + { + "$set": { + "memory_items": memory_items, + "hash": memory_hash, + "created_time": created_time, + "last_modified": last_modified, + } + }, + ) + + # 处理边的信息 + db_edges = list(db.graph_data.edges.find()) + memory_edges = list(self.memory_graph.G.edges(data=True)) + + # 创建边的哈希值字典 + db_edge_dict = {} + for edge in db_edges: + edge_hash = self.hippocampus.calculate_edge_hash(edge["source"], edge["target"]) + db_edge_dict[(edge["source"], edge["target"])] = {"hash": edge_hash, "strength": edge.get("strength", 1)} + + # 检查并更新边 + for source, target, data in memory_edges: + edge_hash = self.hippocampus.calculate_edge_hash(source, target) + edge_key = (source, target) + strength = data.get("strength", 1) + + # 获取边的时间信息 + created_time = data.get("created_time", datetime.datetime.now().timestamp()) + last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) + + if edge_key not in db_edge_dict: + # 添加新边 + edge_data = { + "source": source, + "target": target, + "strength": strength, + "hash": edge_hash, + "created_time": created_time, + "last_modified": last_modified, + } + db.graph_data.edges.insert_one(edge_data) + else: + # 检查边的特征值是否变化 + if db_edge_dict[edge_key]["hash"] != edge_hash: + db.graph_data.edges.update_one( + {"source": source, "target": target}, + { + "$set": { + "hash": edge_hash, + "strength": strength, + "created_time": created_time, + "last_modified": last_modified, + } + }, + ) + + def sync_memory_from_db(self): + """从数据库同步数据到内存中的图结构""" + current_time = datetime.datetime.now().timestamp() + need_update = False + + # 清空当前图 + self.memory_graph.G.clear() + + # 从数据库加载所有节点 + nodes = list(db.graph_data.nodes.find()) + for node in nodes: + concept = node["concept"] + memory_items = node.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 检查时间字段是否存在 + if "created_time" not in node or "last_modified" not in node: + need_update = True + # 更新数据库中的节点 + update_data = {} + if "created_time" not in node: + update_data["created_time"] = current_time + if "last_modified" not in node: + update_data["last_modified"] = current_time + + db.graph_data.nodes.update_one({"concept": concept}, {"$set": update_data}) + logger.info(f"[时间更新] 节点 {concept} 添加缺失的时间字段") + + # 获取时间信息(如果不存在则使用当前时间) + created_time = node.get("created_time", current_time) + last_modified = node.get("last_modified", current_time) + + # 添加节点到图中 + self.memory_graph.G.add_node( + concept, memory_items=memory_items, created_time=created_time, last_modified=last_modified + ) + + # 从数据库加载所有边 + edges = list(db.graph_data.edges.find()) + for edge in edges: + source = edge["source"] + target = edge["target"] + strength = edge.get("strength", 1) + + # 检查时间字段是否存在 + if "created_time" not in edge or "last_modified" not in edge: + need_update = True + # 更新数据库中的边 + update_data = {} + if "created_time" not in edge: + update_data["created_time"] = current_time + if "last_modified" not in edge: + update_data["last_modified"] = current_time + + db.graph_data.edges.update_one({"source": source, "target": target}, {"$set": update_data}) + logger.info(f"[时间更新] 边 {source} - {target} 添加缺失的时间字段") + + # 获取时间信息(如果不存在则使用当前时间) + created_time = edge.get("created_time", current_time) + last_modified = edge.get("last_modified", current_time) + + # 只有当源节点和目标节点都存在时才添加边 + if source in self.memory_graph.G and target in self.memory_graph.G: + self.memory_graph.G.add_edge( + source, target, strength=strength, created_time=created_time, last_modified=last_modified + ) + + if need_update: + logger.success("[数据库] 已为缺失的时间字段进行补充") + + async def resync_memory_to_db(self): + """清空数据库并重新同步所有记忆数据""" + start_time = time.time() + logger.info("[数据库] 开始重新同步所有记忆数据...") + + # 清空数据库 + clear_start = time.time() + db.graph_data.nodes.delete_many({}) + db.graph_data.edges.delete_many({}) + clear_end = time.time() + logger.info(f"[数据库] 清空数据库耗时: {clear_end - clear_start:.2f}秒") + + # 获取所有节点和边 + memory_nodes = list(self.memory_graph.G.nodes(data=True)) + memory_edges = list(self.memory_graph.G.edges(data=True)) + + # 重新写入节点 + node_start = time.time() + for concept, data in memory_nodes: + memory_items = data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + node_data = { + "concept": concept, + "memory_items": memory_items, + "hash": self.hippocampus.calculate_node_hash(concept, memory_items), + "created_time": data.get("created_time", datetime.datetime.now().timestamp()), + "last_modified": data.get("last_modified", datetime.datetime.now().timestamp()), + } + db.graph_data.nodes.insert_one(node_data) + node_end = time.time() + logger.info(f"[数据库] 写入 {len(memory_nodes)} 个节点耗时: {node_end - node_start:.2f}秒") + + # 重新写入边 + edge_start = time.time() + for source, target, data in memory_edges: + edge_data = { + "source": source, + "target": target, + "strength": data.get("strength", 1), + "hash": self.hippocampus.calculate_edge_hash(source, target), + "created_time": data.get("created_time", datetime.datetime.now().timestamp()), + "last_modified": data.get("last_modified", datetime.datetime.now().timestamp()), + } + db.graph_data.edges.insert_one(edge_data) + edge_end = time.time() + logger.info(f"[数据库] 写入 {len(memory_edges)} 条边耗时: {edge_end - edge_start:.2f}秒") + + end_time = time.time() + logger.success(f"[数据库] 重新同步完成,总耗时: {end_time - start_time:.2f}秒") + logger.success(f"[数据库] 同步了 {len(memory_nodes)} 个节点和 {len(memory_edges)} 条边") + + # 负责整合,遗忘,合并记忆 class ParahippocampalGyrus: def __init__(self, hippocampus: Hippocampus): @@ -1942,19 +1509,14 @@ class HippocampusManager: return response 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, - fast_retrieval: bool = False, + self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3 ) -> list: """从文本中获取相关记忆的公共接口""" if not self._initialized: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") try: response = await self._hippocampus.get_memory_from_topic( - valid_keywords, max_memory_num, max_memory_length, max_depth, fast_retrieval + valid_keywords, max_memory_num, max_memory_length, max_depth ) except Exception as e: logger.error(f"文本激活记忆失败: {e}") diff --git a/src/plugins/memory_system/manually_alter_memory.py b/src/plugins/memory_system/manually_alter_memory.py index 81874211..1452d3d5 100644 --- a/src/plugins/memory_system/manually_alter_memory.py +++ b/src/plugins/memory_system/manually_alter_memory.py @@ -5,7 +5,8 @@ import time from pathlib import Path import datetime from rich.console import Console -from memory_manual_build import Memory_graph, Hippocampus # 海马体和记忆图 +from Hippocampus import Hippocampus # 海马体和记忆图 + from dotenv import load_dotenv @@ -45,13 +46,13 @@ else: # 查询节点信息 -def query_mem_info(memory_graph: Memory_graph): +def query_mem_info(hippocampus: Hippocampus): while True: query = input("\n请输入新的查询概念(输入'退出'以结束):") if query.lower() == "退出": break - items_list = memory_graph.get_related_item(query) + items_list = hippocampus.memory_graph.get_related_item(query) if items_list: have_memory = False first_layer, second_layer = items_list @@ -312,14 +313,11 @@ def alter_mem_edge(hippocampus: Hippocampus): async def main(): start_time = time.time() - # 创建记忆图 - memory_graph = Memory_graph() - # 创建海马体 - hippocampus = Hippocampus(memory_graph) + hippocampus = Hippocampus() # 从数据库同步数据 - hippocampus.sync_memory_from_db() + hippocampus.entorhinal_cortex.sync_memory_from_db() end_time = time.time() logger.info(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m") @@ -338,7 +336,7 @@ async def main(): query = -1 if query == 0: - query_mem_info(memory_graph) + query_mem_info(hippocampus.memory_graph) elif query == 1: add_mem_node(hippocampus) elif query == 2: @@ -355,7 +353,7 @@ async def main(): print("已结束操作") break - hippocampus.sync_memory_to_db() + hippocampus.entorhinal_cortex.sync_memory_to_db() if __name__ == "__main__": diff --git a/src/plugins/message/message_base.py b/src/plugins/message/message_base.py index 2f177670..b853d469 100644 --- a/src/plugins/message/message_base.py +++ b/src/plugins/message/message_base.py @@ -12,7 +12,6 @@ class Seg: - 对于 text 类型,data 是字符串 - 对于 image 类型,data 是 base64 字符串 - 对于 seglist 类型,data 是 Seg 列表 - translated_data: 经过翻译处理的数据(可选) """ type: str diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 7930a035..365b15a6 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -2,9 +2,11 @@ import asyncio import json import re from datetime import datetime -from typing import Tuple, Union +from typing import Tuple, Union, Dict, Any import aiohttp +from aiohttp.client import ClientResponse + from src.common.logger import get_module_logger import base64 from PIL import Image @@ -16,19 +18,72 @@ from ...config.config import global_config logger = get_module_logger("model_utils") +class PayLoadTooLargeError(Exception): + """自定义异常类,用于处理请求体过大错误""" + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self): + return "请求体过大,请尝试压缩图片或减少输入内容。" + + +class RequestAbortException(Exception): + """自定义异常类,用于处理请求中断异常""" + + def __init__(self, message: str, response: ClientResponse): + super().__init__(message) + self.message = message + self.response = response + + def __str__(self): + return self.message + + +class PermissionDeniedException(Exception): + """自定义异常类,用于处理访问拒绝的异常""" + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self): + return self.message + + +# 常见Error Code Mapping +error_code_mapping = { + 400: "参数不正确", + 401: "API key 错误,认证失败,请检查/config/bot_config.toml和.env中的配置是否正确哦~", + 402: "账号余额不足", + 403: "需要实名,或余额不足", + 404: "Not Found", + 429: "请求过于频繁,请稍后再试", + 500: "服务器内部故障", + 503: "服务器负载过高", +} + + class LLMRequest: # 定义需要转换的模型列表,作为类变量避免重复 MODELS_NEEDING_TRANSFORMATION = [ - "o3-mini", - "o1-mini", - "o1-preview", + "o1", "o1-2024-12-17", - "o1-preview-2024-09-12", - "o3-mini-2025-01-31", + "o1-mini", "o1-mini-2024-09-12", + "o1-preview", + "o1-preview-2024-09-12", + "o1-pro", + "o1-pro-2025-03-19", + "o3", + "o3-2025-04-16", + "o3-mini", + "o3-mini-2025-01-31o4-mini", + "o4-mini-2025-04-16", ] - def __init__(self, model, **kwargs): + def __init__(self, model: dict, **kwargs): # 将大写的配置键转换为小写并从config中获取实际值 try: self.api_key = os.environ[model["key"]] @@ -37,7 +92,7 @@ class LLMRequest: logger.error(f"原始 model dict 信息:{model}") logger.error(f"配置错误:找不到对应的配置项 - {str(e)}") raise ValueError(f"配置错误:找不到对应的配置项 - {str(e)}") from e - self.model_name = model["name"] + self.model_name: str = model["name"] self.params = kwargs self.stream = model.get("stream", False) @@ -123,6 +178,7 @@ class LLMRequest: output_cost = (completion_tokens / 1000000) * self.pri_out return round(input_cost + output_cost, 6) + ''' async def _execute_request( self, endpoint: str, @@ -509,6 +565,404 @@ class LLMRequest: logger.error(f"模型 {self.model_name} 达到最大重试次数,请求仍然失败") raise RuntimeError(f"模型 {self.model_name} 达到最大重试次数,API请求仍然失败") + ''' + + async def _prepare_request( + self, + endpoint: str, + prompt: str = None, + image_base64: str = None, + image_format: str = None, + payload: dict = None, + retry_policy: dict = None, + ) -> Dict[str, Any]: + """配置请求参数 + Args: + endpoint: API端点路径 (如 "chat/completions") + prompt: prompt文本 + image_base64: 图片的base64编码 + image_format: 图片格式 + payload: 请求体数据 + retry_policy: 自定义重试策略 + request_type: 请求类型 + """ + + # 合并重试策略 + default_retry = { + "max_retries": 3, + "base_wait": 10, + "retry_codes": [429, 413, 500, 503], + "abort_codes": [400, 401, 402, 403], + } + policy = {**default_retry, **(retry_policy or {})} + + api_url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" + + stream_mode = self.stream + + # 构建请求体 + if image_base64: + payload = await self._build_payload(prompt, image_base64, image_format) + elif payload is None: + payload = await self._build_payload(prompt) + + if stream_mode: + payload["stream"] = stream_mode + + return { + "policy": policy, + "payload": payload, + "api_url": api_url, + "stream_mode": stream_mode, + "image_base64": image_base64, # 保留必要的exception处理所需的原始数据 + "image_format": image_format, + "prompt": prompt, + } + + async def _execute_request( + self, + endpoint: str, + prompt: str = None, + image_base64: str = None, + image_format: str = None, + payload: dict = None, + retry_policy: dict = None, + response_handler: callable = None, + user_id: str = "system", + request_type: str = None, + ): + """统一请求执行入口 + Args: + endpoint: API端点路径 (如 "chat/completions") + prompt: prompt文本 + image_base64: 图片的base64编码 + image_format: 图片格式 + payload: 请求体数据 + retry_policy: 自定义重试策略 + response_handler: 自定义响应处理器 + user_id: 用户ID + request_type: 请求类型 + """ + # 获取请求配置 + request_content = await self._prepare_request( + endpoint, prompt, image_base64, image_format, payload, retry_policy + ) + if request_type is None: + request_type = self.request_type + for retry in range(request_content["policy"]["max_retries"]): + try: + # 使用上下文管理器处理会话 + headers = await self._build_headers() + # 似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响 + if request_content["stream_mode"]: + headers["Accept"] = "text/event-stream" + async with aiohttp.ClientSession() as session: + async with session.post( + request_content["api_url"], headers=headers, json=request_content["payload"] + ) as response: + handled_result = await self._handle_response( + response, request_content, retry, response_handler, user_id, request_type, endpoint + ) + return handled_result + except Exception as e: + handled_payload, count_delta = await self._handle_exception(e, retry, request_content) + retry += count_delta # 降级不计入重试次数 + if handled_payload: + # 如果降级成功,重新构建请求体 + request_content["payload"] = handled_payload + continue + + logger.error(f"模型 {self.model_name} 达到最大重试次数,请求仍然失败") + raise RuntimeError(f"模型 {self.model_name} 达到最大重试次数,API请求仍然失败") + + async def _handle_response( + self, + response: ClientResponse, + request_content: Dict[str, Any], + retry_count: int, + response_handler: callable, + user_id, + request_type, + endpoint, + ) -> Union[Dict[str, Any], None]: + policy = request_content["policy"] + stream_mode = request_content["stream_mode"] + if response.status in policy["retry_codes"] or response.status in policy["abort_codes"]: + await self._handle_error_response(response, retry_count, policy) + return + + response.raise_for_status() + result = {} + if stream_mode: + # 将流式输出转化为非流式输出 + result = await self._handle_stream_output(response) + else: + result = await response.json() + return ( + response_handler(result) + if response_handler + else self._default_response_handler(result, user_id, request_type, endpoint) + ) + + async def _handle_stream_output(self, response: ClientResponse) -> Dict[str, Any]: + flag_delta_content_finished = False + accumulated_content = "" + usage = None # 初始化usage变量,避免未定义错误 + reasoning_content = "" + content = "" + async for line_bytes in response.content: + try: + line = line_bytes.decode("utf-8").strip() + if not line: + continue + if line.startswith("data:"): + data_str = line[5:].strip() + if data_str == "[DONE]": + break + try: + chunk = json.loads(data_str) + if flag_delta_content_finished: + chunk_usage = chunk.get("usage", None) + if chunk_usage: + usage = chunk_usage # 获取token用量 + else: + delta = chunk["choices"][0]["delta"] + delta_content = delta.get("content") + if delta_content is None: + delta_content = "" + accumulated_content += delta_content + # 检测流式输出文本是否结束 + finish_reason = chunk["choices"][0].get("finish_reason") + if delta.get("reasoning_content", None): + reasoning_content += delta["reasoning_content"] + if finish_reason == "stop": + chunk_usage = chunk.get("usage", None) + if chunk_usage: + usage = chunk_usage + break + # 部分平台在文本输出结束前不会返回token用量,此时需要再获取一次chunk + flag_delta_content_finished = True + except Exception as e: + logger.exception(f"模型 {self.model_name} 解析流式输出错误: {str(e)}") + except Exception as e: + if isinstance(e, GeneratorExit): + log_content = f"模型 {self.model_name} 流式输出被中断,正在清理资源..." + else: + log_content = f"模型 {self.model_name} 处理流式输出时发生错误: {str(e)}" + logger.warning(log_content) + # 确保资源被正确清理 + try: + await response.release() + except Exception as cleanup_error: + logger.error(f"清理资源时发生错误: {cleanup_error}") + # 返回已经累积的内容 + content = accumulated_content + if not content: + content = accumulated_content + think_match = re.search(r"(.*?)", content, re.DOTALL) + if think_match: + reasoning_content = think_match.group(1).strip() + content = re.sub(r".*?", "", content, flags=re.DOTALL).strip() + result = { + "choices": [ + { + "message": { + "content": content, + "reasoning_content": reasoning_content, + # 流式输出可能没有工具调用,此处不需要添加tool_calls字段 + } + } + ], + "usage": usage, + } + return result + + async def _handle_error_response( + self, response: ClientResponse, retry_count: int, policy: Dict[str, Any] + ) -> Union[Dict[str, any]]: + if response.status in policy["retry_codes"]: + wait_time = policy["base_wait"] * (2**retry_count) + logger.warning(f"模型 {self.model_name} 错误码: {response.status}, 等待 {wait_time}秒后重试") + if response.status == 413: + logger.warning("请求体过大,尝试压缩...") + raise PayLoadTooLargeError("请求体过大") + elif response.status in [500, 503]: + logger.error( + f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}" + ) + raise RuntimeError("服务器负载过高,模型恢复失败QAQ") + else: + logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...") + raise RuntimeError("请求限制(429)") + elif response.status in policy["abort_codes"]: + if response.status != 403: + raise RequestAbortException("请求出现错误,中断处理", response) + else: + raise PermissionDeniedException("模型禁止访问") + + async def _handle_exception( + self, exception, retry_count: int, request_content: Dict[str, Any] + ) -> Union[Tuple[Dict[str, Any], int], Tuple[None, int]]: + policy = request_content["policy"] + payload = request_content["payload"] + wait_time = policy["base_wait"] * (2**retry_count) + if retry_count < policy["max_retries"] - 1: + keep_request = True + if isinstance(exception, RequestAbortException): + response = exception.response + logger.error( + f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}" + ) + # 尝试获取并记录服务器返回的详细错误信息 + try: + error_json = await response.json() + if error_json and isinstance(error_json, list) and len(error_json) > 0: + # 处理多个错误的情况 + for error_item in error_json: + if "error" in error_item and isinstance(error_item["error"], dict): + error_obj: dict = error_item["error"] + error_code = error_obj.get("code") + error_message = error_obj.get("message") + error_status = error_obj.get("status") + logger.error( + f"服务器错误详情: 代码={error_code}, 状态={error_status}, 消息={error_message}" + ) + elif isinstance(error_json, dict) and "error" in error_json: + # 处理单个错误对象的情况 + error_obj = error_json.get("error", {}) + error_code = error_obj.get("code") + error_message = error_obj.get("message") + error_status = error_obj.get("status") + logger.error(f"服务器错误详情: 代码={error_code}, 状态={error_status}, 消息={error_message}") + else: + # 记录原始错误响应内容 + logger.error(f"服务器错误响应: {error_json}") + except Exception as e: + logger.warning(f"无法解析服务器错误响应: {str(e)}") + raise RuntimeError(f"请求被拒绝: {error_code_mapping.get(response.status)}") + + elif isinstance(exception, PermissionDeniedException): + # 只针对硅基流动的V3和R1进行降级处理 + if self.model_name.startswith("Pro/deepseek-ai") and self.base_url == "https://api.siliconflow.cn/v1/": + old_model_name = self.model_name + self.model_name = self.model_name[4:] # 移除"Pro/"前缀 + logger.warning(f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}") + + # 对全局配置进行更新 + if global_config.llm_normal.get("name") == old_model_name: + global_config.llm_normal["name"] = self.model_name + logger.warning(f"将全局配置中的 llm_normal 模型临时降级至{self.model_name}") + if global_config.llm_reasoning.get("name") == old_model_name: + global_config.llm_reasoning["name"] = self.model_name + logger.warning(f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}") + + if payload and "model" in payload: + payload["model"] = self.model_name + + await asyncio.sleep(wait_time) + return payload, -1 + raise RuntimeError(f"请求被拒绝: {error_code_mapping.get(403)}") + + elif isinstance(exception, PayLoadTooLargeError): + if keep_request: + image_base64 = request_content["image_base64"] + compressed_image_base64 = compress_base64_image_by_scale(image_base64) + new_payload = await self._build_payload( + request_content["prompt"], compressed_image_base64, request_content["image_format"] + ) + return new_payload, 0 + else: + return None, 0 + + elif isinstance(exception, aiohttp.ClientError) or isinstance(exception, asyncio.TimeoutError): + if keep_request: + logger.error(f"模型 {self.model_name} 网络错误,等待{wait_time}秒后重试... 错误: {str(exception)}") + await asyncio.sleep(wait_time) + return None, 0 + else: + logger.critical(f"模型 {self.model_name} 网络错误达到最大重试次数: {str(exception)}") + raise RuntimeError(f"网络请求失败: {str(exception)}") + + elif isinstance(exception, aiohttp.ClientResponseError): + # 处理aiohttp抛出的,除了policy中的status的响应错误 + if keep_request: + logger.error( + f"模型 {self.model_name} HTTP响应错误,等待{wait_time}秒后重试... 状态码: {exception.status}, 错误: {exception.message}" + ) + try: + error_text = await exception.response.text() + error_json = json.loads(error_text) + if isinstance(error_json, list) and len(error_json) > 0: + # 处理多个错误的情况 + for error_item in error_json: + if "error" in error_item and isinstance(error_item["error"], dict): + error_obj = error_item["error"] + logger.error( + f"模型 {self.model_name} 服务器错误详情: 代码={error_obj.get('code')}, " + f"状态={error_obj.get('status')}, " + f"消息={error_obj.get('message')}" + ) + elif isinstance(error_json, dict) and "error" in error_json: + error_obj = error_json.get("error", {}) + logger.error( + f"模型 {self.model_name} 服务器错误详情: 代码={error_obj.get('code')}, " + f"状态={error_obj.get('status')}, " + f"消息={error_obj.get('message')}" + ) + else: + logger.error(f"模型 {self.model_name} 服务器错误响应: {error_json}") + except (json.JSONDecodeError, TypeError) as json_err: + logger.warning( + f"模型 {self.model_name} 响应不是有效的JSON: {str(json_err)}, 原始内容: {error_text[:200]}" + ) + except Exception as parse_err: + logger.warning(f"模型 {self.model_name} 无法解析响应错误内容: {str(parse_err)}") + + await asyncio.sleep(wait_time) + return None, 0 + else: + logger.critical( + f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {exception.status}, 错误: {exception.message}" + ) + # 安全地检查和记录请求详情 + handled_payload = await self._safely_record(request_content, payload) + logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}") + raise RuntimeError( + f"模型 {self.model_name} API请求失败: 状态码 {exception.status}, {exception.message}" + ) + + else: + if keep_request: + logger.error(f"模型 {self.model_name} 请求失败,等待{wait_time}秒后重试... 错误: {str(exception)}") + await asyncio.sleep(wait_time) + return None, 0 + else: + logger.critical(f"模型 {self.model_name} 请求失败: {str(exception)}") + # 安全地检查和记录请求详情 + handled_payload = await self._safely_record(request_content, payload) + logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}") + raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(exception)}") + + async def _safely_record(self, request_content: Dict[str, Any], payload: Dict[str, Any]): + image_base64: str = request_content.get("image_base64") + image_format: str = request_content.get("image_format") + if ( + image_base64 + and payload + and isinstance(payload, dict) + and "messages" in payload + and len(payload["messages"]) > 0 + ): + if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: + content = payload["messages"][0]["content"] + if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: + payload["messages"][0]["content"][1]["image_url"]["url"] = ( + f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," + f"{image_base64[:10]}...{image_base64[-10:]}" + ) + # if isinstance(content, str) and len(content) > 100: + # payload["messages"][0]["content"] = content[:100] + return payload async def _transform_parameters(self, params: dict) -> dict: """ @@ -532,30 +986,27 @@ class LLMRequest: # 复制一份参数,避免直接修改 self.params params_copy = await self._transform_parameters(self.params) if image_base64: - payload = { - "model": self.model_name, - "messages": [ - { - "role": "user", - "content": [ - {"type": "text", "text": prompt}, - { - "type": "image_url", - "image_url": {"url": f"data:image/{image_format.lower()};base64,{image_base64}"}, - }, - ], - } - ], - "max_tokens": global_config.max_response_length, - **params_copy, - } + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image_url", + "image_url": {"url": f"data:image/{image_format.lower()};base64,{image_base64}"}, + }, + ], + } + ] else: - payload = { - "model": self.model_name, - "messages": [{"role": "user", "content": prompt}], - "max_tokens": global_config.max_response_length, - **params_copy, - } + messages = [{"role": "user", "content": prompt}] + payload = { + "model": self.model_name, + "messages": messages, + **params_copy, + } + if "max_tokens" not in payload and "max_completion_tokens" not in payload: + payload["max_tokens"] = global_config.max_response_length # 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查 if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload: payload["max_completion_tokens"] = payload.pop("max_tokens") @@ -648,11 +1099,10 @@ class LLMRequest: async def generate_response_async(self, prompt: str, **kwargs) -> Union[str, Tuple]: """异步方式根据输入的提示生成模型的响应""" - # 构建请求体 + # 构建请求体,不硬编码max_tokens data = { "model": self.model_name, "messages": [{"role": "user", "content": prompt}], - "max_tokens": global_config.max_response_length, **self.params, **kwargs, } diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index 8105b330..d903213f 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -169,7 +169,7 @@ class PersonInfoManager: """给某个用户取名""" if not person_id: logger.debug("取名失败:person_id不能为空") - return + return None old_name = await self.get_value(person_id, "person_name") old_reason = await self.get_value(person_id, "name_reason") @@ -198,9 +198,9 @@ class PersonInfoManager: "nickname": "昵称", "reason": "理由" }""" - logger.debug(f"取名提示词:{qv_name_prompt}") + # logger.debug(f"取名提示词:{qv_name_prompt}") response = await self.qv_name_llm.generate_response(qv_name_prompt) - logger.debug(f"取名回复:{response}") + logger.debug(f"取名提示词:{qv_name_prompt}\n取名回复:{response}") result = self._extract_json_from_text(response[0]) if not result["nickname"]: @@ -217,7 +217,7 @@ class PersonInfoManager: await self.update_one_field(person_id, "name_reason", result["reason"]) self.person_name_list[person_id] = result["nickname"] - logger.debug(f"用户 {person_id} 的名称已更新为 {result['nickname']},原因:{result['reason']}") + # logger.debug(f"用户 {person_id} 的名称已更新为 {result['nickname']},原因:{result['reason']}") return result else: existing_names += f"{result['nickname']}、" diff --git a/src/plugins/person_info/relationship_manager.py b/src/plugins/person_info/relationship_manager.py index 556e59f4..a688242b 100644 --- a/src/plugins/person_info/relationship_manager.py +++ b/src/plugins/person_info/relationship_manager.py @@ -89,8 +89,8 @@ class RelationshipManager: person_id = person_info_manager.get_person_id(platform, user_id) is_qved = await person_info_manager.has_one_field(person_id, "person_name") old_name = await person_info_manager.get_value(person_id, "person_name") - print(f"old_name: {old_name}") - print(f"is_qved: {is_qved}") + # print(f"old_name: {old_name}") + # print(f"is_qved: {is_qved}") if is_qved and old_name is not None: return True else: diff --git a/src/plugins/remote/remote.py b/src/plugins/remote/remote.py index 0d119a3e..5bc4dab1 100644 --- a/src/plugins/remote/remote.py +++ b/src/plugins/remote/remote.py @@ -134,3 +134,4 @@ def main(): heartbeat_thread.start() return heartbeat_thread # 返回线程对象,便于外部控制 + return None diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index f0a52e76..e4e2a2a8 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,8 +1,7 @@ [inner] -version = "1.3.1" +version = "1.4.0" - -#以下是给开发人员阅读的,一般用户不需要阅读 +#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 #如果新增项目,请在BotConfig类下新增相应的变量 #1.如果你修改的是[]层级项目,例如你新增了 [memory],那么请在config.py的 load_config函数中的include_configs字典中新增"内容":{ @@ -19,11 +18,12 @@ version = "1.3.1" # 次版本号:当你做了向下兼容的功能性新增, # 修订号:当你做了向下兼容的问题修正。 # 先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。 +#----以上是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- [bot] -qq = 114514 +qq = 1145141919810 nickname = "麦麦" -alias_names = ["麦叠", "牢麦"] +alias_names = ["麦叠", "牢麦"] #该选项还在调试中,暂时未生效 [groups] talk_allowed = [ @@ -41,23 +41,24 @@ personality_sides = [ "用一句话或几句话描述人格的一些细节", "用一句话或几句话描述人格的一些细节", "用一句话或几句话描述人格的一些细节", -]# 条数任意 +]# 条数任意,不能为0, 该选项还在调试中,可能未完全生效 [identity] #アイデンティティがない 生まれないらららら # 兴趣爱好 未完善,有些条目未使用 identity_detail = [ "身份特点", "身份特点", -]# 条数任意 +]# 条数任意,不能为0, 该选项还在调试中,可能未完全生效 #外貌特征 -height = 170 # 身高 单位厘米 -weight = 50 # 体重 单位千克 -age = 20 # 年龄 单位岁 -gender = "男" # 性别 -appearance = "用几句话描述外貌特征" # 外貌特征 +height = 170 # 身高 单位厘米 该选项还在调试中,暂时未生效 +weight = 50 # 体重 单位千克 该选项还在调试中,暂时未生效 +age = 20 # 年龄 单位岁 该选项还在调试中,暂时未生效 +gender = "男" # 性别 该选项还在调试中,暂时未生效 +appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效 [schedule] -enable_schedule_gen = true # 是否启用日程表(尚未完成) +enable_schedule_gen = true # 是否启用日程表 +enable_schedule_interaction = true # 日程表是否影响回复模式 prompt_schedule_gen = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表" schedule_doing_update_interval = 900 # 日程表更新间隔 单位秒 schedule_temperature = 0.1 # 日程表温度,建议0.1-0.5 @@ -67,19 +68,25 @@ time_zone = "Asia/Shanghai" # 给你的机器人设置时区,可以解决运 nonebot-qq="http://127.0.0.1:18002/api/message" [response] #群聊的回复策略 -#reasoning:推理模式,麦麦会根据上下文进行推理,并给出回复 -#heart_flow:结合了PFC模式和心流模式,麦麦会进行主动的观察和回复,并给出回复 -response_mode = "heart_flow" # 回复策略,可选值:heart_flow(心流),reasoning(推理) +enable_heart_flowC = true +# 该功能还在完善中 +# 是否启用heart_flowC(心流聊天,HFC)模式 +# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token -#推理回复参数 -model_r1_probability = 0.7 # 麦麦回答时选择主要回复模型1 模型的概率 -model_v3_probability = 0.3 # 麦麦回答时选择次要回复模型2 模型的概率 +#一般回复参数 +model_reasoning_probability = 0.7 # 麦麦回答时选择推理模型 模型的概率 +model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概率 + +[heartflow] #启用启用heart_flowC(心流聊天)模式时生效,需要填写以下参数 +reply_trigger_threshold = 3.0 # 心流聊天触发阈值,越低越容易进入心流聊天 +probability_decay_factor_per_second = 0.2 # 概率衰减因子,越大衰减越快,越高越容易退出心流聊天 +default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入心流聊天 +initial_duration = 60 # 初始持续时间,越大心流聊天持续的时间越长 -[heartflow] # 注意:可能会消耗大量token,请谨慎开启,仅会使用v3模型 -sub_heart_flow_update_interval = 60 # 子心流更新频率,间隔 单位秒 -sub_heart_flow_freeze_time = 100 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒 sub_heart_flow_stop_time = 500 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒 -heart_flow_update_interval = 600 # 心流更新频率,间隔 单位秒 +# sub_heart_flow_update_interval = 60 +# sub_heart_flow_freeze_time = 100 +# heart_flow_update_interval = 600 observation_context_size = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩 compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5 @@ -87,11 +94,13 @@ compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下 [message] -max_context_size = 12 # 麦麦获得的上文数量,建议12,太短太长都会导致脑袋尖尖 -emoji_chance = 0.2 # 麦麦使用表情包的概率,设置为1让麦麦自己决定发不发 -thinking_timeout = 60 # 麦麦最长思考时间,超过这个时间的思考会放弃 -max_response_length = 256 # 麦麦回答的最大token数 +max_context_size = 12 # 麦麦回复时获得的上文数量,建议12,太短太长都会导致脑袋尖尖 +emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率,设置为1让麦麦自己决定发不发 +thinking_timeout = 100 # 麦麦最长思考时间,超过这个时间的思考会放弃(往往是api反应太慢) +max_response_length = 256 # 麦麦单次回答的最大token数 message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟 + +# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息 ban_words = [ # "403","张三" ] @@ -103,22 +112,23 @@ ban_msgs_regex = [ # "\\[CQ:at,qq=\\d+\\]" # 匹配@ ] -[willing] +[willing] # 一般回复模式的回复意愿设置 willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,动态模式:dynamic,mxp模式:mxp,自定义模式:custom(需要你自己实现) response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法 -emoji_response_penalty = 0.1 # 表情包回复惩罚系数,设为0为不回复单个表情包,减少单独回复表情包的概率 +emoji_response_penalty = 0 # 表情包回复惩罚系数,设为0为不回复单个表情包,减少单独回复表情包的概率 mentioned_bot_inevitable_reply = false # 提及 bot 必然回复 at_bot_inevitable_reply = false # @bot 必然回复 [emoji] -max_emoji_num = 120 # 表情包最大数量 +max_emoji_num = 90 # 表情包最大数量 max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包 check_interval = 30 # 检查表情包(注册,破损,删除)的时间间隔(分钟) auto_save = true # 是否保存表情包和图片 -enable_check = false # 是否启用表情包过滤 -check_prompt = "符合公序良俗" # 表情包过滤要求 + +enable_check = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 +check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 [memory] build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 @@ -131,7 +141,8 @@ forget_memory_interval = 1000 # 记忆遗忘间隔 单位秒 间隔越低, memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时 memory_forget_percentage = 0.01 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 -memory_ban_words = [ #不希望记忆的词 +#不希望记忆的词,已经记忆的不会受到影响 +memory_ban_words = [ # "403","张三" ] @@ -167,7 +178,7 @@ word_replace_rate=0.006 # 整词替换概率 [response_splitter] enable_response_splitter = true # 是否启用回复分割器 -response_max_length = 100 # 回复允许的最大长度 +response_max_length = 256 # 回复允许的最大长度 response_max_sentence_num = 4 # 回复允许的最大句子数 [remote] #发送统计信息,主要是看全球有多少只麦麦 diff --git a/template/template.env b/template/template.env index 06e9b07e..c1a6dd0d 100644 --- a/template/template.env +++ b/template/template.env @@ -29,8 +29,18 @@ CHAT_ANY_WHERE_KEY= SILICONFLOW_KEY= # 定义日志相关配置 -SIMPLE_OUTPUT=true # 精简控制台输出格式 -CONSOLE_LOG_LEVEL=INFO # 自定义日志的默认控制台输出日志级别 -FILE_LOG_LEVEL=DEBUG # 自定义日志的默认文件输出日志级别 -DEFAULT_CONSOLE_LOG_LEVEL=SUCCESS # 原生日志的控制台输出日志级别(nonebot就是这一类) -DEFAULT_FILE_LOG_LEVEL=DEBUG # 原生日志的默认文件输出日志级别(nonebot就是这一类) \ No newline at end of file + +# 精简控制台输出格式 +SIMPLE_OUTPUT=true + +# 自定义日志的默认控制台输出日志级别 +CONSOLE_LOG_LEVEL=INFO + +# 自定义日志的默认文件输出日志级别 +FILE_LOG_LEVEL=DEBUG + +# 原生日志的控制台输出日志级别(nonebot就是这一类) +DEFAULT_CONSOLE_LOG_LEVEL=SUCCESS + +# 原生日志的默认文件输出日志级别(nonebot就是这一类) +DEFAULT_FILE_LOG_LEVEL=DEBUG