diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 9645569af..d77deaa67 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,8 +1,9 @@ # Changelog -## [0.7.0] -2025-5-19 +## [0.7.0] -2025-6-1 - 重构数据库,弃用MongoDB,采用轻量sqlite,无需额外安装 -- 重构HFC,可扩展的聊天模式,初步支持插件v0.1 +- 重构HFC,可扩展的聊天模式 +- HFC初步支持插件v0.1(测试版) - 重构表情包模块 - 移除日程系统 diff --git a/docs/HeartFC_readme.md b/docs/HeartFC_readme.md index 10b1aa1fd..790fc5bb7 100644 --- a/docs/HeartFC_readme.md +++ b/docs/HeartFC_readme.md @@ -4,8 +4,8 @@ HeartFC_chat 是一个基于心流理论的聊天系统,通过模拟人类的 ## 核心工作流程 -### 1. 消息处理与存储 (HeartFCProcessor) -[代码位置: src/plugins/focus_chat/heartflow_processor.py] +### 1. 消息处理与存储 (HeartFCMessageReceiver) +[代码位置: src/plugins/focus_chat/heartflow_message_receiver.py] 消息处理器负责接收和预处理消息,主要完成以下工作: ```mermaid @@ -132,7 +132,7 @@ graph TD ### 关键参数 - LLM配置:`model_normal` [heartFC_generator.py 行号: 32-37] -- 过滤规则:`_check_ban_words()`, `_check_ban_regex()` [heartflow_processor.py 行号: 196-215] +- 过滤规则:`_check_ban_words()`, `_check_ban_regex()` [heartflow_message_receiver.py 行号: 196-215] - 状态控制:`INITIAL_DURATION = 60.0` [focus_chat.py 行号: 11] ### 优化建议 diff --git a/docs/HeartFC_system.md b/docs/HeartFC_system.md index e48a7b5d7..1c1db6a14 100644 --- a/docs/HeartFC_system.md +++ b/docs/HeartFC_system.md @@ -113,7 +113,7 @@ c HeartFChatting工作方式 ### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow) - **关注点分离**: 系统严格区分了接收和处理传入消息的流程与决定和生成回复的流程。 - **消息处理 (Processing)**: - - 由一个独立的处理器(例如 `HeartFCProcessor`)负责接收原始消息数据。 + - 由一个独立的处理器(例如 `HeartFCMessageReceiver`)负责接收原始消息数据。 - 职责包括:消息解析 (`MessageRecv`)、过滤(屏蔽词、正则表达式)、基于记忆系统的初步兴趣计算 (`HippocampusManager`)、消息存储 (`MessageStorage`) 以及用户关系更新 (`RelationshipManager`)。 - 处理后的消息信息(如计算出的兴趣度)会传递给对应的 `SubHeartflow`。 - **回复决策与生成 (Replying)**: @@ -121,7 +121,7 @@ c HeartFChatting工作方式 - 基于其内部状态 (`ChatState`、`SubMind` 的思考结果)、观察到的信息 (`Observation` 提供的内容) 以及 `InterestChatting` 的状态来决定是否回复、何时回复以及如何回复。 - **消息缓冲 (Message Caching)**: - `message_buffer` 模块会对某些传入消息进行临时缓存,尤其是在处理连续的多部分消息(如多张图片)时。 - - 这个缓冲机制发生在 `HeartFCProcessor` 处理流程中,确保消息的完整性,然后才进行后续的存储和兴趣计算。 + - 这个缓冲机制发生在 `HeartFCMessageReceiver` 处理流程中,确保消息的完整性,然后才进行后续的存储和兴趣计算。 - 缓存的消息最终仍会流向对应的 `ChatStream`(与 `SubHeartflow` 关联),但核心的消息处理与回复决策仍然是分离的步骤。 ## 2. 核心控制与状态管理 (Core Control and State Management) @@ -148,7 +148,7 @@ c HeartFChatting工作方式 - **管理对象**: 每个 `SubHeartflow` 实例内部维护其 `ChatStateInfo`,包含当前的 `ChatState`。 - **状态及含义**: - `ChatState.ABSENT` (不参与/没在看): 初始或停用状态。子心流不观察新信息,不进行思考,也不回复。 - - `ChatState.CHAT` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`。 + - `ChatState.NORMAL` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`。 * `ChatState.FOCUSED` (专注/认真聊天): 专注聊天模式。激活 `HeartFlowChatInstance`。 - **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。 - **状态转换机制** (由 `SubHeartflowManager` 驱动,更细致的说明): @@ -156,7 +156,7 @@ c HeartFChatting工作方式 - **`ABSENT` -> `CHAT` (激活闲聊)**: - **触发条件**: `Heartflow` 的主状态 (`MaiState`) 允许 `CHAT` 模式,且当前 `CHAT` 状态的子心流数量未达上限。 - **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。 - - **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)`。 + - **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.NORMAL)`。 - **`CHAT` -> `FOCUSED` (激活专注)**: - **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。 - **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_focus` 方法定期检查满足条件的 `CHAT` 子心流。 diff --git a/src/api/config_api.py b/src/api/config_api.py index 8b99fb93e..3e3ff286c 100644 --- a/src/api/config_api.py +++ b/src/api/config_api.py @@ -41,7 +41,7 @@ class APIBotConfig: allow_focus_mode: bool # 是否允许专注聊天状态 base_normal_chat_num: int # 最多允许多少个群进行普通聊天 base_focused_chat_num: int # 最多允许多少个群进行专注聊天 - chat.observation_context_size: int # 观察到的最长上下文大小 + observation_context_size: int # 观察到的最长上下文大小 message_buffer: bool # 是否启用消息缓冲 ban_words: List[str] # 禁止词列表 ban_msgs_regex: List[str] # 禁止消息的正则表达式列表 @@ -128,7 +128,7 @@ class APIBotConfig: llm_reasoning: Dict[str, Any] # 推理模型配置 llm_normal: Dict[str, Any] # 普通模型配置 llm_topic_judge: Dict[str, Any] # 主题判断模型配置 - model.summary: Dict[str, Any] # 总结模型配置 + summary: Dict[str, Any] # 总结模型配置 vlm: Dict[str, Any] # VLM模型配置 llm_heartflow: Dict[str, Any] # 心流模型配置 llm_observation: Dict[str, Any] # 观察模型配置 @@ -203,7 +203,7 @@ class APIBotConfig: "llm_reasoning", "llm_normal", "llm_topic_judge", - "model.summary", + "summary", "vlm", "llm_heartflow", "llm_observation", diff --git a/src/chat/focus_chat/expressors/default_expressor.py b/src/chat/focus_chat/expressors/default_expressor.py index d3d21e074..3b95b185c 100644 --- a/src/chat/focus_chat/expressors/default_expressor.py +++ b/src/chat/focus_chat/expressors/default_expressor.py @@ -17,7 +17,7 @@ from src.manager.mood_manager import mood_manager from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.message_receive.chat_stream import ChatStream from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat import time @@ -281,7 +281,6 @@ class DefaultExpressor: in_mind_reply, target_message, ) -> str: - individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) # Determine if it's a group chat @@ -294,7 +293,7 @@ class DefaultExpressor: message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), - limit=global_config.chat.observation_context_size, + limit=global_config.focus_chat.observation_context_size, ) chat_talking_prompt = await build_readable_messages( message_list_before_now, diff --git a/src/chat/focus_chat/expressors/exprssion_learner.py b/src/chat/focus_chat/expressors/exprssion_learner.py index 7766fde56..348de9f09 100644 --- a/src/chat/focus_chat/expressors/exprssion_learner.py +++ b/src/chat/focus_chat/expressors/exprssion_learner.py @@ -36,24 +36,6 @@ def init_prompt() -> None: """ Prompt(learn_style_prompt, "learn_style_prompt") - personality_expression_prompt = """ -{personality} - -请从以上人设中总结出这个角色可能的语言风格 -思考回复的特殊内容和情感 -思考有没有特殊的梗,一并总结成语言风格 -总结成如下格式的规律,总结的内容要详细,但具有概括性: -当"xxx"时,可以"xxx", xxx不超过10个字 - -例如: -当"表示十分惊叹"时,使用"我嘞个xxxx" -当"表示讽刺的赞同,不想讲道理"时,使用"对对对" -当"想说明某个观点,但懒得明说",使用"懂的都懂" - -现在请你概括 -""" - Prompt(personality_expression_prompt, "personality_expression_prompt") - learn_grammar_prompt = """ {chat_str} @@ -278,44 +260,6 @@ class ExpressionLearner: expressions.append((chat_id, situation, style)) return expressions - async def extract_and_store_personality_expressions(self): - """ - 检查data/expression/personality目录,不存在则创建。 - 用peronality变量作为chat_str,调用LLM生成表达风格,解析后count=100,存储到expressions.json。 - """ - dir_path = os.path.join("data", "expression", "personality") - os.makedirs(dir_path, exist_ok=True) - file_path = os.path.join(dir_path, "expressions.json") - - # 构建prompt - prompt = await global_prompt_manager.format_prompt( - "personality_expression_prompt", - personality=global_config.personality.expression_style, - ) - # logger.info(f"个性表达方式提取prompt: {prompt}") - - try: - response, _ = await self.express_learn_model.generate_response_async(prompt) - except Exception as e: - logger.error(f"个性表达方式提取失败: {e}") - return - - logger.info(f"个性表达方式提取response: {response}") - # chat_id用personality - expressions = self.parse_expression_response(response, "personality") - # 转为dict并count=100 - result = [] - for _, situation, style in expressions: - result.append({"situation": situation, "style": style, "count": 100}) - # 超过50条时随机删除多余的,只保留50条 - if len(result) > 50: - remove_count = len(result) - 50 - remove_indices = set(random.sample(range(len(result)), remove_count)) - result = [item for idx, item in enumerate(result) if idx not in remove_indices] - with open(file_path, "w", encoding="utf-8") as f: - json.dump(result, f, ensure_ascii=False, indent=2) - logger.info(f"已写入{len(result)}条表达到{file_path}") - init_prompt() diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index 4f17f9bdf..8dd096d75 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -3,7 +3,7 @@ import contextlib import time import traceback from collections import deque -from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine +from typing import List, Optional, Dict, Any, Deque from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import chat_manager from rich.traceback import install diff --git a/src/chat/focus_chat/heartflow_processor.py b/src/chat/focus_chat/heartflow_message_revceiver.py similarity index 92% rename from src/chat/focus_chat/heartflow_processor.py rename to src/chat/focus_chat/heartflow_message_revceiver.py index a4cf360a5..9f5c78c30 100644 --- a/src/chat/focus_chat/heartflow_processor.py +++ b/src/chat/focus_chat/heartflow_message_revceiver.py @@ -5,7 +5,6 @@ from ...config.config import global_config from ..message_receive.message import MessageRecv from ..message_receive.storage import MessageStorage from ..utils.utils import is_mentioned_bot_in_message -from maim_message import Seg from src.chat.heart_flow.heartflow import heartflow from src.common.logger_manager import get_logger from ..message_receive.chat_stream import chat_manager @@ -79,26 +78,26 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: return interested_rate, is_mentioned -def _get_message_type(message: MessageRecv) -> str: - """获取消息类型 +# def _get_message_type(message: MessageRecv) -> str: +# """获取消息类型 - Args: - message: 消息对象 +# Args: +# message: 消息对象 - Returns: - str: 消息类型 - """ - if message.message_segment.type != "seglist": - return message.message_segment.type +# Returns: +# str: 消息类型 +# """ +# if message.message_segment.type != "seglist": +# return message.message_segment.type - 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 - ): - return message.message_segment.data[0].type +# 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 +# ): +# return message.message_segment.data[0].type - return "seglist" +# return "seglist" def _check_ban_words(text: str, chat, userinfo) -> bool: @@ -141,7 +140,7 @@ def _check_ban_regex(text: str, chat, userinfo) -> bool: return False -class HeartFCProcessor: +class HeartFCMessageReceiver: """心流处理器,负责处理接收到的消息并计算兴趣度""" def __init__(self): diff --git a/src/chat/focus_chat/heartflow_prompt_builder.py b/src/chat/focus_chat/heartflow_prompt_builder.py index 532ceccd1..3209bbe46 100644 --- a/src/chat/focus_chat/heartflow_prompt_builder.py +++ b/src/chat/focus_chat/heartflow_prompt_builder.py @@ -1,6 +1,6 @@ from src.config.config import global_config from src.common.logger_manager import get_logger -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from src.chat.person_info.relationship_manager import relationship_manager @@ -103,7 +103,6 @@ class PromptBuilder: return None async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str: - individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=2) is_group_chat = bool(chat_stream.group_info) @@ -112,7 +111,7 @@ class PromptBuilder: who_chat_in_group = get_recent_group_speaker( chat_stream.stream_id, (chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None, - limit=global_config.chat.observation_context_size, + limit=global_config.focus_chat.observation_context_size, ) elif chat_stream.user_info: who_chat_in_group.append( @@ -160,7 +159,7 @@ class PromptBuilder: message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), - limit=global_config.chat.observation_context_size, + limit=global_config.focus_chat.observation_context_size, ) chat_talking_prompt = await build_readable_messages( message_list_before_now, diff --git a/src/chat/focus_chat/info/action_info.py b/src/chat/focus_chat/info/action_info.py index 1bb6b96a6..8c97029d0 100644 --- a/src/chat/focus_chat/info/action_info.py +++ b/src/chat/focus_chat/info/action_info.py @@ -80,4 +80,4 @@ class ActionInfo(InfoBase): Returns: bool: 如果有任何动作需要添加或移除则返回True """ - return bool(self.get_add_actions() or self.get_remove_actions()) \ No newline at end of file + return bool(self.get_add_actions() or self.get_remove_actions()) diff --git a/src/chat/focus_chat/info_processors/action_processor.py b/src/chat/focus_chat/info_processors/action_processor.py index a952b38c8..b53d45689 100644 --- a/src/chat/focus_chat/info_processors/action_processor.py +++ b/src/chat/focus_chat/info_processors/action_processor.py @@ -1,14 +1,10 @@ from typing import List, Optional, Any -from src.chat.focus_chat.info.obs_info import ObsInfo from src.chat.heart_flow.observation.observation import Observation from src.chat.focus_chat.info.info_base import InfoBase from src.chat.focus_chat.info.action_info import ActionInfo from .base_processor import BaseProcessor from src.common.logger_manager import get_logger -from src.chat.heart_flow.observation.chatting_observation import ChattingObservation from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation -from src.chat.focus_chat.info.cycle_info import CycleInfo -from datetime import datetime from typing import Dict from src.chat.models.utils_model import LLMRequest from src.config.config import global_config @@ -55,10 +51,7 @@ class ActionProcessor(BaseProcessor): # 处理Observation对象 if observations: for obs in observations: - if isinstance(obs, HFCloopObservation): - - # 创建动作信息 action_info = ActionInfo() action_changes = await self.analyze_loop_actions(obs) @@ -75,7 +68,6 @@ class ActionProcessor(BaseProcessor): return processed_infos - async def analyze_loop_actions(self, obs: HFCloopObservation) -> Dict[str, List[str]]: """分析最近的循环内容并决定动作的增减 @@ -87,29 +79,29 @@ class ActionProcessor(BaseProcessor): } """ result = {"add": [], "remove": []} - + # 获取最近10次循环 recent_cycles = obs.history_loop[-10:] if len(obs.history_loop) > 10 else obs.history_loop if not recent_cycles: return result - + # 统计no_reply的数量 no_reply_count = 0 reply_sequence = [] # 记录最近的动作序列 - + for cycle in recent_cycles: action_type = cycle.loop_plan_info["action_result"]["action_type"] if action_type == "no_reply": no_reply_count += 1 reply_sequence.append(action_type == "reply") - + # 检查no_reply比例 if len(recent_cycles) >= 5 and (no_reply_count / len(recent_cycles)) >= 0.8: result["add"].append("exit_focus_chat") - + # 获取最近三次的reply状态 last_three = reply_sequence[-3:] if len(reply_sequence) >= 3 else reply_sequence - + # 根据最近的reply情况决定是否移除reply动作 if len(last_three) >= 3 and all(last_three): # 如果最近三次都是reply,直接移除 @@ -122,5 +114,5 @@ class ActionProcessor(BaseProcessor): # 如果最近一次是reply,20%概率移除 if random.random() < 0.2: result["remove"].append("reply") - + return result diff --git a/src/chat/focus_chat/info_processors/mind_processor.py b/src/chat/focus_chat/info_processors/mind_processor.py index afd7921d4..0457ea1e8 100644 --- a/src/chat/focus_chat/info_processors/mind_processor.py +++ b/src/chat/focus_chat/info_processors/mind_processor.py @@ -5,7 +5,7 @@ from src.config.config import global_config import time import traceback from src.common.logger_manager import get_logger -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.utils.json_utils import safe_json_dumps from src.chat.message_receive.chat_stream import chat_manager @@ -194,7 +194,6 @@ class MindProcessor(BaseProcessor): # ---------- 3. 准备个性化数据 ---------- # 获取个性化信息 - individuality = Individuality.get_instance() relation_prompt = "" for person in person_list: diff --git a/src/chat/focus_chat/info_processors/self_processor.py b/src/chat/focus_chat/info_processors/self_processor.py index 4e447294d..0b65fe77c 100644 --- a/src/chat/focus_chat/info_processors/self_processor.py +++ b/src/chat/focus_chat/info_processors/self_processor.py @@ -5,7 +5,7 @@ from src.config.config import global_config import time import traceback from src.common.logger_manager import get_logger -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.message_receive.chat_stream import chat_manager from src.chat.person_info.relationship_manager import relationship_manager @@ -21,8 +21,9 @@ logger = get_logger("processor") def init_prompt(): indentify_prompt = """ -你的名字是{bot_name},你是一个AI,但是你伪装成了一个人类,你的人格是,{prompt_personality}。 -你的头像形象是一只橙色的鱼,头上有绿色的树叶。 +{name_block} +你是一个AI,但是你伪装成了一个人类,你的人格是,{prompt_personality}。 +{indentify_block} {relation_prompt} {memory_str} @@ -125,16 +126,22 @@ class SelfProcessor(BaseProcessor): # hfcloop_observe_info = observation.get_observe_info() pass - individuality = Individuality.get_instance() - personality_block = individuality.get_prompt(x_person=2, level=2) + nickname_str = "" + for nicknames in global_config.bot.alias_names: + nickname_str += f"{nicknames}," + name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。" + + personality_block = individuality.get_personality_prompt(x_person=2, level=2) + identity_block = individuality.get_identity_prompt(x_person=2, level=2) relation_prompt = "" for person in person_list: relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True) prompt = (await global_prompt_manager.get_prompt_async("indentify_prompt")).format( - bot_name=individuality.name, + name_block=name_block, prompt_personality=personality_block, + indentify_block=identity_block, memory_str=memory_str, relation_prompt=relation_prompt, time_now=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), diff --git a/src/chat/focus_chat/info_processors/tool_processor.py b/src/chat/focus_chat/info_processors/tool_processor.py index de9a9a216..294f130e8 100644 --- a/src/chat/focus_chat/info_processors/tool_processor.py +++ b/src/chat/focus_chat/info_processors/tool_processor.py @@ -3,7 +3,7 @@ from src.chat.models.utils_model import LLMRequest from src.config.config import global_config import time from src.common.logger_manager import get_logger -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.tools.tool_use import ToolUser from src.chat.utils.json_utils import process_llm_tool_calls @@ -133,7 +133,7 @@ class ToolProcessor(BaseProcessor): relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True) # 获取个性信息 - individuality = Individuality.get_instance() + # prompt_personality = individuality.get_prompt(x_person=2, level=2) # 获取时间信息 diff --git a/src/chat/focus_chat/planners/action_manager.py b/src/chat/focus_chat/planners/action_manager.py index 60ab0babf..a10c48847 100644 --- a/src/chat/focus_chat/planners/action_manager.py +++ b/src/chat/focus_chat/planners/action_manager.py @@ -1,9 +1,8 @@ -from typing import Dict, List, Optional, Callable, Coroutine, Type, Any +from typing import Dict, List, Optional, Type, Any from src.chat.focus_chat.planners.actions.base_action import BaseAction, _ACTION_REGISTRY from src.chat.heart_flow.observation.observation import Observation from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor from src.chat.message_receive.chat_stream import ChatStream -from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail from src.common.logger_manager import get_logger import importlib import pkgutil diff --git a/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py b/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py index 6aeb68ccd..92bb7656c 100644 --- a/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py +++ b/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py @@ -1,12 +1,9 @@ import asyncio import traceback from src.common.logger_manager import get_logger -from src.chat.utils.timer_calculator import Timer from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action -from typing import Tuple, List, Callable, Coroutine +from typing import Tuple, List from src.chat.heart_flow.observation.observation import Observation -from src.chat.heart_flow.observation.chatting_observation import ChattingObservation -from src.chat.heart_flow.sub_heartflow import SubHeartFlow from src.chat.message_receive.chat_stream import ChatStream from src.chat.heart_flow.heartflow import heartflow from src.chat.heart_flow.sub_heartflow import ChatState @@ -61,8 +58,6 @@ class ExitFocusChatAction(BaseAction): self._shutting_down = shutting_down self.chat_id = chat_stream.stream_id - - async def handle_action(self) -> Tuple[bool, str]: """ 处理退出专注聊天的情况 @@ -95,7 +90,6 @@ class ExitFocusChatAction(BaseAction): logger.warning(f"{self.log_prefix} {warning_msg}") return False, warning_msg - return True, status_message except asyncio.CancelledError: @@ -105,4 +99,4 @@ class ExitFocusChatAction(BaseAction): error_msg = f"处理 'exit_focus_chat' 时发生错误: {str(e)}" logger.error(f"{self.log_prefix} {error_msg}") logger.error(traceback.format_exc()) - return False, error_msg \ No newline at end of file + return False, error_msg diff --git a/src/chat/focus_chat/planners/actions/no_reply_action.py b/src/chat/focus_chat/planners/actions/no_reply_action.py index 6e31d5abb..1d9abc7b7 100644 --- a/src/chat/focus_chat/planners/actions/no_reply_action.py +++ b/src/chat/focus_chat/planners/actions/no_reply_action.py @@ -3,7 +3,7 @@ import traceback from src.common.logger_manager import get_logger from src.chat.utils.timer_calculator import Timer from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action -from typing import Tuple, List, Callable, Coroutine +from typing import Tuple, List from src.chat.heart_flow.observation.observation import Observation from src.chat.heart_flow.observation.chatting_observation import ChattingObservation from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp diff --git a/src/chat/focus_chat/planners/planner.py b/src/chat/focus_chat/planners/planner.py index ca35d3096..8e1e8a0a0 100644 --- a/src/chat/focus_chat/planners/planner.py +++ b/src/chat/focus_chat/planners/planner.py @@ -12,7 +12,7 @@ from src.chat.focus_chat.info.action_info import ActionInfo from src.chat.focus_chat.info.structured_info import StructuredInfo from src.common.logger_manager import get_logger from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.chat.focus_chat.planners.action_manager import ActionManager logger = get_logger("planner") @@ -92,37 +92,37 @@ class ActionPlanner: try: # 获取观察信息 extra_info: list[str] = [] - + # 首先处理动作变更 for info in all_plan_info: if isinstance(info, ActionInfo) and info.has_changes(): add_actions = info.get_add_actions() remove_actions = info.get_remove_actions() reason = info.get_reason() - + # 处理动作的增加 for action_name in add_actions: if action_name in self.action_manager.get_registered_actions(): self.action_manager.add_action_to_using(action_name) logger.debug(f"{self.log_prefix}添加动作: {action_name}, 原因: {reason}") - + # 处理动作的移除 for action_name in remove_actions: self.action_manager.remove_action_from_using(action_name) logger.debug(f"{self.log_prefix}移除动作: {action_name}, 原因: {reason}") - + # 如果当前选择的动作被移除了,更新为no_reply if action in remove_actions: action = "no_reply" reasoning = f"之前选择的动作{action}已被移除,原因: {reason}" - + # 继续处理其他信息 for info in all_plan_info: if isinstance(info, ObsInfo): observed_messages = info.get_talking_message() observed_messages_str = info.get_talking_message_str_truncate() chat_type = info.get_chat_type() - is_group_chat = (chat_type == "group") + is_group_chat = chat_type == "group" elif isinstance(info, MindInfo): current_mind = info.get_current_mind() elif isinstance(info, CycleInfo): @@ -134,20 +134,16 @@ class ActionPlanner: # 获取当前可用的动作 current_available_actions = self.action_manager.get_using_actions() - + # 如果没有可用动作,直接返回no_reply if not current_available_actions: logger.warning(f"{self.log_prefix}没有可用的动作,将使用no_reply") action = "no_reply" reasoning = "没有可用的动作" return { - "action_result": { - "action_type": action, - "action_data": action_data, - "reasoning": reasoning - }, + "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning}, "current_mind": current_mind, - "observed_messages": observed_messages + "observed_messages": observed_messages, } # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- @@ -271,7 +267,6 @@ class ActionPlanner: else: mind_info_block = "你刚参与聊天" - individuality = Individuality.get_instance() personality_block = individuality.get_prompt(x_person=2, level=2) action_options_block = "" diff --git a/src/chat/heart_flow/background_tasks.py b/src/chat/heart_flow/background_tasks.py index 28b248bdc..4d2438b6f 100644 --- a/src/chat/heart_flow/background_tasks.py +++ b/src/chat/heart_flow/background_tasks.py @@ -4,7 +4,7 @@ from typing import Optional, Coroutine, Callable, Any, List from src.common.logger_manager import get_logger from src.chat.heart_flow.mai_state_manager import MaiStateManager, MaiStateInfo from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager - +from src.config.config import global_config logger = get_logger("background_tasks") @@ -94,13 +94,6 @@ class BackgroundTaskManager: f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s", "_cleanup_task", ), - # 新增兴趣评估任务配置 - ( - self._run_into_focus_cycle, - "debug", # 设为debug,避免过多日志 - f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s", - "_into_focus_task", - ), # 新增私聊激活任务配置 ( # Use lambda to pass the interval to the runner function @@ -111,6 +104,19 @@ class BackgroundTaskManager: ), ] + # 根据 chat_mode 条件添加专注评估任务 + if not (global_config.chat.chat_mode == "normal"): + task_configs.append( + ( + self._run_into_focus_cycle, + "debug", # 设为debug,避免过多日志 + f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s", + "_into_focus_task", + ) + ) + else: + logger.info("聊天模式为 normal,跳过启动专注评估任务") + # 统一启动所有任务 for task_func, log_level, log_msg, task_attr_name in task_configs: # 检查任务变量是否存在且未完成 @@ -183,7 +189,6 @@ class BackgroundTaskManager: logger.info("检测到离线,停用所有子心流") await self.subheartflow_manager.deactivate_all_subflows() - async def _perform_cleanup_work(self): """执行子心流清理任务 1. 获取需要清理的不活跃子心流列表 @@ -209,18 +214,15 @@ class BackgroundTaskManager: # 记录最终清理结果 logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流") - # --- 新增兴趣评估工作函数 --- async def _perform_into_focus_work(self): """执行一轮子心流兴趣评估与提升检查。""" # 直接调用 subheartflow_manager 的方法,并传递当前状态信息 - await self.subheartflow_manager.sbhf_absent_into_focus() - + await self.subheartflow_manager.sbhf_normal_into_focus() + async def _run_state_update_cycle(self, interval: int): await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work) - - async def _run_cleanup_cycle(self): await _run_periodic_loop( task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work diff --git a/src/chat/heart_flow/chat_state_info.py b/src/chat/heart_flow/chat_state_info.py index 972882201..db4c2d5c7 100644 --- a/src/chat/heart_flow/chat_state_info.py +++ b/src/chat/heart_flow/chat_state_info.py @@ -4,13 +4,13 @@ import enum class ChatState(enum.Enum): ABSENT = "没在看群" - CHAT = "随便水群" + NORMAL = "随便水群" FOCUSED = "认真水群" class ChatStateInfo: def __init__(self): - self.chat_status: ChatState = ChatState.CHAT + self.chat_status: ChatState = ChatState.NORMAL self.current_state_time = 120 self.mood_manager = mood_manager diff --git a/src/chat/heart_flow/heartflow.py b/src/chat/heart_flow/heartflow.py index bad0683ce..6e7a55b44 100644 --- a/src/chat/heart_flow/heartflow.py +++ b/src/chat/heart_flow/heartflow.py @@ -1,9 +1,6 @@ from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState -from src.chat.models.utils_model import LLMRequest -from src.config.config import global_config from src.common.logger_manager import get_logger from typing import Any, Optional -from src.tools.tool_use import ToolUser from src.chat.heart_flow.mai_state_manager import MaiStateInfo, MaiStateManager from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager from src.chat.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager diff --git a/src/chat/heart_flow/mai_state_manager.py b/src/chat/heart_flow/mai_state_manager.py index c5e272796..1564400eb 100644 --- a/src/chat/heart_flow/mai_state_manager.py +++ b/src/chat/heart_flow/mai_state_manager.py @@ -4,21 +4,10 @@ import random from typing import List, Tuple, Optional from src.common.logger_manager import get_logger from src.manager.mood_manager import mood_manager -from src.config.config import global_config logger = get_logger("mai_state") -# -- 状态相关的可配置参数 (可以从 glocal_config 加载) -- -# The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls -# whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to -# `False`, it means that the debugging feature for unlimited focused chatting is disabled. -# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 -enable_unlimited_hfc_chat = False -prevent_offline_state = True -# 目前默认不启用OFFLINE状 - - class MaiState(enum.Enum): """ 聊天状态: @@ -141,10 +130,6 @@ class MaiStateManager: ) next_state = resolved_candidate - if enable_unlimited_hfc_chat: - logger.debug("调试用:开挂了,强制切换到专注聊天") - next_state = MaiState.FOCUSED_CHAT - if next_state is not None and next_state != current_status: return next_state else: diff --git a/src/chat/heart_flow/observation/chatting_observation.py b/src/chat/heart_flow/observation/chatting_observation.py index 9bd10e511..a1375f587 100644 --- a/src/chat/heart_flow/observation/chatting_observation.py +++ b/src/chat/heart_flow/observation/chatting_observation.py @@ -57,7 +57,7 @@ class ChattingObservation(Observation): self.talking_message_str_truncate = "" self.name = global_config.bot.nickname self.nick_name = global_config.bot.alias_names - self.max_now_obs_len = global_config.chat.observation_context_size + self.max_now_obs_len = global_config.focus_chat.observation_context_size self.overlap_len = global_config.focus_chat.compressed_length self.mid_memories = [] self.max_mid_memory_len = global_config.focus_chat.compress_length_limit diff --git a/src/chat/heart_flow/observation/hfcloop_observation.py b/src/chat/heart_flow/observation/hfcloop_observation.py index d712b83be..2e047f071 100644 --- a/src/chat/heart_flow/observation/hfcloop_observation.py +++ b/src/chat/heart_flow/observation/hfcloop_observation.py @@ -18,7 +18,7 @@ class HFCloopObservation: self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间 self.history_loop: List[CycleDetail] = [] self.action_manager: ActionManager = None - + self.all_actions = {} def get_observe_info(self): diff --git a/src/chat/heart_flow/sub_heartflow.py b/src/chat/heart_flow/sub_heartflow.py index c440f8cfd..4019abe7d 100644 --- a/src/chat/heart_flow/sub_heartflow.py +++ b/src/chat/heart_flow/sub_heartflow.py @@ -2,7 +2,7 @@ from .observation.observation import Observation from src.chat.heart_flow.observation.chatting_observation import ChattingObservation import asyncio import time -from typing import Optional, List, Dict, Tuple, Callable, Coroutine +from typing import Optional, List, Dict, Tuple import traceback from src.common.logger_manager import get_logger from src.chat.message_receive.message import MessageRecv @@ -23,7 +23,6 @@ class SubHeartflow: self, subheartflow_id, mai_states: MaiStateInfo, - hfc_no_reply_callback: Callable[[], Coroutine[None, None, None]], ): """子心流初始化函数 @@ -35,7 +34,6 @@ class SubHeartflow: # 基础属性,两个值是一样的 self.subheartflow_id = subheartflow_id self.chat_id = subheartflow_id - self.hfc_no_reply_callback = hfc_no_reply_callback # 麦麦的状态 self.mai_states = mai_states @@ -92,7 +90,7 @@ class SubHeartflow: # 创建并初始化 normal_chat_instance chat_stream = chat_manager.get_stream(self.chat_id) if chat_stream: - self.normal_chat_instance = NormalChat(chat_stream=chat_stream,interest_dict=self.get_interest_dict()) + self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) await self.normal_chat_instance.initialize() await self.normal_chat_instance.start_chat() logger.info(f"{self.log_prefix} NormalChat 实例已创建并启动。") @@ -189,7 +187,7 @@ class SubHeartflow: # 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数 self.heart_fc_instance = HeartFChatting( chat_id=self.subheartflow_id, - observations=self.observations, + observations=self.observations, ) # 初始化并启动 HeartFChatting @@ -216,7 +214,7 @@ class SubHeartflow: state_changed = False log_prefix = f"[{self.log_prefix}]" - if new_state == ChatState.CHAT: + if new_state == ChatState.NORMAL: logger.debug(f"{log_prefix} 准备进入或保持 普通聊天 状态") if await self._start_normal_chat(): logger.debug(f"{log_prefix} 成功进入或保持 NormalChat 状态。") diff --git a/src/chat/heart_flow/subheartflow_manager.py b/src/chat/heart_flow/subheartflow_manager.py index 22bab6a40..a557392d1 100644 --- a/src/chat/heart_flow/subheartflow_manager.py +++ b/src/chat/heart_flow/subheartflow_manager.py @@ -2,13 +2,11 @@ import asyncio import time import random from typing import Dict, Any, Optional, List -import functools from src.common.logger_manager import get_logger from src.chat.message_receive.chat_stream import chat_manager from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState from src.chat.heart_flow.mai_state_manager import MaiStateInfo from src.chat.heart_flow.observation.chatting_observation import ChattingObservation -from src.config.config import global_config # 初始化日志记录器 @@ -62,7 +60,6 @@ class SubHeartflowManager: self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问 self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例 - async def force_change_state(self, subflow_id: Any, target_state: ChatState) -> bool: """强制改变指定子心流的状态""" async with self._lock: @@ -101,16 +98,10 @@ class SubHeartflowManager: return subflow try: - # --- 使用 functools.partial 创建 HFC 回调 --- # - # 将 manager 的 _handle_hfc_no_reply 方法与当前的 subheartflow_id 绑定 - hfc_callback = functools.partial(self._handle_hfc_no_reply, subheartflow_id) - # --- 结束创建回调 --- # - - # 初始化子心流, 传入 mai_state_info 和 partial 创建的回调 + # 初始化子心流, 传入 mai_state_info new_subflow = SubHeartflow( subheartflow_id, self.mai_state_info, - hfc_callback, # <-- 传递 partial 创建的回调 ) # 异步初始化 @@ -199,22 +190,14 @@ class SubHeartflowManager: f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。" ) - async def sbhf_absent_into_focus(self): + async def sbhf_normal_into_focus(self): """评估子心流兴趣度,满足条件则提升到FOCUSED状态(基于start_hfc_probability)""" try: - current_state = self.mai_state_info.get_current_state() - - # 检查是否允许进入 FOCUS 模式 - if not global_config.chat.allow_focus_mode: - if int(time.time()) % 60 == 0: # 每60秒输出一次日志避免刷屏 - logger.trace("未开启 FOCUSED 状态 (allow_focus_mode=False)") - return - for sub_hf in list(self.subheartflows.values()): flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id - # 跳过非CHAT状态或已经是FOCUSED状态的子心流 + # 跳过已经是FOCUSED状态的子心流 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue @@ -225,13 +208,6 @@ class SubHeartflowManager: f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}" ) - # 调试用 - from .mai_state_manager import enable_unlimited_hfc_chat - - if not enable_unlimited_hfc_chat: - if sub_hf.chat_state.chat_status != ChatState.CHAT: - continue - if random.random() >= sub_hf.interest_chatting.start_hfc_probability: continue @@ -250,12 +226,11 @@ class SubHeartflowManager: except Exception as e: logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True) - - async def sbhf_focus_into_absent_or_chat(self, subflow_id: Any): + async def sbhf_focus_into_normal(self, subflow_id: Any): """ - 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 CHAT。 + 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 NORMAL。 通常在连续多次 "no_reply" 后被调用。 - 对于私聊和群聊,都转换为 CHAT。 + 对于私聊和群聊,都转换为 NORMAL。 Args: subflow_id: 需要转换状态的子心流 ID。 @@ -263,15 +238,15 @@ class SubHeartflowManager: async with self._lock: subflow = self.subheartflows.get(subflow_id) if not subflow: - logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 CHAT") + logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 NORMAL") return stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id current_state = subflow.chat_state.chat_status if current_state == ChatState.FOCUSED: - target_state = ChatState.CHAT - log_reason = "转为CHAT" + target_state = ChatState.NORMAL + log_reason = "转为NORMAL" logger.info( f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})" @@ -292,34 +267,10 @@ class SubHeartflowManager: f"[状态转换请求] 转换 {stream_name} 到 {target_state.value} 时出错: {e}", exc_info=True ) elif current_state == ChatState.ABSENT: - logger.debug(f"[状态转换请求] {stream_name} 处于 ABSENT 状态,尝试转为 CHAT") - await subflow.change_chat_state(ChatState.CHAT) + logger.debug(f"[状态转换请求] {stream_name} 处于 ABSENT 状态,尝试转为 NORMAL") + await subflow.change_chat_state(ChatState.NORMAL) else: - logger.debug( - f"[状态转换请求] {stream_name} 当前状态为 {current_state.value},无需转换" - ) - - def count_subflows_by_state(self, state: ChatState) -> int: - """统计指定状态的子心流数量""" - count = 0 - # 遍历所有子心流实例 - for subheartflow in self.subheartflows.values(): - # 检查子心流状态是否匹配 - if subheartflow.chat_state.chat_status == state: - count += 1 - return count - - def count_subflows_by_state_nolock(self, state: ChatState) -> int: - """ - 统计指定状态的子心流数量 (不上锁版本)。 - 警告:仅应在已持有 self._lock 的上下文中使用此方法。 - """ - count = 0 - for subheartflow in self.subheartflows.values(): - if subheartflow.chat_state.chat_status == state: - count += 1 - return count - + logger.debug(f"[状态转换请求] {stream_name} 当前状态为 {current_state.value},无需转换") async def delete_subflow(self, subheartflow_id: Any): """删除指定的子心流。""" @@ -336,28 +287,14 @@ class SubHeartflowManager: else: logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}") - - async def _handle_hfc_no_reply(self, subheartflow_id: Any): - """处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)""" - # 注意:这里不需要再获取锁,因为 sbhf_focus_into_absent_or_chat 内部会处理锁 - logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号") - await self.sbhf_focus_into_absent_or_chat(subheartflow_id) - # --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- # async def sbhf_absent_private_into_focus(self): - """检查 ABSENT 状态的私聊子心流是否有新活动,若有且未达 FOCUSED 上限,则直接转换为 FOCUSED。""" + """检查 ABSENT 状态的私聊子心流是否有新活动,若有则直接转换为 FOCUSED。""" log_prefix_task = "[私聊激活检查]" transitioned_count = 0 checked_count = 0 - # --- 检查是否允许 FOCUS 模式 --- # - if not global_config.chat.allow_focus_mode: - return - async with self._lock: - # --- 获取当前 FOCUSED 计数 (不上锁版本) --- # - current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED) - # --- 筛选出所有 ABSENT 状态的私聊子心流 --- # eligible_subflows = [ hf @@ -372,7 +309,6 @@ class SubHeartflowManager: # --- 遍历评估每个符合条件的私聊 --- # for sub_hf in eligible_subflows: - flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id log_prefix = f"[{stream_name}]({log_prefix_task})" @@ -393,13 +329,12 @@ class SubHeartflowManager: else: logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") - # --- 如果活跃且未达上限,则尝试转换 --- # + # --- 如果活跃,则尝试转换 --- # if is_active: await sub_hf.change_chat_state(ChatState.FOCUSED) # 确认转换成功 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: transitioned_count += 1 - current_focused_count += 1 # 更新计数器以供本轮后续检查 logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。") else: logger.warning( diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index 1695a3948..68758e298 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -11,7 +11,6 @@ import jieba import networkx as nx import numpy as np from collections import Counter -from ...common.database.database import memory_db as db from ...chat.models.utils_model import LLMRequest from src.common.logger_manager import get_logger from src.chat.memory_system.sample_distribution import MemoryBuildScheduler # 分布生成器 diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index 3b9a6f929..d7f13d92d 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -7,7 +7,7 @@ from src.chat.message_receive.chat_stream import chat_manager from src.chat.message_receive.message import MessageRecv from src.experimental.only_message_process import MessageProcessor from src.experimental.PFC.pfc_manager import PFCManager -from src.chat.focus_chat.heartflow_processor import HeartFCProcessor +from src.chat.focus_chat.heartflow_message_revceiver import HeartFCMessageReceiver from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.config.config import global_config @@ -23,7 +23,7 @@ class ChatBot: self.bot = None # bot 实例引用 self._started = False self.mood_manager = mood_manager # 获取情绪管理器单例 - self.heartflow_processor = HeartFCProcessor() # 新增 + self.heartflow_message_receiver = HeartFCMessageReceiver() # 新增 # 创建初始化PFC管理器的任务,会在_ensure_started时执行 self.only_process_chat = MessageProcessor() @@ -110,11 +110,11 @@ class ChatBot: # 禁止PFC,进入普通的心流消息处理逻辑 else: logger.trace("进入普通心流私聊处理") - await self.heartflow_processor.process_message(message_data) + await self.heartflow_message_receiver.process_message(message_data) # 群聊默认进入心流消息处理逻辑 else: logger.trace(f"检测到群聊消息,群ID: {group_info.group_id}") - await self.heartflow_processor.process_message(message_data) + await self.heartflow_message_receiver.process_message(message_data) if template_group_name: async with global_prompt_manager.async_message_scope(template_group_name): diff --git a/src/chat/normal_chat/normal_chat.py b/src/chat/normal_chat/normal_chat.py index bd5322137..c32e79e6d 100644 --- a/src/chat/normal_chat/normal_chat.py +++ b/src/chat/normal_chat/normal_chat.py @@ -26,7 +26,7 @@ logger = get_logger("normal_chat") class NormalChat: - def __init__(self, chat_stream: ChatStream, interest_dict: dict = {}): + def __init__(self, chat_stream: ChatStream, interest_dict: dict = None): """初始化 NormalChat 实例。只进行同步操作。""" # Basic info from chat_stream (sync) @@ -36,7 +36,7 @@ class NormalChat: self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id # Interest dict - self.interest_dict = interest_dict + self.interest_dict = interest_dict or {} # --- Initialize attributes (defaults) --- self.is_group_chat: bool = False @@ -200,7 +200,6 @@ class NormalChat: logger.info(f"[{self.stream_name}] 兴趣监控任务被取消或置空,退出") break - items_to_process = list(self.interest_dict.items()) if not items_to_process: continue diff --git a/src/chat/normal_chat/willing/mode_classical.py b/src/chat/normal_chat/willing/mode_classical.py index a9f04273a..c282651dd 100644 --- a/src/chat/normal_chat/willing/mode_classical.py +++ b/src/chat/normal_chat/willing/mode_classical.py @@ -49,7 +49,7 @@ class ClassicalWillingManager(BaseWillingManager): # 检查群组权限(如果是群聊) if ( willing_info.group_info - and willing_info.group_info.group_id in global_config.chat_target.talk_frequency_down_groups + and willing_info.group_info.group_id in global_config.normal_chat.talk_frequency_down_groups ): reply_probability = reply_probability / global_config.normal_chat.down_frequency_rate diff --git a/src/chat/normal_chat/willing/mode_mxp.py b/src/chat/normal_chat/willing/mode_mxp.py index 1e7d5856d..edfbca8c1 100644 --- a/src/chat/normal_chat/willing/mode_mxp.py +++ b/src/chat/normal_chat/willing/mode_mxp.py @@ -180,7 +180,7 @@ class MxpWillingManager(BaseWillingManager): if w_info.is_emoji: probability *= global_config.normal_chat.emoji_response_penalty - if w_info.group_info and w_info.group_info.group_id in global_config.chat_target.talk_frequency_down_groups: + if w_info.group_info and w_info.group_info.group_id in global_config.normal_chat.talk_frequency_down_groups: probability /= global_config.normal_chat.down_frequency_rate self.temporary_willing = current_willing diff --git a/src/chat/person_info/person_info.py b/src/chat/person_info/person_info.py index 562cdc235..de120c6a4 100644 --- a/src/chat/person_info/person_info.py +++ b/src/chat/person_info/person_info.py @@ -9,7 +9,7 @@ import asyncio import numpy as np from src.chat.models.utils_model import LLMRequest from src.config.config import global_config -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality import matplotlib @@ -257,7 +257,6 @@ class PersonInfoManager: current_name_set = set(self.person_name_list.values()) while current_try < max_retries: - individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=1) bot_name = individuality.personality.bot_nickname diff --git a/src/chat/utils/info_catcher.py b/src/chat/utils/info_catcher.py index 93cda5113..bbc85dd47 100644 --- a/src/chat/utils/info_catcher.py +++ b/src/chat/utils/info_catcher.py @@ -127,7 +127,7 @@ class InfoCatcher: Messages.select() .where((Messages.chat_id == chat_id_val) & (Messages.message_id < message_id_val)) .order_by(Messages.time.desc()) - .limit(global_config.chat.observation_context_size * 3) + .limit(global_config.focus_chat.observation_context_size * 3) ) return list(messages_before_query) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 44697530d..25e0e6e12 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -6,16 +6,13 @@ from collections import Counter import jieba import numpy as np from maim_message import UserInfo -from pymongo.errors import PyMongoError from src.common.logger import get_module_logger from src.manager.mood_manager import mood_manager from ..message_receive.message import MessageRecv from ..models.utils_model import LLMRequest from .typo_generator import ChineseTypoGenerator -from ...common.database.database import db from ...config.config import global_config -from ...common.database.database_model import Messages from ...common.message_repository import find_messages, count_messages logger = get_module_logger("chat_utils") @@ -112,11 +109,7 @@ async def get_embedding(text, request_type="embedding"): def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False): filter_query = {"chat_id": chat_stream_id} sort_order = [("time", -1)] - recent_messages = find_messages( - message_filter=filter_query, - sort=sort_order, - limit=limit - ) + recent_messages = find_messages(message_filter=filter_query, sort=sort_order, limit=limit) if not recent_messages: return [] @@ -141,23 +134,21 @@ def get_recent_group_speaker(chat_stream_id: str, sender, limit: int = 12) -> li # 获取当前群聊记录内发言的人 filter_query = {"chat_id": chat_stream_id} sort_order = [("time", -1)] - recent_messages = find_messages( - message_filter=filter_query, - sort=sort_order, - limit=limit - ) + recent_messages = find_messages(message_filter=filter_query, sort=sort_order, limit=limit) if not recent_messages: return [] who_chat_in_group = [] for msg_db_data in recent_messages: - user_info = UserInfo.from_dict({ - "platform": msg_db_data["user_platform"], - "user_id": msg_db_data["user_id"], - "user_nickname": msg_db_data["user_nickname"], - "user_cardname": msg_db_data.get("user_cardname", "") - }) + user_info = UserInfo.from_dict( + { + "platform": msg_db_data["user_platform"], + "user_id": msg_db_data["user_id"], + "user_nickname": msg_db_data["user_nickname"], + "user_cardname": msg_db_data.get("user_cardname", ""), + } + ) if ( (user_info.platform, user_info.user_id) != sender and user_info.user_id != global_config.bot.qq_account @@ -579,14 +570,13 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) - # 使用message_repository中的count_messages和find_messages函数 - # 构建查询条件 filter_query = {"chat_id": stream_id, "time": {"$gt": start_time, "$lte": end_time}} try: # 先获取消息数量 count = count_messages(filter_query) - + # 获取消息内容计算总长度 messages = find_messages(message_filter=filter_query) total_length = sum(len(msg.get("processed_plain_text", "")) for msg in messages) diff --git a/src/common/remote.py b/src/common/remote.py index d69fce977..4b12e26ce 100644 --- a/src/common/remote.py +++ b/src/common/remote.py @@ -71,9 +71,9 @@ class TelemetryHeartBeatTask(AsyncTask): ) logger.debug(f"{TELEMETRY_SERVER_URL}/stat/reg_client") - + logger.debug(local_storage["deploy_time"]) - + logger.debug(response) if response.status_code == 200: @@ -111,7 +111,7 @@ class TelemetryHeartBeatTask(AsyncTask): } logger.debug(f"正在发送心跳到服务器: {self.server_url}") - + logger.debug(headers) response = requests.post( @@ -119,7 +119,7 @@ class TelemetryHeartBeatTask(AsyncTask): headers=headers, json=self.info_dict, ) - + logger.debug(response) # 处理响应 diff --git a/src/config/config.py b/src/config/config.py index e6b7c5326..b963e61d0 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -14,10 +14,9 @@ from rich.traceback import install from src.config.config_base import ConfigBase from src.config.official_configs import ( BotConfig, - ChatTargetConfig, PersonalityConfig, IdentityConfig, - PlatformsConfig, + ExpressionConfig, ChatConfig, NormalChatConfig, FocusChatConfig, @@ -30,6 +29,7 @@ from src.config.official_configs import ( TelemetryConfig, ExperimentalConfig, ModelConfig, + MessageReceiveConfig, ) install(extra_lines=3) @@ -139,14 +139,14 @@ class Config(ConfigBase): MMC_VERSION: str = field(default=MMC_VERSION, repr=False, init=False) # 硬编码的版本信息 bot: BotConfig - chat_target: ChatTargetConfig personality: PersonalityConfig identity: IdentityConfig - platforms: PlatformsConfig chat: ChatConfig + message_receive: MessageReceiveConfig normal_chat: NormalChatConfig focus_chat: FocusChatConfig emoji: EmojiConfig + expression: ExpressionConfig memory: MemoryConfig mood: MoodConfig keyword_reaction: KeywordReactionConfig diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 6ad4648ba..e7e605748 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -26,23 +26,6 @@ class BotConfig(ConfigBase): """别名列表""" -@dataclass -class ChatTargetConfig(ConfigBase): - """ - 聊天目标配置类 - 此类中有聊天的群组和用户配置 - """ - - talk_allowed_groups: set[str] = field(default_factory=lambda: set()) - """允许聊天的群组列表""" - - talk_frequency_down_groups: set[str] = field(default_factory=lambda: set()) - """降低聊天频率的群组列表""" - - ban_user_id: set[str] = field(default_factory=lambda: set()) - """禁止聊天的用户列表""" - - @dataclass class PersonalityConfig(ConfigBase): """人格配置类""" @@ -50,9 +33,6 @@ class PersonalityConfig(ConfigBase): personality_core: str """核心人格""" - expression_style: str - """表达风格""" - personality_sides: list[str] = field(default_factory=lambda: []) """人格侧写""" @@ -80,32 +60,17 @@ class IdentityConfig(ConfigBase): """身份特征""" -@dataclass -class PlatformsConfig(ConfigBase): - """平台配置类""" - - qq: str - """QQ适配器连接URL配置""" - - @dataclass class ChatConfig(ConfigBase): """聊天配置类""" - allow_focus_mode: bool = True - """是否允许专注聊天状态""" + chat_mode: str = "normal" + """聊天模式""" - base_normal_chat_num: int = 3 - """最多允许多少个群进行普通聊天""" - base_focused_chat_num: int = 2 - """最多允许多少个群进行专注聊天""" - - observation_context_size: int = 12 - """可观察到的最长上下文大小,超过这个值的上下文会被压缩""" - - message_buffer: bool = True - """消息缓冲器""" +@dataclass +class MessageReceiveConfig(ConfigBase): + """消息接收配置类""" ban_words: set[str] = field(default_factory=lambda: set()) """过滤词列表""" @@ -124,6 +89,12 @@ class NormalChatConfig(ConfigBase): 选择普通模型的概率为 1 - reasoning_normal_model_probability """ + max_context_size: int = 15 + """上下文长度""" + + message_buffer: bool = True + """消息缓冲器""" + emoji_chance: float = 0.2 """发送表情包的基础概率""" @@ -139,6 +110,9 @@ class NormalChatConfig(ConfigBase): response_interested_rate_amplifier: float = 1.0 """回复兴趣度放大系数""" + talk_frequency_down_groups: list[str] = field(default_factory=lambda: []) + """降低回复频率的群组""" + down_frequency_rate: float = 3.0 """降低回复频率的群组回复意愿降低系数""" @@ -162,6 +136,9 @@ class FocusChatConfig(ConfigBase): default_decay_rate_per_second: float = 0.98 """默认衰减率,越大衰减越快""" + observation_context_size: int = 12 + """可观察到的最长上下文大小,超过这个值的上下文会被压缩""" + consecutive_no_reply_threshold: int = 3 """连续不回复的次数阈值""" @@ -172,6 +149,20 @@ class FocusChatConfig(ConfigBase): """最多压缩份数,超过该数值的压缩上下文会被删除""" +@dataclass +class ExpressionConfig(ConfigBase): + """表达配置类""" + + expression_style: str = "" + """表达风格""" + + learning_interval: int = 300 + """学习间隔(秒)""" + + enable_expression_learning: bool = True + """是否启用表达学习""" + + @dataclass class EmojiConfig(ConfigBase): """表情包配置类""" @@ -340,11 +331,8 @@ class TelemetryConfig(ConfigBase): class ExperimentalConfig(ConfigBase): """实验功能配置类""" - # enable_friend_chat: bool = False - # """是否启用好友聊天""" - - # talk_allowed_private: set[str] = field(default_factory=lambda: set()) - # """允许聊天的私聊列表""" + enable_friend_chat: bool = False + """是否启用好友聊天""" pfc_chatting: bool = False """是否启用PFC""" diff --git a/src/experimental/PFC/action_planner.py b/src/experimental/PFC/action_planner.py index c0bff5887..2726f9c8b 100644 --- a/src/experimental/PFC/action_planner.py +++ b/src/experimental/PFC/action_planner.py @@ -5,7 +5,7 @@ from src.chat.models.utils_model import LLMRequest from src.config.config import global_config from src.experimental.PFC.chat_observer import ChatObserver from src.experimental.PFC.pfc_utils import get_items_from_json -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.experimental.PFC.observation_info import ObservationInfo from src.experimental.PFC.conversation_info import ConversationInfo from src.chat.utils.chat_message_builder import build_readable_messages @@ -113,7 +113,7 @@ class ActionPlanner: max_tokens=1500, request_type="action_planning", ) - self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) + self.personality_info = individuality.get_prompt(x_person=2, level=3) self.name = global_config.bot.nickname self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) diff --git a/src/experimental/PFC/pfc.py b/src/experimental/PFC/pfc.py index 80e75c5bf..ec34e8281 100644 --- a/src/experimental/PFC/pfc.py +++ b/src/experimental/PFC/pfc.py @@ -4,7 +4,7 @@ from src.chat.models.utils_model import LLMRequest from src.config.config import global_config from src.experimental.PFC.chat_observer import ChatObserver from src.experimental.PFC.pfc_utils import get_items_from_json -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from src.experimental.PFC.conversation_info import ConversationInfo from src.experimental.PFC.observation_info import ObservationInfo from src.chat.utils.chat_message_builder import build_readable_messages @@ -47,7 +47,7 @@ class GoalAnalyzer: model=global_config.model.normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal" ) - self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) + self.personality_info = individuality.get_prompt(x_person=2, level=3) self.name = global_config.bot.nickname self.nick_name = global_config.bot.alias_names self.private_name = private_name diff --git a/src/experimental/PFC/reply_generator.py b/src/experimental/PFC/reply_generator.py index bac8a769f..c2e770248 100644 --- a/src/experimental/PFC/reply_generator.py +++ b/src/experimental/PFC/reply_generator.py @@ -4,7 +4,7 @@ from src.chat.models.utils_model import LLMRequest from src.config.config import global_config from src.experimental.PFC.chat_observer import ChatObserver from src.experimental.PFC.reply_checker import ReplyChecker -from src.individuality.individuality import Individuality +from src.individuality.individuality import individuality from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from src.chat.utils.chat_message_builder import build_readable_messages @@ -92,7 +92,7 @@ class ReplyGenerator: max_tokens=300, request_type="reply_generation", ) - self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) + self.personality_info = individuality.get_prompt(x_person=2, level=3) self.name = global_config.bot.nickname self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) diff --git a/src/experimental/PFC/waiter.py b/src/experimental/PFC/waiter.py index 452446589..d5f994fe5 100644 --- a/src/experimental/PFC/waiter.py +++ b/src/experimental/PFC/waiter.py @@ -2,7 +2,7 @@ from src.common.logger import get_module_logger from .chat_observer import ChatObserver from .conversation_info import ConversationInfo -# from src.individuality.individuality import Individuality # 不再需要 +# from src.individuality.individuality import individuality,Individuality # 不再需要 from src.config.config import global_config import time import asyncio diff --git a/src/individuality/expression_style.py b/src/individuality/expression_style.py new file mode 100644 index 000000000..c642a86c3 --- /dev/null +++ b/src/individuality/expression_style.py @@ -0,0 +1,159 @@ +import random +from src.common.logger_manager import get_logger +from src.chat.models.utils_model import LLMRequest +from src.config.config import global_config +from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from typing import List, Tuple +import os +import json + +logger = get_logger("expressor") + + +def init_prompt() -> None: + personality_expression_prompt = """ +{personality} + +请从以上人设中总结出这个角色可能的语言风格 +思考回复的特殊内容和情感 +思考有没有特殊的梗,一并总结成语言风格 +总结成如下格式的规律,总结的内容要详细,但具有概括性: +当"xxx"时,可以"xxx", xxx不超过10个字 + +例如: +当"表示十分惊叹"时,使用"我嘞个xxxx" +当"表示讽刺的赞同,不想讲道理"时,使用"对对对" +当"想说明某个观点,但懒得明说",使用"懂的都懂" + +现在请你概括 +""" + Prompt(personality_expression_prompt, "personality_expression_prompt") + + +class PersonalityExpression: + def __init__(self): + self.express_learn_model: LLMRequest = LLMRequest( + model=global_config.model.normal, + temperature=0.1, + max_tokens=256, + request_type="response_heartflow", + ) + self.meta_file_path = os.path.join("data", "expression", "personality", "expression_style_meta.json") + self.expressions_file_path = os.path.join("data", "expression", "personality", "expressions.json") + self.max_calculations = 5 + + def _read_meta_data(self): + if os.path.exists(self.meta_file_path): + try: + with open(self.meta_file_path, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError: + logger.warning(f"无法解析 {self.meta_file_path} 中的JSON数据,将重新开始。") + return {"last_style_text": None, "count": 0} + return {"last_style_text": None, "count": 0} + + def _write_meta_data(self, data): + os.makedirs(os.path.dirname(self.meta_file_path), exist_ok=True) + with open(self.meta_file_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + async def extract_and_store_personality_expressions(self): + """ + 检查data/expression/personality目录,不存在则创建。 + 用peronality变量作为chat_str,调用LLM生成表达风格,解析后count=100,存储到expressions.json。 + 如果expression_style发生变化,则删除旧的expressions.json并重置计数。 + 对于相同的expression_style,最多计算self.max_calculations次。 + """ + os.makedirs(os.path.dirname(self.expressions_file_path), exist_ok=True) + + current_style_text = global_config.expression.expression_style + meta_data = self._read_meta_data() + + last_style_text = meta_data.get("last_style_text") + count = meta_data.get("count", 0) + + if current_style_text != last_style_text: + logger.info(f"表达风格已从 '{last_style_text}' 变为 '{current_style_text}'。重置计数。") + count = 0 + if os.path.exists(self.expressions_file_path): + try: + os.remove(self.expressions_file_path) + logger.info(f"已删除旧的表达文件: {self.expressions_file_path}") + except OSError as e: + logger.error(f"删除旧的表达文件 {self.expressions_file_path} 失败: {e}") + + if count >= self.max_calculations: + logger.info(f"对于风格 '{current_style_text}' 已达到最大计算次数 ({self.max_calculations})。跳过提取。") + # 即使跳过,也更新元数据以反映当前风格已被识别且计数已满 + self._write_meta_data({"last_style_text": current_style_text, "count": count}) + return + + # 构建prompt + prompt = await global_prompt_manager.format_prompt( + "personality_expression_prompt", + personality=current_style_text, + ) + # logger.info(f"个性表达方式提取prompt: {prompt}") + + try: + response, _ = await self.express_learn_model.generate_response_async(prompt) + except Exception as e: + logger.error(f"个性表达方式提取失败: {e}") + # 如果提取失败,保存当前的风格和未增加的计数 + self._write_meta_data({"last_style_text": current_style_text, "count": count}) + return + + logger.info(f"个性表达方式提取response: {response}") + # chat_id用personality + expressions = self.parse_expression_response(response, "personality") + # 转为dict并count=100 + result = [] + for _, situation, style in expressions: + result.append({"situation": situation, "style": style, "count": 100}) + # 超过50条时随机删除多余的,只保留50条 + if len(result) > 50: + remove_count = len(result) - 50 + remove_indices = set(random.sample(range(len(result)), remove_count)) + result = [item for idx, item in enumerate(result) if idx not in remove_indices] + + with open(self.expressions_file_path, "w", encoding="utf-8") as f: + json.dump(result, f, ensure_ascii=False, indent=2) + logger.info(f"已写入{len(result)}条表达到{self.expressions_file_path}") + + # 成功提取后更新元数据 + count += 1 + self._write_meta_data({"last_style_text": current_style_text, "count": count}) + logger.info(f"成功处理。风格 '{current_style_text}' 的计数现在是 {count}。") + + def parse_expression_response(self, response: str, chat_id: str) -> List[Tuple[str, str, str]]: + """ + 解析LLM返回的表达风格总结,每一行提取"当"和"使用"之间的内容,存储为(situation, style)元组 + """ + expressions: List[Tuple[str, str, str]] = [] + for line in response.splitlines(): + line = line.strip() + if not line: + continue + # 查找"当"和下一个引号 + idx_when = line.find('当"') + if idx_when == -1: + continue + idx_quote1 = idx_when + 1 + idx_quote2 = line.find('"', idx_quote1 + 1) + if idx_quote2 == -1: + continue + situation = line[idx_quote1 + 1 : idx_quote2] + # 查找"使用" + idx_use = line.find('使用"', idx_quote2) + if idx_use == -1: + continue + idx_quote3 = idx_use + 2 + idx_quote4 = line.find('"', idx_quote3 + 1) + if idx_quote4 == -1: + continue + style = line[idx_quote3 + 1 : idx_quote4] + expressions.append((chat_id, situation, style)) + return expressions + + +init_prompt() diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index a28f0bc8c..ba462c5e3 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -3,6 +3,7 @@ from typing import Optional from numpy import double from .personality import Personality from .identity import Identity +from .expression_style import PersonalityExpression import random from rich.traceback import install @@ -12,36 +13,15 @@ install(extra_lines=3) class Individuality: """个体特征管理类""" - _instance = None - def __init__(self): - if Individuality._instance is not None: - raise RuntimeError("Individuality 类是单例,请使用 get_instance() 方法获取实例。") - # 正常初始化实例属性 self.personality: Optional[Personality] = None self.identity: Optional[Identity] = None + self.express_style: PersonalityExpression = PersonalityExpression() self.name = "" - @classmethod - def get_instance(cls) -> "Individuality": - """获取Individuality单例实例 - - Returns: - Individuality: 单例实例 - """ - if cls._instance is None: - # 实例不存在,调用 cls() 创建新实例 - # cls() 会调用 __init__ - # 因为此时 cls._instance 仍然是 None,__init__ 会正常执行初始化 - new_instance = cls() - # 将新创建的实例赋值给类变量 _instance - cls._instance = new_instance - # 返回(新创建的或已存在的)单例实例 - return cls._instance - - def initialize( + async def initialize( self, bot_nickname: str, personality_core: str, @@ -76,6 +56,8 @@ class Individuality: identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance ) + await self.express_style.extract_and_store_personality_expressions() + self.name = bot_nickname def to_dict(self) -> dict: @@ -88,7 +70,7 @@ class Individuality: @classmethod def from_dict(cls, data: dict) -> "Individuality": """从字典创建个体特征实例""" - instance = cls.get_instance() + instance = cls() if data.get("personality"): instance.personality = Personality.from_dict(data["personality"]) if data.get("identity"): @@ -176,6 +158,10 @@ class Individuality: identity_parts.append(f"年龄大约{self.identity.age}岁") if self.identity.gender: identity_parts.append(f"性别是{self.identity.gender}") + if self.identity.height: + identity_parts.append(f"身高大约{self.identity.height}厘米") + if self.identity.weight: + identity_parts.append(f"体重大约{self.identity.weight}千克") if identity_parts: details_str = ",".join(identity_parts) @@ -252,3 +238,6 @@ class Individuality: elif factor == "neuroticism": return self.personality.neuroticism return None + + +individuality = Individuality() diff --git a/src/individuality/offline_llm.py b/src/individuality/not_using/offline_llm.py similarity index 100% rename from src/individuality/offline_llm.py rename to src/individuality/not_using/offline_llm.py diff --git a/src/individuality/per_bf_gen.py b/src/individuality/not_using/per_bf_gen.py similarity index 97% rename from src/individuality/per_bf_gen.py rename to src/individuality/not_using/per_bf_gen.py index 7e630bdd9..2d0961cb1 100644 --- a/src/individuality/per_bf_gen.py +++ b/src/individuality/not_using/per_bf_gen.py @@ -17,9 +17,9 @@ with open(config_path, "r", encoding="utf-8") as f: config = toml.load(f) # 现在可以导入src模块 -from src.individuality.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa E402 -from src.individuality.questionnaire import FACTOR_DESCRIPTIONS # noqa E402 -from src.individuality.offline_llm import LLMRequestOff # noqa E402 +from individuality.not_using.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa E402 +from individuality.not_using.questionnaire import FACTOR_DESCRIPTIONS # noqa E402 +from individuality.not_using.offline_llm import LLMRequestOff # noqa E402 # 加载环境变量 env_path = os.path.join(root_path, ".env") diff --git a/src/individuality/questionnaire.py b/src/individuality/not_using/questionnaire.py similarity index 100% rename from src/individuality/questionnaire.py rename to src/individuality/not_using/questionnaire.py diff --git a/src/individuality/scene.py b/src/individuality/not_using/scene.py similarity index 100% rename from src/individuality/scene.py rename to src/individuality/not_using/scene.py diff --git a/src/main.py b/src/main.py index 4f8af28ef..fb138fd50 100644 --- a/src/main.py +++ b/src/main.py @@ -16,7 +16,7 @@ from .chat.message_receive.storage import MessageStorage from .config.config import global_config from .chat.message_receive.bot import chat_bot from .common.logger_manager import get_logger -from .individuality.individuality import Individuality +from .individuality.individuality import individuality, Individuality from .common.server import global_server, Server from rich.traceback import install from .chat.focus_chat.expressors.exprssion_learner import expression_learner @@ -30,7 +30,7 @@ logger = get_logger("main") class MainSystem: def __init__(self): self.hippocampus_manager: HippocampusManager = HippocampusManager.get_instance() - self.individuality: Individuality = Individuality.get_instance() + self.individuality: Individuality = individuality # 使用消息API替代直接的FastAPI实例 from src.common.message import global_api @@ -91,7 +91,7 @@ class MainSystem: self.app.register_message_handler(chat_bot.message_process) # 初始化个体特征 - self.individuality.initialize( + await self.individuality.initialize( bot_nickname=global_config.bot.nickname, personality_core=global_config.personality.personality_core, personality_sides=global_config.personality.personality_sides, @@ -104,9 +104,6 @@ class MainSystem: ) logger.success("个体特征初始化成功") - # 初始化表达方式 - await expression_learner.extract_and_store_personality_expressions() - try: # 启动全局消息管理器 (负责消息发送/排队) await message_manager.start() @@ -169,7 +166,7 @@ class MainSystem: async def learn_and_store_expression_task(): """学习并存储表达方式任务""" while True: - await asyncio.sleep(60) + await asyncio.sleep(global_config.expression.learning_interval) print("\033[1;32m[表达方式学习]\033[0m 开始学习表达方式...") await expression_learner.learn_and_store_expression() print("\033[1;32m[表达方式学习]\033[0m 表达方式学习完成") diff --git a/src/manager/mood_manager.py b/src/manager/mood_manager.py index c83fbeb7c..f1253bbc9 100644 --- a/src/manager/mood_manager.py +++ b/src/manager/mood_manager.py @@ -7,7 +7,7 @@ from typing import Dict, Tuple from ..config.config import global_config from ..common.logger_manager import get_logger from ..manager.async_task_manager import AsyncTask -from ..individuality.individuality import Individuality +from ..individuality.individuality import individuality logger = get_logger("mood") @@ -54,7 +54,7 @@ class MoodUpdateTask(AsyncTask): agreeableness_bias = 0 # 宜人性偏置 neuroticism_factor = 0.5 # 神经质系数 # 获取人格特质 - personality = Individuality.get_instance().personality + personality = individuality.personality if personality: # 神经质:影响情绪变化速度 neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4 diff --git a/start_personality.bat b/start_personality.bat deleted file mode 100644 index e2aa5c06a..000000000 --- a/start_personality.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -chcp 65001 > nul -setlocal enabledelayedexpansion -cd /d %~dp0 - -title 麦麦人格生成 - -cls -echo ====================================== -echo 警告提示 -echo ====================================== -echo 1.这是一个demo系统,仅供体验,特性可能会在将来移除 -echo ====================================== - -echo. -echo ====================================== -echo 请选择Python环境: -echo 1 - venv (推荐) -echo 2 - conda -echo ====================================== -choice /c 12 /n /m "请输入数字选择(1或2): " - -if errorlevel 2 ( - echo ====================================== - set "CONDA_ENV=" - set /p CONDA_ENV="请输入要激活的 conda 环境名称: " - - :: 检查输入是否为空 - if "!CONDA_ENV!"=="" ( - echo 错误:环境名称不能为空 - pause - exit /b 1 - ) - - call conda activate !CONDA_ENV! - if errorlevel 1 ( - echo 激活 conda 环境失败 - pause - exit /b 1 - ) - - echo Conda 环境 "!CONDA_ENV!" 激活成功 - python src/individuality/per_bf_gen.py -) else ( - if exist "venv\Scripts\python.exe" ( - venv\Scripts\python src/individuality/per_bf_gen.py - ) else ( - echo ====================================== - echo 错误: venv环境不存在,请先创建虚拟环境 - pause - exit /b 1 - ) -) - -endlocal -pause diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 943422029..947351deb 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "2.2.0" +version = "2.3.0" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -15,12 +15,9 @@ version = "2.2.0" [bot] qq_account = 1145141919810 nickname = "麦麦" -alias_names = ["麦叠", "牢麦"] #该选项还在调试中,暂时未生效 +alias_names = ["麦叠", "牢麦"] #仅在 专注聊天 有效 -[chat_target] -talk_frequency_down_groups = [] #降低回复频率的群号码 - -[personality] #未完善 +[personality] personality_core = "用一句话或几句话描述人格的核心特点" # 建议20字以内,谁再写3000字小作文敲谁脑袋 personality_sides = [ "用一句话或几句话描述人格的一些细节", @@ -30,35 +27,30 @@ personality_sides = [ "用一句话或几句话描述人格的一些细节", ]# 条数任意,不能为0, 该选项还在调试中,可能未完全生效 -# 表达方式 -expression_style = "描述麦麦说话的表达风格,表达习惯" - - +# 身份特点 部分选项仅在 专注聊天 有效 [identity] #アイデンティティがない 生まれないらららら -# 兴趣爱好 未完善,有些条目未使用 identity_detail = [ "身份特点", "身份特点", -]# 条数任意,不能为0, 该选项还在调试中 +]# 条数任意,不能为0 #外貌特征 age = 18 # 年龄 单位岁 gender = "女" # 性别 height = "170" # 身高(单位cm) weight = "50" # 体重(单位kg) -appearance = "用一句或几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效 - -[platforms] # 必填项目,填写每个平台适配器提供的链接 -qq="http://127.0.0.1:18002/api/message" +appearance = "用一句或几句话描述外貌特征" # 外貌特征 [chat] #麦麦的聊天通用设置 -allow_focus_mode = false # 是否允许专注聊天状态 -# 是否启用heart_flowC(HFC)模式 -# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token +chat_mode = "normal" # 聊天模式 —— 普通模式:normal,专注模式:focus,在普通模式和专注模式之间自动切换 +# chat_mode = "focus" +# chat_mode = "auto" -chat.observation_context_size = 15 # 观察到的最长上下文大小,建议15,太短太长都会导致脑袋尖尖 -message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟 +# 普通模式下,麦麦会针对感兴趣的消息进行回复,token消耗量较低 +# 专注模式下,麦麦会进行主动的观察和回复,并给出回复,token消耗量较高 +# 自动模式下,麦麦会根据消息内容自动切换到专注模式或普通模式 +[message_receive] # 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息 ban_words = [ # "403","张三" @@ -74,9 +66,10 @@ ban_msgs_regex = [ [normal_chat] #普通聊天 #一般回复参数 reasoning_model_probability = 0.3 # 麦麦回答时选择推理模型的概率(与之相对的,普通模型的概率为1 - reasoning_model_probability) - +max_context_size = 15 #上下文长度 emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率,设置为1让麦麦自己决定发不发 thinking_timeout = 120 # 麦麦最长思考时间,超过这个时间的思考会放弃(往往是api反应太慢) +message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟 willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,mxp模式:mxp,自定义模式:custom(需要你自己实现) response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 @@ -85,16 +78,23 @@ down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 emoji_response_penalty = 0 # 表情包回复惩罚系数,设为0为不回复单个表情包,减少单独回复表情包的概率 mentioned_bot_inevitable_reply = false # 提及 bot 必然回复 at_bot_inevitable_reply = false # @bot 必然回复 +talk_frequency_down_groups = [] #降低回复频率的群号码 [focus_chat] #专注聊天 reply_trigger_threshold = 3.0 # 专注聊天触发阈值,越低越容易进入专注聊天 default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入专注聊天 consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天 -# 以下选项暂时无效 +observation_context_size = 15 # 观察到的最长上下文大小,建议15,太短太长都会导致脑袋尖尖 compressed_length = 5 # 不能大于chat.observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5 compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除 +[expression] +# 表达方式 +expression_style = "描述麦麦说话的表达风格,表达习惯" +enable_expression_learning = true # 是否启用表达学习 +learning_interval = 300 # 学习间隔 单位秒 + [emoji] max_reg_num = 40 # 表情包最大注册数量 @@ -124,7 +124,7 @@ consolidation_check_percentage = 0.01 # 检查节点比例 #不希望记忆的词,已经记忆的不会受到影响 memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ] -[mood] +[mood] # 仅在 普通聊天 有效 mood_update_interval = 1.0 # 情绪更新间隔 单位秒 mood_decay_rate = 0.95 # 情绪衰减率 mood_intensity_factor = 1.0 # 情绪强度因子 @@ -164,6 +164,7 @@ enable_kaomoji_protection = false # 是否启用颜文字保护 enable = true [experimental] #实验性功能 +enable_friend_chat = false # 是否启用好友聊天 pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env自定义的宏,使用自定义模型则选择定位相似的模型自己填写 diff --git a/tests/common/test_message_repository.py b/tests/common/test_message_repository.py index 798fa16b1..8a372161d 100644 --- a/tests/common/test_message_repository.py +++ b/tests/common/test_message_repository.py @@ -1,5 +1,4 @@ import unittest -from unittest.mock import patch, MagicMock import datetime import sys import os diff --git a/tests/test_build_readable_messages.py b/tests/test_build_readable_messages.py index 71d91a46d..3bdabe966 100644 --- a/tests/test_build_readable_messages.py +++ b/tests/test_build_readable_messages.py @@ -1,11 +1,9 @@ import unittest import sys import os -import datetime import time import asyncio import traceback -import json import copy # 添加项目根目录到Python路径 diff --git a/tests/test_extract_messages.py b/tests/test_extract_messages.py index 95ddb523f..4dc96a09e 100644 --- a/tests/test_extract_messages.py +++ b/tests/test_extract_messages.py @@ -9,8 +9,6 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..") from src.common.message_repository import find_messages from src.chat.utils.chat_message_builder import get_raw_msg_by_timestamp_with_chat -from peewee import SqliteDatabase -from src.common.database.database import db # 导入实际的数据库连接 class TestExtractMessages(unittest.TestCase):