feat:添加来自困惑内容的主动提问发言

pull/1294/head
SengokuCola 2025-10-04 17:18:43 +08:00
parent eaf218ff15
commit 35c3fe5b10
16 changed files with 666 additions and 334 deletions

View File

@ -451,7 +451,7 @@ class ExpressionLearner:
) )
# print(f"random_msg_str:{random_msg_str}") # print(f"random_msg_str:{random_msg_str}")
logger.info(f"学习{type_str}的prompt: {prompt}") # logger.info(f"学习{type_str}的prompt: {prompt}")
try: try:
response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3) 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}") logger.error(f"学习{type_str}失败: {e}")
return None 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) expressions: List[Tuple[str, str]] = self.parse_expression_response(response)

View File

@ -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.heart_flow.hfc_utils import send_typing, stop_typing
from src.chat.express.expression_learner import expression_learner_manager from src.chat.express.expression_learner import expression_learner_manager
from src.chat.frequency_control.frequency_control import frequency_control_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.person_info.person_info import Person
from src.plugin_system.base.component_types import EventType, ActionInfo from src.plugin_system.base.component_types import EventType, ActionInfo
from src.plugin_system.core import events_manager from src.plugin_system.core import events_manager
@ -101,6 +103,8 @@ class HeartFChatting:
self.is_mute = False self.is_mute = False
self.last_active_time = time.time() # 记录上一次非noreply时间
async def start(self): async def start(self):
"""检查是否需要启动主循环,如果未激活则启动。""" """检查是否需要启动主循环,如果未激活则启动。"""
@ -176,6 +180,18 @@ class HeartFChatting:
filter_command=True, 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: if len(recent_messages_list) >= 1:
# for message in recent_messages_list: # for message in recent_messages_list:
# print(message.processed_plain_text) # print(message.processed_plain_text)
@ -509,6 +525,89 @@ class HeartFChatting:
traceback.print_exc() traceback.print_exc()
return False, "" 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( async def _send_response(
self, self,
reply_set: "ReplySetModel", 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() 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: if need_reply:
logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复") logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复")
@ -600,8 +699,6 @@ class HeartFChatting:
action_name="no_reply_until_call", action_name="no_reply_until_call",
action_reasoning=reason, action_reasoning=reason,
) )
return {"action_type": "no_reply_until_call", "success": True, "result": "保持沉默,直到有人直接叫的名字", "command": ""} return {"action_type": "no_reply_until_call", "success": True, "result": "保持沉默,直到有人直接叫的名字", "command": ""}
elif action_planner_info.action_type == "reply": elif action_planner_info.action_type == "reply":
@ -650,6 +747,7 @@ class HeartFChatting:
actions=chosen_action_plan_infos, actions=chosen_action_plan_infos,
selected_expressions=selected_expressions, selected_expressions=selected_expressions,
) )
self.last_active_time = time.time()
return { return {
"action_type": "reply", "action_type": "reply",
"success": True, "success": True,
@ -667,6 +765,8 @@ class HeartFChatting:
thinking_id = thinking_id, thinking_id = thinking_id,
action_message= action_planner_info.action_message, action_message= action_planner_info.action_message,
) )
self.last_active_time = time.time()
return { return {
"action_type": action_planner_info.action_type, "action_type": action_planner_info.action_type,
"success": success, "success": success,

View File

@ -469,8 +469,8 @@ class ActionPlanner:
# 调用LLM # 调用LLM
llm_content, (reasoning_content, _, _) = await self.planner_llm.generate_response_async(prompt=prompt) 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}规划器原始提示词: {prompt}")
logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") # logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}")
if global_config.debug.show_prompt: if global_config.debug.show_prompt:
logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}")

View File

