From 35c3fe5b1025bc254320b8d02d172f212eadd617 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 4 Oct 2025 17:18:43 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E6=9D=A5?= =?UTF-8?q?=E8=87=AA=E5=9B=B0=E6=83=91=E5=86=85=E5=AE=B9=E7=9A=84=E4=B8=BB?= =?UTF-8?q?=E5=8A=A8=E6=8F=90=E9=97=AE=E5=8F=91=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/express/expression_learner.py | 4 +- src/chat/heart_flow/heartFC_chat.py | 106 ++++- src/chat/planner_actions/planner.py | 4 +- src/chat/replyer/group_generator.py | 21 +- src/chat/replyer/private_generator.py | 2 +- src/chat/replyer/prompt/replyer_prompt.py | 4 +- src/common/database/database_model.py | 40 +- src/curiousity/questions.py | 239 ------------ src/memory_system/Memory_chest.py | 41 +- src/memory_system/memory_management_task.py | 3 +- src/memory_system/memory_utils.py | 64 +++ src/memory_system/question_maker.py | 46 +++ src/memory_system/questions.py | 406 ++++++++++++++++++++ src/migrate_helper/migrate.py | 1 - src/plugins/built_in/MaiCurious/plugin.py | 17 +- src/plugins/built_in/emoji_plugin/emoji.py | 2 +- 16 files changed, 666 insertions(+), 334 deletions(-) delete mode 100644 src/curiousity/questions.py create mode 100644 src/memory_system/question_maker.py create mode 100644 src/memory_system/questions.py diff --git a/src/chat/express/expression_learner.py b/src/chat/express/expression_learner.py index eeff7ba3..3d736cb6 100644 --- a/src/chat/express/expression_learner.py +++ b/src/chat/express/expression_learner.py @@ -451,7 +451,7 @@ class ExpressionLearner: ) # print(f"random_msg_str:{random_msg_str}") - logger.info(f"学习{type_str}的prompt: {prompt}") + # logger.info(f"学习{type_str}的prompt: {prompt}") try: response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3) @@ -459,7 +459,7 @@ class ExpressionLearner: logger.error(f"学习{type_str}失败: {e}") return None - logger.debug(f"学习{type_str}的response: {response}") + # logger.debug(f"学习{type_str}的response: {response}") expressions: List[Tuple[str, str]] = self.parse_expression_response(response) diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 5b840aee..a205ddb3 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -19,6 +19,8 @@ from src.chat.heart_flow.hfc_utils import CycleDetail from src.chat.heart_flow.hfc_utils import send_typing, stop_typing from src.chat.express.expression_learner import expression_learner_manager from src.chat.frequency_control.frequency_control import frequency_control_manager +from src.memory_system.question_maker import QuestionMaker +from src.memory_system.questions import global_conflict_tracker from src.person_info.person_info import Person from src.plugin_system.base.component_types import EventType, ActionInfo from src.plugin_system.core import events_manager @@ -100,6 +102,8 @@ class HeartFChatting: self.no_reply_until_call = False self.is_mute = False + + self.last_active_time = time.time() # 记录上一次非noreply时间 async def start(self): @@ -176,6 +180,18 @@ class HeartFChatting: filter_command=True, ) + force_reply = False + if time.time() - self.last_active_time > 1: #长久没有回复,可以试试主动发言 + print(f"{self.log_prefix} 长久没有回复,可以试试主动发言") + if random.random() < 0.01: # 30%概率主动发言 + print(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() + await global_conflict_tracker.track_conflict(question, conflict_context, True, self.stream_id) + await self._lift_question_reply(question,cycle_timers,thinking_id) + + if len(recent_messages_list) >= 1: # for message in recent_messages_list: # print(message.processed_plain_text) @@ -509,6 +525,89 @@ 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}\"感到好奇,想要和群友讨论" + new_msg = get_raw_msg_before_timestamp_with_chat( + chat_id=self.stream_id, + timestamp=time.time(), + limit=1, + ) + + reply_action_info = ActionPlannerInfo( + action_type="reply", + reasoning= "", + action_data={}, + action_message=new_msg[0], + available_actions=None, + loop_start_time=time.time(), + action_reasoning=reason) + self.action_planner.add_plan_log(reasoning=reason, 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={ + "raw_reply": f"我对这个问题感到好奇:{question}", + "reason": reason, + }, + ) + + if not success or not llm_response or not llm_response.reply_set: + logger.info("主动提问发言失败") + self.action_planner.add_plan_excute_log(result="主动回复生成失败") + return {"action_type": "reply", "success": False, "result": "主动回复生成失败", "loop_info": None} + + if success: + for reply_seg in llm_response.reply_set.reply_data: + send_data = reply_seg.content + await send_api.text_to_stream( + text=send_data, + stream_id=self.stream_id, + ) + + 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_text": llm_response.reply_set.reply_data[0].content}, + action_name="reply", + ) + + # 构建循环信息 + loop_info: Dict[str, Any] = { + "loop_plan_info": { + "action_result": [reply_action_info], + }, + "loop_action_info": { + "action_taken": True, + "reply_text": llm_response.reply_set.reply_data[0].content, + "command": "", + "taken_time": time.time(), + }, + } + self.last_active_time = time.time() + self.action_planner.add_plan_excute_log(result=f"你提问:{question}") + + return { + "action_type": "reply", + "success": True, + "result": f"你提问:{question}", + "loop_info": loop_info, + } + + async def _send_response( self, reply_set: "ReplySetModel", @@ -519,7 +618,7 @@ class HeartFChatting: chat_id=self.chat_stream.stream_id, start_time=self.last_read_time, end_time=time.time() ) - need_reply = new_message_count >= random.randint(2, 4) + need_reply = new_message_count >= random.randint(2, 3) if need_reply: logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复") @@ -600,8 +699,6 @@ class HeartFChatting: action_name="no_reply_until_call", action_reasoning=reason, ) - - return {"action_type": "no_reply_until_call", "success": True, "result": "保持沉默,直到有人直接叫的名字", "command": ""} elif action_planner_info.action_type == "reply": @@ -650,6 +747,7 @@ class HeartFChatting: actions=chosen_action_plan_infos, selected_expressions=selected_expressions, ) + self.last_active_time = time.time() return { "action_type": "reply", "success": True, @@ -667,6 +765,8 @@ class HeartFChatting: thinking_id = thinking_id, action_message= action_planner_info.action_message, ) + + self.last_active_time = time.time() return { "action_type": action_planner_info.action_type, "success": success, diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 1e773bbd..f76b663b 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -469,8 +469,8 @@ class ActionPlanner: # 调用LLM llm_content, (reasoning_content, _, _) = await self.planner_llm.generate_response_async(prompt=prompt) - logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") - logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") + # logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") + # logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") if global_config.debug.show_prompt: logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 887a9260..9b9d0334 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -7,6 +7,7 @@ import re from typing import List, Optional, Dict, Any, Tuple from datetime import datetime from src.memory_system.Memory_chest import global_memory_chest +from src.memory_system.questions import global_conflict_tracker from src.common.logger import get_logger from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.info_data_model import ActionPlannerInfo @@ -277,6 +278,20 @@ class DefaultReplyer: else: return "" + async def build_question_block(self) -> str: + """构建问题块""" + # if not global_config.question.enable_question: + # return "" + questions = global_conflict_tracker.get_questions_by_chat_id(self.chat_stream.stream_id) + questions_str = "" + for question in questions: + questions_str += f"- {question.question}\n" + if questions_str: + return f"你在聊天中,有以下问题想要得到解答:\n{questions_str}" + else: + return "" + + async def build_tool_info(self, chat_history: str, sender: str, target: str, enable_tool: bool = True) -> str: """构建工具信息块 @@ -619,6 +634,7 @@ class DefaultReplyer: self._time_and_run_task(self.build_actions_prompt(available_actions, chosen_actions), "actions_info"), self._time_and_run_task(self.build_personality_prompt(), "personality_prompt"), self._time_and_run_task(self.build_mood_state_prompt(), "mood_state_prompt"), + self._time_and_run_task(self.build_question_block(), "question_block"), ) # 任务名称中英文映射 @@ -632,6 +648,7 @@ class DefaultReplyer: "actions_info": "动作信息", "personality_prompt": "人格信息", "mood_state_prompt": "情绪状态", + "question_block": "问题", } # 处理结果 @@ -661,6 +678,7 @@ class DefaultReplyer: prompt_info: str = results_dict["prompt_info"] # 直接使用格式化后的结果 actions_info: str = results_dict["actions_info"] personality_prompt: str = results_dict["personality_prompt"] + question_block: str = results_dict["question_block"] keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) mood_state_prompt: str = results_dict["mood_state_prompt"] @@ -704,6 +722,7 @@ class DefaultReplyer: 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 @@ -728,6 +747,7 @@ class DefaultReplyer: 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( @@ -760,7 +780,6 @@ class DefaultReplyer: # 并行执行2个构建任务 (expression_habits_block, _), personality_prompt = await asyncio.gather( self.build_expression_habits(chat_talking_prompt_half, target), - # self.build_relation_info(chat_talking_prompt_half, sender, []), self.build_personality_prompt(), ) diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 707b89c9..0e81ece9 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -288,7 +288,7 @@ class PrivateReplyer: mood_state = await mood_manager.get_mood_by_chat_id(self.chat_stream.stream_id).get_mood() return f"你现在的心情是:{mood_state}" - + async def build_memory_block(self) -> str: """构建记忆块 """ diff --git a/src/chat/replyer/prompt/replyer_prompt.py b/src/chat/replyer/prompt/replyer_prompt.py index 1d3b1517..236eb2b6 100644 --- a/src/chat/replyer/prompt/replyer_prompt.py +++ b/src/chat/replyer/prompt/replyer_prompt.py @@ -13,7 +13,7 @@ def init_replyer_prompt(): Prompt( """{knowledge_prompt}{tool_info_block}{extra_info_block} -{expression_habits_block}{memory_block} +{expression_habits_block}{memory_block}{question_block} 你正在qq群里聊天,下面是群里正在聊的内容: {time_block} @@ -34,7 +34,7 @@ def init_replyer_prompt(): Prompt( """{knowledge_prompt}{tool_info_block}{extra_info_block} -{expression_habits_block}{memory_block} +{expression_habits_block}{memory_block}{question_block} 你正在qq群里聊天,下面是群里正在聊的内容: {time_block} diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index de8eb950..4dc8171a 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -338,44 +338,12 @@ class MemoryConflict(BaseModel): answer = TextField(null=True) # 回答内容 create_time = FloatField() # 创建时间 update_time = FloatField() # 更新时间 + context = TextField(null=True) # 上下文 class Meta: table_name = "memory_conflicts" - - -class GraphNodes(BaseModel): - """ - 用于存储记忆图节点的模型 - """ - - concept = TextField(unique=True, index=True) # 节点概念 - memory_items = TextField() # JSON格式存储的记忆列表 - weight = FloatField(default=0.0) # 节点权重 - hash = TextField() # 节点哈希值 - created_time = FloatField() # 创建时间戳 - last_modified = FloatField() # 最后修改时间戳 - - class Meta: - table_name = "graph_nodes" - - -class GraphEdges(BaseModel): - """ - 用于存储记忆图边的模型 - """ - - source = TextField(index=True) # 源节点 - target = TextField(index=True) # 目标节点 - strength = IntegerField() # 连接强度 - hash = TextField() # 边哈希值 - created_time = FloatField() # 创建时间戳 - last_modified = FloatField() # 最后修改时间戳 - - class Meta: - table_name = "graph_edges" - def create_tables(): """ @@ -393,8 +361,6 @@ def create_tables(): OnlineTime, PersonInfo, Expression, - GraphNodes, # 添加图节点表 - GraphEdges, # 添加图边表 ActionRecords, # 添加 ActionRecords 到初始化列表 MemoryChest, MemoryConflict, # 添加记忆冲突表 @@ -422,8 +388,6 @@ def initialize_database(sync_constraints=False): OnlineTime, PersonInfo, Expression, - GraphNodes, - GraphEdges, ActionRecords, # 添加 ActionRecords 到初始化列表 MemoryChest, MemoryConflict, @@ -521,8 +485,6 @@ def sync_field_constraints(): OnlineTime, PersonInfo, Expression, - GraphNodes, - GraphEdges, ActionRecords, MemoryChest, MemoryConflict, diff --git a/src/curiousity/questions.py b/src/curiousity/questions.py deleted file mode 100644 index 324761ad..00000000 --- a/src/curiousity/questions.py +++ /dev/null @@ -1,239 +0,0 @@ -import time -import asyncio -from src.common.logger import get_logger -from src.common.database.database_model import MemoryConflict -from src.chat.utils.chat_message_builder import ( - get_raw_msg_by_timestamp_with_chat, - build_readable_messages, -) -from src.llm_models.utils_model import LLMRequest -from src.config.config import model_config, global_config - -logger = get_logger("conflict_tracker") - -class QuestionTracker: - """ - 用于跟踪一个问题在后续聊天中的解答情况 - """ - - def __init__(self, question: str, chat_id: str) -> None: - self.question = question - self.chat_id = chat_id - now = time.time() - self.start_time = now - self.last_read_time = now - self.active = True - # 将 LLM 实例作为类属性,使用 utils 模型 - self.llm_request = LLMRequest(model_set=model_config.model_task_config.utils, request_type="conflict.judge") - - def stop(self) -> None: - self.active = False - - async def judge_answer(self, conversation_text: str) -> tuple[bool, str]: - """ - 使用小模型判定问题是否已得到解答。 - 返回 (已解答, 答案) - """ - prompt = ( - "你是一个严谨的判定器。下面给出聊天记录以及一个问题。\n" - "任务:判断在这段聊天中,该问题是否已经得到明确解答。\n" - "如果已解答,请只输出:YES: <简短答案>\n" - "如果没有,请只输出:NO\n\n" - f"问题:{self.question}\n" - "聊天记录如下:\n" - f"{conversation_text}" - ) - - if global_config.debug.show_prompt: - logger.info(f"判定提示词: {prompt}") - else: - logger.debug("已发送判定提示词") - - result_text, _ = await self.llm_request.generate_response_async(prompt, temperature=0.5) - - logger.info(f"判定结果: {prompt}\n{result_text}") - - if not result_text: - return False, "" - - text = result_text.strip() - if text.upper().startswith("YES:"): - answer = text[4:].strip() - return True, answer - if text.upper().startswith("YES"): - # 兼容仅输出 YES 或 YES - answer = text[3:].strip().lstrip(":").strip() - return True, answer - return False, "" - -class ConflictTracker: - """ - 记忆整合冲突追踪器 - - 用于记录和存储记忆整合过程中的冲突内容 - """ - - async def record_conflict(self, conflict_content: str, start_following: bool = False,chat_id: str = "") -> bool: - """ - 记录冲突内容 - - Args:k - conflict_content: 冲突内容 - - Returns: - bool: 是否成功记录 - """ - try: - if not conflict_content or conflict_content.strip() == "": - return False - - # 若需要跟随后续消息以判断是否得到解答,则进入跟踪流程 - if start_following and chat_id: - tracker = QuestionTracker(conflict_content.strip(), chat_id) - # 后台启动跟踪任务,避免阻塞 - asyncio.create_task(self._follow_and_record(tracker, conflict_content.strip())) - return True - - # 默认:直接记录,不进行跟踪 - MemoryConflict.create( - conflict_content=conflict_content, - create_time=time.time(), - update_time=time.time(), - answer="", - ) - - logger.info(f"记录冲突内容: {len(conflict_content)} 字符") - return True - - except Exception as e: - logger.error(f"记录冲突内容时出错: {e}") - return False - - async def _follow_and_record(self, tracker: QuestionTracker, original_question: str) -> None: - """ - 后台任务:跟踪问题是否被解答,并写入数据库。 - """ - try: - max_duration = 30 * 60 # 30 分钟 - max_messages = 100 # 最多 100 条消息 - poll_interval = 2.0 # 秒 - logger.info(f"开始跟踪问题: {original_question}") - while tracker.active: - now_ts = time.time() - # 终止条件:时长达到上限 - if now_ts - tracker.start_time >= max_duration: - logger.info("问题跟踪达到30分钟上限,判定为未解答") - break - - # 统计最近一段是否有新消息(不过滤机器人,过滤命令) - recent_msgs = get_raw_msg_by_timestamp_with_chat( - chat_id=tracker.chat_id, - timestamp_start=tracker.last_read_time, - timestamp_end=now_ts, - limit=30, - limit_mode="latest", - filter_bot=False, - filter_command=True, - ) - - if len(recent_msgs) > 0: - tracker.last_read_time = now_ts - - # 统计从开始到现在的总消息数(用于触发100条上限) - all_msgs = get_raw_msg_by_timestamp_with_chat( - chat_id=tracker.chat_id, - timestamp_start=tracker.start_time, - timestamp_end=now_ts, - limit=0, - limit_mode="latest", - filter_bot=False, - filter_command=True, - ) - - # 构建可读聊天文本 - chat_text = build_readable_messages( - all_msgs, - replace_bot_name=True, - timestamp_mode="relative", - read_mark=0.0, - truncate=False, - show_actions=False, - show_pic=False, - remove_emoji_stickers=True, - ) - - # 让小模型判断是否有答案 - answered, answer_text = await tracker.judge_answer(chat_text) - if answered: - logger.info("问题已得到解答,结束跟踪并写入答案") - tracker.stop() - MemoryConflict.create( - conflict_content=tracker.question, - create_time=tracker.start_time, - update_time=time.time(), - answer=answer_text or "", - ) - return - - if len(all_msgs) >= max_messages: - logger.info("问题跟踪达到100条消息上限,判定为未解答") - break - - # 无新消息时稍作等待 - await asyncio.sleep(poll_interval) - - # 未获取到答案,仅存储问题 - MemoryConflict.create( - conflict_content=original_question, - create_time=time.time(), - update_time=time.time(), - answer="", - ) - logger.info(f"记录冲突内容(未解答): {len(original_question)} 字符") - except Exception as e: - logger.error(f"后台问题跟踪任务异常: {e}") - - async def record_memory_merge_conflict(self, part2_content: str) -> bool: - """ - 记录记忆整合过程中的冲突内容(part2) - - Args: - part2_content: 冲突内容(part2) - - Returns: - bool: 是否成功记录 - """ - if not part2_content or part2_content.strip() == "": - return False - - return await self.record_conflict(part2_content) - - async def get_all_conflicts(self) -> list: - """ - 获取所有冲突记录 - - Returns: - list: 冲突记录列表 - """ - try: - conflicts = list(MemoryConflict.select()) - return conflicts - except Exception as e: - logger.error(f"获取冲突记录时出错: {e}") - return [] - - async def get_conflict_count(self) -> int: - """ - 获取冲突记录数量 - - Returns: - int: 记录数量 - """ - try: - return MemoryConflict.select().count() - except Exception as e: - logger.error(f"获取冲突记录数量时出错: {e}") - return 0 - -# 全局冲突追踪器实例 -global_conflict_tracker = ConflictTracker() \ No newline at end of file diff --git a/src/memory_system/Memory_chest.py b/src/memory_system/Memory_chest.py index aff269c4..7062f73a 100644 --- a/src/memory_system/Memory_chest.py +++ b/src/memory_system/Memory_chest.py @@ -12,9 +12,12 @@ from src.config.config import global_config from src.plugin_system.apis.message_api import build_readable_messages from src.plugin_system.apis.message_api import get_raw_msg_by_timestamp_with_chat from json_repair import repair_json +from src.memory_system.questions import global_conflict_tracker from .memory_utils import ( find_best_matching_memory, - check_title_exists_fuzzy + check_title_exists_fuzzy, + get_all_titles, + ) logger = get_logger("memory") @@ -186,32 +189,6 @@ class MemoryChest: return running_content - - - def get_all_titles(self, exclude_locked: bool = False) -> list[str]: - """ - 获取记忆仓库中的所有标题 - - Args: - exclude_locked: 是否排除锁定的记忆,默认为 False - - Returns: - list: 包含所有标题的列表 - """ - try: - # 查询所有记忆记录的标题 - titles = [] - for memory in MemoryChestModel.select(): - if memory.title: - # 如果 exclude_locked 为 True 且记忆已锁定,则跳过 - if exclude_locked and memory.locked: - continue - titles.append(memory.title) - return titles - except Exception as e: - print(f"获取记忆标题时出错: {e}") - return [] - async def get_answer_by_question(self, chat_id: str = "", question: str = "") -> str: """ 根据问题获取答案 @@ -306,7 +283,7 @@ class MemoryChest: str: 选择的标题 """ # 获取所有标题并构建格式化字符串(排除锁定的记忆) - titles = self.get_all_titles(exclude_locked=True) + titles = get_all_titles(exclude_locked=True) formatted_titles = "" for title in titles: formatted_titles += f"{title}\n" @@ -329,7 +306,7 @@ class MemoryChest: title, (reasoning_content, model_name, tool_calls) = await self.LLMRequest.generate_response_async(prompt) # 根据 title 获取 titles 里的对应项 - titles = self.get_all_titles() + titles = get_all_titles() selected_title = None # 使用模糊查找匹配标题 @@ -427,7 +404,7 @@ class MemoryChest: list[str]: 选中的记忆内容列表 """ try: - all_titles = self.get_all_titles(exclude_locked=True) + all_titles = get_all_titles(exclude_locked=True) content = "" display_index = 1 for title in all_titles: @@ -462,7 +439,7 @@ class MemoryChest: 请输出JSON格式,不要输出其他内容: """ - logger.info(f"选择合并目标 prompt: {prompt}") + # logger.info(f"选择合并目标 prompt: {prompt}") if global_config.debug.show_prompt: logger.info(f"选择合并目标 prompt: {prompt}") @@ -692,8 +669,6 @@ class MemoryChest: # 处理part2:独立记录冲突内容(无论part1是否为空) if part2_content and part2_content.strip() != "none": logger.info(f"合并记忆part2记录冲突内容: {len(part2_content)} 字符") - # 导入冲突追踪器 - from src.curiousity.questions import global_conflict_tracker # 记录冲突到数据库 await global_conflict_tracker.record_memory_merge_conflict(part2_content) diff --git a/src/memory_system/memory_management_task.py b/src/memory_system/memory_management_task.py index 0a940e32..09e90a71 100644 --- a/src/memory_system/memory_management_task.py +++ b/src/memory_system/memory_management_task.py @@ -8,6 +8,7 @@ from src.memory_system.Memory_chest import global_memory_chest from src.common.logger import get_logger from src.common.database.database_model import MemoryChest as MemoryChestModel from src.config.config import global_config +from src.memory_system.memory_utils import get_all_titles logger = get_logger("memory") @@ -130,7 +131,7 @@ class MemoryManagementTask(AsyncTask): """随机获取一个记忆标题""" try: # 获取所有记忆标题 - all_titles = global_memory_chest.get_all_titles() + all_titles = get_all_titles() if not all_titles: return "" diff --git a/src/memory_system/memory_utils.py b/src/memory_system/memory_utils.py index ce65bd1f..c9297691 100644 --- a/src/memory_system/memory_utils.py +++ b/src/memory_system/memory_utils.py @@ -3,15 +3,79 @@ 记忆系统工具函数 包含模糊查找、相似度计算等工具函数 """ +import json import re from difflib import SequenceMatcher from typing import List, Tuple, Optional from src.common.database.database_model import MemoryChest as MemoryChestModel from src.common.logger import get_logger +from json_repair import repair_json + logger = get_logger("memory_utils") +def get_all_titles(exclude_locked: bool = False) -> list[str]: + """ + 获取记忆仓库中的所有标题 + + Args: + exclude_locked: 是否排除锁定的记忆,默认为 False + + Returns: + list: 包含所有标题的列表 + """ + try: + # 查询所有记忆记录的标题 + titles = [] + for memory in MemoryChestModel.select(): + if memory.title: + # 如果 exclude_locked 为 True 且记忆已锁定,则跳过 + if exclude_locked and memory.locked: + continue + titles.append(memory.title) + return titles + except Exception as e: + print(f"获取记忆标题时出错: {e}") + return [] + +def parse_md_json(json_text: str) -> list[str]: + """从Markdown格式的内容中提取JSON对象和推理内容""" + json_objects = [] + reasoning_content = "" + + # 使用正则表达式查找```json包裹的JSON内容 + json_pattern = r"```json\s*(.*?)\s*```" + matches = re.findall(json_pattern, json_text, re.DOTALL) + + # 提取JSON之前的内容作为推理文本 + if matches: + # 找到第一个```json的位置 + first_json_pos = json_text.find("```json") + if first_json_pos > 0: + reasoning_content = json_text[:first_json_pos].strip() + # 清理推理内容中的注释标记 + reasoning_content = re.sub(r"^//\s*", "", reasoning_content, flags=re.MULTILINE) + reasoning_content = reasoning_content.strip() + + for match in matches: + try: + # 清理可能的注释和格式问题 + json_str = re.sub(r"//.*?\n", "\n", match) # 移除单行注释 + json_str = re.sub(r"/\*.*?\*/", "", json_str, flags=re.DOTALL) # 移除多行注释 + if json_str := json_str.strip(): + json_obj = json.loads(json_str) + if isinstance(json_obj, dict): + json_objects.append(json_obj) + elif isinstance(json_obj, list): + for item in json_obj: + if isinstance(item, dict): + json_objects.append(item) + except Exception as e: + logger.warning(f"解析JSON块失败: {e}, 块内容: {match[:100]}...") + continue + + return json_objects, reasoning_content def calculate_similarity(text1: str, text2: str) -> float: """ diff --git a/src/memory_system/question_maker.py b/src/memory_system/question_maker.py new file mode 100644 index 00000000..3fe18d85 --- /dev/null +++ b/src/memory_system/question_maker.py @@ -0,0 +1,46 @@ +import time +import random +from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages +from src.common.database.database_model import MemoryConflict +from src.config.config import global_config + + +class QuestionMaker: + def __init__(self, chat_id: str,context: str = ""): + self.chat_id = chat_id + self.context = context + + def get_context(self): + latest_30_msgs = get_raw_msg_before_timestamp_with_chat( + chat_id=self.chat_id, + timestamp=time.time(), + limit=30, + ) + + all_dialogue_prompt_str = build_readable_messages( + latest_30_msgs, + replace_bot_name=True, + timestamp_mode="normal_no_YMD", + ) + return all_dialogue_prompt_str + + + async def get_all_conflicts(self): + conflicts = list(MemoryConflict.select()) + return conflicts + + async def get_un_answered_conflict(self): + conflicts = await self.get_all_conflicts() + return [conflict for conflict in conflicts if not conflict.answer] + + async def get_random_unanswered_conflict(self): + conflicts = await self.get_un_answered_conflict() + return random.choice(conflicts) + + async def make_question(self): + conflict = await self.get_random_unanswered_conflict() + question = conflict.conflict_content + conflict_context = conflict.context + chat_context = self.get_context() + + return question, conflict_context \ No newline at end of file diff --git a/src/memory_system/questions.py b/src/memory_system/questions.py new file mode 100644 index 00000000..72367809 --- /dev/null +++ b/src/memory_system/questions.py @@ -0,0 +1,406 @@ +import time +import asyncio +from src.common.logger import get_logger +from src.common.database.database_model import MemoryConflict +from src.chat.utils.chat_message_builder import ( + get_raw_msg_by_timestamp_with_chat, + build_readable_messages, +) +from src.llm_models.utils_model import LLMRequest +from src.config.config import model_config, global_config +from typing import List +from src.memory_system.memory_utils import parse_md_json + +logger = get_logger("conflict_tracker") + +class QuestionTracker: + """ + 用于跟踪一个问题在后续聊天中的解答情况 + """ + + def __init__(self, question: str, chat_id: str, context: str = "") -> None: + self.question = question + self.chat_id = chat_id + now = time.time() + self.context = context + self.start_time = now + self.last_read_time = now + self.last_judge_time = now # 上次判定的时间 + self.judge_debounce_interval = 10.0 # 判定防抖间隔:10秒 + self.consecutive_end_count = 0 # 连续END计数 + self.active = True + # 将 LLM 实例作为类属性,使用 utils 模型 + self.llm_request = LLMRequest(model_set=model_config.model_task_config.utils, request_type="conflict.judge") + + def stop(self) -> None: + self.active = False + + def should_judge_now(self) -> bool: + """ + 检查是否应该进行判定(防抖检查) + + Returns: + bool: 是否可以判定 + """ + now = time.time() + # 检查是否已经过了10秒的防抖间隔 + return (now - self.last_judge_time) >= self.judge_debounce_interval + + def __eq__(self, other) -> bool: + """比较两个追踪器是否相等(基于问题内容和聊天ID)""" + if not isinstance(other, QuestionTracker): + return False + return self.question == other.question and self.chat_id == other.chat_id + + def __hash__(self) -> int: + """为对象提供哈希值,支持集合操作""" + return hash((self.question, self.chat_id)) + + async def judge_answer(self, conversation_text: str) -> tuple[bool, str, str]: + """ + 使用模型判定问题是否已得到解答。 + + Returns: + tuple[bool, str, str]: (是否结束跟踪, 结束原因或答案, 判定类型) + - True: 结束跟踪(已解答、话题转向等) + - False: 继续跟踪 + 判定类型: "ANSWERED", "END", "CONTINUE" + """ + prompt = f"""你是一个严谨的判定器。下面给出聊天记录以及一个问题。 +任务:判断在这段聊天中,该问题是否已经得到明确解答。 +**你必须严格按照聊天记录的内容,不要添加额外的信息** + +输出规则: +- 如果聊天记录内容的信息已解答问题,请只输出:YES: <简短答案> +- 如果聊天记录内容与问题无关,话题已转向其他方向,请只输出:END +- 如果问题尚未解答但聊天仍在相关话题上,请只输出:NO + +**问题** +{self.question} + + +**聊天记录** +{conversation_text} +""" + + if global_config.debug.show_prompt: + logger.info(f"判定提示词: {prompt}") + else: + logger.debug("已发送判定提示词") + + result_text, _ = await self.llm_request.generate_response_async(prompt, temperature=0.5) + + logger.info(f"判定结果: {prompt}\n{result_text}") + + # 更新上次判定时间 + self.last_judge_time = time.time() + + if not result_text: + return False, "", "CONTINUE" + + text = result_text.strip() + if text.upper().startswith("YES:"): + answer = text[4:].strip() + return True, answer, "ANSWERED" + if text.upper().startswith("YES"): + # 兼容仅输出 YES 或 YES + answer = text[3:].strip().lstrip(":").strip() + return True, answer, "ANSWERED" + if text.upper().startswith("END"): + # 聊天内容与问题无关,放弃该问题思考 + return True, "话题已转向其他方向,放弃该问题思考", "END" + return False, "", "CONTINUE" + +class ConflictTracker: + """ + 记忆整合冲突追踪器 + + 用于记录和存储记忆整合过程中的冲突内容 + """ + def __init__(self): + self.question_tracker_list:List[QuestionTracker] = [] + + self.LLMRequest_tracker = LLMRequest( + model_set=model_config.model_task_config.utils, + request_type="conflict_tracker", + ) + + def get_questions_by_chat_id(self, chat_id: str) -> List[QuestionTracker]: + return [tracker for tracker in self.question_tracker_list if tracker.chat_id == chat_id] + + async def track_conflict(self, question: str, context: str = "",start_following: bool = False,chat_id: str = "") -> bool: + """ + 跟踪冲突内容 + """ + tracker = QuestionTracker(question.strip(), chat_id, context) + self.question_tracker_list.append(tracker) + asyncio.create_task(self._follow_and_record(tracker, question.strip())) + return True + + async def record_conflict(self, conflict_content: str, context: str = "",start_following: bool = False,chat_id: str = "") -> bool: + """ + 记录冲突内容 + + Args:k + conflict_content: 冲突内容 + + Returns: + bool: 是否成功记录 + """ + try: + if not conflict_content or conflict_content.strip() == "": + return False + + # 若需要跟随后续消息以判断是否得到解答,则进入跟踪流程 + if start_following and chat_id: + tracker = QuestionTracker(conflict_content.strip(), chat_id, context) + self.question_tracker_list.append(tracker) + # 后台启动跟踪任务,避免阻塞 + asyncio.create_task(self._follow_and_record(tracker, conflict_content.strip())) + return True + + # 默认:直接记录,不进行跟踪 + MemoryConflict.create( + conflict_content=conflict_content, + create_time=time.time(), + update_time=time.time(), + answer="", + ) + + logger.info(f"记录冲突内容: {len(conflict_content)} 字符") + return True + + except Exception as e: + logger.error(f"记录冲突内容时出错: {e}") + return False + + async def _follow_and_record(self, tracker: QuestionTracker, original_question: str) -> None: + """ + 后台任务:跟踪问题是否被解答,并写入数据库。 + """ + try: + max_duration = 10 * 60 # 30 分钟 + max_messages = 50 # 最多 100 条消息 + poll_interval = 2.0 # 秒 + logger.info(f"开始跟踪问题: {original_question}") + while tracker.active: + now_ts = time.time() + # 终止条件:时长达到上限 + if now_ts - tracker.start_time >= max_duration: + logger.info("问题跟踪达到10分钟上限,判定为未解答") + break + + # 统计最近一段是否有新消息(不过滤机器人,过滤命令) + recent_msgs = get_raw_msg_by_timestamp_with_chat( + chat_id=tracker.chat_id, + timestamp_start=tracker.last_read_time, + timestamp_end=now_ts, + limit=30, + limit_mode="latest", + filter_bot=False, + filter_command=True, + ) + + if len(recent_msgs) > 0: + tracker.last_read_time = now_ts + + # 统计从开始到现在的总消息数(用于触发100条上限) + all_msgs = get_raw_msg_by_timestamp_with_chat( + chat_id=tracker.chat_id, + timestamp_start=tracker.start_time, + timestamp_end=now_ts, + limit=0, + limit_mode="latest", + filter_bot=False, + filter_command=True, + ) + + # 检查是否应该进行判定(防抖检查) + if not tracker.should_judge_now(): + logger.debug(f"判定防抖中,跳过本次判定: {tracker.question}") + await asyncio.sleep(poll_interval) + continue + + # 构建可读聊天文本 + chat_text = build_readable_messages( + all_msgs, + replace_bot_name=True, + timestamp_mode="relative", + read_mark=0.0, + truncate=False, + show_actions=False, + show_pic=False, + remove_emoji_stickers=True, + ) + + # 让小模型判断是否有答案 + answered, answer_text, judge_type = await tracker.judge_answer(chat_text) + + if judge_type == "ANSWERED": + # 问题已解答,直接结束跟踪 + logger.info("问题已得到解答,结束跟踪并写入答案") + await self.add_or_update_conflict( + conflict_content=tracker.question, + create_time=tracker.start_time, + update_time=time.time(), + answer=answer_text or "", + ) + return + elif judge_type == "END": + # 话题转向,增加END计数 + tracker.consecutive_end_count += 1 + logger.info(f"话题已转向,连续END次数: {tracker.consecutive_end_count}") + + if tracker.consecutive_end_count >= 2: + # 连续两次END,结束跟踪 + logger.info("连续两次END,结束跟踪") + break + else: + # 第一次END,重置计数器并继续跟踪 + logger.info("第一次END,继续跟踪") + continue + elif judge_type == "CONTINUE": + # 继续跟踪,重置END计数器 + tracker.consecutive_end_count = 0 + continue + + if len(all_msgs) >= max_messages: + logger.info("问题跟踪达到100条消息上限,判定为未解答") + logger.info(f"追踪结束:{tracker.question}") + break + + # 无新消息时稍作等待 + await asyncio.sleep(poll_interval) + + # 未获取到答案,仅存储问题 + await self.add_or_update_conflict( + conflict_content=original_question, + create_time=time.time(), + update_time=time.time(), + answer="", + ) + logger.info(f"记录冲突内容(未解答): {len(original_question)} 字符") + logger.info(f"问题跟踪结束:{original_question}") + except Exception as e: + logger.error(f"后台问题跟踪任务异常: {e}") + finally: + # 无论任务成功还是失败,都要从追踪列表中移除 + tracker.stop() + self.remove_tracker(tracker) + + def remove_tracker(self, tracker: QuestionTracker) -> None: + """ + 从追踪列表中移除指定的追踪器 + + Args: + tracker: 要移除的追踪器对象 + """ + try: + if tracker in self.question_tracker_list: + self.question_tracker_list.remove(tracker) + logger.info(f"已从追踪列表中移除追踪器: {tracker.question}") + else: + logger.warning(f"尝试移除不存在的追踪器: {tracker.question}") + except Exception as e: + logger.error(f"移除追踪器时出错: {e}") + + async def add_or_update_conflict(self,conflict_content: str,create_time: float,update_time: float,answer: str = "",context: str = "") -> bool: + """ + 根据conflict_content匹配数据库内容,如果找到相同的就更新update_time和answer, + 如果没有相同的,就新建一条保存全部内容 + """ + try: + # 尝试根据conflict_content查找现有记录 + existing_conflict = MemoryConflict.get_or_none( + MemoryConflict.conflict_content == conflict_content + ) + + if existing_conflict: + # 如果找到相同的conflict_content,更新update_time和answer + existing_conflict.update_time = update_time + existing_conflict.answer = answer + existing_conflict.save() + return True + else: + # 如果没有找到相同的,创建新记录 + MemoryConflict.create( + conflict_content=conflict_content, + create_time=create_time, + update_time=update_time, + answer=answer, + context=context, + ) + return True + except Exception as e: + # 记录错误并返回False + logger.error(f"添加或更新冲突记录时出错: {e}") + return False + + async def record_memory_merge_conflict(self, part2_content: str) -> bool: + """ + 记录记忆整合过程中的冲突内容(part2) + + Args: + part2_content: 冲突内容(part2) + + Returns: + bool: 是否成功记录 + """ + if not part2_content or part2_content.strip() == "": + return False + + prompt = f"""以下是一段有冲突的信息,请你根据这些信息总结出几个具体的提问: +冲突信息: +{part2_content} + +要求: +1.提问必须具体,明确 +2.提问最好涉及指向明确的事物,而不是代称 +3.如果缺少上下文,不要强行提问,可以忽略 + +请用json格式输出,不要输出其他内容,仅输出提问理由和具体提的提问: +**示例** +// 理由文本 +```json +{{ + "question":"提问", +}} +``` +```json +{{ + "question":"提问" +}} +``` +...提问数量在1-3个之间,不要重复,现在请输出:""" + + question_response, (reasoning_content, model_name, tool_calls) = await self.LLMRequest_tracker.generate_response_async(prompt) + + # 解析JSON响应 + questions, reasoning_content = parse_md_json(question_response) + + print(prompt) + print(question_response) + + for question in questions: + await self.record_conflict( + conflict_content=question["question"], + context=reasoning_content, + start_following=False, + ) + return True + + async def get_conflict_count(self) -> int: + """ + 获取冲突记录数量 + + Returns: + int: 记录数量 + """ + try: + return MemoryConflict.select().count() + except Exception as e: + logger.error(f"获取冲突记录数量时出错: {e}") + return 0 + +# 全局冲突追踪器实例 +global_conflict_tracker = ConflictTracker() \ No newline at end of file diff --git a/src/migrate_helper/migrate.py b/src/migrate_helper/migrate.py index a556414f..487b1f3f 100644 --- a/src/migrate_helper/migrate.py +++ b/src/migrate_helper/migrate.py @@ -1,7 +1,6 @@ import json import os import asyncio -from src.common.database.database_model import GraphNodes from src.common.logger import get_logger logger = get_logger("migrate") diff --git a/src/plugins/built_in/MaiCurious/plugin.py b/src/plugins/built_in/MaiCurious/plugin.py index 901d8de5..fa984a03 100644 --- a/src/plugins/built_in/MaiCurious/plugin.py +++ b/src/plugins/built_in/MaiCurious/plugin.py @@ -16,7 +16,7 @@ from src.plugin_system.base.component_types import ActionActivationType from src.plugin_system.apis import config_api from src.plugin_system.apis import frequency_api from src.plugin_system.apis import generator_api -from src.curiousity.questions import global_conflict_tracker +from src.memory_system.questions import global_conflict_tracker logger = get_logger("question_actions") @@ -25,7 +25,7 @@ logger = get_logger("question_actions") class CuriousAction(BaseAction): """频率调节动作 - 调整聊天发言频率""" - activation_type = ActionActivationType.LLM_JUDGE + activation_type = ActionActivationType.ALWAYS parallel_action = True # 动作基本信息 @@ -37,10 +37,6 @@ class CuriousAction(BaseAction): action_parameters = { "question": "对存在疑问的信息提出一个问题,描述全面", } - - # 动作使用场景 - bot_name = config_api.get_global_config("bot.nickname") - action_require = [ f"当聊天记录中的信息存在逻辑上的矛盾时使用", @@ -56,6 +52,9 @@ class CuriousAction(BaseAction): async def execute(self) -> Tuple[bool, str]: """执行频率调节动作""" try: + if len(global_conflict_tracker.question_tracker_list) > 3: + return False, "当前有太多问题,请先解答完再提问,不要再使用make_question动作" + question = self.action_data.get("question", "") # 存储问题到冲突追踪器 @@ -64,10 +63,10 @@ class CuriousAction(BaseAction): logger.info(f"已存储问题到冲突追踪器: {question}") await self.store_action_info( action_build_into_prompt=True, - action_prompt_display=f"你产生了一个问题:{question},尝试向其他人提问或回忆", + action_prompt_display=f"你产生了一个问题:{question}", action_done=True, ) - return True, "问题已记录" + return True, f"问题{question}已记录,不要重复提问该问题" except Exception as e: error_msg = f"问题生成失败: {str(e)}" logger.error(f"{self.log_prefix} {error_msg}", exc_info=True) @@ -103,7 +102,7 @@ class CuriousPlugin(BasePlugin): # 配置Schema定义 config_schema: dict = { "plugin": { - "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), "config_version": ConfigField(type=str, default="3.0.0", description="配置文件版本"), } } diff --git a/src/plugins/built_in/emoji_plugin/emoji.py b/src/plugins/built_in/emoji_plugin/emoji.py index 91f3ff0e..7078a817 100644 --- a/src/plugins/built_in/emoji_plugin/emoji.py +++ b/src/plugins/built_in/emoji_plugin/emoji.py @@ -138,7 +138,7 @@ class EmojiAction(BaseAction): action_prompt_display=f"你发送了表情包,原因:{reason}", action_done=True, ) - return True, f"成功发送表情包:{emoji_description}" + return True, f"成功发送表情包:[表情包:{chosen_emotion}]" else: error_msg = "发送表情包失败" logger.error(f"{self.log_prefix} {error_msg}")