diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index dfafa19e..2bc5b2f5 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -1,4 +1,5 @@ import asyncio +from multiprocessing import context import time import traceback import random @@ -196,19 +197,22 @@ class HeartFChatting: # print(f"{self.log_prefix} questioned: {self.questioned},len: {len(global_conflict_tracker.get_questions_by_chat_id(self.stream_id))}") if question_probability > 0 and not self.questioned and len(global_conflict_tracker.get_questions_by_chat_id(self.stream_id)) == 0: #长久没有回复,可以试试主动发言,提问概率随着时间增加 - logger.info(f"{self.log_prefix} 长久没有回复,可以试试主动发言,概率: {question_probability}") + # logger.info(f"{self.log_prefix} 长久没有回复,可以试试主动发言,概率: {question_probability}") if random.random() < question_probability: # 30%概率主动发言 try: self.questioned = True self.last_active_time = time.time() - print(f"{self.log_prefix} 长久没有回复,可以试试主动发言,开始生成问题") + # print(f"{self.log_prefix} 长久没有回复,可以试试主动发言,开始生成问题") + logger.info(f"{self.log_prefix} 长久没有回复,可以试试主动发言,开始生成问题") cycle_timers, thinking_id = self.start_cycle() question_maker = QuestionMaker(self.stream_id) - question, conflict_context = await question_maker.make_question() - logger.info(f"{self.log_prefix} 问题: {question}") + question, context,conflict_context = await question_maker.make_question() if question: + logger.info(f"{self.log_prefix} 问题: {question}") await global_conflict_tracker.track_conflict(question, conflict_context, True, self.stream_id) - await self._lift_question_reply(question,cycle_timers,thinking_id) + await self._lift_question_reply(question,context,cycle_timers,thinking_id) + else: + logger.info(f"{self.log_prefix} 无问题") # self.end_cycle(cycle_timers, thinking_id) except Exception as e: logger.error(f"{self.log_prefix} 主动提问失败: {e}") @@ -548,8 +552,8 @@ class HeartFChatting: traceback.print_exc() return False, "" - async def _lift_question_reply(self, question: str, cycle_timers: Dict[str, float], thinking_id: str): - reason = f"你对问题\"{question}\"感到好奇,想要和群友讨论" + async def _lift_question_reply(self, question: str, context: str, cycle_timers: Dict[str, float], thinking_id: str): + reason = f"在聊天中:\n{context}\n你对问题\"{question}\"感到好奇,想要和群友讨论" new_msg = get_raw_msg_before_timestamp_with_chat( chat_id=self.stream_id, timestamp=time.time(), @@ -564,19 +568,8 @@ class HeartFChatting: available_actions=None, loop_start_time=time.time(), action_reasoning=reason) - self.action_planner.add_plan_log(reasoning=reason, actions=[reply_action_info]) + self.action_planner.add_plan_log(reasoning=f"你对问题\"{question}\"感到好奇,想要和群友讨论", actions=[reply_action_info]) - await database_api.store_action_info( - chat_stream=self.chat_stream, - action_build_into_prompt=False, - action_prompt_display=reason, - action_done=True, - thinking_id=thinking_id, - action_data=reply_action_info.action_data, - action_name="reply", - action_reasoning=reason, - ) - success, llm_response = await generator_api.rewrite_reply( chat_stream=self.chat_stream, reply_data={ diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 58ce21a6..717607f0 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -27,6 +27,7 @@ from src.chat.utils.chat_message_builder import ( replace_user_references, ) from src.chat.express.expression_selector import expression_selector +from src.plugin_system.apis.message_api import translate_pid_to_description # from src.memory_system.memory_activator import MemoryActivator from src.person_info.person_info import Person @@ -358,6 +359,59 @@ class DefaultReplyer: target = parts[1].strip() return sender, target + def _replace_picids_with_descriptions(self, text: str) -> str: + """将文本中的[picid:xxx]替换为具体的图片描述 + + Args: + text: 包含picid标记的文本 + + Returns: + 替换后的文本 + """ + # 匹配 [picid:xxxxx] 格式 + pic_pattern = r"\[picid:([^\]]+)\]" + + def replace_pic_id(match: re.Match) -> str: + pic_id = match.group(1) + description = translate_pid_to_description(pic_id) + return f"[图片:{description}]" + + return re.sub(pic_pattern, replace_pic_id, text) + + def _analyze_target_content(self, target: str) -> Tuple[bool, bool, str, str]: + """分析target内容类型(基于原始picid格式) + + Args: + target: 目标消息内容(包含[picid:xxx]格式) + + Returns: + Tuple[bool, bool, str, str]: (是否只包含图片, 是否包含文字, 图片部分, 文字部分) + """ + if not target or not target.strip(): + return False, False, "", "" + + # 检查是否只包含picid标记 + picid_pattern = r"\[picid:[^\]]+\]" + picid_matches = re.findall(picid_pattern, target) + + # 移除所有picid标记后检查是否还有文字内容 + text_without_picids = re.sub(picid_pattern, "", target).strip() + + has_only_pics = len(picid_matches) > 0 and not text_without_picids + has_text = bool(text_without_picids) + + # 提取图片部分(转换为[图片:描述]格式) + pic_part = "" + if picid_matches: + pic_descriptions = [] + for picid_match in picid_matches: + pic_id = picid_match[6:-1] # 提取picid:xxx中的xxx部分 + description = translate_pid_to_description(pic_id) + pic_descriptions.append(f"[图片:{description}]") + pic_part = "".join(pic_descriptions) + + return has_only_pics, has_text, pic_part, text_without_picids + async def build_keywords_reaction_prompt(self, target: Optional[str]) -> str: """构建关键词反应提示 @@ -582,7 +636,12 @@ class DefaultReplyer: target = reply_message.processed_plain_text target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) - target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) + + # 在picid替换之前分析内容类型(防止prompt注入) + has_only_pics, has_text, pic_part, text_part = self._analyze_target_content(target) + + # 将[picid:xxx]替换为具体的图片描述 + target = self._replace_picids_with_descriptions(target) message_list_before_now_long = get_raw_msg_before_timestamp_with_chat( chat_id=chat_id, @@ -695,10 +754,19 @@ class DefaultReplyer: moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" if sender: - if is_group_chat: - reply_target_block = f"现在{sender}说的:{target}。引起了你的注意" - else: # private chat - reply_target_block = f"现在{sender}说的:{target}。引起了你的注意" + # 使用预先分析的内容类型结果 + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = f"现在{sender}发送的图片:{pic_part}。引起了你的注意" + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意" + elif has_text: + # 只包含文字 + reply_target_block = f"现在{sender}说的:{text_part}。引起了你的注意" + else: + # 其他情况(空内容等) + reply_target_block = f"现在{sender}说的:{target}。引起了你的注意" else: reply_target_block = "" @@ -707,51 +775,28 @@ class DefaultReplyer: message_list_before_now_long, user_id, sender ) - if global_config.bot.qq_account == user_id and platform == global_config.bot.platform: - return await global_prompt_manager.format_prompt( - "replyer_self_prompt", - expression_habits_block=expression_habits_block, - tool_info_block=tool_info, - memory_block=memory_block, - knowledge_prompt=prompt_info, - mood_state=mood_state_prompt, - # memory_block=memory_block, - # relation_info_block=relation_info, - extra_info_block=extra_info_block, - identity=personality_prompt, - action_descriptions=actions_info, - background_dialogue_prompt=background_dialogue_prompt, - time_block=time_block, - target=target, - reason=reply_reason, - reply_style=global_config.personality.reply_style, - question_block=question_block, - keywords_reaction_prompt=keywords_reaction_prompt, - moderation_prompt=moderation_prompt_block, - ), selected_expressions - else: - return await global_prompt_manager.format_prompt( - "replyer_prompt", - expression_habits_block=expression_habits_block, - tool_info_block=tool_info, - memory_block=memory_block, - knowledge_prompt=prompt_info, - mood_state=mood_state_prompt, - # memory_block=memory_block, - # relation_info_block=relation_info, - extra_info_block=extra_info_block, - identity=personality_prompt, - action_descriptions=actions_info, - sender_name=sender, - background_dialogue_prompt=background_dialogue_prompt, - time_block=time_block, - core_dialogue_prompt=core_dialogue_prompt, - reply_target_block=reply_target_block, - reply_style=global_config.personality.reply_style, - keywords_reaction_prompt=keywords_reaction_prompt, - moderation_prompt=moderation_prompt_block, - question_block=question_block, - ), selected_expressions + return await global_prompt_manager.format_prompt( + "replyer_prompt", + expression_habits_block=expression_habits_block, + tool_info_block=tool_info, + memory_block=memory_block, + knowledge_prompt=prompt_info, + mood_state=mood_state_prompt, + # memory_block=memory_block, + # relation_info_block=relation_info, + extra_info_block=extra_info_block, + identity=personality_prompt, + action_descriptions=actions_info, + sender_name=sender, + background_dialogue_prompt=background_dialogue_prompt, + time_block=time_block, + core_dialogue_prompt=core_dialogue_prompt, + reply_target_block=reply_target_block, + reply_style=global_config.personality.reply_style, + keywords_reaction_prompt=keywords_reaction_prompt, + moderation_prompt=moderation_prompt_block, + question_block=question_block, + ), selected_expressions async def build_prompt_rewrite_context( self, @@ -765,7 +810,12 @@ class DefaultReplyer: sender, target = self._parse_reply_target(reply_to) target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) - target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) + + # 在picid替换之前分析内容类型(防止prompt注入) + has_only_pics, has_text, pic_part, text_part = self._analyze_target_content(target) + + # 将[picid:xxx]替换为具体的图片描述 + target = self._replace_picids_with_descriptions(target) message_list_before_now_half = get_raw_msg_before_timestamp_with_chat( chat_id=chat_id, @@ -795,18 +845,39 @@ class DefaultReplyer: ) if sender and target: + # 使用预先分析的内容类型结果 if is_group_chat: if sender: - reply_target_block = ( - f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。" - ) + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = ( + f"现在{sender}发送的图片:{pic_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = ( + f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + else: + # 只包含文字 + reply_target_block = ( + f"现在{sender}说的:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) elif target: reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" else: reply_target_block = "现在,你想要在群里发言或者回复消息。" else: # private chat if sender: - reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,针对这条消息回复。" + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = f"现在{sender}发送的图片:{pic_part}。引起了你的注意,针对这条消息回复。" + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,针对这条消息回复。" + else: + # 只包含文字 + reply_target_block = f"现在{sender}说的:{text_part}。引起了你的注意,针对这条消息回复。" elif target: reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。" else: diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 6c2cac97..da14064a 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -25,6 +25,7 @@ from src.chat.utils.chat_message_builder import ( replace_user_references, ) from src.chat.express.expression_selector import expression_selector +from src.plugin_system.apis.message_api import translate_pid_to_description from src.mood.mood_manager import mood_manager # from src.memory_system.memory_activator import MemoryActivator @@ -362,6 +363,59 @@ class PrivateReplyer: target = parts[1].strip() return sender, target + def _replace_picids_with_descriptions(self, text: str) -> str: + """将文本中的[picid:xxx]替换为具体的图片描述 + + Args: + text: 包含picid标记的文本 + + Returns: + 替换后的文本 + """ + # 匹配 [picid:xxxxx] 格式 + pic_pattern = r"\[picid:([^\]]+)\]" + + def replace_pic_id(match: re.Match) -> str: + pic_id = match.group(1) + description = translate_pid_to_description(pic_id) + return f"[图片:{description}]" + + return re.sub(pic_pattern, replace_pic_id, text) + + def _analyze_target_content(self, target: str) -> Tuple[bool, bool, str, str]: + """分析target内容类型(基于原始picid格式) + + Args: + target: 目标消息内容(包含[picid:xxx]格式) + + Returns: + Tuple[bool, bool, str, str]: (是否只包含图片, 是否包含文字, 图片部分, 文字部分) + """ + if not target or not target.strip(): + return False, False, "", "" + + # 检查是否只包含picid标记 + picid_pattern = r"\[picid:[^\]]+\]" + picid_matches = re.findall(picid_pattern, target) + + # 移除所有picid标记后检查是否还有文字内容 + text_without_picids = re.sub(picid_pattern, "", target).strip() + + has_only_pics = len(picid_matches) > 0 and not text_without_picids + has_text = bool(text_without_picids) + + # 提取图片部分(转换为[图片:描述]格式) + pic_part = "" + if picid_matches: + pic_descriptions = [] + for picid_match in picid_matches: + pic_id = picid_match[6:-1] # 提取picid:xxx中的xxx部分 + description = translate_pid_to_description(pic_id) + pic_descriptions.append(f"[图片:{description}]") + pic_part = "".join(pic_descriptions) + + return has_only_pics, has_text, pic_part, text_without_picids + async def build_keywords_reaction_prompt(self, target: Optional[str]) -> str: """构建关键词反应提示 @@ -510,7 +564,12 @@ class PrivateReplyer: target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) - target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) + + # 在picid替换之前分析内容类型(防止prompt注入) + has_only_pics, has_text, pic_part, text_part = self._analyze_target_content(target) + + # 将[picid:xxx]替换为具体的图片描述 + target = self._replace_picids_with_descriptions(target) message_list_before_now_long = get_raw_msg_before_timestamp_with_chat( chat_id=chat_id, @@ -629,9 +688,19 @@ class PrivateReplyer: moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" - reply_target_block = ( - f"现在对方说的:{target}。引起了你的注意" - ) + # 使用预先分析的内容类型结果 + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = f"现在对方发送的图片:{pic_part}。引起了你的注意" + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = f"现在对方发送了图片:{pic_part},并说:{text_part}。引起了你的注意" + elif has_text: + # 只包含文字 + reply_target_block = f"现在对方说的:{text_part}。引起了你的注意" + else: + # 其他情况(空内容等) + reply_target_block = f"现在对方说的:{target}。引起了你的注意" if global_config.bot.qq_account == user_id and platform == global_config.bot.platform: return await global_prompt_manager.format_prompt( @@ -687,7 +756,12 @@ class PrivateReplyer: sender, target = self._parse_reply_target(reply_to) target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) - target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) + + # 在picid替换之前分析内容类型(防止prompt注入) + has_only_pics, has_text, pic_part, text_part = self._analyze_target_content(target) + + # 将[picid:xxx]替换为具体的图片描述 + target = self._replace_picids_with_descriptions(target) @@ -720,18 +794,39 @@ class PrivateReplyer: ) if sender and target: + # 使用预先分析的内容类型结果 if is_group_chat: if sender: - reply_target_block = ( - f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。" - ) + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = ( + f"现在{sender}发送的图片:{pic_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = ( + f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + else: + # 只包含文字 + reply_target_block = ( + f"现在{sender}说的:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) elif target: reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" else: reply_target_block = "现在,你想要在群里发言或者回复消息。" else: # private chat if sender: - reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,针对这条消息回复。" + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = f"现在{sender}发送的图片:{pic_part}。引起了你的注意,针对这条消息回复。" + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,针对这条消息回复。" + else: + # 只包含文字 + reply_target_block = f"现在{sender}说的:{text_part}。引起了你的注意,针对这条消息回复。" elif target: reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。" else: diff --git a/src/chat/replyer/prompt/replyer_prompt.py b/src/chat/replyer/prompt/replyer_prompt.py index 236eb2b6..d8f59e91 100644 --- a/src/chat/replyer/prompt/replyer_prompt.py +++ b/src/chat/replyer/prompt/replyer_prompt.py @@ -25,32 +25,11 @@ def init_replyer_prompt(): 你正在群里聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,{mood_state} 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。 {reply_style} -请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 -{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", +请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出一句回复内容就好。 +{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 +现在,你说:""", "replyer_prompt", ) - - - - Prompt( - """{knowledge_prompt}{tool_info_block}{extra_info_block} -{expression_habits_block}{memory_block}{question_block} - -你正在qq群里聊天,下面是群里正在聊的内容: -{time_block} -{background_dialogue_prompt} - -你现在想补充说明你刚刚自己的发言内容:{target},原因是{reason} -请你根据聊天内容,组织一条新回复。注意,{target} 是刚刚你自己的发言,你要在这基础上进一步发言,请按照你自己的角度来继续进行回复。注意保持上下文的连贯性。{mood_state} -{identity} -尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。 -{reply_style} -请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 -{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 -""", - "replyer_self_prompt", - ) - Prompt( diff --git a/src/memory_system/question_maker.py b/src/memory_system/question_maker.py index 6dc82874..9a44d408 100644 --- a/src/memory_system/question_maker.py +++ b/src/memory_system/question_maker.py @@ -10,10 +10,10 @@ class QuestionMaker: self.chat_id = chat_id self.context = context - def get_context(self): + def get_context(self,timestamp: float = time.time()): latest_30_msgs = get_raw_msg_before_timestamp_with_chat( chat_id=self.chat_id, - timestamp=time.time(), + timestamp=timestamp, limit=30, ) @@ -42,9 +42,10 @@ class QuestionMaker: async def make_question(self): conflict = await self.get_random_unanswered_conflict() if not conflict: - return None, None + return None, None, None question = conflict.conflict_content conflict_context = conflict.context - chat_context = self.get_context() + create_time = conflict.create_time + chat_context = self.get_context(create_time) - return question, conflict_context \ No newline at end of file + return question, chat_context, conflict_context \ No newline at end of file diff --git a/src/plugins/built_in/relation/plugin.py b/src/plugins/built_in/relation/plugin.py index 577eb94c..8ab48e4d 100644 --- a/src/plugins/built_in/relation/plugin.py +++ b/src/plugins/built_in/relation/plugin.py @@ -77,8 +77,8 @@ class RelationActionsPlugin(BasePlugin): # 配置Schema定义 config_schema: dict = { "plugin": { - "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), - "config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"), + "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + "config_version": ConfigField(type=str, default="1.0.1", description="配置文件版本"), }, "components": { "relation_max_memory_num": ConfigField(type=int, default=10, description="关系记忆最大数量"),