@ -7,6 +7,7 @@ import re
from typing import List, Optional, Dict, Any, Tuple from typing import List, Optional, Dict, Any, Tuple
from datetime import datetime from datetime import datetime
from src.memory_system.Memory_chest import global_memory_chest 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.logger import get_logger
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
from src.common.data_models.info_data_model import ActionPlannerInfo from src.common.data_models.info_data_model import ActionPlannerInfo
@ -277,6 +278,20 @@ class DefaultReplyer:
else: else:
return "" 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: 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_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_personality_prompt(), "personality_prompt"),
self._time_and_run_task(self.build_mood_state_prompt(), "mood_state_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": "动作信息", "actions_info": "动作信息",
"personality_prompt": "人格信息", "personality_prompt": "人格信息",
"mood_state_prompt": "情绪状态", "mood_state_prompt": "情绪状态",
"question_block": "问题",
} }
# 处理结果 # 处理结果
@ -661,6 +678,7 @@ class DefaultReplyer:
prompt_info: str = results_dict["prompt_info"] # 直接使用格式化后的结果 prompt_info: str = results_dict["prompt_info"] # 直接使用格式化后的结果
actions_info: str = results_dict["actions_info"] actions_info: str = results_dict["actions_info"]
personality_prompt: str = results_dict["personality_prompt"] personality_prompt: str = results_dict["personality_prompt"]
question_block: str = results_dict["question_block"]
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
mood_state_prompt: str = results_dict["mood_state_prompt"] mood_state_prompt: str = results_dict["mood_state_prompt"]
@ -704,6 +722,7 @@ class DefaultReplyer:
target=target, target=target,
reason=reply_reason, reason=reply_reason,
reply_style=global_config.personality.reply_style, reply_style=global_config.personality.reply_style,
question_block=question_block,
keywords_reaction_prompt=keywords_reaction_prompt, keywords_reaction_prompt=keywords_reaction_prompt,
moderation_prompt=moderation_prompt_block, moderation_prompt=moderation_prompt_block,
), selected_expressions ), selected_expressions
@ -728,6 +747,7 @@ class DefaultReplyer:
reply_style=global_config.personality.reply_style, reply_style=global_config.personality.reply_style,
keywords_reaction_prompt=keywords_reaction_prompt, keywords_reaction_prompt=keywords_reaction_prompt,
moderation_prompt=moderation_prompt_block, moderation_prompt=moderation_prompt_block,
question_block=question_block,
), selected_expressions ), selected_expressions
async def build_prompt_rewrite_context( async def build_prompt_rewrite_context(
@ -760,7 +780,6 @@ class DefaultReplyer:
# 并行执行2个构建任务 # 并行执行2个构建任务
(expression_habits_block, _), personality_prompt = await asyncio.gather( (expression_habits_block, _), personality_prompt = await asyncio.gather(
self.build_expression_habits(chat_talking_prompt_half, target), self.build_expression_habits(chat_talking_prompt_half, target),
# self.build_relation_info(chat_talking_prompt_half, sender, []),
self.build_personality_prompt(), self.build_personality_prompt(),
) )

View File

@ -13,7 +13,7 @@ def init_replyer_prompt():
Prompt( Prompt(
"""{knowledge_prompt}{tool_info_block}{extra_info_block} """{knowledge_prompt}{tool_info_block}{extra_info_block}
{expression_habits_block}{memory_block} {expression_habits_block}{memory_block}{question_block}
你正在qq群里聊天下面是群里正在聊的内容: 你正在qq群里聊天下面是群里正在聊的内容:
{time_block} {time_block}
@ -34,7 +34,7 @@ def init_replyer_prompt():
Prompt( Prompt(
"""{knowledge_prompt}{tool_info_block}{extra_info_block} """{knowledge_prompt}{tool_info_block}{extra_info_block}
{expression_habits_block}{memory_block} {expression_habits_block}{memory_block}{question_block}
你正在qq群里聊天下面是群里正在聊的内容: 你正在qq群里聊天下面是群里正在聊的内容:
{time_block} {time_block}

View File

@ -338,45 +338,13 @@ class MemoryConflict(BaseModel):
answer = TextField(null=True) # 回答内容 answer = TextField(null=True) # 回答内容
create_time = FloatField() # 创建时间 create_time = FloatField() # 创建时间
update_time = FloatField() # 更新时间 update_time = FloatField() # 更新时间
context = TextField(null=True) # 上下文
class Meta: class Meta:
table_name = "memory_conflicts" 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(): def create_tables():
""" """
创建所有在模型中定义的数据库表 创建所有在模型中定义的数据库表
@ -393,8 +361,6 @@ def create_tables():
OnlineTime, OnlineTime,
PersonInfo, PersonInfo,
Expression, Expression,
GraphNodes, # 添加图节点表
GraphEdges, # 添加图边表
ActionRecords, # 添加 ActionRecords 到初始化列表 ActionRecords, # 添加 ActionRecords 到初始化列表
MemoryChest, MemoryChest,
MemoryConflict, # 添加记忆冲突表 MemoryConflict, # 添加记忆冲突表
@ -422,8 +388,6 @@ def initialize_database(sync_constraints=False):
OnlineTime, OnlineTime,
PersonInfo, PersonInfo,
Expression, Expression,
GraphNodes,
GraphEdges,
ActionRecords, # 添加 ActionRecords 到初始化列表 ActionRecords, # 添加 ActionRecords 到初始化列表
MemoryChest, MemoryChest,
MemoryConflict, MemoryConflict,
@ -521,8 +485,6 @@ def sync_field_constraints():
OnlineTime, OnlineTime,
PersonInfo, PersonInfo,
Expression, Expression,
GraphNodes,
GraphEdges,
ActionRecords, ActionRecords,
MemoryChest, MemoryChest,
MemoryConflict, MemoryConflict,

View File

@ -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>
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()

View File

@ -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 build_readable_messages
from src.plugin_system.apis.message_api import get_raw_msg_by_timestamp_with_chat from src.plugin_system.apis.message_api import get_raw_msg_by_timestamp_with_chat
from json_repair import repair_json from json_repair import repair_json
from src.memory_system.questions import global_conflict_tracker
from .memory_utils import ( from .memory_utils import (
find_best_matching_memory, find_best_matching_memory,
check_title_exists_fuzzy check_title_exists_fuzzy,
get_all_titles,
) )
logger = get_logger("memory") logger = get_logger("memory")
@ -186,32 +189,6 @@ class MemoryChest:
return running_content 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: async def get_answer_by_question(self, chat_id: str = "", question: str = "") -> str:
""" """
根据问题获取答案 根据问题获取答案
@ -306,7 +283,7 @@ class MemoryChest:
str: 选择的标题 str: 选择的标题
""" """
# 获取所有标题并构建格式化字符串(排除锁定的记忆) # 获取所有标题并构建格式化字符串(排除锁定的记忆)
titles = self.get_all_titles(exclude_locked=True) titles = get_all_titles(exclude_locked=True)
formatted_titles = "" formatted_titles = ""
for title in titles: for title in titles:
formatted_titles += f"{title}\n" 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, (reasoning_content, model_name, tool_calls) = await self.LLMRequest.generate_response_async(prompt)
# 根据 title 获取 titles 里的对应项 # 根据 title 获取 titles 里的对应项
titles = self.get_all_titles() titles = get_all_titles()
selected_title = None selected_title = None
# 使用模糊查找匹配标题 # 使用模糊查找匹配标题
@ -427,7 +404,7 @@ class MemoryChest:
list[str]: 选中的记忆内容列表 list[str]: 选中的记忆内容列表
""" """
try: try:
all_titles = self.get_all_titles(exclude_locked=True) all_titles = get_all_titles(exclude_locked=True)
content = "" content = ""
display_index = 1 display_index = 1
for title in all_titles: for title in all_titles:
@ -462,7 +439,7 @@ class MemoryChest:
请输出JSON格式不要输出其他内容 请输出JSON格式不要输出其他内容
""" """
logger.info(f"选择合并目标 prompt: {prompt}") # logger.info(f"选择合并目标 prompt: {prompt}")
if global_config.debug.show_prompt: if global_config.debug.show_prompt:
logger.info(f"选择合并目标 prompt: {prompt}") logger.info(f"选择合并目标 prompt: {prompt}")
@ -692,8 +669,6 @@ class MemoryChest:
# 处理part2独立记录冲突内容无论part1是否为空 # 处理part2独立记录冲突内容无论part1是否为空
if part2_content and part2_content.strip() != "none": if part2_content and part2_content.strip() != "none":
logger.info(f"合并记忆part2记录冲突内容: {len(part2_content)} 字符") 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) await global_conflict_tracker.record_memory_merge_conflict(part2_content)

View File

@ -8,6 +8,7 @@ from src.memory_system.Memory_chest import global_memory_chest
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.database.database_model import MemoryChest as MemoryChestModel from src.common.database.database_model import MemoryChest as MemoryChestModel
from src.config.config import global_config from src.config.config import global_config
from src.memory_system.memory_utils import get_all_titles
logger = get_logger("memory") logger = get_logger("memory")
@ -130,7 +131,7 @@ class MemoryManagementTask(AsyncTask):
"""随机获取一个记忆标题""" """随机获取一个记忆标题"""
try: try:
# 获取所有记忆标题 # 获取所有记忆标题
all_titles = global_memory_chest.get_all_titles() all_titles = get_all_titles()
if not all_titles: if not all_titles:
return "" return ""

View File

@ -3,15 +3,79 @@
记忆系统工具函数 记忆系统工具函数
包含模糊查找相似度计算等工具函数 包含模糊查找相似度计算等工具函数
""" """
import json
import re import re
from difflib import SequenceMatcher from difflib import SequenceMatcher
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from src.common.database.database_model import MemoryChest as MemoryChestModel from src.common.database.database_model import MemoryChest as MemoryChestModel
from src.common.logger import get_logger from src.common.logger import get_logger
from json_repair import repair_json
logger = get_logger("memory_utils") 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: def calculate_similarity(text1: str, text2: str) -> float:
""" """

View File

@ -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

View File

@ -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>
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()

View File

@ -1,7 +1,6 @@
import json import json
import os import os
import asyncio import asyncio
from src.common.database.database_model import GraphNodes
from src.common.logger import get_logger from src.common.logger import get_logger
logger = get_logger("migrate") logger = get_logger("migrate")

View File

@ -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 config_api
from src.plugin_system.apis import frequency_api from src.plugin_system.apis import frequency_api
from src.plugin_system.apis import generator_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") logger = get_logger("question_actions")
@ -25,7 +25,7 @@ logger = get_logger("question_actions")
class CuriousAction(BaseAction): class CuriousAction(BaseAction):
"""频率调节动作 - 调整聊天发言频率""" """频率调节动作 - 调整聊天发言频率"""
activation_type = ActionActivationType.LLM_JUDGE activation_type = ActionActivationType.ALWAYS
parallel_action = True parallel_action = True
# 动作基本信息 # 动作基本信息
@ -38,10 +38,6 @@ class CuriousAction(BaseAction):
"question": "对存在疑问的信息提出一个问题,描述全面", "question": "对存在疑问的信息提出一个问题,描述全面",
} }
# 动作使用场景
bot_name = config_api.get_global_config("bot.nickname")
action_require = [ action_require = [
f"当聊天记录中的信息存在逻辑上的矛盾时使用", f"当聊天记录中的信息存在逻辑上的矛盾时使用",
f"当有人反对或否定你提出的信息时使用", f"当有人反对或否定你提出的信息时使用",
@ -56,6 +52,9 @@ class CuriousAction(BaseAction):
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行频率调节动作""" """执行频率调节动作"""
try: try:
if len(global_conflict_tracker.question_tracker_list) > 3:
return False, "当前有太多问题请先解答完再提问不要再使用make_question动作"
question = self.action_data.get("question", "") question = self.action_data.get("question", "")
# 存储问题到冲突追踪器 # 存储问题到冲突追踪器
@ -64,10 +63,10 @@ class CuriousAction(BaseAction):
logger.info(f"已存储问题到冲突追踪器: {question}") logger.info(f"已存储问题到冲突追踪器: {question}")
await self.store_action_info( await self.store_action_info(
action_build_into_prompt=True, action_build_into_prompt=True,
action_prompt_display=f"你产生了一个问题:{question},尝试向其他人提问或回忆", action_prompt_display=f"你产生了一个问题:{question}",
action_done=True, action_done=True,
) )
return True, "问题已记录" return True, f"问题{question}已记录,不要重复提问该问题"
except Exception as e: except Exception as e:
error_msg = f"问题生成失败: {str(e)}" error_msg = f"问题生成失败: {str(e)}"
logger.error(f"{self.log_prefix} {error_msg}", exc_info=True) logger.error(f"{self.log_prefix} {error_msg}", exc_info=True)
@ -103,7 +102,7 @@ class CuriousPlugin(BasePlugin):
# 配置Schema定义 # 配置Schema定义
config_schema: dict = { config_schema: dict = {
"plugin": { "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="配置文件版本"), "config_version": ConfigField(type=str, default="3.0.0", description="配置文件版本"),
} }
} }

View File

@ -138,7 +138,7 @@ class EmojiAction(BaseAction):
action_prompt_display=f"你发送了表情包,原因:{reason}", action_prompt_display=f"你发送了表情包,原因:{reason}",
action_done=True, action_done=True,
) )
return True, f"成功发送表情包:{emoji_description}" return True, f"成功发送表情包:[表情包:{chosen_emotion}]"
else: else:
error_msg = "发送表情包失败" error_msg = "发送表情包失败"
logger.error(f"{self.log_prefix} {error_msg}") logger.error(f"{self.log_prefix} {error_msg}")