diff --git a/requirements.txt b/requirements.txt index 7abdffb48..1e374f4eb 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/chat/focus_chat/expressors/default_expressor.py b/src/chat/focus_chat/expressors/default_expressor.py index 37c50c0dc..411b08a05 100644 --- a/src/chat/focus_chat/expressors/default_expressor.py +++ b/src/chat/focus_chat/expressors/default_expressor.py @@ -10,7 +10,7 @@ from src.config.config import global_config from src.chat.utils.utils_image import image_path_to_base64 # Local import needed after move from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.emoji_system.emoji_manager import emoji_manager -from src.chat.focus_chat.heartflow_prompt_builder import prompt_builder +from src.chat.focus_chat.heartflow_prompt_builder import prompt_builder,Prompt from src.chat.focus_chat.heartFC_sender import HeartFCSender from src.chat.utils.utils import process_llm_response from src.chat.utils.info_catcher import info_catcher_manager @@ -18,9 +18,70 @@ 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.config.config import global_config +from src.common.logger_manager import get_logger +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 +from src.chat.utils.utils import get_embedding +import time +from typing import Union, Optional +from src.common.database import db +from src.chat.utils.utils import get_recent_group_speaker +from src.manager.mood_manager import mood_manager +from src.chat.memory_system.Hippocampus import HippocampusManager +from src.chat.knowledge.knowledge_lib import qa_manager +from src.chat.focus_chat.expressors.exprssion_learner import expression_learner +import random logger = get_logger("expressor") +def init_prompt(): + Prompt( + """ +你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中: +{style_habbits} + +你现在正在群里聊天,以下是群里正在进行的聊天内容: +{chat_info} + +以上是聊天内容,你需要了解聊天记录中的内容 + +{chat_target} +你的名字是{bot_name},{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复 +你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。 +请你根据情景使用以下句法: +{grammar_habbits} +回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短,但重组后保持语意通顺。 +回复不要浮夸,不要用夸张修辞,平淡一些。不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。 +现在,你说: +""", + "default_expressor_prompt", + ) + + Prompt( + """ +你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中: +{style_habbits} + +你现在正在群里聊天,以下是群里正在进行的聊天内容: +{chat_info} + +以上是聊天内容,你需要了解聊天记录中的内容 + +{chat_target} +你的名字是{bot_name},{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复 +你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。 +请你根据情景使用以下句法: +{grammar_habbits} +回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短,但重组后保持语意通顺。 +回复不要浮夸,不要用夸张修辞,平淡一些。不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。 +现在,你说: +""", + "default_expressor_private_prompt", # New template for private FOCUSED chat + ) + class DefaultExpressor: def __init__(self, chat_id: str): @@ -106,7 +167,7 @@ class DefaultExpressor: if reply: with Timer("发送消息", cycle_timers): - sent_msg_list = await self._send_response_messages( + sent_msg_list = await self.send_response_messages( anchor_message=anchor_message, thinking_id=thinking_id, response_set=reply, @@ -162,13 +223,10 @@ class DefaultExpressor: # 3. 构建 Prompt with Timer("构建Prompt", {}): # 内部计时器,可选保留 - prompt = await prompt_builder.build_prompt( - build_mode="focus", + prompt = await self.build_prompt_focus( chat_stream=self.chat_stream, # Pass the stream object in_mind_reply=in_mind_reply, reason=reason, - current_mind_info="", - structured_info="", sender_name=sender_name_for_prompt, # Pass determined name target_message=target_message, ) @@ -222,11 +280,111 @@ class DefaultExpressor: logger.error(f"{self.log_prefix}回复生成意外失败: {e}") traceback.print_exc() return None + + async def build_prompt_focus( + self, + reason, + chat_stream, + sender_name, + 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 + is_group_chat = bool(chat_stream.group_info) + + # Use sender_name passed from caller for private chat, otherwise use a default for group + # Default sender_name for group chat isn't used in the group prompt template, but set for consistency + effective_sender_name = sender_name if not is_group_chat else "某人" + + message_list_before_now = get_raw_msg_before_timestamp_with_chat( + chat_id=chat_stream.stream_id, + timestamp=time.time(), + limit=global_config.observation_context_size, + ) + chat_talking_prompt = await build_readable_messages( + message_list_before_now, + replace_bot_name=True, + merge_messages=True, + timestamp_mode="relative", + read_mark=0.0, + truncate=True, + ) + + ( + learnt_style_expressions, + learnt_grammar_expressions, + personality_expressions, + ) = await expression_learner.get_expression_by_chat_id(chat_stream.stream_id) + + style_habbits = [] + grammar_habbits = [] + # 1. learnt_expressions加权随机选3条 + if learnt_style_expressions: + weights = [expr["count"] for expr in learnt_style_expressions] + selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 3) + for expr in selected_learnt: + if isinstance(expr, dict) and "situation" in expr and "style" in expr: + style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") + # 2. learnt_grammar_expressions加权随机选3条 + if learnt_grammar_expressions: + weights = [expr["count"] for expr in learnt_grammar_expressions] + selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 3) + for expr in selected_learnt: + if isinstance(expr, dict) and "situation" in expr and "style" in expr: + grammar_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") + # 3. personality_expressions随机选1条 + if personality_expressions: + expr = random.choice(personality_expressions) + if isinstance(expr, dict) and "situation" in expr and "style" in expr: + style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") + + style_habbits_str = "\n".join(style_habbits) + grammar_habbits_str = "\n".join(grammar_habbits) + + logger.debug("开始构建 focus prompt") + + # --- Choose template based on chat type --- + if is_group_chat: + template_name = "default_expressor_prompt" + # Group specific formatting variables (already fetched or default) + chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") + # chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") + + prompt = await global_prompt_manager.format_prompt( + template_name, + style_habbits=style_habbits_str, + grammar_habbits=grammar_habbits_str, + chat_target=chat_target_1, + chat_info=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality="", + reason=reason, + in_mind_reply=in_mind_reply, + target_message=target_message, + ) + else: # Private chat + template_name = "default_expressor_private_prompt" + prompt = await global_prompt_manager.format_prompt( + template_name, + sender_name=effective_sender_name, # Used in private template + chat_talking_prompt=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + reason=reason, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + + + return prompt + # --- 发送器 (Sender) --- # - async def _send_response_messages( - self, anchor_message: Optional[MessageRecv], response_set: List[Tuple[str, str]], thinking_id: str + async def send_response_messages( + self, anchor_message: Optional[MessageRecv], response_set: List[Tuple[str, str]], thinking_id: str = "" ) -> Optional[MessageSending]: """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender""" chat = self.chat_stream @@ -241,7 +399,11 @@ class DefaultExpressor: stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志 # 检查思考过程是否仍在进行,并获取开始时间 - thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id) + if thinking_id: + thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id) + else: + thinking_id = "ds"+ str(round(time.time(),2)) + thinking_start_time = time.time() if thinking_start_time is None: logger.error(f"[{stream_name}]思考过程未找到或已结束,无法发送回复。") @@ -274,6 +436,7 @@ class DefaultExpressor: reply_to=reply_to, is_emoji=is_emoji, thinking_id=thinking_id, + thinking_start_time=thinking_start_time, ) try: @@ -295,6 +458,7 @@ class DefaultExpressor: except Exception as e: logger.error(f"{self.log_prefix}发送回复片段 {i} ({part_message_id}) 时失败: {e}") + traceback.print_exc() # 这里可以选择是继续发送下一个片段还是中止 # 在尝试发送完所有片段后,完成原始的 thinking_id 状态 @@ -325,10 +489,10 @@ class DefaultExpressor: reply_to: bool, is_emoji: bool, thinking_id: str, + thinking_start_time: float, ) -> MessageSending: """构建单个发送消息""" - thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(self.chat_id, thinking_id) bot_user_info = UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, @@ -348,3 +512,40 @@ class DefaultExpressor: ) return bot_message + + + +def weighted_sample_no_replacement(items, weights, k) -> list: + """ + 加权且不放回地随机抽取k个元素。 + + 参数: + items: 待抽取的元素列表 + weights: 每个元素对应的权重(与items等长,且为正数) + k: 需要抽取的元素个数 + 返回: + selected: 按权重加权且不重复抽取的k个元素组成的列表 + + 如果 items 中的元素不足 k 个,就只会返回所有可用的元素 + + 实现思路: + 每次从当前池中按权重加权随机选出一个元素,选中后将其从池中移除,重复k次。 + 这样保证了: + 1. count越大被选中概率越高 + 2. 不会重复选中同一个元素 + """ + selected = [] + pool = list(zip(items, weights)) + for _ in range(min(k, len(pool))): + total = sum(w for _, w in pool) + r = random.uniform(0, total) + upto = 0 + for idx, (item, weight) in enumerate(pool): + upto += weight + if upto >= r: + selected.append(item) + pool.pop(idx) + break + return selected + +init_prompt() \ No newline at end of file diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index 4a28652d1..7a1671897 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -14,16 +14,17 @@ from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail from src.chat.focus_chat.info.info_base import InfoBase from src.chat.focus_chat.info_processors.chattinginfo_processor import ChattingInfoProcessor from src.chat.focus_chat.info_processors.mind_processor import MindProcessor -from src.chat.heart_flow.observation.memory_observation import MemoryObservation +from src.chat.focus_chat.info_processors.working_memory_processor import WorkingMemoryProcessor from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation -from src.chat.heart_flow.observation.working_observation import WorkingObservation +from src.chat.heart_flow.observation.working_observation import WorkingMemoryObservation +from src.chat.heart_flow.observation.chatting_observation import ChattingObservation from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor from src.chat.focus_chat.memory_activator import MemoryActivator from src.chat.focus_chat.info_processors.base_processor import BaseProcessor from src.chat.focus_chat.planners.planner import ActionPlanner -from src.chat.focus_chat.planners.action_factory import ActionManager - +from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.focus_chat.working_memory.working_memory import WorkingMemory install(extra_lines=3) @@ -57,7 +58,7 @@ async def _handle_cycle_delay(action_taken_this_cycle: bool, cycle_start_time: f class HeartFChatting: """ - 管理一个连续的Plan-Replier-Sender循环 + 管理一个连续的Focus Chat循环 用于在特定聊天流中生成回复。 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ @@ -79,19 +80,22 @@ class HeartFChatting: # 基础属性 self.stream_id: str = chat_id # 聊天流ID self.chat_stream: Optional[ChatStream] = None # 关联的聊天流 - self.observations: List[Observation] = observations # 关联的观察列表,用于监控聊天流状态 self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback self.log_prefix: str = str(chat_id) # Initial default, will be updated - - self.memory_observation = MemoryObservation(observe_id=self.stream_id) self.hfcloop_observation = HFCloopObservation(observe_id=self.stream_id) - self.working_observation = WorkingObservation(observe_id=self.stream_id) + self.chatting_observation = observations[0] + self.memory_activator = MemoryActivator() + self.working_memory = WorkingMemory(chat_id=self.stream_id) + self.working_observation = WorkingMemoryObservation(observe_id=self.stream_id, working_memory=self.working_memory) + self.expressor = DefaultExpressor(chat_id=self.stream_id) self.action_manager = ActionManager() self.action_planner = ActionPlanner(log_prefix=self.log_prefix, action_manager=self.action_manager) - - + + self.hfcloop_observation.set_action_manager(self.action_manager) + + self.all_observations = observations # --- 处理器列表 --- self.processors: List[BaseProcessor] = [] self._register_default_processors() @@ -108,9 +112,7 @@ class HeartFChatting: self._cycle_counter = 0 self._cycle_history: Deque[CycleDetail] = deque(maxlen=10) # 保留最近10个循环的信息 self._current_cycle: Optional[CycleDetail] = None - self.total_no_reply_count: int = 0 # 连续不回复计数器 self._shutting_down: bool = False # 关闭标志位 - self.total_waiting_time: float = 0.0 # 累计等待时间 async def _initialize(self) -> bool: """ @@ -151,6 +153,7 @@ class HeartFChatting: self.processors.append(ChattingInfoProcessor()) self.processors.append(MindProcessor(subheartflow_id=self.stream_id)) self.processors.append(ToolProcessor(subheartflow_id=self.stream_id)) + self.processors.append(WorkingMemoryProcessor(subheartflow_id=self.stream_id)) logger.info(f"{self.log_prefix} 已注册默认处理器: {[p.__class__.__name__ for p in self.processors]}") async def start(self): @@ -349,13 +352,12 @@ class HeartFChatting: async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> tuple[bool, str]: try: with Timer("观察", cycle_timers): - await self.observations[0].observe() - await self.memory_observation.observe() + # await self.observations[0].observe() + await self.chatting_observation.observe() await self.working_observation.observe() await self.hfcloop_observation.observe() observations: List[Observation] = [] - observations.append(self.observations[0]) - observations.append(self.memory_observation) + observations.append(self.chatting_observation) observations.append(self.working_observation) observations.append(self.hfcloop_observation) @@ -363,6 +365,8 @@ class HeartFChatting: "observations": observations, } + self.all_observations = observations + with Timer("回忆", cycle_timers): running_memorys = await self.memory_activator.activate_memory(observations) @@ -395,8 +399,7 @@ class HeartFChatting: elif action_type == "no_reply": action_str = "不回复" else: - action_type = "unknown" - action_str = "未知动作" + action_str = action_type logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'") @@ -452,14 +455,14 @@ class HeartFChatting: reasoning=reasoning, cycle_timers=cycle_timers, thinking_id=thinking_id, - observations=self.observations, + observations=self.all_observations, expressor=self.expressor, chat_stream=self.chat_stream, current_cycle=self._current_cycle, log_prefix=self.log_prefix, on_consecutive_no_reply_callback=self.on_consecutive_no_reply_callback, - total_no_reply_count=self.total_no_reply_count, - total_waiting_time=self.total_waiting_time, + # total_no_reply_count=self.total_no_reply_count, + # total_waiting_time=self.total_waiting_time, shutting_down=self._shutting_down, ) @@ -470,14 +473,6 @@ class HeartFChatting: # 处理动作并获取结果 success, reply_text = await action_handler.handle_action() - # 更新状态计数器 - if action == "no_reply": - self.total_no_reply_count = getattr(action_handler, "total_no_reply_count", self.total_no_reply_count) - self.total_waiting_time = getattr(action_handler, "total_waiting_time", self.total_waiting_time) - elif action == "reply": - self.total_no_reply_count = 0 - self.total_waiting_time = 0.0 - return success, reply_text except Exception as e: @@ -526,5 +521,3 @@ class HeartFChatting: if last_n is not None: history = history[-last_n:] return [cycle.to_dict() for cycle in history] - - diff --git a/src/chat/focus_chat/heartFC_sender.py b/src/chat/focus_chat/heartFC_sender.py index 057668579..81d463b02 100644 --- a/src/chat/focus_chat/heartFC_sender.py +++ b/src/chat/focus_chat/heartFC_sender.py @@ -106,6 +106,7 @@ class HeartFCSender: and not message.is_private_message() and message.reply.processed_plain_text != "[System Trigger Context]" ): + message.set_reply(message.reply) logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...") await message.process() diff --git a/src/chat/focus_chat/heartflow_prompt_builder.py b/src/chat/focus_chat/heartflow_prompt_builder.py index 55fb79b46..830a1cfad 100644 --- a/src/chat/focus_chat/heartflow_prompt_builder.py +++ b/src/chat/focus_chat/heartflow_prompt_builder.py @@ -6,14 +6,13 @@ from src.chat.utils.chat_message_builder import build_readable_messages, get_raw from src.chat.person_info.relationship_manager import relationship_manager from src.chat.utils.utils import get_embedding import time -from typing import Union, Optional, Dict, Any +from typing import Union, Optional from src.common.database import db from src.chat.utils.utils import get_recent_group_speaker from src.manager.mood_manager import mood_manager from src.chat.memory_system.Hippocampus import HippocampusManager from src.chat.knowledge.knowledge_lib import qa_manager from src.chat.focus_chat.expressors.exprssion_learner import expression_learner -import traceback import random @@ -21,27 +20,6 @@ logger = get_logger("prompt") def init_prompt(): - Prompt( - """ -你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中: -{style_habbits} - -你现在正在群里聊天,以下是群里正在进行的聊天内容: -{chat_info} - -以上是聊天内容,你需要了解聊天记录中的内容 - -{chat_target} -你的名字是{bot_name},{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复 -你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。 -请你根据情景使用以下句法: -{grammar_habbits} -回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短,但重组后保持语意通顺。 -回复不要浮夸,不要用夸张修辞,平淡一些。不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。 -现在,你说: -""", - "heart_flow_prompt", - ) Prompt( """ @@ -82,29 +60,6 @@ def init_prompt(): Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") - # --- Template for HeartFChatting (FOCUSED mode) --- - Prompt( - """ -{info_from_tools} -你正在和 {sender_name} 私聊。 -聊天记录如下: -{chat_talking_prompt} -现在你想要回复。 - -你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"。 -你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,然后给出日常且口语化的回复,平淡一些。 -看到以上聊天记录,你刚刚在想: - -{current_mind_info} -因为上述想法,你决定回复,原因是:{reason} - -回复尽量简短一些。请注意把握聊天内容,{reply_style2}。{prompt_ger},不要复读自己说的话 -{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容。 -{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", - "heart_flow_private_prompt", # New template for private FOCUSED chat - ) - - # --- Template for NormalChat (CHAT mode) --- Prompt( """ {memory_prompt} @@ -126,118 +81,6 @@ def init_prompt(): ) -async def _build_prompt_focus( - reason, current_mind_info, structured_info, chat_stream, sender_name, 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 - is_group_chat = bool(chat_stream.group_info) - - # Use sender_name passed from caller for private chat, otherwise use a default for group - # Default sender_name for group chat isn't used in the group prompt template, but set for consistency - effective_sender_name = sender_name if not is_group_chat else "某人" - - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=chat_stream.stream_id, - timestamp=time.time(), - limit=global_config.observation_context_size, - ) - chat_talking_prompt = await build_readable_messages( - message_list_before_now, - replace_bot_name=True, - merge_messages=True, - timestamp_mode="relative", - read_mark=0.0, - truncate=True, - ) - - if structured_info: - structured_info_prompt = await global_prompt_manager.format_prompt( - "info_from_tools", structured_info=structured_info - ) - else: - structured_info_prompt = "" - - # 从/data/expression/对应chat_id/expressions.json中读取表达方式 - ( - learnt_style_expressions, - learnt_grammar_expressions, - personality_expressions, - ) = await expression_learner.get_expression_by_chat_id(chat_stream.stream_id) - - style_habbits = [] - grammar_habbits = [] - # 1. learnt_expressions加权随机选3条 - if learnt_style_expressions: - weights = [expr["count"] for expr in learnt_style_expressions] - selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 3) - for expr in selected_learnt: - if isinstance(expr, dict) and "situation" in expr and "style" in expr: - style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") - # 2. learnt_grammar_expressions加权随机选3条 - if learnt_grammar_expressions: - weights = [expr["count"] for expr in learnt_grammar_expressions] - selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 3) - for expr in selected_learnt: - if isinstance(expr, dict) and "situation" in expr and "style" in expr: - grammar_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") - # 3. personality_expressions随机选1条 - if personality_expressions: - expr = random.choice(personality_expressions) - if isinstance(expr, dict) and "situation" in expr and "style" in expr: - style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") - - style_habbits_str = "\n".join(style_habbits) - grammar_habbits_str = "\n".join(grammar_habbits) - - logger.debug("开始构建 focus prompt") - - # --- Choose template based on chat type --- - if is_group_chat: - template_name = "heart_flow_prompt" - # Group specific formatting variables (already fetched or default) - chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") - # chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - - prompt = await global_prompt_manager.format_prompt( - template_name, - # info_from_tools=structured_info_prompt, - style_habbits=style_habbits_str, - grammar_habbits=grammar_habbits_str, - chat_target=chat_target_1, # Used in group template - # chat_talking_prompt=chat_talking_prompt, - chat_info=chat_talking_prompt, - bot_name=global_config.BOT_NICKNAME, - # prompt_personality=prompt_personality, - prompt_personality="", - reason=reason, - in_mind_reply=in_mind_reply, - target_message=target_message, - # moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - # sender_name is not used in the group template - ) - else: # Private chat - template_name = "heart_flow_private_prompt" - prompt = await global_prompt_manager.format_prompt( - template_name, - info_from_tools=structured_info_prompt, - sender_name=effective_sender_name, # Used in private template - chat_talking_prompt=chat_talking_prompt, - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - # chat_target and chat_target_2 are not used in private template - current_mind_info=current_mind_info, - reason=reason, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - ) - # --- End choosing template --- - - # logger.debug(f"focus_chat_prompt (is_group={is_group_chat}): \n{prompt}") - return prompt - - class PromptBuilder: def __init__(self): self.prompt_built = "" @@ -257,17 +100,6 @@ class PromptBuilder: ) -> Optional[str]: if build_mode == "normal": return await self._build_prompt_normal(chat_stream, message_txt or "", sender_name) - - elif build_mode == "focus": - return await _build_prompt_focus( - reason, - current_mind_info, - structured_info, - chat_stream, - sender_name, - in_mind_reply, - target_message, - ) return None async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str: @@ -689,40 +521,5 @@ class PromptBuilder: # 返回所有找到的内容,用换行分隔 return "\n".join(str(result["content"]) for result in results) - -def weighted_sample_no_replacement(items, weights, k) -> list: - """ - 加权且不放回地随机抽取k个元素。 - - 参数: - items: 待抽取的元素列表 - weights: 每个元素对应的权重(与items等长,且为正数) - k: 需要抽取的元素个数 - 返回: - selected: 按权重加权且不重复抽取的k个元素组成的列表 - - 如果 items 中的元素不足 k 个,就只会返回所有可用的元素 - - 实现思路: - 每次从当前池中按权重加权随机选出一个元素,选中后将其从池中移除,重复k次。 - 这样保证了: - 1. count越大被选中概率越高 - 2. 不会重复选中同一个元素 - """ - selected = [] - pool = list(zip(items, weights)) - for _ in range(min(k, len(pool))): - total = sum(w for _, w in pool) - r = random.uniform(0, total) - upto = 0 - for idx, (item, weight) in enumerate(pool): - upto += weight - if upto >= r: - selected.append(item) - pool.pop(idx) - break - return selected - - init_prompt() prompt_builder = PromptBuilder() diff --git a/src/chat/focus_chat/info/info_base.py b/src/chat/focus_chat/info/info_base.py index 7779d913a..fbf060ba6 100644 --- a/src/chat/focus_chat/info/info_base.py +++ b/src/chat/focus_chat/info/info_base.py @@ -17,6 +17,7 @@ class InfoBase: type: str = "base" data: Dict[str, Any] = field(default_factory=dict) + processed_info:str = "" def get_type(self) -> str: """获取信息类型 @@ -58,3 +59,11 @@ class InfoBase: if isinstance(value, list): return value return [] + + def get_processed_info(self) -> str: + """获取处理后的信息 + + Returns: + str: 处理后的信息字符串 + """ + return self.processed_info diff --git a/src/chat/focus_chat/info_processors/chattinginfo_processor.py b/src/chat/focus_chat/info_processors/chattinginfo_processor.py index 12bc8560a..0accc2a34 100644 --- a/src/chat/focus_chat/info_processors/chattinginfo_processor.py +++ b/src/chat/focus_chat/info_processors/chattinginfo_processor.py @@ -54,6 +54,8 @@ class ChattingInfoProcessor(BaseProcessor): for obs in observations: # print(f"obs: {obs}") if isinstance(obs, ChattingObservation): + # print("1111111111111111111111读取111111111111111") + obs_info = ObsInfo() await self.chat_compress(obs) diff --git a/src/chat/focus_chat/info_processors/mind_processor.py b/src/chat/focus_chat/info_processors/mind_processor.py index 1a104e123..95233a9f7 100644 --- a/src/chat/focus_chat/info_processors/mind_processor.py +++ b/src/chat/focus_chat/info_processors/mind_processor.py @@ -16,11 +16,6 @@ from .base_processor import BaseProcessor from src.chat.focus_chat.info.mind_info import MindInfo from typing import List, Optional from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation -from src.chat.focus_chat.info_processors.processor_utils import ( - calculate_similarity, - calculate_replacement_probability, - get_spark, -) from typing import Dict from src.chat.focus_chat.info.info_base import InfoBase @@ -28,7 +23,6 @@ logger = get_logger("processor") def init_prompt(): - # --- Group Chat Prompt --- group_prompt = """ 你的名字是{bot_name} {memory_str} @@ -44,31 +38,29 @@ def init_prompt(): 现在请你继续输出观察和规划,输出要求: 1. 先关注未读新消息的内容和近期回复历史 2. 根据新信息,修改和删除之前的观察和规划 -3. 根据聊天内容继续输出观察和规划,{hf_do_next} +3. 根据聊天内容继续输出观察和规划 4. 注意群聊的时间线索,话题由谁发起,进展状况如何,思考聊天的时间线。 6. 语言简洁自然,不要分点,不要浮夸,不要修辞,仅输出思考内容就好""" Prompt(group_prompt, "sub_heartflow_prompt_before") - # --- Private Chat Prompt --- private_prompt = """ +你的名字是{bot_name} {memory_str} {extra_info} {relation_prompt} -你的名字是{bot_name},{prompt_personality},你现在{mood_info} {cycle_info_block} -现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容: +现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: {chat_observe_info} -以下是你之前对聊天的观察和规划: + +以下是你之前对聊天的观察和规划,你的名字是{bot_name}: {last_mind} -请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 -请思考你要不要回复以及如何回复对方。 -思考并输出你的内心想法 -输出要求: -1. 根据聊天内容生成你的想法,{hf_do_next} -2. 不要分点、不要使用表情符号 -3. 避免多余符号(冒号、引号、括号等) -4. 语言简洁自然,不要浮夸 -5. 如果你刚发言,对方没有回复你,请谨慎回复""" + +现在请你继续输出观察和规划,输出要求: +1. 先关注未读新消息的内容和近期回复历史 +2. 根据新信息,修改和删除之前的观察和规划 +3. 根据聊天内容继续输出观察和规划 +4. 注意群聊的时间线索,话题由谁发起,进展状况如何,思考聊天的时间线。 +6. 语言简洁自然,不要分点,不要浮夸,不要修辞,仅输出思考内容就好""" Prompt(private_prompt, "sub_heartflow_prompt_private_before") @@ -210,45 +202,28 @@ class MindProcessor(BaseProcessor): for person in person_list: relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True) - # 构建个性部分 - # prompt_personality = individuality.get_prompt(x_person=2, level=2) - # 获取当前时间 - time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - - spark_prompt = get_spark() - - # ---------- 5. 构建最终提示词 ---------- template_name = "sub_heartflow_prompt_before" if is_group_chat else "sub_heartflow_prompt_private_before" logger.debug(f"{self.log_prefix} 使用{'群聊' if is_group_chat else '私聊'}思考模板") prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( + bot_name=individuality.name, memory_str=memory_str, extra_info=self.structured_info_str, - # prompt_personality=prompt_personality, relation_prompt=relation_prompt, - bot_name=individuality.name, - time_now=time_now, + time_now=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), chat_observe_info=chat_observe_info, - # mood_info="mood_info", - hf_do_next=spark_prompt, last_mind=previous_mind, cycle_info_block=hfcloop_observe_info, chat_target_name=chat_target_name, ) - # 在构建完提示词后,生成最终的prompt字符串 - final_prompt = prompt - - content = "" # 初始化内容变量 + content = "(不知道该想些什么...)" try: - # 调用LLM生成响应 - response, _ = await self.llm_model.generate_response_async(prompt=final_prompt) - - # 直接使用LLM返回的文本响应作为 content - content = response if response else "" - + content, _ = await self.llm_model.generate_response_async(prompt=prompt) + if not content: + logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") except Exception as e: # 处理总体异常 logger.error(f"{self.log_prefix} 执行LLM请求或处理响应时出错: {e}") @@ -256,16 +231,8 @@ class MindProcessor(BaseProcessor): content = "思考过程中出现错误" # 记录初步思考结果 - logger.debug(f"{self.log_prefix} 思考prompt: \n{final_prompt}\n") - - # 处理空响应情况 - if not content: - content = "(不知道该想些什么...)" - logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") - - # ---------- 8. 更新思考状态并返回结果 ---------- + logger.debug(f"{self.log_prefix} 思考prompt: \n{prompt}\n") logger.info(f"{self.log_prefix} 思考结果: {content}") - # 更新当前思考内容 self.update_current_mind(content) return content @@ -275,138 +242,5 @@ class MindProcessor(BaseProcessor): self.past_mind.append(self.current_mind) self.current_mind = response - def de_similar(self, previous_mind, new_content): - try: - similarity = calculate_similarity(previous_mind, new_content) - replacement_prob = calculate_replacement_probability(similarity) - logger.debug(f"{self.log_prefix} 新旧想法相似度: {similarity:.2f}, 替换概率: {replacement_prob:.2f}") - - # 定义词语列表 (移到判断之前) - yu_qi_ci_liebiao = ["嗯", "哦", "啊", "唉", "哈", "唔"] - zhuan_zhe_liebiao = ["但是", "不过", "然而", "可是", "只是"] - cheng_jie_liebiao = ["然后", "接着", "此外", "而且", "另外"] - zhuan_jie_ci_liebiao = zhuan_zhe_liebiao + cheng_jie_liebiao - - if random.random() < replacement_prob: - # 相似度非常高时,尝试去重或特殊处理 - if similarity == 1.0: - logger.debug(f"{self.log_prefix} 想法完全重复 (相似度 1.0),执行特殊处理...") - # 随机截取大约一半内容 - if len(new_content) > 1: # 避免内容过短无法截取 - split_point = max( - 1, len(new_content) // 2 + random.randint(-len(new_content) // 4, len(new_content) // 4) - ) - truncated_content = new_content[:split_point] - else: - truncated_content = new_content # 如果只有一个字符或者为空,就不截取了 - - # 添加语气词和转折/承接词 - yu_qi_ci = random.choice(yu_qi_ci_liebiao) - zhuan_jie_ci = random.choice(zhuan_jie_ci_liebiao) - content = f"{yu_qi_ci}{zhuan_jie_ci},{truncated_content}" - logger.debug(f"{self.log_prefix} 想法重复,特殊处理后: {content}") - - else: - # 相似度较高但非100%,执行标准去重逻辑 - logger.debug(f"{self.log_prefix} 执行概率性去重 (概率: {replacement_prob:.2f})...") - logger.debug( - f"{self.log_prefix} previous_mind类型: {type(previous_mind)}, new_content类型: {type(new_content)}" - ) - - matcher = difflib.SequenceMatcher(None, previous_mind, new_content) - logger.debug(f"{self.log_prefix} matcher类型: {type(matcher)}") - - deduplicated_parts = [] - last_match_end_in_b = 0 - - # 获取并记录所有匹配块 - matching_blocks = matcher.get_matching_blocks() - logger.debug(f"{self.log_prefix} 匹配块数量: {len(matching_blocks)}") - logger.debug( - f"{self.log_prefix} 匹配块示例(前3个): {matching_blocks[:3] if len(matching_blocks) > 3 else matching_blocks}" - ) - - # get_matching_blocks()返回形如[(i, j, n), ...]的列表,其中i是a中的索引,j是b中的索引,n是匹配的长度 - for idx, match in enumerate(matching_blocks): - if not isinstance(match, tuple): - logger.error(f"{self.log_prefix} 匹配块 {idx} 不是元组类型,而是 {type(match)}: {match}") - continue - - try: - _i, j, n = match # 解包元组为三个变量 - logger.debug(f"{self.log_prefix} 匹配块 {idx}: i={_i}, j={j}, n={n}") - - if last_match_end_in_b < j: - # 确保添加的是字符串,而不是元组 - try: - non_matching_part = new_content[last_match_end_in_b:j] - logger.debug( - f"{self.log_prefix} 添加非匹配部分: '{non_matching_part}', 类型: {type(non_matching_part)}" - ) - if not isinstance(non_matching_part, str): - logger.warning( - f"{self.log_prefix} 非匹配部分不是字符串类型: {type(non_matching_part)}" - ) - non_matching_part = str(non_matching_part) - deduplicated_parts.append(non_matching_part) - except Exception as e: - logger.error(f"{self.log_prefix} 处理非匹配部分时出错: {e}") - logger.error(traceback.format_exc()) - last_match_end_in_b = j + n - except Exception as e: - logger.error(f"{self.log_prefix} 处理匹配块时出错: {e}") - logger.error(traceback.format_exc()) - - logger.debug(f"{self.log_prefix} 去重前部分列表: {deduplicated_parts}") - logger.debug(f"{self.log_prefix} 列表元素类型: {[type(part) for part in deduplicated_parts]}") - - # 确保所有元素都是字符串 - deduplicated_parts = [str(part) for part in deduplicated_parts] - - # 防止列表为空 - if not deduplicated_parts: - logger.warning(f"{self.log_prefix} 去重后列表为空,添加空字符串") - deduplicated_parts = [""] - - logger.debug(f"{self.log_prefix} 处理后的部分列表: {deduplicated_parts}") - - try: - deduplicated_content = "".join(deduplicated_parts).strip() - logger.debug(f"{self.log_prefix} 拼接后的去重内容: '{deduplicated_content}'") - except Exception as e: - logger.error(f"{self.log_prefix} 拼接去重内容时出错: {e}") - logger.error(traceback.format_exc()) - deduplicated_content = "" - - if deduplicated_content: - # 根据概率决定是否添加词语 - prefix_str = "" - if random.random() < 0.3: # 30% 概率添加语气词 - prefix_str += random.choice(yu_qi_ci_liebiao) - if random.random() < 0.7: # 70% 概率添加转折/承接词 - prefix_str += random.choice(zhuan_jie_ci_liebiao) - - # 组合最终结果 - if prefix_str: - content = f"{prefix_str},{deduplicated_content}" # 更新 content - logger.debug(f"{self.log_prefix} 去重并添加引导词后: {content}") - else: - content = deduplicated_content # 更新 content - logger.debug(f"{self.log_prefix} 去重后 (未添加引导词): {content}") - else: - logger.warning(f"{self.log_prefix} 去重后内容为空,保留原始LLM输出: {new_content}") - content = new_content # 保留原始 content - else: - logger.debug(f"{self.log_prefix} 未执行概率性去重 (概率: {replacement_prob:.2f})") - # content 保持 new_content 不变 - - except Exception as e: - logger.error(f"{self.log_prefix} 应用概率性去重或特殊处理时出错: {e}") - logger.error(traceback.format_exc()) - # 出错时保留原始 content - content = new_content - - return content - init_prompt() diff --git a/src/chat/focus_chat/info_processors/processor_utils.py b/src/chat/focus_chat/info_processors/processor_utils.py deleted file mode 100644 index 77cdc7a6b..000000000 --- a/src/chat/focus_chat/info_processors/processor_utils.py +++ /dev/null @@ -1,56 +0,0 @@ -import difflib -import random -import time - - -def calculate_similarity(text_a: str, text_b: str) -> float: - """ - 计算两个文本字符串的相似度。 - """ - if not text_a or not text_b: - return 0.0 - matcher = difflib.SequenceMatcher(None, text_a, text_b) - return matcher.ratio() - - -def calculate_replacement_probability(similarity: float) -> float: - """ - 根据相似度计算替换的概率。 - 规则: - - 相似度 <= 0.4: 概率 = 0 - - 相似度 >= 0.9: 概率 = 1 - - 相似度 == 0.6: 概率 = 0.7 - - 0.4 < 相似度 <= 0.6: 线性插值 (0.4, 0) 到 (0.6, 0.7) - - 0.6 < 相似度 < 0.9: 线性插值 (0.6, 0.7) 到 (0.9, 1.0) - """ - if similarity <= 0.4: - return 0.0 - elif similarity >= 0.9: - return 1.0 - elif 0.4 < similarity <= 0.6: - # p = 3.5 * s - 1.4 - probability = 3.5 * similarity - 1.4 - return max(0.0, probability) - else: # 0.6 < similarity < 0.9 - # p = s + 0.1 - probability = similarity + 0.1 - return min(1.0, max(0.0, probability)) - - -def get_spark(): - local_random = random.Random() - current_minute = int(time.strftime("%M")) - local_random.seed(current_minute) - - hf_options = [ - ("可以参考之前的想法,在原来想法的基础上继续思考", 0.2), - ("可以参考之前的想法,在原来的想法上尝试新的话题", 0.4), - ("不要太深入", 0.2), - ("进行深入思考", 0.2), - ] - # 加权随机选择思考指导 - hf_do_next = local_random.choices( - [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 - )[0] - - return hf_do_next diff --git a/src/chat/focus_chat/info_processors/tool_processor.py b/src/chat/focus_chat/info_processors/tool_processor.py index 8840c1ae4..39e0c293c 100644 --- a/src/chat/focus_chat/info_processors/tool_processor.py +++ b/src/chat/focus_chat/info_processors/tool_processor.py @@ -155,7 +155,7 @@ class ToolProcessor(BaseProcessor): ) # 调用LLM,专注于工具使用 - logger.debug(f"开始执行工具调用{prompt}") + # logger.debug(f"开始执行工具调用{prompt}") response, _, tool_calls = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools) logger.debug(f"获取到工具原始输出:\n{tool_calls}") diff --git a/src/chat/focus_chat/planners/action_factory.py b/src/chat/focus_chat/planners/action_manager.py similarity index 75% rename from src/chat/focus_chat/planners/action_factory.py rename to src/chat/focus_chat/planners/action_manager.py index 257156a25..72ff4a73e 100644 --- a/src/chat/focus_chat/planners/action_factory.py +++ b/src/chat/focus_chat/planners/action_manager.py @@ -1,18 +1,18 @@ -from typing import Dict, List, Optional, Callable, Coroutine, Type, Any, Union -import os -import importlib -from src.chat.focus_chat.planners.actions.base_action import BaseAction, _ACTION_REGISTRY, _DEFAULT_ACTIONS +from typing import Dict, List, Optional, Callable, Coroutine, 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 +import os # 导入动作类,确保装饰器被执行 -from src.chat.focus_chat.planners.actions.reply_action import ReplyAction -from src.chat.focus_chat.planners.actions.no_reply_action import NoReplyAction +import src.chat.focus_chat.planners.actions # noqa -logger = get_logger("action_factory") +logger = get_logger("action_manager") # 定义动作信息类型 ActionInfo = Dict[str, Any] @@ -31,20 +31,18 @@ class ActionManager: self._using_actions: Dict[str, ActionInfo] = {} # 临时备份原始使用中的动作 self._original_actions_backup: Optional[Dict[str, ActionInfo]] = None - + # 默认动作集,仅作为快照,用于恢复默认 self._default_actions: Dict[str, ActionInfo] = {} - + # 加载所有已注册动作 self._load_registered_actions() + # 加载插件动作 + self._load_plugin_actions() + # 初始化时将默认动作加载到使用中的动作 self._using_actions = self._default_actions.copy() - - # logger.info(f"当前可用动作: {list(self._using_actions.keys())}") - # for action_name, action_info in self._using_actions.items(): - # logger.info(f"动作名称: {action_name}, 动作信息: {action_info}") - def _load_registered_actions(self) -> None: """ @@ -54,37 +52,78 @@ class ActionManager: # 从_ACTION_REGISTRY获取所有已注册动作 for action_name, action_class in _ACTION_REGISTRY.items(): # 获取动作相关信息 - action_description:str = getattr(action_class, "action_description", "") - action_parameters:dict[str:str] = getattr(action_class, "action_parameters", {}) - action_require:list[str] = getattr(action_class, "action_require", []) - is_default:bool = getattr(action_class, "default", False) + # 不读取插件动作和基类 + if action_name == "base_action" or action_name == "plugin_action": + continue + + action_description: str = getattr(action_class, "action_description", "") + action_parameters: dict[str:str] = getattr(action_class, "action_parameters", {}) + action_require: list[str] = getattr(action_class, "action_require", []) + is_default: bool = getattr(action_class, "default", False) + if action_name and action_description: # 创建动作信息字典 action_info = { "description": action_description, "parameters": action_parameters, - "require": action_require + "require": action_require, } - - # 注册2 - print("注册2") - print(action_info) - + # 添加到所有已注册的动作 self._registered_actions[action_name] = action_info - + # 添加到默认动作(如果是默认动作) if is_default: self._default_actions[action_name] = action_info - + logger.info(f"所有注册动作: {list(self._registered_actions.keys())}") logger.info(f"默认动作: {list(self._default_actions.keys())}") - # for action_name, action_info in self._default_actions.items(): - # logger.info(f"动作名称: {action_name}, 动作信息: {action_info}") - + for action_name, action_info in self._default_actions.items(): + logger.info(f"动作名称: {action_name}, 动作信息: {action_info}") + except Exception as e: logger.error(f"加载已注册动作失败: {e}") + + def _load_plugin_actions(self) -> None: + """ + 加载所有插件目录中的动作 + """ + try: + # 检查插件目录是否存在 + plugin_path = "src.plugins" + plugin_dir = plugin_path.replace('.', os.path.sep) + if not os.path.exists(plugin_dir): + logger.info(f"插件目录 {plugin_dir} 不存在,跳过插件动作加载") + return + + # 导入插件包 + try: + plugins_package = importlib.import_module(plugin_path) + except ImportError as e: + logger.error(f"导入插件包失败: {e}") + return + + # 遍历插件包中的所有子包 + for _, plugin_name, is_pkg in pkgutil.iter_modules(plugins_package.__path__, plugins_package.__name__ + '.'): + if not is_pkg: + continue + + # 检查插件是否有actions子包 + plugin_actions_path = f"{plugin_name}.actions" + try: + # 尝试导入插件的actions包 + importlib.import_module(plugin_actions_path) + logger.info(f"成功加载插件动作模块: {plugin_actions_path}") + except ImportError as e: + logger.debug(f"插件 {plugin_name} 没有actions子包或导入失败: {e}") + continue + + # 再次从_ACTION_REGISTRY获取所有动作(包括刚刚从插件加载的) + self._load_registered_actions() + + except Exception as e: + logger.error(f"加载插件动作失败: {e}") def create_action( self, @@ -99,8 +138,8 @@ class ActionManager: current_cycle: CycleDetail, log_prefix: str, on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]], - total_no_reply_count: int = 0, - total_waiting_time: float = 0.0, + # total_no_reply_count: int = 0, + # total_waiting_time: float = 0.0, shutting_down: bool = False, ) -> Optional[BaseAction]: """ @@ -129,14 +168,14 @@ class ActionManager: if action_name not in self._using_actions: logger.warning(f"当前不可用的动作类型: {action_name}") return None - + handler_class = _ACTION_REGISTRY.get(action_name) if not handler_class: logger.warning(f"未注册的动作类型: {action_name}") return None try: - # 创建动作实例并传递所有必要参数 + # 创建动作实例 instance = handler_class( action_name=action_name, action_data=action_data, @@ -144,16 +183,16 @@ class ActionManager: cycle_timers=cycle_timers, thinking_id=thinking_id, observations=observations, - on_consecutive_no_reply_callback=on_consecutive_no_reply_callback, - current_cycle=current_cycle, - log_prefix=log_prefix, - total_no_reply_count=total_no_reply_count, - total_waiting_time=total_waiting_time, - shutting_down=shutting_down, expressor=expressor, chat_stream=chat_stream, + current_cycle=current_cycle, + log_prefix=log_prefix, + on_consecutive_no_reply_callback=on_consecutive_no_reply_callback, + # total_no_reply_count=total_no_reply_count, + # total_waiting_time=total_waiting_time, + shutting_down=shutting_down, ) - + return instance except Exception as e: @@ -167,7 +206,7 @@ class ActionManager: def get_default_actions(self) -> Dict[str, ActionInfo]: """获取默认动作集""" return self._default_actions.copy() - + def get_using_actions(self) -> Dict[str, ActionInfo]: """获取当前正在使用的动作集""" return self._using_actions.copy() @@ -175,21 +214,21 @@ class ActionManager: def add_action_to_using(self, action_name: str) -> bool: """ 添加已注册的动作到当前使用的动作集 - + Args: action_name: 动作名称 - + Returns: bool: 添加是否成功 """ if action_name not in self._registered_actions: logger.warning(f"添加失败: 动作 {action_name} 未注册") return False - + if action_name in self._using_actions: logger.info(f"动作 {action_name} 已经在使用中") return True - + self._using_actions[action_name] = self._registered_actions[action_name] logger.info(f"添加动作 {action_name} 到使用集") return True @@ -197,17 +236,17 @@ class ActionManager: def remove_action_from_using(self, action_name: str) -> bool: """ 从当前使用的动作集中移除指定动作 - + Args: action_name: 动作名称 - + Returns: bool: 移除是否成功 """ if action_name not in self._using_actions: logger.warning(f"移除失败: 动作 {action_name} 不在当前使用的动作集中") return False - + del self._using_actions[action_name] logger.info(f"已从使用集中移除动作 {action_name}") return True @@ -215,30 +254,26 @@ class ActionManager: def add_action(self, action_name: str, description: str, parameters: Dict = None, require: List = None) -> bool: """ 添加新的动作到注册集 - + Args: action_name: 动作名称 description: 动作描述 parameters: 动作参数定义,默认为空字典 require: 动作依赖项,默认为空列表 - + Returns: bool: 添加是否成功 """ if action_name in self._registered_actions: return False - + if parameters is None: parameters = {} if require is None: require = [] - - action_info = { - "description": description, - "parameters": parameters, - "require": require - } - + + action_info = {"description": description, "parameters": parameters, "require": require} + self._registered_actions[action_name] = action_info return True @@ -264,7 +299,7 @@ class ActionManager: if self._original_actions_backup is not None: self._using_actions = self._original_actions_backup.copy() self._original_actions_backup = None - + def restore_default_actions(self) -> None: """恢复默认动作集到使用集""" self._using_actions = self._default_actions.copy() @@ -273,15 +308,12 @@ class ActionManager: def get_action(self, action_name: str) -> Optional[Type[BaseAction]]: """ 获取指定动作的处理器类 - + Args: action_name: 动作名称 - + Returns: Optional[Type[BaseAction]]: 动作处理器类,如果不存在则返回None """ return _ACTION_REGISTRY.get(action_name) - -# 创建全局实例 -ActionFactory = ActionManager() diff --git a/src/chat/focus_chat/planners/actions/__init__.py b/src/chat/focus_chat/planners/actions/__init__.py new file mode 100644 index 000000000..435d0d4b4 --- /dev/null +++ b/src/chat/focus_chat/planners/actions/__init__.py @@ -0,0 +1,5 @@ +# 导入所有动作模块以确保装饰器被执行 +from . import reply_action # noqa +from . import no_reply_action # noqa + +# 在此处添加更多动作模块导入 \ No newline at end of file diff --git a/src/chat/focus_chat/planners/actions/base_action.py b/src/chat/focus_chat/planners/actions/base_action.py index 7c77c300c..82d259677 100644 --- a/src/chat/focus_chat/planners/actions/base_action.py +++ b/src/chat/focus_chat/planners/actions/base_action.py @@ -12,7 +12,7 @@ _DEFAULT_ACTIONS: Dict[str, str] = {} def register_action(cls): """ 动作注册装饰器 - + 用法: @register_action class MyAction(BaseAction): @@ -24,22 +24,22 @@ def register_action(cls): if not hasattr(cls, "action_name") or not hasattr(cls, "action_description"): logger.error(f"动作类 {cls.__name__} 缺少必要的属性: action_name 或 action_description") return cls - - action_name = getattr(cls, "action_name") - action_description = getattr(cls, "action_description") + + action_name = cls.action_name + action_description = cls.action_description is_default = getattr(cls, "default", False) - + if not action_name or not action_description: logger.error(f"动作类 {cls.__name__} 的 action_name 或 action_description 为空") return cls - + # 将动作类注册到全局注册表 _ACTION_REGISTRY[action_name] = cls - + # 如果是默认动作,添加到默认动作集 if is_default: _DEFAULT_ACTIONS[action_name] = action_description - + logger.info(f"已注册动作: {action_name} -> {cls.__name__},默认: {is_default}") return cls @@ -60,15 +60,14 @@ class BaseAction(ABC): cycle_timers: 计时器字典 thinking_id: 思考ID """ - #每个动作必须实现 - self.action_name:str = "base_action" - self.action_description:str = "基础动作" - self.action_parameters:dict = {} - self.action_require:list[str] = [] - - self.default:bool = False - - + # 每个动作必须实现 + self.action_name: str = "base_action" + self.action_description: str = "基础动作" + self.action_parameters: dict = {} + self.action_require: list[str] = [] + + self.default: bool = False + self.action_data = action_data self.reasoning = reasoning self.cycle_timers = cycle_timers 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 a29812c7a..406ddbdc2 100644 --- a/src/chat/focus_chat/planners/actions/no_reply_action.py +++ b/src/chat/focus_chat/planners/actions/no_reply_action.py @@ -29,7 +29,7 @@ class NoReplyAction(BaseAction): action_require = [ "话题无关/无聊/不感兴趣/不懂", "最后一条消息是你自己发的且无人回应你", - "你发送了太多消息,且无人回复" + "你发送了太多消息,且无人回复", ] default = True @@ -43,10 +43,10 @@ class NoReplyAction(BaseAction): on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]], current_cycle: CycleDetail, log_prefix: str, - total_no_reply_count: int = 0, - total_waiting_time: float = 0.0, + # total_no_reply_count: int = 0, + # total_waiting_time: float = 0.0, shutting_down: bool = False, - **kwargs + **kwargs, ): """初始化不回复动作处理器 @@ -69,8 +69,8 @@ class NoReplyAction(BaseAction): self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback self._current_cycle = current_cycle self.log_prefix = log_prefix - self.total_no_reply_count = total_no_reply_count - self.total_waiting_time = total_waiting_time + # self.total_no_reply_count = total_no_reply_count + # self.total_waiting_time = total_waiting_time self._shutting_down = shutting_down async def handle_action(self) -> Tuple[bool, str]: @@ -96,34 +96,6 @@ class NoReplyAction(BaseAction): # 从计时器获取实际等待时间 current_waiting = self.cycle_timers.get("等待新消息", 0.0) - if not self._shutting_down: - self.total_no_reply_count += 1 - self.total_waiting_time += current_waiting # 累加等待时间 - logger.debug( - f"{self.log_prefix} 连续不回复计数增加: {self.total_no_reply_count}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, " - f"本次等待: {current_waiting:.2f}秒, 累计等待: {self.total_waiting_time:.2f}秒" - ) - - # 检查是否同时达到次数和时间阈值 - time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD - if ( - self.total_no_reply_count >= CONSECUTIVE_NO_REPLY_THRESHOLD - and self.total_waiting_time >= time_threshold - ): - logger.info( - f"{self.log_prefix} 连续不回复达到阈值 ({self.total_no_reply_count}次) " - f"且累计等待时间达到 {self.total_waiting_time:.2f}秒 (阈值 {time_threshold}秒)," - f"调用回调请求状态转换" - ) - # 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。 - await self.on_consecutive_no_reply_callback() - elif self.total_no_reply_count >= CONSECUTIVE_NO_REPLY_THRESHOLD: - # 仅次数达到阈值,但时间未达到 - logger.debug( - f"{self.log_prefix} 连续不回复次数达到阈值 ({self.total_no_reply_count}次) " - f"但累计等待时间 {self.total_waiting_time:.2f}秒 未达到时间阈值 ({time_threshold}秒),暂不调用回调" - ) - # else: 次数和时间都未达到阈值,不做处理 return True, "" # 不回复动作没有回复文本 diff --git a/src/chat/focus_chat/planners/actions/plugin_action.py b/src/chat/focus_chat/planners/actions/plugin_action.py new file mode 100644 index 000000000..aec879e97 --- /dev/null +++ b/src/chat/focus_chat/planners/actions/plugin_action.py @@ -0,0 +1,215 @@ +import traceback +from typing import Tuple, Dict, List, Any, Optional +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action +from src.chat.heart_flow.observation.chatting_observation import ChattingObservation +from src.chat.focus_chat.hfc_utils import create_empty_anchor_message +from src.common.logger_manager import get_logger +from src.chat.person_info.person_info import person_info_manager +from abc import abstractmethod + +logger = get_logger("plugin_action") + +class PluginAction(BaseAction): + """插件动作基类 + + 封装了主程序内部依赖,提供简化的API接口给插件开发者 + """ + + def __init__(self, action_data: dict, reasoning: str, cycle_timers: dict, thinking_id: str, **kwargs): + """初始化插件动作基类""" + super().__init__(action_data, reasoning, cycle_timers, thinking_id) + + # 存储内部服务和对象引用 + self._services = {} + + # 从kwargs提取必要的内部服务 + if "observations" in kwargs: + self._services["observations"] = kwargs["observations"] + if "expressor" in kwargs: + self._services["expressor"] = kwargs["expressor"] + if "chat_stream" in kwargs: + self._services["chat_stream"] = kwargs["chat_stream"] + if "current_cycle" in kwargs: + self._services["current_cycle"] = kwargs["current_cycle"] + + self.log_prefix = kwargs.get("log_prefix", "") + + async def get_user_id_by_person_name(self, person_name: str) -> Tuple[str, str]: + """根据用户名获取用户ID""" + person_id = person_info_manager.get_person_id_by_person_name(person_name) + user_id = await person_info_manager.get_value(person_id, "user_id") + platform = await person_info_manager.get_value(person_id, "platform") + return platform, user_id + + # 提供简化的API方法 + async def send_message(self, text: str, target: Optional[str] = None) -> bool: + """发送消息的简化方法 + + Args: + text: 要发送的消息文本 + target: 目标消息(可选) + + Returns: + bool: 是否发送成功 + """ + try: + expressor = self._services.get("expressor") + chat_stream = self._services.get("chat_stream") + + if not expressor or not chat_stream: + logger.error(f"{self.log_prefix} 无法发送消息:缺少必要的内部服务") + return False + + # 构造简化的动作数据 + reply_data = { + "text": text, + "target": target or "", + "emojis": [] + } + + # 获取锚定消息(如果有) + observations = self._services.get("observations", []) + + chatting_observation: ChattingObservation = next( + obs for obs in observations + if isinstance(obs, ChattingObservation) + ) + anchor_message = chatting_observation.search_message_by_text(reply_data["target"]) + + # 如果没有找到锚点消息,创建一个占位符 + if not anchor_message: + logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符") + anchor_message = await create_empty_anchor_message( + chat_stream.platform, chat_stream.group_info, chat_stream + ) + else: + anchor_message.update_chat_stream(chat_stream) + + response_set = [ + ("text", text), + ] + + # 调用内部方法发送消息 + success = await expressor.send_response_messages( + anchor_message=anchor_message, + response_set=response_set, + ) + + return success + except Exception as e: + logger.error(f"{self.log_prefix} 发送消息时出错: {e}") + traceback.print_exc() + return False + + + async def send_message_by_expressor(self, text: str, target: Optional[str] = None) -> bool: + """发送消息的简化方法 + + Args: + text: 要发送的消息文本 + target: 目标消息(可选) + + Returns: + bool: 是否发送成功 + """ + try: + expressor = self._services.get("expressor") + chat_stream = self._services.get("chat_stream") + + if not expressor or not chat_stream: + logger.error(f"{self.log_prefix} 无法发送消息:缺少必要的内部服务") + return False + + # 构造简化的动作数据 + reply_data = { + "text": text, + "target": target or "", + "emojis": [] + } + + # 获取锚定消息(如果有) + observations = self._services.get("observations", []) + + chatting_observation: ChattingObservation = next( + obs for obs in observations + if isinstance(obs, ChattingObservation) + ) + anchor_message = chatting_observation.search_message_by_text(reply_data["target"]) + + # 如果没有找到锚点消息,创建一个占位符 + if not anchor_message: + logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符") + anchor_message = await create_empty_anchor_message( + chat_stream.platform, chat_stream.group_info, chat_stream + ) + else: + anchor_message.update_chat_stream(chat_stream) + + # 调用内部方法发送消息 + success, _ = await expressor.deal_reply( + cycle_timers=self.cycle_timers, + action_data=reply_data, + anchor_message=anchor_message, + reasoning=self.reasoning, + thinking_id=self.thinking_id + ) + + return success + except Exception as e: + logger.error(f"{self.log_prefix} 发送消息时出错: {e}") + return False + + def get_chat_type(self) -> str: + """获取当前聊天类型 + + Returns: + str: 聊天类型 ("group" 或 "private") + """ + chat_stream = self._services.get("chat_stream") + if chat_stream and hasattr(chat_stream, "group_info"): + return "group" if chat_stream.group_info else "private" + return "unknown" + + def get_recent_messages(self, count: int = 5) -> List[Dict[str, Any]]: + """获取最近的消息 + + Args: + count: 要获取的消息数量 + + Returns: + List[Dict]: 消息列表,每个消息包含发送者、内容等信息 + """ + messages = [] + observations = self._services.get("observations", []) + + if observations and len(observations) > 0: + obs = observations[0] + if hasattr(obs, "get_talking_message"): + raw_messages = obs.get_talking_message() + # 转换为简化格式 + for msg in raw_messages[-count:]: + simple_msg = { + "sender": msg.get("sender", "未知"), + "content": msg.get("content", ""), + "timestamp": msg.get("timestamp", 0) + } + messages.append(simple_msg) + + return messages + + @abstractmethod + async def process(self) -> Tuple[bool, str]: + """插件处理逻辑,子类必须实现此方法 + + Returns: + Tuple[bool, str]: (是否执行成功, 回复文本) + """ + pass + + async def handle_action(self) -> Tuple[bool, str]: + """实现BaseAction的抽象方法,调用子类的process方法 + + Returns: + Tuple[bool, str]: (是否执行成功, 回复文本) + """ + return await self.process() diff --git a/src/chat/focus_chat/planners/actions/reply_action.py b/src/chat/focus_chat/planners/actions/reply_action.py index 7b2e88fa0..51e3b8eaa 100644 --- a/src/chat/focus_chat/planners/actions/reply_action.py +++ b/src/chat/focus_chat/planners/actions/reply_action.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - 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, Optional +from typing import Tuple, List 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 @@ -22,23 +20,22 @@ class ReplyAction(BaseAction): 处理构建和发送消息回复的动作。 """ - action_name:str = "reply" - action_description:str = "表达想法,可以只包含文本、表情或两者都有" - action_parameters:dict[str:str] = { + action_name: str = "reply" + action_description: str = "表达想法,可以只包含文本、表情或两者都有" + action_parameters: dict[str:str] = { "text": "你想要表达的内容(可选)", "emojis": "描述当前使用表情包的场景(可选)", "target": "你想要回复的原始文本内容(非必须,仅文本,不包含发送者)(可选)", } - action_require:list[str] = [ + action_require: list[str] = [ "有实质性内容需要表达", "有人提到你,但你还没有回应他", "在合适的时候添加表情(不要总是添加)", - "如果你要回复特定某人的某句话,或者你想回复较早的消息,请在target中指定那句话的原始文本", - "除非有明确的回复目标,如果选择了target,不用特别提到某个人的人名", + "如果你有明确的,要回复特定某人的某句话,或者你想回复较早的消息,请在target中指定那句话的原始文本", "一次只回复一个人,一次只回复一个话题,突出重点", "如果是自己发的消息想继续,需自然衔接", "避免重复或评价自己的发言,不要和自己聊天", - "注意:回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。" + "注意:回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。不要有额外的符号,尽量简单简短", ] default = True @@ -54,7 +51,7 @@ class ReplyAction(BaseAction): chat_stream: ChatStream, current_cycle: CycleDetail, log_prefix: str, - **kwargs + **kwargs, ): """初始化回复动作处理器 @@ -89,9 +86,9 @@ class ReplyAction(BaseAction): reasoning=self.reasoning, reply_data=self.action_data, cycle_timers=self.cycle_timers, - thinking_id=self.thinking_id + thinking_id=self.thinking_id, ) - + async def _handle_reply( self, reasoning: str, reply_data: dict, cycle_timers: dict, thinking_id: str ) -> tuple[bool, str]: @@ -105,13 +102,16 @@ class ReplyAction(BaseAction): "emojis": "微笑" # 表情关键词列表(可选) } """ - # 重置连续不回复计数器 - self.total_no_reply_count = 0 - self.total_waiting_time = 0.0 # 从聊天观察获取锚定消息 - observations: ChattingObservation = self.observations[0] - anchor_message = observations.serch_message_by_text(reply_data["target"]) + chatting_observation: ChattingObservation = next( + obs for obs in self.observations + if isinstance(obs, ChattingObservation) + ) + if reply_data.get("target"): + anchor_message = chatting_observation.search_message_by_text(reply_data["target"]) + else: + anchor_message = None # 如果没有找到锚点消息,创建一个占位符 if not anchor_message: diff --git a/src/chat/focus_chat/planners/planner.py b/src/chat/focus_chat/planners/planner.py index bb87e1da7..79044a5a6 100644 --- a/src/chat/focus_chat/planners/planner.py +++ b/src/chat/focus_chat/planners/planner.py @@ -4,7 +4,6 @@ from typing import List, Dict, Any, Optional from rich.traceback import install from src.chat.models.utils_model import LLMRequest from src.config.config import global_config -from src.chat.focus_chat.heartflow_prompt_builder import prompt_builder from src.chat.focus_chat.info.info_base import InfoBase from src.chat.focus_chat.info.obs_info import ObsInfo from src.chat.focus_chat.info.cycle_info import CycleInfo @@ -13,16 +12,21 @@ 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.chat.focus_chat.planners.action_factory import ActionManager -from src.chat.focus_chat.planners.action_factory import ActionInfo +from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.focus_chat.planners.action_manager import ActionInfo + logger = get_logger("planner") install(extra_lines=3) + def init_prompt(): Prompt( - """你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话: + """{extra_info_block} + +你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话: {chat_content_block} + {mind_info_block} {cycle_info_block} @@ -44,20 +48,20 @@ def init_prompt(): }} 请输出你的决策 JSON:""", -"planner_prompt",) - + "planner_prompt", + ) + Prompt( """ action_name: {action_name} 描述:{action_description} 参数: - {action_parameters} +{action_parameters} 动作要求: - {action_require} - """, +{action_require}""", "action_prompt", ) - + class ActionPlanner: def __init__(self, log_prefix: str, action_manager: ActionManager): @@ -68,7 +72,7 @@ class ActionPlanner: max_tokens=1000, request_type="action_planning", # 用于动作规划 ) - + self.action_manager = action_manager async def plan(self, all_plan_info: List[InfoBase], cycle_timers: dict) -> Dict[str, Any]: @@ -85,6 +89,7 @@ class ActionPlanner: try: # 获取观察信息 + extra_info: list[str] = [] for info in all_plan_info: if isinstance(info, ObsInfo): logger.debug(f"{self.log_prefix} 观察信息: {info}") @@ -104,9 +109,11 @@ class ActionPlanner: elif isinstance(info, StructuredInfo): logger.debug(f"{self.log_prefix} 结构化信息: {info}") structured_info = info.get_data() + else: + extra_info.append(info.get_processed_info()) current_available_actions = self.action_manager.get_using_actions() - + # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- prompt = await self.build_planner_prompt( is_group_chat=is_group_chat, # <-- Pass HFC state @@ -116,6 +123,7 @@ class ActionPlanner: # structured_info=structured_info, # <-- Pass SubMind info current_available_actions=current_available_actions, # <-- Pass determined actions cycle_info=cycle_info, # <-- Pass cycle info + extra_info=extra_info, ) # --- 调用 LLM (普通文本生成) --- @@ -142,15 +150,13 @@ class ActionPlanner: extracted_action = parsed_json.get("action", "no_reply") extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由") - # 新的reply格式 - if extracted_action == "reply": - action_data = { - "text": parsed_json.get("text", []), - "emojis": parsed_json.get("emojis", []), - "target": parsed_json.get("target", ""), - } - else: - action_data = {} # 其他动作可能不需要额外数据 + # 将所有其他属性添加到action_data + action_data = {} + for key, value in parsed_json.items(): + if key not in ["action", "reasoning"]: + action_data[key] = value + + # 对于reply动作不需要额外处理,因为相关字段已经在上面的循环中添加到action_data if extracted_action not in current_available_actions: logger.warning( @@ -197,7 +203,6 @@ class ActionPlanner: # 返回结果字典 return plan_result - async def build_planner_prompt( self, is_group_chat: bool, # Now passed as argument @@ -206,6 +211,7 @@ class ActionPlanner: current_mind: Optional[str], current_available_actions: Dict[str, ActionInfo], cycle_info: Optional[str], + extra_info: list[str], ) -> str: """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: @@ -218,7 +224,6 @@ class ActionPlanner: ) chat_context_description = f"你正在和 {chat_target_name} 私聊" - chat_content_block = "" if observed_messages_str: chat_content_block = f"聊天记录:\n{observed_messages_str}" @@ -234,7 +239,6 @@ class ActionPlanner: individuality = Individuality.get_instance() personality_block = individuality.get_prompt(x_person=2, level=2) - action_options_block = "" for using_actions_name, using_actions_info in current_available_actions.items(): # print(using_actions_name) @@ -242,29 +246,29 @@ class ActionPlanner: # print(using_actions_info["parameters"]) # print(using_actions_info["require"]) # print(using_actions_info["description"]) - + using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt") - + param_text = "" for param_name, param_description in using_actions_info["parameters"].items(): - param_text += f"{param_name}: {param_description}\n" - + param_text += f" {param_name}: {param_description}\n" + require_text = "" for require_item in using_actions_info["require"]: - require_text += f"- {require_item}\n" - + require_text += f" - {require_item}\n" + using_action_prompt = using_action_prompt.format( action_name=using_actions_name, action_description=using_actions_info["description"], action_parameters=param_text, action_require=require_text, ) - + action_options_block += using_action_prompt - + extra_info_block = "\n".join(extra_info) + extra_info_block = f"以下是一些额外的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是一些额外的信息,现在请你阅读以下内容,进行决策" - planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") prompt = planner_prompt_template.format( bot_name=global_config.BOT_NICKNAME, @@ -274,6 +278,7 @@ class ActionPlanner: mind_info_block=mind_info_block, cycle_info_block=cycle_info, action_options_text=action_options_block, + extra_info_block=extra_info_block, ) return prompt diff --git a/src/chat/heart_flow/observation/chatting_observation.py b/src/chat/heart_flow/observation/chatting_observation.py index a51eba5e2..017f24da9 100644 --- a/src/chat/heart_flow/observation/chatting_observation.py +++ b/src/chat/heart_flow/observation/chatting_observation.py @@ -14,6 +14,7 @@ from typing import Optional import difflib from src.chat.message_receive.message import MessageRecv # 添加 MessageRecv 导入 from src.chat.heart_flow.observation.observation import Observation + from src.common.logger_manager import get_logger from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.utils.prompt_builder import Prompt @@ -43,6 +44,7 @@ class ChattingObservation(Observation): def __init__(self, chat_id): super().__init__(chat_id) self.chat_id = chat_id + self.platform = "qq" # --- Initialize attributes (defaults) --- self.is_group_chat: bool = False @@ -105,7 +107,7 @@ class ChattingObservation(Observation): mid_memory_str += f"{mid_memory['theme']}\n" return mid_memory_str + "现在群里正在聊:\n" + self.talking_message_str - def serch_message_by_text(self, text: str) -> Optional[MessageRecv]: + def search_message_by_text(self, text: str) -> Optional[MessageRecv]: """ 根据回复的纯文本 1. 在talking_message中查找最新的,最匹配的消息 @@ -150,7 +152,7 @@ class ChattingObservation(Observation): } message_info = { - "platform": find_msg.get("platform"), + "platform": self.platform, "message_id": find_msg.get("message_id"), "time": find_msg.get("time"), "group_info": group_info, diff --git a/src/chat/heart_flow/observation/hfcloop_observation.py b/src/chat/heart_flow/observation/hfcloop_observation.py index 470671e28..d950e3512 100644 --- a/src/chat/heart_flow/observation/hfcloop_observation.py +++ b/src/chat/heart_flow/observation/hfcloop_observation.py @@ -3,6 +3,7 @@ from datetime import datetime from src.common.logger_manager import get_logger from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail +from src.chat.focus_chat.planners.action_manager import ActionManager from typing import List # Import the new utility function @@ -16,14 +17,16 @@ class HFCloopObservation: self.observe_id = observe_id self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间 self.history_loop: List[CycleDetail] = [] + self.action_manager = ActionManager() def get_observe_info(self): return self.observe_info def add_loop_info(self, loop_info: CycleDetail): - # logger.debug(f"添加循环信息111111111111111111111111111111111111: {loop_info}") - # print(f"添加循环信息111111111111111111111111111111111111: {loop_info}") self.history_loop.append(loop_info) + + def set_action_manager(self, action_manager: ActionManager): + self.action_manager = action_manager async def observe(self): recent_active_cycles: List[CycleDetail] = [] @@ -62,7 +65,6 @@ class HFCloopObservation: if cycle_info_block: cycle_info_block = f"\n你最近的回复\n{cycle_info_block}\n" else: - # 如果最近的活动循环不是文本回复,或者没有活动循环 cycle_info_block = "\n" # 获取history_loop中最新添加的 @@ -72,8 +74,17 @@ class HFCloopObservation: end_time = last_loop.end_time if start_time is not None and end_time is not None: time_diff = int(end_time - start_time) - cycle_info_block += f"\n距离你上一次阅读消息已经过去了{time_diff}分钟\n" + if time_diff > 60: + cycle_info_block += f"\n距离你上一次阅读消息已经过去了{time_diff/60}分钟\n" + else: + cycle_info_block += f"\n距离你上一次阅读消息已经过去了{time_diff}秒\n" else: - cycle_info_block += "\n无法获取上一次阅读消息的时间\n" + cycle_info_block += "\n你还没看过消息\n" + + using_actions = self.action_manager.get_using_actions() + for action_name, action_info in using_actions.items(): + action_description = action_info["description"] + cycle_info_block += f"\n你在聊天中可以使用{action_name},这个动作的描述是{action_description}\n" + self.observe_info = cycle_info_block diff --git a/src/chat/heart_flow/observation/observation.py b/src/chat/heart_flow/observation/observation.py index 97e254fc0..8ab9ab9a4 100644 --- a/src/chat/heart_flow/observation/observation.py +++ b/src/chat/heart_flow/observation/observation.py @@ -5,7 +5,6 @@ from src.common.logger_manager import get_logger logger = get_logger("observation") - # 所有观察的基类 class Observation: def __init__(self, observe_id): diff --git a/src/chat/person_info/person_info.py b/src/chat/person_info/person_info.py index 605b86b23..2460ab4ff 100644 --- a/src/chat/person_info/person_info.py +++ b/src/chat/person_info/person_info.py @@ -94,6 +94,15 @@ class PersonInfoManager: return True else: return False + + def get_person_id_by_person_name(self, person_name: str): + """根据用户名获取用户ID""" + document = db.person_info.find_one({"person_name": person_name}) + if document: + return document["person_id"] + else: + return "" + @staticmethod async def create_person_info(person_id: str, data: dict = None): diff --git a/src/plugins.md b/src/plugins.md new file mode 100644 index 000000000..71ca741a6 --- /dev/null +++ b/src/plugins.md @@ -0,0 +1,101 @@ +# 如何编写MaiBot插件 + +## 基本步骤 + +1. 在`src/plugins/你的插件名/actions/`目录下创建插件文件 +2. 继承`PluginAction`基类 +3. 实现`process`方法 + +## 插件结构示例 + +```python +from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action +from typing import Tuple + +logger = get_logger("your_action_name") + +@register_action +class YourAction(PluginAction): + """你的动作描述""" + + action_name = "your_action_name" # 动作名称,必须唯一 + action_description = "这个动作的详细描述,会展示给用户" + action_parameters = { + "param1": "参数1的说明(可选)", + "param2": "参数2的说明(可选)" + } + action_require = [ + "使用场景1", + "使用场景2" + ] + default = False # 是否默认启用 + + async def process(self) -> Tuple[bool, str]: + """插件核心逻辑""" + # 你的代码逻辑... + return True, "执行结果" +``` + +## 可用的API方法 + +插件可以使用`PluginAction`基类提供的以下API: + +### 1. 发送消息 + +```python +await self.send_message("要发送的文本", target="可选的回复目标") +``` + +### 2. 获取聊天类型 + +```python +chat_type = self.get_chat_type() # 返回 "group" 或 "private" 或 "unknown" +``` + +### 3. 获取最近消息 + +```python +messages = self.get_recent_messages(count=5) # 获取最近5条消息 +# 返回格式: [{"sender": "发送者", "content": "内容", "timestamp": 时间戳}, ...] +``` + +### 4. 获取动作参数 + +```python +param_value = self.action_data.get("param_name", "默认值") +``` + +### 5. 日志记录 + +```python +logger.info(f"{self.log_prefix} 你的日志信息") +logger.warning("警告信息") +logger.error("错误信息") +``` + +## 返回值说明 + +`process`方法必须返回一个元组,包含两个元素: +- 第一个元素(bool): 表示动作是否执行成功 +- 第二个元素(str): 执行结果的文本描述 + +```python +return True, "执行成功的消息" +# 或 +return False, "执行失败的原因" +``` + +## 最佳实践 + +1. 使用`action_parameters`清晰定义你的动作需要的参数 +2. 使用`action_require`描述何时应该使用你的动作 +3. 使用`action_description`准确描述你的动作功能 +4. 使用`logger`记录重要信息,方便调试 +5. 避免操作底层系统,尽量使用`PluginAction`提供的API + +## 注册与加载 + +插件会在系统启动时自动加载,只要放在正确的目录并添加了`@register_action`装饰器。 + +若设置`default = True`,插件会自动添加到默认动作集;否则需要在系统中手动启用。 diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py new file mode 100644 index 000000000..b557a4258 --- /dev/null +++ b/src/plugins/__init__.py @@ -0,0 +1 @@ +"""插件系统包""" \ No newline at end of file diff --git a/src/plugins/test_plugin/__init__.py b/src/plugins/test_plugin/__init__.py new file mode 100644 index 000000000..867ef417c --- /dev/null +++ b/src/plugins/test_plugin/__init__.py @@ -0,0 +1,4 @@ +"""测试插件包""" +""" +这是一个测试插件 +""" \ No newline at end of file diff --git a/src/plugins/test_plugin/actions/__init__.py b/src/plugins/test_plugin/actions/__init__.py new file mode 100644 index 000000000..8599d2326 --- /dev/null +++ b/src/plugins/test_plugin/actions/__init__.py @@ -0,0 +1,6 @@ +"""测试插件动作模块""" + +# 导入所有动作模块以确保装饰器被执行 +from . import test_action # noqa +# from . import online_action # noqa +from . import mute_action # noqa \ No newline at end of file diff --git a/src/plugins/test_plugin/actions/mute_action.py b/src/plugins/test_plugin/actions/mute_action.py new file mode 100644 index 000000000..723571806 --- /dev/null +++ b/src/plugins/test_plugin/actions/mute_action.py @@ -0,0 +1,48 @@ +from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action +from typing import Tuple + +logger = get_logger("mute_action") + +@register_action +class MuteAction(PluginAction): + """测试动作处理类""" + + action_name = "mute_action" + action_description = "如果某人违反了公序良俗,或者别人戳你太多,,或者某人刷屏,一定要禁言某人,如果你很生气,可以禁言某人" + action_parameters = { + "target": "禁言对象,输入你要禁言的对象的名字,必填,", + "duration": "禁言时长,输入你要禁言的时长,单位为秒,必填", + } + action_require = [ + "当有人违反了公序良俗时使用", + "当有人刷屏时使用", + "当有人要求禁言自己时使用", + "当有人戳你两次以上时,防止刷屏,禁言他,必须牢记", + "当千石可乐或可乐酱要求你禁言时使用", + "当你想回避某个话题时使用", + ] + default = True # 不是默认动作,需要手动添加到使用集 + + async def process(self) -> Tuple[bool, str]: + """处理测试动作""" + logger.info(f"{self.log_prefix} 执行online动作: {self.reasoning}") + + # 发送测试消息 + target = self.action_data.get("target") + duration = self.action_data.get("duration") + reason = self.action_data.get("reason") + platform, user_id = await self.get_user_id_by_person_name(target) + + await self.send_message_by_expressor(f"我要禁言{target},{platform},时长{duration}秒,理由{reason},表达情绪") + + try: + await self.send_message(f"[command]mute,{user_id},{duration}") + + except Exception as e: + logger.error(f"{self.log_prefix} 执行mute动作时出错: {e}") + await self.send_message_by_expressor(f"执行mute动作时出错: {e}") + + return False, "执行mute动作时出错" + + return True, "测试动作执行成功" \ No newline at end of file diff --git a/src/plugins/test_plugin/actions/online_action.py b/src/plugins/test_plugin/actions/online_action.py new file mode 100644 index 000000000..67e2d2cc9 --- /dev/null +++ b/src/plugins/test_plugin/actions/online_action.py @@ -0,0 +1,44 @@ +from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action +from typing import Tuple + +logger = get_logger("check_online_action") + +@register_action +class CheckOnlineAction(PluginAction): + """测试动作处理类""" + + action_name = "check_online_action" + action_description = "这是一个检查在线状态的动作,当有人要求你检查Maibot(麦麦 机器人)在线状态时使用" + action_parameters = { + "mode": "查看模式" + } + action_require = [ + "当有人要求你检查Maibot(麦麦 机器人)在线状态时使用", + "mode参数为version时查看在线版本状态,默认用这种", + "mode参数为type时查看在线系统类型分布", + ] + default = True # 不是默认动作,需要手动添加到使用集 + + async def process(self) -> Tuple[bool, str]: + """处理测试动作""" + logger.info(f"{self.log_prefix} 执行online动作: {self.reasoning}") + + # 发送测试消息 + mode = self.action_data.get("mode", "type") + + await self.send_message_by_expressor("我看看") + + try: + if mode == "type": + await self.send_message(f"#online detail") + elif mode == "version": + await self.send_message(f"#online") + + except Exception as e: + logger.error(f"{self.log_prefix} 执行online动作时出错: {e}") + await self.send_message_by_expressor("执行online动作时出错: {e}") + + return False, "执行online动作时出错" + + return True, "测试动作执行成功" \ No newline at end of file diff --git a/src/plugins/test_plugin/actions/test_action.py b/src/plugins/test_plugin/actions/test_action.py new file mode 100644 index 000000000..3634dbe78 --- /dev/null +++ b/src/plugins/test_plugin/actions/test_action.py @@ -0,0 +1,38 @@ +from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action +from typing import Tuple + +logger = get_logger("test_action") + +@register_action +class TestAction(PluginAction): + """测试动作处理类""" + + action_name = "test_action" + action_description = "这是一个测试动作,当有人要求你测试插件系统时使用" + action_parameters = { + "test_param": "测试参数(可选)" + } + action_require = [ + "测试情况下使用", + "想测试插件动作加载时使用", + ] + default = False # 不是默认动作,需要手动添加到使用集 + + async def process(self) -> Tuple[bool, str]: + """处理测试动作""" + logger.info(f"{self.log_prefix} 执行测试动作: {self.reasoning}") + + # 获取聊天类型 + chat_type = self.get_chat_type() + logger.info(f"{self.log_prefix} 当前聊天类型: {chat_type}") + + # 获取最近消息 + recent_messages = self.get_recent_messages(3) + logger.info(f"{self.log_prefix} 最近3条消息: {recent_messages}") + + # 发送测试消息 + test_param = self.action_data.get("test_param", "默认参数") + await self.send_message_by_expressor(f"测试动作执行成功,参数: {test_param}") + + return True, "测试动作执行成功" \ No newline at end of file