feat: 完全分离回复 兴趣和 消息阅读;添加概率回复机制,优化兴趣监控逻辑,重构相关功能以支持更灵活的回复触发条件

This commit is contained in:
SengokuCola
2025-04-17 16:51:35 +08:00
parent cfdaf00559
commit a2333f9f82
7 changed files with 730 additions and 376 deletions

View File

@@ -2,7 +2,7 @@ import tkinter as tk
from tkinter import ttk from tkinter import ttk
import time import time
import os import os
from datetime import datetime from datetime import datetime, timedelta
import random import random
from collections import deque from collections import deque
import json # 引入 json import json # 引入 json
@@ -37,24 +37,59 @@ class InterestMonitorApp:
# 使用 deque 来存储有限的历史数据点 # 使用 deque 来存储有限的历史数据点
# key: stream_id, value: deque([(timestamp, interest_level), ...]) # key: stream_id, value: deque([(timestamp, interest_level), ...])
self.stream_history = {} self.stream_history = {}
# key: stream_id, value: deque([(timestamp, reply_probability), ...]) # <--- 新增:存储概率历史
self.probability_history = {}
self.stream_colors = {} # 为每个 stream 分配颜色 self.stream_colors = {} # 为每个 stream 分配颜色
self.stream_display_names = {} # *** New: Store display names (group_name) *** self.stream_display_names = {} # *** New: Store display names (group_name) ***
self.selected_stream_id = tk.StringVar() # 用于 Combobox 绑定
# --- UI 元素 --- # --- UI 元素 ---
# 创建 Notebook (选项卡控件)
self.notebook = ttk.Notebook(root)
self.notebook.pack(pady=10, padx=10, fill=tk.BOTH, expand=1)
# --- 第一个选项卡:所有流 ---
self.frame_all = ttk.Frame(self.notebook, padding="5 5 5 5")
self.notebook.add(self.frame_all, text='所有聊天流')
# 状态标签 # 状态标签
self.status_label = tk.Label(root, text="Initializing...", anchor="w", fg="grey") self.status_label = tk.Label(root, text="Initializing...", anchor="w", fg="grey")
self.status_label.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=2) self.status_label.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=2)
# Matplotlib 图表设置 # Matplotlib 图表设置 (用于第一个选项卡)
self.fig = Figure(figsize=(5, 4), dpi=100) self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111) self.ax = self.fig.add_subplot(111)
# 配置在 update_plot 中进行,避免重复 # 配置在 update_plot 中进行,避免重复
# 创建 Tkinter 画布嵌入 Matplotlib 图表 # 创建 Tkinter 画布嵌入 Matplotlib 图表 (用于第一个选项卡)
self.canvas = FigureCanvasTkAgg(self.fig, master=root) self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame_all) # <--- 放入 frame_all
self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1) self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# --- 第二个选项卡:单个流 ---
self.frame_single = ttk.Frame(self.notebook, padding="5 5 5 5")
self.notebook.add(self.frame_single, text='单个聊天流详情')
# 单个流选项卡的上部控制区域
self.control_frame_single = ttk.Frame(self.frame_single)
self.control_frame_single.pack(side=tk.TOP, fill=tk.X, pady=5)
ttk.Label(self.control_frame_single, text="选择聊天流:").pack(side=tk.LEFT, padx=(0, 5))
self.stream_selector = ttk.Combobox(self.control_frame_single, textvariable=self.selected_stream_id, state="readonly", width=50)
self.stream_selector.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.stream_selector.bind("<<ComboboxSelected>>", self.on_stream_selected)
# Matplotlib 图表设置 (用于第二个选项卡)
self.fig_single = Figure(figsize=(5, 4), dpi=100)
# 修改:创建两个子图,一个显示兴趣度,一个显示概率
self.ax_single_interest = self.fig_single.add_subplot(211) # 2行1列的第1个
self.ax_single_probability = self.fig_single.add_subplot(212, sharex=self.ax_single_interest) # 2行1列的第2个共享X轴
# 创建 Tkinter 画布嵌入 Matplotlib 图表 (用于第二个选项卡)
self.canvas_single = FigureCanvasTkAgg(self.fig_single, master=self.frame_single) # <--- 放入 frame_single
self.canvas_widget_single = self.canvas_single.get_tk_widget()
self.canvas_widget_single.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# --- 初始化和启动刷新 --- # --- 初始化和启动刷新 ---
self.update_display() # 首次加载并开始刷新循环 self.update_display() # 首次加载并开始刷新循环
@@ -72,6 +107,7 @@ class InterestMonitorApp:
# *** Reset display names each time we reload *** # *** Reset display names each time we reload ***
new_stream_history = {} new_stream_history = {}
new_stream_display_names = {} new_stream_display_names = {}
new_probability_history = {} # <--- 重置概率历史
read_count = 0 read_count = 0
error_count = 0 error_count = 0
# *** Calculate the timestamp threshold for the last 30 minutes *** # *** Calculate the timestamp threshold for the last 30 minutes ***
@@ -93,6 +129,7 @@ class InterestMonitorApp:
stream_id = log_entry.get("stream_id") stream_id = log_entry.get("stream_id")
interest_level = log_entry.get("interest_level") interest_level = log_entry.get("interest_level")
group_name = log_entry.get("group_name", stream_id) # *** Get group_name, fallback to stream_id *** group_name = log_entry.get("group_name", stream_id) # *** Get group_name, fallback to stream_id ***
reply_probability = log_entry.get("reply_probability") # <--- 获取概率值
# *** Check other required fields AFTER time filtering *** # *** Check other required fields AFTER time filtering ***
if stream_id is None or interest_level is None: if stream_id is None or interest_level is None:
@@ -102,6 +139,7 @@ class InterestMonitorApp:
# 如果是第一次读到这个 stream_id则创建 deque # 如果是第一次读到这个 stream_id则创建 deque
if stream_id not in new_stream_history: if stream_id not in new_stream_history:
new_stream_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) new_stream_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS)
new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # <--- 创建概率 deque
# 检查是否已有颜色,没有则分配 # 检查是否已有颜色,没有则分配
if stream_id not in self.stream_colors: if stream_id not in self.stream_colors:
self.stream_colors[stream_id] = self.get_random_color() self.stream_colors[stream_id] = self.get_random_color()
@@ -111,6 +149,13 @@ class InterestMonitorApp:
# 添加数据点 # 添加数据点
new_stream_history[stream_id].append((float(timestamp), float(interest_level))) new_stream_history[stream_id].append((float(timestamp), float(interest_level)))
# 添加概率数据点 (如果存在)
if reply_probability is not None:
try:
new_probability_history[stream_id].append((float(timestamp), float(reply_probability)))
except (TypeError, ValueError):
# 如果概率值无效,可以跳过或记录一个默认值,这里跳过
pass
except json.JSONDecodeError: except json.JSONDecodeError:
error_count += 1 error_count += 1
@@ -124,6 +169,7 @@ class InterestMonitorApp:
# 读取完成后,用新数据替换旧数据 # 读取完成后,用新数据替换旧数据
self.stream_history = new_stream_history self.stream_history = new_stream_history
self.stream_display_names = new_stream_display_names # *** Update display names *** self.stream_display_names = new_stream_display_names # *** Update display names ***
self.probability_history = new_probability_history # <--- 更新概率历史
status_msg = f"Data loaded at {datetime.now().strftime('%H:%M:%S')}. Lines read: {read_count}." status_msg = f"Data loaded at {datetime.now().strftime('%H:%M:%S')}. Lines read: {read_count}."
if error_count > 0: if error_count > 0:
status_msg += f" Skipped {error_count} invalid lines." status_msg += f" Skipped {error_count} invalid lines."
@@ -136,12 +182,39 @@ class InterestMonitorApp:
except Exception as e: except Exception as e:
self.set_status(f"An unexpected error occurred during loading: {e}", "red") self.set_status(f"An unexpected error occurred during loading: {e}", "red")
# --- 更新 Combobox ---
self.update_stream_selector()
def update_plot(self): def update_stream_selector(self):
"""更新 Matplotlib 图""" """更新单个流选项卡中的 Combobox 列"""
# 创建 (display_name, stream_id) 对的列表,按 display_name 排序
available_streams = sorted(
[(name, sid) for sid, name in self.stream_display_names.items() if sid in self.stream_history and self.stream_history[sid]],
key=lambda item: item[0] # 按显示名称排序
)
# 更新 Combobox 的值 (仅显示 display_name)
self.stream_selector['values'] = [name for name, sid in available_streams]
# 检查当前选中的 stream_id 是否仍然有效
current_selection_name = self.selected_stream_id.get()
current_selection_valid = any(name == current_selection_name for name, sid in available_streams)
if not current_selection_valid and available_streams:
# 如果当前选择无效,并且有可选流,则默认选中第一个
self.selected_stream_id.set(available_streams[0][0])
# 手动触发一次更新,因为 set 不会触发 <<ComboboxSelected>>
self.update_single_stream_plot()
elif not available_streams:
# 如果没有可选流,清空选择
self.selected_stream_id.set("")
self.update_single_stream_plot() # 清空图表
def update_all_streams_plot(self):
"""更新第一个选项卡的 Matplotlib 图表 (显示所有流)"""
self.ax.clear() # 清除旧图 self.ax.clear() # 清除旧图
# *** 设置中文标题和标签 *** # *** 设置中文标题和标签 ***
self.ax.set_title("兴趣度随时间变化图") self.ax.set_title("兴趣度随时间变化图 (所有活跃流)")
self.ax.set_xlabel("时间") self.ax.set_xlabel("时间")
self.ax.set_ylabel("兴趣度") self.ax.set_ylabel("兴趣度")
self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
@@ -213,6 +286,25 @@ class InterestMonitorApp:
self.canvas.draw() # 重绘画布 self.canvas.draw() # 重绘画布
def update_single_stream_plot(self):
"""更新第二个选项卡的 Matplotlib 图表 (显示单个选定的流)"""
self.ax_single_interest.clear()
self.ax_single_probability.clear()
# 设置子图标题和标签
self.ax_single_interest.set_title("兴趣度")
self.ax_single_interest.set_ylabel("兴趣度")
self.ax_single_interest.grid(True)
self.ax_single_interest.set_ylim(0, 10) # 固定 Y 轴范围 0-10
self.ax_single_probability.set_title("回复评估概率")
self.ax_single_probability.set_xlabel("时间")
self.ax_single_probability.set_ylabel("概率")
self.ax_single_probability.grid(True)
self.ax_single_probability.set_ylim(0, 1.05) # 固定 Y 轴范围 0-1
self.ax_single_probability.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
selected_name = self.selected_stream_id.get()
def update_display(self): def update_display(self):
"""主更新循环""" """主更新循环"""
try: try:

View File

@@ -26,13 +26,12 @@ class ToolUser:
@staticmethod @staticmethod
async def _build_tool_prompt( async def _build_tool_prompt(
message_txt: str, sender_name: str, chat_stream: ChatStream, subheartflow: SubHeartflow = None self, message_txt: str, chat_stream: ChatStream, subheartflow: SubHeartflow = None
): ):
"""构建工具使用的提示词 """构建工具使用的提示词
Args: Args:
message_txt: 用户消息文本 message_txt: 用户消息文本
sender_name: 发送者名称
chat_stream: 聊天流对象 chat_stream: 聊天流对象
Returns: Returns:
@@ -44,28 +43,28 @@ class ToolUser:
else: else:
mid_memory_info = "" mid_memory_info = ""
stream_id = chat_stream.stream_id # stream_id = chat_stream.stream_id
chat_talking_prompt = "" # chat_talking_prompt = ""
if stream_id: # if stream_id:
chat_talking_prompt = get_recent_group_detailed_plain_text( # chat_talking_prompt = get_recent_group_detailed_plain_text(
stream_id, limit=global_config.MAX_CONTEXT_SIZE, combine=True # stream_id, limit=global_config.MAX_CONTEXT_SIZE, combine=True
) # )
new_messages = list( # new_messages = list(
db.messages.find({"chat_id": chat_stream.stream_id, "time": {"$gt": time.time()}}).sort("time", 1).limit(15) # db.messages.find({"chat_id": chat_stream.stream_id, "time": {"$gt": time.time()}}).sort("time", 1).limit(15)
) # )
new_messages_str = "" # new_messages_str = ""
for msg in new_messages: # for msg in new_messages:
if "detailed_plain_text" in msg: # if "detailed_plain_text" in msg:
new_messages_str += f"{msg['detailed_plain_text']}" # new_messages_str += f"{msg['detailed_plain_text']}"
# 这些信息应该从调用者传入而不是从self获取 # 这些信息应该从调用者传入而不是从self获取
bot_name = global_config.BOT_NICKNAME bot_name = global_config.BOT_NICKNAME
prompt = "" prompt = ""
prompt += mid_memory_info prompt += mid_memory_info
prompt += "你正在思考如何回复群里的消息。\n" prompt += "你正在思考如何回复群里的消息。\n"
prompt += "之前群里进行了如下讨论:\n" prompt += f"之前群里进行了如下讨论:\n"
prompt += chat_talking_prompt prompt += message_txt
prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n" # prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n"
prompt += f"注意你就是{bot_name}{bot_name}是你的名字。根据之前的聊天记录补充问题信息,搜索时避开你的名字。\n" prompt += f"注意你就是{bot_name}{bot_name}是你的名字。根据之前的聊天记录补充问题信息,搜索时避开你的名字。\n"
prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。" prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。"
return prompt return prompt
@@ -119,7 +118,7 @@ class ToolUser:
return None return None
async def use_tool( async def use_tool(
self, message_txt: str, sender_name: str, chat_stream: ChatStream, sub_heartflow: SubHeartflow = None self, message_txt: str, chat_stream: ChatStream, sub_heartflow: SubHeartflow = None
): ):
"""使用工具辅助思考,判断是否需要额外信息 """使用工具辅助思考,判断是否需要额外信息
@@ -134,7 +133,7 @@ class ToolUser:
""" """
try: try:
# 构建提示词 # 构建提示词
prompt = await self._build_tool_prompt(message_txt, sender_name, chat_stream, sub_heartflow) prompt = await self._build_tool_prompt(message_txt, chat_stream, sub_heartflow)
# 定义可用工具 # 定义可用工具
tools = self._define_tools() tools = self._define_tools()

View File

@@ -4,6 +4,9 @@ from src.plugins.moods.moods import MoodManager
from src.plugins.models.utils_model import LLMRequest from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config from src.config.config import global_config
import time import time
from typing import Optional
from datetime import datetime
import traceback
from src.plugins.chat.message import UserInfo from src.plugins.chat.message import UserInfo
from src.plugins.chat.utils import parse_text_timestamps from src.plugins.chat.utils import parse_text_timestamps
@@ -113,6 +116,8 @@ class SubHeartflow:
self.running_knowledges = [] self.running_knowledges = []
self._thinking_lock = asyncio.Lock() # 添加思考锁,防止并发思考
self.bot_name = global_config.BOT_NICKNAME self.bot_name = global_config.BOT_NICKNAME
def add_observation(self, observation: Observation): def add_observation(self, observation: Observation):
@@ -138,142 +143,170 @@ class SubHeartflow:
"""清空所有observation对象""" """清空所有observation对象"""
self.observations.clear() self.observations.clear()
def _get_primary_observation(self) -> Optional[ChattingObservation]:
"""获取主要的通常是第一个ChattingObservation实例"""
if self.observations and isinstance(self.observations[0], ChattingObservation):
return self.observations[0]
logger.warning(f"SubHeartflow {self.subheartflow_id} 没有找到有效的 ChattingObservation")
return None
async def subheartflow_start_working(self): async def subheartflow_start_working(self):
while True: while True:
current_time = time.time() current_time = time.time()
if ( # --- 调整后台任务逻辑 --- #
current_time - self.last_reply_time > global_config.sub_heart_flow_freeze_time # 这个后台循环现在主要负责检查是否需要自我销毁
): # 120秒无回复/不在场,冻结 # 不再主动进行思考或状态更新,这些由 HeartFC_Chat 驱动
self.is_active = False
await asyncio.sleep(global_config.sub_heart_flow_update_interval) # 每60秒检查一次
else:
self.is_active = True
self.last_active_time = current_time # 更新最后激活时间
self.current_state.update_current_state_info() # 检查是否需要冻结(这个逻辑可能需要重新审视,因为激活状态现在由外部驱动)
# if current_time - self.last_reply_time > global_config.sub_heart_flow_freeze_time:
# self.is_active = False
# else:
# self.is_active = True
# self.last_active_time = current_time # 由外部调用(如 thinking更新
# 检查是否超过指定时间没有激活 (例如,没有被调用进行思考)
if current_time - self.last_active_time > global_config.sub_heart_flow_stop_time: # 例如 5 分钟
logger.info(f"子心流 {self.subheartflow_id} 超过 {global_config.sub_heart_flow_stop_time} 秒没有激活,正在销毁..."
f" (Last active: {datetime.fromtimestamp(self.last_active_time).strftime('%Y-%m-%d %H:%M:%S')})")
# 在这里添加实际的销毁逻辑,例如从主 Heartflow 管理器中移除自身
# heartflow.remove_subheartflow(self.subheartflow_id) # 假设有这样的方法
break # 退出循环以停止任务
# 不再需要内部驱动的状态更新和思考
# self.current_state.update_current_state_info()
# await self.do_a_thinking() # await self.do_a_thinking()
# await self.judge_willing() # await self.judge_willing()
await asyncio.sleep(global_config.sub_heart_flow_update_interval)
# 检查是否超过10分钟没有激活 await asyncio.sleep(global_config.sub_heart_flow_update_interval) # 定期检查销毁条件
if (
current_time - self.last_active_time > global_config.sub_heart_flow_stop_time async def ensure_observed(self):
): # 5分钟无回复/不在场,销毁 """确保在思考前执行了观察"""
logger.info(f"子心流 {self.subheartflow_id} 已经5分钟没有激活正在销毁...") observation = self._get_primary_observation()
break # 退出循环,销毁自己 if observation:
try:
await observation.observe()
logger.trace(f"[{self.subheartflow_id}] Observation updated before thinking.")
except Exception as e:
logger.error(f"[{self.subheartflow_id}] Error during pre-thinking observation: {e}")
logger.error(traceback.format_exc())
async def do_observe(self): async def do_observe(self):
observation = self.observations[0] # 现在推荐使用 ensure_observed(),但保留此方法以兼容旧用法(或特定场景)
observation = self._get_primary_observation()
if observation:
await observation.observe() await observation.observe()
else:
logger.error(f"[{self.subheartflow_id}] do_observe called but no valid observation found.")
async def do_thinking_before_reply( async def do_thinking_before_reply(
self, message_txt: str, sender_info: UserInfo, chat_stream: ChatStream, extra_info: str, obs_id: int = None self, message_txt: str, sender_info: UserInfo, chat_stream: ChatStream, extra_info: str, obs_id: list[str] = None # 修改 obs_id 类型为 list[str]
): ):
async with self._thinking_lock: # 获取思考锁
# --- 在思考前确保观察已执行 --- #
await self.ensure_observed()
self.last_active_time = time.time() # 更新最后激活时间戳
current_thinking_info = self.current_mind current_thinking_info = self.current_mind
mood_info = self.current_state.mood mood_info = self.current_state.mood
# mood_info = "你很生气,很愤怒" observation = self._get_primary_observation()
observation = self.observations[0] if not observation:
logger.error(f"[{self.subheartflow_id}] Cannot perform thinking without observation.")
return "", [] # 返回空结果
# --- 获取观察信息 --- #
chat_observe_info = ""
if obs_id: if obs_id:
print(f"11111111111有id,开始获取观察信息{obs_id}") try:
chat_observe_info = observation.get_observe_info(obs_id) chat_observe_info = observation.get_observe_info(obs_id)
logger.debug(f"[{self.subheartflow_id}] Using specific observation IDs: {obs_id}")
except Exception as e:
logger.error(f"[{self.subheartflow_id}] Error getting observe info with IDs {obs_id}: {e}. Falling back.")
chat_observe_info = observation.get_observe_info() # 出错时回退到默认观察
else: else:
chat_observe_info = observation.get_observe_info() chat_observe_info = observation.get_observe_info()
logger.debug(f"[{self.subheartflow_id}] Using default observation info.")
# --- 构建 Prompt (基本逻辑不变) --- #
extra_info_prompt = "" extra_info_prompt = ""
if extra_info:
for tool_name, tool_data in extra_info.items(): for tool_name, tool_data in extra_info.items():
extra_info_prompt += f"{tool_name} 相关信息:\n" extra_info_prompt += f"{tool_name} 相关信息:\n"
for item in tool_data: for item in tool_data:
extra_info_prompt += f"- {item['name']}: {item['content']}\n" extra_info_prompt += f"- {item['name']}: {item['content']}\n"
else:
extra_info_prompt = "无工具信息。\n" # 提供默认值
# 开始构建prompt
prompt_personality = f"你的名字是{self.bot_name},你"
# person
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
prompt_personality = f"你的名字是{self.bot_name},你"
personality_core = individuality.personality.personality_core prompt_personality += individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides) if personality_sides: random.shuffle(personality_sides); prompt_personality += f",{personality_sides[0]}"
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail) if identity_detail: random.shuffle(identity_detail); prompt_personality += f",{identity_detail[0]}"
prompt_personality += f",{identity_detail[0]}"
# 关系
who_chat_in_group = [ who_chat_in_group = [
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname) (chat_stream.platform, sender_info.user_id, sender_info.user_nickname) # 先添加当前发送者
] ]
who_chat_in_group += get_recent_group_speaker( # 获取最近发言者,排除当前发送者,避免重复
recent_speakers = get_recent_group_speaker(
chat_stream.stream_id, chat_stream.stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id), (chat_stream.platform, sender_info.user_id),
limit=global_config.MAX_CONTEXT_SIZE, limit=global_config.MAX_CONTEXT_SIZE -1 # 减去当前发送者
) )
who_chat_in_group.extend(recent_speakers)
relation_prompt = "" relation_prompt = ""
for person in who_chat_in_group: unique_speakers = set() # 确保人物信息不重复
relation_prompt += await relationship_manager.build_relationship_info(person) for person_tuple in who_chat_in_group:
person_key = (person_tuple[0], person_tuple[1]) # 使用 platform+id 作为唯一标识
if person_key not in unique_speakers:
relation_prompt += await relationship_manager.build_relationship_info(person_tuple)
unique_speakers.add(person_key)
# relation_prompt_all = (
# f"{relation_prompt}关系等级越大,关系越好,请分析聊天记录,"
# f"根据你和说话者{sender_name}的关系和态度进行回复,明确你的立场和情感。"
# )
relation_prompt_all = (await global_prompt_manager.get_prompt_async("relationship_prompt")).format( relation_prompt_all = (await global_prompt_manager.get_prompt_async("relationship_prompt")).format(
relation_prompt, sender_info.user_nickname relation_prompt, sender_info.user_nickname
) )
sender_name_sign = ( sender_name_sign = (
f"<{chat_stream.platform}:{sender_info.user_id}:{sender_info.user_nickname}:{sender_info.user_cardname}>" f"<{chat_stream.platform}:{sender_info.user_id}:{sender_info.user_nickname}:{sender_info.user_cardname or 'NoCard'}>"
) )
# prompt = ""
# # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n"
# if tool_result.get("used_tools", False):
# prompt += f"{collected_info}\n"
# prompt += f"{relation_prompt_all}\n"
# prompt += f"{prompt_personality}\n"
# prompt += f"刚刚你的想法是{current_thinking_info}。如果有新的内容,记得转换话题\n"
# prompt += "-----------------------------------\n"
# prompt += f"现在你正在上网和qq群里的网友们聊天群里正在聊的话题是{chat_observe_info}\n"
# prompt += f"你现在{mood_info}\n"
# prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n"
# prompt += "现在你接下去继续思考,产生新的想法,不要分点输出,输出连贯的内心独白"
# prompt += "思考时可以想想如何对群聊内容进行回复。回复的要求是:平淡一些,简短一些,说中文,尽量不要说你说过的话\n"
# prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写"
# prompt += f"记得结合上述的消息,生成内心想法,文字不要浮夸,注意你就是{self.bot_name}{self.bot_name}指的就是你。"
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format(
extra_info_prompt, extra_info=extra_info_prompt,
# prompt_schedule, relation_prompt_all=relation_prompt_all,
relation_prompt_all, prompt_personality=prompt_personality,
prompt_personality, current_thinking_info=current_thinking_info,
current_thinking_info, time_now=time_now,
time_now, chat_observe_info=chat_observe_info,
chat_observe_info, mood_info=mood_info,
mood_info, sender_name=sender_name_sign,
sender_name_sign, message_txt=message_txt,
message_txt, bot_name=self.bot_name,
self.bot_name,
) )
prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt) prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
prompt = parse_text_timestamps(prompt, mode="lite") prompt = parse_text_timestamps(prompt, mode="lite")
logger.debug(f"[{self.subheartflow_id}] Thinking Prompt:\n{prompt}")
try: try:
response, reasoning_content = await self.llm_model.generate_response_async(prompt) response, reasoning_content = await self.llm_model.generate_response_async(prompt)
if not response: # 如果 LLM 返回空,给一个默认想法
response = "(不知道该想些什么...)"
logger.warning(f"[{self.subheartflow_id}] LLM returned empty response for thinking.")
except Exception as e: except Exception as e:
logger.error(f"回复前内心独白获取失败: {e}") logger.error(f"[{self.subheartflow_id}] 内心独白获取失败: {e}")
response = "" response = "(思考时发生错误...)" # 错误时的默认想法
self.update_current_mind(response) self.update_current_mind(response)
self.current_mind = response # self.current_mind 已经在 update_current_mind 中更新
logger.info(f"prompt:\n{prompt}\n") logger.info(f"[{self.subheartflow_id}] 思考前脑内状态:{self.current_mind}")
logger.info(f"麦麦的思考前脑内状态:{self.current_mind}")
return self.current_mind, self.past_mind return self.current_mind, self.past_mind
async def do_thinking_after_observe( async def do_thinking_after_observe(
@@ -337,7 +370,6 @@ class SubHeartflow:
f"<{chat_stream.platform}:{sender_info.user_id}:{sender_info.user_nickname}:{sender_info.user_cardname}>" f"<{chat_stream.platform}:{sender_info.user_id}:{sender_info.user_nickname}:{sender_info.user_cardname}>"
) )
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_after_observe")).format( prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_after_observe")).format(
@@ -436,6 +468,24 @@ class SubHeartflow:
self.past_mind.append(self.current_mind) self.past_mind.append(self.current_mind)
self.current_mind = response self.current_mind = response
async def check_reply_trigger(self) -> bool:
"""根据观察到的信息和内部状态,判断是否应该触发一次回复。
TODO: 实现具体的判断逻辑。
例如:检查 self.observations[0].now_message_info 是否包含提及、问题,
或者 self.current_mind 中是否包含强烈的回复意图等。
"""
# Placeholder: 目前始终返回 False需要后续实现
logger.trace(f"[{self.subheartflow_id}] check_reply_trigger called. (Logic Pending)")
# --- 实现触发逻辑 --- #
# 示例:如果观察到的最新消息包含自己的名字,则有一定概率触发
# observation = self._get_primary_observation()
# if observation and self.bot_name in observation.now_message_info[-100:]: # 检查最后100个字符
# if random.random() < 0.3: # 30% 概率触发
# logger.info(f"[{self.subheartflow_id}] Triggering reply based on mention.")
# return True
# ------------------ #
return False # 默认不触发
init_prompt() init_prompt()
# subheartflow = SubHeartflow() # subheartflow = SubHeartflow()

View File

@@ -48,9 +48,7 @@ class ResponseGenerator:
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
with Timer() as t_generate_response: with Timer() as t_generate_response:
checked = False
if random.random() > 0:
checked = False
current_model = self.model_normal current_model = self.model_normal
current_model.temperature = ( current_model.temperature = (
global_config.llm_normal["temp"] * arousal_multiplier global_config.llm_normal["temp"] * arousal_multiplier
@@ -59,34 +57,12 @@ class ResponseGenerator:
message, current_model, thinking_id, mode="normal" message, current_model, thinking_id, mode="normal"
) )
model_checked_response = model_response
else:
checked = True
current_model = self.model_normal
current_model.temperature = (
global_config.llm_normal["temp"] * arousal_multiplier
) # 激活度越高,温度越高
print(f"生成{message.processed_plain_text}回复温度是:{current_model.temperature}")
model_response = await self._generate_response_with_model(
message, current_model, thinking_id, mode="simple"
)
current_model.temperature = global_config.llm_normal["temp"]
model_checked_response = await self._check_response_with_model(
message, model_response, current_model, thinking_id
)
if model_response: if model_response:
if checked:
logger.info(
f"{global_config.BOT_NICKNAME}的回复是:{model_response},思忖后,回复是:{model_checked_response},生成回复时间: {t_generate_response.human_readable}"
)
else:
logger.info( logger.info(
f"{global_config.BOT_NICKNAME}的回复是:{model_response},生成回复时间: {t_generate_response.human_readable}" f"{global_config.BOT_NICKNAME}的回复是:{model_response},生成回复时间: {t_generate_response.human_readable}"
) )
model_processed_response = await self._process_response(model_response)
model_processed_response = await self._process_response(model_checked_response)
return model_processed_response return model_processed_response
else: else:

View File

@@ -18,6 +18,8 @@ from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from ...utils.timer_calculater import Timer from ...utils.timer_calculater import Timer
from src.do_tool.tool_use import ToolUser from src.do_tool.tool_use import ToolUser
from .interest import InterestManager, InterestChatting from .interest import InterestManager, InterestChatting
from src.plugins.chat.chat_stream import chat_manager
from src.plugins.chat.message import MessageInfo
# 定义日志配置 # 定义日志配置
chat_config = LogConfig( chat_config = LogConfig(
@@ -28,7 +30,6 @@ chat_config = LogConfig(
logger = get_module_logger("heartFC_chat", config=chat_config) logger = get_module_logger("heartFC_chat", config=chat_config)
# 新增常量 # 新增常量
INTEREST_LEVEL_REPLY_THRESHOLD = 4.0
INTEREST_MONITOR_INTERVAL_SECONDS = 1 INTEREST_MONITOR_INTERVAL_SECONDS = 1
class HeartFC_Chat: class HeartFC_Chat:
@@ -41,87 +42,105 @@ class HeartFC_Chat:
self._interest_monitor_task: Optional[asyncio.Task] = None self._interest_monitor_task: Optional[asyncio.Task] = None
async def start(self): async def start(self):
"""Starts asynchronous tasks like the interest monitor.""" """启动异步任务,如兴趣监控器"""
logger.info("HeartFC_Chat starting asynchronous tasks...") logger.info("HeartFC_Chat 正在启动异步任务...")
await self.interest_manager.start_background_tasks() await self.interest_manager.start_background_tasks()
self._initialize_monitor_task() self._initialize_monitor_task()
logger.info("HeartFC_Chat asynchronous tasks started.") logger.info("HeartFC_Chat 异步任务启动完成")
def _initialize_monitor_task(self): def _initialize_monitor_task(self):
"""启动后台兴趣监控任务""" """启动后台兴趣监控任务,可以检查兴趣是否足以开启心流对话"""
if self._interest_monitor_task is None or self._interest_monitor_task.done(): if self._interest_monitor_task is None or self._interest_monitor_task.done():
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
self._interest_monitor_task = loop.create_task(self._interest_monitor_loop()) self._interest_monitor_task = loop.create_task(self._interest_monitor_loop())
logger.info(f"Interest monitor task created. Interval: {INTEREST_MONITOR_INTERVAL_SECONDS}s, Level Threshold: {INTEREST_LEVEL_REPLY_THRESHOLD}") logger.info(f"兴趣监控任务已创建。监控间隔: {INTEREST_MONITOR_INTERVAL_SECONDS}秒。")
except RuntimeError: except RuntimeError:
logger.error("Failed to create interest monitor task: No running event loop.") logger.error("创建兴趣监控任务失败:没有运行中的事件循环。")
raise raise
else: else:
logger.warning("Interest monitor task creation skipped: already running or exists.") logger.warning("跳过兴趣监控任务创建:任务已存在或正在运行。")
async def _interest_monitor_loop(self): async def _interest_monitor_loop(self):
"""后台任务,定期检查兴趣度变化并触发回复""" """后台任务,定期检查兴趣度变化并触发回复"""
logger.info("Interest monitor loop starting...") logger.info("兴趣监控循环开始...")
await asyncio.sleep(0.3)
while True: while True:
await asyncio.sleep(INTEREST_MONITOR_INTERVAL_SECONDS) await asyncio.sleep(INTEREST_MONITOR_INTERVAL_SECONDS)
try: try:
interest_items_snapshot: List[tuple[str, InterestChatting]] = [] # --- 修改:遍历 SubHeartflow 并检查触发器 ---
stream_ids = list(self.interest_manager.interest_dict.keys()) active_stream_ids = list(heartflow.get_all_subheartflows_streams_ids()) # 需要 heartflow 提供此方法
for stream_id in stream_ids: logger.trace(f"检查 {len(active_stream_ids)} 个活跃流是否足以开启心流对话...")
chatting_instance = self.interest_manager.get_interest_chatting(stream_id)
if chatting_instance:
interest_items_snapshot.append((stream_id, chatting_instance))
for stream_id, chatting_instance in interest_items_snapshot: for stream_id in active_stream_ids:
triggering_message = chatting_instance.last_triggering_message sub_hf = heartflow.get_subheartflow(stream_id)
current_interest = chatting_instance.get_interest() if not sub_hf:
logger.warning(f"监控循环: 无法获取活跃流 {stream_id} 的 sub_hf")
continue
# 添加调试日志,检查触发条件 # --- 获取 Observation 和消息列表 --- #
# logger.debug(f"[兴趣监控][{stream_id}] 当前兴趣: {current_interest:.2f}, 阈值: {INTEREST_LEVEL_REPLY_THRESHOLD}, 触发消息存在: {triggering_message is not None}") observation = sub_hf._get_primary_observation()
if not observation:
logger.warning(f"[{stream_id}] SubHeartflow 没有在观察,无法检查触发器。")
continue
observed_messages = observation.talking_message # 获取消息字典列表
# --- 结束获取 --- #
if current_interest > INTEREST_LEVEL_REPLY_THRESHOLD and triggering_message is not None: should_trigger = False
logger.info(f"[{stream_id}] 检测到高兴趣度 ({current_interest:.2f} > {INTEREST_LEVEL_REPLY_THRESHOLD}). 基于消息 ID: {triggering_message.message_info.message_id} 的上下文触发回复") # 更新日志信息使其更清晰 try:
# check_reply_trigger 可以选择性地接收 observed_messages 作为参数
should_trigger = await sub_hf.check_reply_trigger() # 目前 check_reply_trigger 还不处理这个
except Exception as e:
logger.error(f"错误调用 check_reply_trigger 流 {stream_id}: {e}")
logger.error(traceback.format_exc())
chatting_instance.reset_trigger_info() if should_trigger:
logger.debug(f"[{stream_id}] Trigger info reset before starting reply task.") logger.info(f"[{stream_id}] SubHeartflow 决定开启心流对话。")
# 调用修改后的处理函数,传递 stream_id 和 observed_messages
asyncio.create_task(self._process_triggered_reply(stream_id, observed_messages))
asyncio.create_task(self._process_triggered_reply(stream_id, triggering_message))
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("Interest monitor loop cancelled.") logger.info("兴趣监控循环已取消。")
break break
except Exception as e: except Exception as e:
logger.error(f"Error in interest monitor loop: {e}") logger.error(f"兴趣监控循环错误: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
await asyncio.sleep(5) await asyncio.sleep(5) # 发生错误时等待
async def _process_triggered_reply(self, stream_id: str, triggering_message: MessageRecv): async def _process_triggered_reply(self, stream_id: str, observed_messages: List[dict]):
"""Helper coroutine to handle the processing of a triggered reply based on interest level.""" """Helper coroutine to handle the processing of a triggered reply based on SubHeartflow trigger."""
try: try:
logger.info(f"[{stream_id}] Starting level-triggered reply generation for message ID: {triggering_message.message_info.message_id}...") logger.info(f"[{stream_id}] SubHeartflow 触发回复...")
await self.trigger_reply_generation(triggering_message) # 调用修改后的 trigger_reply_generation
await self.trigger_reply_generation(stream_id, observed_messages)
# 在回复处理后降低兴趣度,降低固定值:新阈值的一半 # --- 调整兴趣降低逻辑 ---
decrease_value = INTEREST_LEVEL_REPLY_THRESHOLD / 2 # 这里的兴趣降低可能不再适用,或者需要基于不同的逻辑
self.interest_manager.decrease_interest(stream_id, value=decrease_value) # 例如,回复后可以将 SubHeartflow 的某种"回复意愿"状态重置
post_trigger_interest = self.interest_manager.get_interest(stream_id) # 暂时注释掉,或根据需要调整
# 更新日志以反映降低的是基于新阈值的固定值 # chatting_instance = self.interest_manager.get_interest_chatting(stream_id)
logger.info(f"[{stream_id}] Interest decreased by fixed value {decrease_value:.2f} (LevelThreshold/2) after processing level-triggered reply. Current interest: {post_trigger_interest:.2f}") # if chatting_instance:
# decrease_value = chatting_instance.trigger_threshold / 2 # 使用实例的阈值
# self.interest_manager.decrease_interest(stream_id, value=decrease_value)
# post_trigger_interest = self.interest_manager.get_interest(stream_id) # 获取更新后的兴趣
# logger.info(f"[{stream_id}] Interest decreased by {decrease_value:.2f} (InstanceThreshold/2) after processing triggered reply. Current interest: {post_trigger_interest:.2f}")
# else:
# logger.warning(f"[{stream_id}] Could not find InterestChatting instance after reply processing to decrease interest.")
logger.debug(f"[{stream_id}] Reply processing finished. (Interest decrease logic needs review).")
except Exception as e: except Exception as e:
logger.error(f"Error processing level-triggered reply for stream_id {stream_id}, context message_id {triggering_message.message_info.message_id}: {e}") logger.error(f"Error processing SubHeartflow-triggered reply for stream_id {stream_id}: {e}") # 更新日志信息
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# --- 结束修改 ---
async def _create_thinking_message(self, message: MessageRecv): async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]):
"""创建思考消息 (message 获取信息)""" """创建思考消息 (尝试锚定到 anchor_message)"""
chat = message.chat_stream if not anchor_message or not anchor_message.chat_stream:
if not chat: logger.error("无法创建思考消息,缺少有效的锚点消息或聊天流。")
logger.error(f"Cannot create thinking message, chat_stream is None for message ID: {message.message_info.message_id}")
return None return None
userinfo = message.message_info.user_info # 发起思考的用户(即原始消息发送者)
messageinfo = message.message_info # 原始消息信息 chat = anchor_message.chat_stream
messageinfo = anchor_message.message_info
bot_user_info = UserInfo( bot_user_info = UserInfo(
user_id=global_config.BOT_QQ, user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME, user_nickname=global_config.BOT_NICKNAME,
@@ -133,17 +152,21 @@ class HeartFC_Chat:
thinking_message = MessageThinking( thinking_message = MessageThinking(
message_id=thinking_id, message_id=thinking_id,
chat_stream=chat, chat_stream=chat,
bot_user_info=bot_user_info, # 思考消息的发出者是 bot bot_user_info=bot_user_info,
reply=message, # 回复的是原始消息 reply=anchor_message, # 回复的是锚点消息
thinking_start_time=thinking_time_point, thinking_start_time=thinking_time_point,
) )
MessageManager().add_message(thinking_message) MessageManager().add_message(thinking_message)
return thinking_id return thinking_id
async def _send_response_messages(self, message: MessageRecv, response_set: List[str], thinking_id) -> MessageSending: async def _send_response_messages(self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id) -> Optional[MessageSending]:
chat = message.chat_stream """发送回复消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error("无法发送回复,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
container = MessageManager().get_container(chat.stream_id) container = MessageManager().get_container(chat.stream_id)
thinking_message = None thinking_message = None
for msg in container.messages: for msg in container.messages:
@@ -152,26 +175,26 @@ class HeartFC_Chat:
container.messages.remove(msg) container.messages.remove(msg)
break break
if not thinking_message: if not thinking_message:
logger.warning("未找到对应的思考消息,可能已超时被移除") logger.warning(f"[{chat.stream_id}] 未找到对应的思考消息 {thinking_id},可能已超时被移除")
return None return None
thinking_start_time = thinking_message.thinking_start_time thinking_start_time = thinking_message.thinking_start_time
message_set = MessageSet(chat, thinking_id) message_set = MessageSet(chat, thinking_id)
mark_head = False mark_head = False
first_bot_msg = None first_bot_msg = None
for msg in response_set: for msg_text in response_set:
message_segment = Seg(type="text", data=msg) message_segment = Seg(type="text", data=msg_text)
bot_message = MessageSending( bot_message = MessageSending(
message_id=thinking_id, message_id=thinking_id, # 使用 thinking_id 作为批次标识
chat_stream=chat, chat_stream=chat,
bot_user_info=UserInfo( bot_user_info=UserInfo(
user_id=global_config.BOT_QQ, user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME, user_nickname=global_config.BOT_NICKNAME,
platform=message.message_info.platform, # 从传入的 message 获取 platform platform=anchor_message.message_info.platform,
), ),
sender_info=message.message_info.user_info, # 发送给 sender_info=anchor_message.message_info.user_info, # 发送给锚点消息的用户
message_segment=message_segment, message_segment=message_segment,
reply=message, # 回复原始消息 reply=anchor_message, # 回复锚点消息
is_head=not mark_head, is_head=not mark_head,
is_emoji=False, is_emoji=False,
thinking_start_time=thinking_start_time, thinking_start_time=thinking_start_time,
@@ -180,101 +203,182 @@ class HeartFC_Chat:
mark_head = True mark_head = True
first_bot_msg = bot_message first_bot_msg = bot_message
message_set.add_message(bot_message) message_set.add_message(bot_message)
if message_set.messages: # 确保有消息才添加
MessageManager().add_message(message_set) MessageManager().add_message(message_set)
return first_bot_msg return first_bot_msg
else:
logger.warning(f"[{chat.stream_id}] 没有生成有效的回复消息集,无法发送。")
return None
async def _handle_emoji(self, message: MessageRecv, response_set, send_emoji=""): async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set, send_emoji=""):
"""处理表情包 (message 获取信息)""" """处理表情包 (尝试锚定到 anchor_message)"""
chat = message.chat_stream if not anchor_message or not anchor_message.chat_stream:
logger.error("无法处理表情包,缺少有效的锚点消息或聊天流。")
return
chat = anchor_message.chat_stream
if send_emoji: if send_emoji:
emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
else: else:
emoji_text_source = "".join(response_set) if response_set else "" emoji_text_source = "".join(response_set) if response_set else ""
emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source) emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source)
if emoji_raw: if emoji_raw:
emoji_path, description = emoji_raw emoji_path, description = emoji_raw
emoji_cq = image_path_to_base64(emoji_path) emoji_cq = image_path_to_base64(emoji_path)
thinking_time_point = round(message.message_info.time, 2) # 使用当前时间戳,因为没有原始消息的时间戳
thinking_time_point = round(time.time(), 2)
message_segment = Seg(type="emoji", data=emoji_cq) message_segment = Seg(type="emoji", data=emoji_cq)
bot_message = MessageSending( bot_message = MessageSending(
message_id="mt" + str(thinking_time_point), message_id="me" + str(thinking_time_point), # 使用不同的 ID 前缀?
chat_stream=chat, chat_stream=chat,
bot_user_info=UserInfo( bot_user_info=UserInfo(
user_id=global_config.BOT_QQ, user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME, user_nickname=global_config.BOT_NICKNAME,
platform=message.message_info.platform, platform=anchor_message.message_info.platform,
), ),
sender_info=message.message_info.user_info, # 发送给谁 sender_info=anchor_message.message_info.user_info,
message_segment=message_segment, message_segment=message_segment,
reply=message, # 回复原始消息 reply=anchor_message, # 回复锚点消息
is_head=False, is_head=False,
is_emoji=True, is_emoji=True,
) )
MessageManager().add_message(bot_message) MessageManager().add_message(bot_message)
async def _update_relationship(self, message: MessageRecv, response_set): async def _update_relationship(self, anchor_message: Optional[MessageRecv], response_set):
"""更新关系情绪""" """更新关系情绪 (尝试基于 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error("无法更新关系情绪,缺少有效的锚点消息或聊天流。")
return
# 关系更新依赖于理解回复是针对谁的,以及原始消息的上下文
# 这里的实现可能需要调整,取决于关系管理器如何工作
ori_response = ",".join(response_set) ori_response = ",".join(response_set)
stance, emotion = await self.gpt._get_emotion_tags(ori_response, message.processed_plain_text) # 注意anchor_message.processed_plain_text 是锚点消息的文本,不一定是思考的全部上下文
stance, emotion = await self.gpt._get_emotion_tags(ori_response, anchor_message.processed_plain_text)
await relationship_manager.calculate_update_relationship_value( await relationship_manager.calculate_update_relationship_value(
chat_stream=message.chat_stream, label=emotion, stance=stance chat_stream=anchor_message.chat_stream, # 使用锚点消息的流
label=emotion,
stance=stance
) )
self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor) self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor)
async def trigger_reply_generation(self, message: MessageRecv): async def trigger_reply_generation(self, stream_id: str, observed_messages: List[dict]):
"""根据意愿阈值触发的实际回复生成和发送逻辑 (V3 - 简化参数)""" """根据 SubHeartflow 的触发信号生成回复 (基于观察)"""
chat = message.chat_stream chat = None
userinfo = message.message_info.user_info sub_hf = None
messageinfo = message.message_info anchor_message: Optional[MessageRecv] = None # <--- 重命名,用于锚定回复的消息对象
userinfo: Optional[UserInfo] = None
messageinfo: Optional[MessageInfo] = None
timing_results = {} timing_results = {}
current_mind = None
response_set = None response_set = None
thinking_id = None thinking_id = None
info_catcher = None info_catcher = None
try: try:
# --- 1. 获取核心对象ChatStream 和 SubHeartflow ---
try: try:
with Timer("观察", timing_results): with Timer("获取聊天流和子心流", timing_results):
sub_hf = heartflow.get_subheartflow(chat.stream_id) chat = chat_manager.get_stream(stream_id)
if not sub_hf: if not chat:
logger.warning(f"尝试观察时未找到 stream_id {chat.stream_id} 的 subheartflow") logger.error(f"[{stream_id}] 无法找到聊天流对象,无法生成回复。")
return
sub_hf = heartflow.get_subheartflow(stream_id)
if not sub_hf:
logger.error(f"[{stream_id}] 无法找到子心流对象,无法生成回复。")
return return
await sub_hf.do_observe()
except Exception as e: except Exception as e:
logger.error(f"心流观察失败: {e}") logger.error(f"[{stream_id}] 获取 ChatStream 或 SubHeartflow 时出错: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return
# --- 2. 尝试从 observed_messages 重建最后一条消息作为锚点 --- #
try:
with Timer("获取最后消息锚点", timing_results):
if observed_messages:
last_msg_dict = observed_messages[-1] # 直接从传入列表获取最后一条
# 尝试从字典重建 MessageRecv 对象(可能需要调整 MessageRecv 的构造方式或创建一个辅助函数)
# 这是一个简化示例,假设 MessageRecv 可以从字典初始化
# 你可能需要根据 MessageRecv 的实际 __init__ 来调整
try:
anchor_message = MessageRecv(last_msg_dict) # 假设 MessageRecv 支持从字典创建
userinfo = anchor_message.message_info.user_info
messageinfo = anchor_message.message_info
logger.debug(f"[{stream_id}] 获取到最后消息作为锚点: ID={messageinfo.message_id}, Sender={userinfo.user_nickname}")
except Exception as e_msg:
logger.error(f"[{stream_id}] 从字典重建最后消息 MessageRecv 失败: {e_msg}. 字典: {last_msg_dict}")
anchor_message = None # 重置以表示失败
else:
logger.warning(f"[{stream_id}] 无法从 Observation 获取最后消息锚点。")
except Exception as e:
logger.error(f"[{stream_id}] 获取最后消息锚点时出错: {e}")
logger.error(traceback.format_exc())
# 即使没有锚点,也可能继续尝试生成非回复性消息,取决于后续逻辑
# --- 3. 检查是否能继续 (需要思考消息锚点) ---
if not anchor_message:
logger.warning(f"[{stream_id}] 没有有效的消息锚点,无法创建思考消息和发送回复。取消回复生成。")
return
# --- 4. 检查并发思考限制 (使用 anchor_message 简化获取) ---
try:
container = MessageManager().get_container(chat.stream_id) container = MessageManager().get_container(chat.stream_id)
thinking_count = container.count_thinking_messages() thinking_count = container.count_thinking_messages()
max_thinking_messages = getattr(global_config, 'max_concurrent_thinking_messages', 3) max_thinking_messages = getattr(global_config, 'max_concurrent_thinking_messages', 3)
if thinking_count >= max_thinking_messages: if thinking_count >= max_thinking_messages:
logger.warning(f"聊天流 {chat.stream_id} 已有 {thinking_count} 条思考消息,取消回复。触发消息: {message.processed_plain_text[:30]}...") logger.warning(f"聊天流 {chat.stream_id} 已有 {thinking_count} 条思考消息,取消回复。")
return
except Exception as e:
logger.error(f"[{stream_id}] 检查并发思考限制时出错: {e}")
return return
# --- 5. 创建思考消息 (使用 anchor_message) ---
try: try:
with Timer("创建思考消息", timing_results): with Timer("创建思考消息", timing_results):
thinking_id = await self._create_thinking_message(message) # 注意:这里传递 anchor_message 给 _create_thinking_message
thinking_id = await self._create_thinking_message(anchor_message)
except Exception as e: except Exception as e:
logger.error(f"心流创建思考消息失败: {e}") logger.error(f"[{stream_id}] 创建思考消息失败: {e}")
return return
if not thinking_id: if not thinking_id:
logger.error("未能成功创建思考消息 ID无法继续回复流程。") logger.error(f"[{stream_id}] 未能成功创建思考消息 ID无法继续回复流程。")
return return
logger.trace(f"创建捕捉器thinking_id:{thinking_id}") # --- 6. 信息捕捉器 (使用 anchor_message) ---
logger.trace(f"[{stream_id}] 创建捕捉器thinking_id:{thinking_id}")
info_catcher = info_catcher_manager.get_info_catcher(thinking_id) info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
info_catcher.catch_decide_to_response(message) info_catcher.catch_decide_to_response(anchor_message)
# --- 7. 思考前使用工具 --- #
get_mid_memory_id = [] get_mid_memory_id = []
tool_result_info = {} tool_result_info = {}
send_emoji = "" send_emoji = ""
observation_context_text = "" # 从 observation 获取上下文文本
try: try:
# --- 使用传入的 observed_messages 构建上下文文本 --- #
if observed_messages:
# 可以选择转换全部消息,或只转换最后几条
# 这里示例转换全部消息
context_texts = []
for msg_dict in observed_messages:
# 假设 detailed_plain_text 字段包含所需文本
# 你可能需要更复杂的逻辑来格式化,例如添加发送者和时间
text = msg_dict.get('detailed_plain_text', '')
if text: context_texts.append(text)
observation_context_text = "\n".join(context_texts)
logger.debug(f"[{stream_id}] Context for tools:\n{observation_context_text[-200:]}...") # 打印部分上下文
else:
logger.warning(f"[{stream_id}] observed_messages 列表为空,无法为工具提供上下文。")
if observation_context_text:
with Timer("思考前使用工具", timing_results): with Timer("思考前使用工具", timing_results):
tool_result = await self.tool_user.use_tool( tool_result = await self.tool_user.use_tool(
message.processed_plain_text, message_txt=observation_context_text, # <--- 使用观察上下文
userinfo.user_nickname, chat_stream=chat,
chat, sub_heartflow=sub_hf
heartflow.get_subheartflow(chat.stream_id),
) )
if tool_result.get("used_tools", False): if tool_result.get("used_tools", False):
if "structured_info" in tool_result: if "structured_info" in tool_result:
@@ -287,78 +391,89 @@ class HeartFC_Chat:
if tool_name == "send_emoji": if tool_name == "send_emoji":
send_emoji = tool_data[0]["content"] send_emoji = tool_data[0]["content"]
except Exception as e: except Exception as e:
logger.error(f"思考前工具调用失败: {e}") logger.error(f"[{stream_id}] 思考前工具调用失败: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
current_mind, past_mind = "", "" # --- 8. 调用 SubHeartflow 进行思考 (不传递具体消息文本和发送者) ---
try: try:
with Timer("思考前脑内状态", timing_results): with Timer("生成内心想法(SubHF)", timing_results):
sub_hf = heartflow.get_subheartflow(chat.stream_id) # 不再传递 message_txt 和 sender_info, SubHeartflow 应基于其内部观察
if sub_hf:
current_mind, past_mind = await sub_hf.do_thinking_before_reply( current_mind, past_mind = await sub_hf.do_thinking_before_reply(
message_txt=message.processed_plain_text,
sender_info=userinfo,
chat_stream=chat, chat_stream=chat,
obs_id=get_mid_memory_id,
extra_info=tool_result_info, extra_info=tool_result_info,
obs_id=get_mid_memory_id,
) )
else: logger.info(f"[{stream_id}] SubHeartflow 思考完成: {current_mind}")
logger.warning(f"尝试思考前状态时未找到 stream_id {chat.stream_id} 的 subheartflow")
except Exception as e: except Exception as e:
logger.error(f"心流思考前脑内状态失败: {e}") logger.error(f"[{stream_id}] SubHeartflow 思考失败: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
if info_catcher: info_catcher.done_catch()
return # 思考失败则不继续
if info_catcher: if info_catcher:
info_catcher.catch_afer_shf_step(timing_results.get("思考前脑内状态"), past_mind, current_mind) info_catcher.catch_afer_shf_step(timing_results.get("生成内心想法(SubHF)"), past_mind, current_mind)
# --- 9. 调用 ResponseGenerator 生成回复 (使用 anchor_message 和 current_mind) ---
try: try:
with Timer("生成回复", timing_results): with Timer("生成最终回复(GPT)", timing_results):
response_set = await self.gpt.generate_response(message, thinking_id) response_set = await self.gpt.generate_response(anchor_message, thinking_id, current_mind=current_mind)
except Exception as e: except Exception as e:
logger.error(f"GPT 生成回复失败: {e}") logger.error(f"[{stream_id}] GPT 生成回复失败: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
if info_catcher: info_catcher.done_catch() if info_catcher: info_catcher.done_catch()
return return
if info_catcher: if info_catcher:
info_catcher.catch_after_generate_response(timing_results.get("生成回复")) info_catcher.catch_after_generate_response(timing_results.get("生成最终回复(GPT)"))
if not response_set: if not response_set:
logger.info("回复生成失败,返回为空") logger.info(f"[{stream_id}] 回复生成失败为空")
if info_catcher: info_catcher.done_catch() if info_catcher: info_catcher.done_catch()
return return
# --- 10. 发送消息 (使用 anchor_message) ---
first_bot_msg = None first_bot_msg = None
try: try:
with Timer("发送消息", timing_results): with Timer("发送消息", timing_results):
first_bot_msg = await self._send_response_messages(message, response_set, thinking_id) first_bot_msg = await self._send_response_messages(anchor_message, response_set, thinking_id)
except Exception as e: except Exception as e:
logger.error(f"心流发送消息失败: {e}") logger.error(f"[{stream_id}] 发送消息失败: {e}")
logger.error(traceback.format_exc())
if info_catcher: if info_catcher:
info_catcher.catch_after_response(timing_results.get("发送消息"), response_set, first_bot_msg) info_catcher.catch_after_response(timing_results.get("发送消息"), response_set, first_bot_msg)
info_catcher.done_catch() info_catcher.done_catch() # 完成捕捉
# --- 11. 处理表情包 (使用 anchor_message) ---
try: try:
with Timer("处理表情包", timing_results): with Timer("处理表情包", timing_results):
if send_emoji: if send_emoji:
logger.info(f"麦麦决定发送表情包{send_emoji}") logger.info(f"[{stream_id}] 决定发送表情包 {send_emoji}")
await self._handle_emoji(message, response_set, send_emoji) await self._handle_emoji(anchor_message, response_set, send_emoji)
except Exception as e: except Exception as e:
logger.error(f"心流处理表情包失败: {e}") logger.error(f"[{stream_id}] 处理表情包失败: {e}")
logger.error(traceback.format_exc())
# --- 12. 记录性能日志 --- #
timing_str = " | ".join([f"{step}: {duration:.2f}" for step, duration in timing_results.items()]) timing_str = " | ".join([f"{step}: {duration:.2f}" for step, duration in timing_results.items()])
trigger_msg = message.processed_plain_text
response_msg = " ".join(response_set) if response_set else "无回复" response_msg = " ".join(response_set) if response_set else "无回复"
logger.info(f"回复任务完成: 触发消息: {trigger_msg[:20]}... | 思维消息: {response_msg[:20]}... | 性能计时: {timing_str}") logger.info(f"[{stream_id}] 回复任务完成 (Observation Triggered): | 思维消息: {response_msg[:30]}... | 性能计时: {timing_str}")
if first_bot_msg: # --- 13. 更新关系情绪 (使用 anchor_message) ---
if first_bot_msg: # 仅在成功发送消息后
try: try:
with Timer("更新关系情绪", timing_results): with Timer("更新关系情绪", timing_results):
await self._update_relationship(message, response_set) await self._update_relationship(anchor_message, response_set)
except Exception as e: except Exception as e:
logger.error(f"更新关系情绪失败: {e}") logger.error(f"[{stream_id}] 更新关系情绪失败: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
except Exception as e: except Exception as e:
logger.error(f"回复生成任务失败 (trigger_reply_generation V3): {e}") logger.error(f"回复生成任务失败 (trigger_reply_generation V4 - Observation Triggered): {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
finally: finally:
# 可以在这里添加清理逻辑,如果有的话
pass pass
# --- 结束重构 ---
# _create_thinking_message, _send_response_messages, _handle_emoji, _update_relationship
# 这几个辅助方法目前仍然依赖 MessageRecv 对象。
# 如果无法可靠地从 Observation 获取并重建最后一条消息的 MessageRecv
# 或者希望回复不锚定具体消息,那么这些方法也需要进一步重构。

View File

@@ -120,7 +120,7 @@ class HeartFC_Processor:
# 更新兴趣度 # 更新兴趣度
try: try:
self.interest_manager.increase_interest(chat.stream_id, value=interested_rate, message=message) self.interest_manager.increase_interest(chat.stream_id, value=interested_rate)
current_interest = self.interest_manager.get_interest(chat.stream_id) # 获取更新后的值用于日志 current_interest = self.interest_manager.get_interest(chat.stream_id) # 获取更新后的值用于日志
logger.trace(f"使用激活率 {interested_rate:.2f} 更新后 (通过缓冲后),当前兴趣度: {current_interest:.2f}") logger.trace(f"使用激活率 {interested_rate:.2f} 更新后 (通过缓冲后),当前兴趣度: {current_interest:.2f}")

View File

@@ -6,6 +6,7 @@ import json # 引入 json
import os # 引入 os import os # 引入 os
import traceback # <--- 添加导入 import traceback # <--- 添加导入
from typing import Optional # <--- 添加导入 from typing import Optional # <--- 添加导入
import random # <--- 添加导入 random
from src.common.logger import get_module_logger, LogConfig, DEFAULT_CONFIG # 引入 DEFAULT_CONFIG from src.common.logger import get_module_logger, LogConfig, DEFAULT_CONFIG # 引入 DEFAULT_CONFIG
from src.plugins.chat.chat_stream import chat_manager # *** Import ChatManager *** from src.plugins.chat.chat_stream import chat_manager # *** Import ChatManager ***
from ...chat.message import MessageRecv # 导入 MessageRecv from ...chat.message import MessageRecv # 导入 MessageRecv
@@ -20,7 +21,6 @@ logger = get_module_logger("InterestManager", config=interest_log_config)
# 定义常量 # 定义常量
DEFAULT_DECAY_RATE_PER_SECOND = 0.95 # 每秒衰减率 (兴趣保留 99%) DEFAULT_DECAY_RATE_PER_SECOND = 0.95 # 每秒衰减率 (兴趣保留 99%)
# DEFAULT_INCREASE_AMOUNT = 10.0 # 不再需要固定增加值
MAX_INTEREST = 10.0 # 最大兴趣值 MAX_INTEREST = 10.0 # 最大兴趣值
MIN_INTEREST_THRESHOLD = 0.1 # 低于此值可能被清理 (可选) MIN_INTEREST_THRESHOLD = 0.1 # 低于此值可能被清理 (可选)
CLEANUP_INTERVAL_SECONDS = 3600 # 清理任务运行间隔 (例如1小时) CLEANUP_INTERVAL_SECONDS = 3600 # 清理任务运行间隔 (例如1小时)
@@ -32,16 +32,39 @@ HISTORY_LOG_FILENAME = "interest_history.log" # 新的历史日志文件名
# 移除阈值,将移至 HeartFC_Chat # 移除阈值,将移至 HeartFC_Chat
# INTEREST_INCREASE_THRESHOLD = 0.5 # INTEREST_INCREASE_THRESHOLD = 0.5
# --- 新增:概率回复相关常量 ---
REPLY_TRIGGER_THRESHOLD = 5.0 # 触发概率回复的兴趣阈值 (示例值)
BASE_REPLY_PROBABILITY = 0.05 # 首次超过阈值时的基础回复概率 (示例值)
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.02 # 高于阈值时,每秒概率增加量 (线性增长, 示例值)
PROBABILITY_DECAY_FACTOR_PER_SECOND = 0.3 # 低于阈值时,每秒概率衰减因子 (指数衰减, 示例值)
MAX_REPLY_PROBABILITY = 0.95 # 回复概率上限 (示例值)
# --- 结束:概率回复相关常量 ---
class InterestChatting: class InterestChatting:
def __init__(self, decay_rate=DEFAULT_DECAY_RATE_PER_SECOND, max_interest=MAX_INTEREST): def __init__(self,
decay_rate=DEFAULT_DECAY_RATE_PER_SECOND,
max_interest=MAX_INTEREST,
trigger_threshold=REPLY_TRIGGER_THRESHOLD,
base_reply_probability=BASE_REPLY_PROBABILITY,
increase_rate=PROBABILITY_INCREASE_RATE_PER_SECOND,
decay_factor=PROBABILITY_DECAY_FACTOR_PER_SECOND,
max_probability=MAX_REPLY_PROBABILITY):
self.interest_level: float = 0.0 self.interest_level: float = 0.0
self.last_update_time: float = time.time() self.last_update_time: float = time.time() # 同时作为兴趣和概率的更新时间基准
self.decay_rate_per_second: float = decay_rate self.decay_rate_per_second: float = decay_rate
# self.increase_amount: float = increase_amount # 移除固定的 increase_amount
self.max_interest: float = max_interest self.max_interest: float = max_interest
# 新增:用于追踪最后一次显著增加的信息,供外部监控任务使用
self.last_increase_amount: float = 0.0 self.last_increase_amount: float = 0.0
self.last_triggering_message: MessageRecv | None = None self.last_interaction_time: float = self.last_update_time # 新增:最后交互时间
# --- 新增:概率回复相关属性 ---
self.trigger_threshold: float = trigger_threshold
self.base_reply_probability: float = base_reply_probability
self.probability_increase_rate: float = increase_rate
self.probability_decay_factor: float = decay_factor
self.max_reply_probability: float = max_probability
self.current_reply_probability: float = 0.0
self.is_above_threshold: bool = False # 标记兴趣值是否高于阈值
# --- 结束:概率回复相关属性 ---
def _calculate_decay(self, current_time: float): def _calculate_decay(self, current_time: float):
"""计算从上次更新到现在的衰减""" """计算从上次更新到现在的衰减"""
@@ -49,6 +72,7 @@ class InterestChatting:
if time_delta > 0: if time_delta > 0:
# 指数衰减: interest = interest * (decay_rate ^ time_delta) # 指数衰减: interest = interest * (decay_rate ^ time_delta)
# 添加处理极小兴趣值避免 math domain error # 添加处理极小兴趣值避免 math domain error
old_interest = self.interest_level
if self.interest_level < 1e-9: if self.interest_level < 1e-9:
self.interest_level = 0.0 self.interest_level = 0.0
else: else:
@@ -71,46 +95,141 @@ class InterestChatting:
# 防止低于阈值 (如果需要) # 防止低于阈值 (如果需要)
# self.interest_level = max(self.interest_level, MIN_INTEREST_THRESHOLD) # self.interest_level = max(self.interest_level, MIN_INTEREST_THRESHOLD)
# 只有在兴趣值发生变化时才更新时间戳
if old_interest != self.interest_level:
self.last_update_time = current_time self.last_update_time = current_time
def increase_interest(self, current_time: float, value: float, message: Optional[MessageRecv]): def _update_reply_probability(self, current_time: float):
"""根据传入的值增加兴趣值,并记录增加量和关联消息""" """根据当前兴趣是否超过阈值及时间差,更新回复概率"""
self._calculate_decay(current_time) # 先计算衰减 time_delta = current_time - self.last_update_time
# 记录这次增加的具体数值和消息,供外部判断是否触发 if time_delta <= 0:
return # 时间未前进,无需更新
currently_above = self.interest_level >= self.trigger_threshold
if currently_above:
if not self.is_above_threshold:
# 刚跨过阈值,重置为基础概率
self.current_reply_probability = self.base_reply_probability
logger.debug(f"兴趣跨过阈值 ({self.trigger_threshold}). 概率重置为基础值: {self.base_reply_probability:.4f}")
else:
# 持续高于阈值,线性增加概率
increase_amount = self.probability_increase_rate * time_delta
self.current_reply_probability += increase_amount
logger.debug(f"兴趣高于阈值 ({self.trigger_threshold}) 持续 {time_delta:.2f}秒. 概率增加 {increase_amount:.4f}{self.current_reply_probability:.4f}")
# 限制概率不超过最大值
self.current_reply_probability = min(self.current_reply_probability, self.max_reply_probability)
else: # 低于阈值
if self.is_above_threshold:
# 刚低于阈值,开始衰减
logger.debug(f"兴趣低于阈值 ({self.trigger_threshold}). 概率衰减开始于 {self.current_reply_probability:.4f}")
# else: # 持续低于阈值,继续衰减
# pass # 不需要特殊处理
# 指数衰减概率
# 检查 decay_factor 是否有效
if 0 < self.probability_decay_factor < 1:
decay_multiplier = math.pow(self.probability_decay_factor, time_delta)
old_prob = self.current_reply_probability
self.current_reply_probability *= decay_multiplier
# 避免因浮点数精度问题导致概率略微大于0直接设为0
if self.current_reply_probability < 1e-6:
self.current_reply_probability = 0.0
logger.debug(f"兴趣低于阈值 ({self.trigger_threshold}) 持续 {time_delta:.2f}秒. 概率从 {old_prob:.4f} 衰减到 {self.current_reply_probability:.4f} (因子: {self.probability_decay_factor})")
elif self.probability_decay_factor <= 0:
# 如果衰减因子无效或为0直接清零
if self.current_reply_probability > 0:
logger.warning(f"无效的衰减因子 ({self.probability_decay_factor}). 设置概率为0.")
self.current_reply_probability = 0.0
# else: decay_factor >= 1, probability will not decay or increase, which might be intended in some cases.
# 确保概率不低于0
self.current_reply_probability = max(self.current_reply_probability, 0.0)
# 更新状态标记
self.is_above_threshold = currently_above
# 更新时间戳放在调用者处,确保 interest 和 probability 基于同一点更新
def increase_interest(self, current_time: float, value: float):
"""根据传入的值增加兴趣值,并记录增加量"""
# 先更新概率和计算衰减(基于上次更新时间)
self._update_reply_probability(current_time)
self._calculate_decay(current_time)
# 记录这次增加的具体数值,供外部判断是否触发
self.last_increase_amount = value self.last_increase_amount = value
self.last_triggering_message = message
# 应用增加 # 应用增加
self.interest_level += value self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest) # 不超过最大值 self.interest_level = min(self.interest_level, self.max_interest) # 不超过最大值
self.last_update_time = current_time # 更新时间戳 self.last_update_time = current_time # 更新时间戳
self.last_interaction_time = current_time # 更新最后交互时间
def decrease_interest(self, current_time: float, value: float): def decrease_interest(self, current_time: float, value: float):
"""降低兴趣值并更新时间 (确保不低于0)""" """降低兴趣值并更新时间 (确保不低于0)"""
# 先更新概率(基于上次更新时间)
self._update_reply_probability(current_time)
# 注意:降低兴趣度是否需要先衰减?取决于具体逻辑,这里假设不衰减直接减 # 注意:降低兴趣度是否需要先衰减?取决于具体逻辑,这里假设不衰减直接减
self.interest_level -= value self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0) # 确保不低于0 self.interest_level = max(self.interest_level, 0.0) # 确保不低于0
self.last_update_time = current_time # 降低也更新时间戳 self.last_update_time = current_time # 降低也更新时间戳
self.last_interaction_time = current_time # 更新最后交互时间
def reset_trigger_info(self): def reset_trigger_info(self):
"""重置触发相关信息,在外部任务处理后调用""" """重置触发相关信息,在外部任务处理后调用"""
self.last_increase_amount = 0.0 self.last_increase_amount = 0.0
self.last_triggering_message = None
def get_interest(self) -> float: def get_interest(self) -> float:
"""获取当前兴趣值 (由后台任务更新)""" """获取当前兴趣值 (计算衰减后)"""
# 注意:这个方法现在会触发概率和兴趣的更新
current_time = time.time()
self._update_reply_probability(current_time)
self._calculate_decay(current_time)
self.last_update_time = current_time # 更新时间戳
return self.interest_level return self.interest_level
def get_state(self) -> dict: def get_state(self) -> dict:
"""获取当前状态字典""" """获取当前状态字典"""
# 不再需要传入 current_time 来计算,直接获取 # 调用 get_interest 来确保状态已更新
interest = self.get_interest() # 使用修改后的 get_interest interest = self.get_interest()
return { return {
"interest_level": round(interest, 2), "interest_level": round(interest, 2),
"last_update_time": self.last_update_time, "last_update_time": self.last_update_time,
"current_reply_probability": round(self.current_reply_probability, 4), # 添加概率到状态
"is_above_threshold": self.is_above_threshold, # 添加阈值状态
"last_interaction_time": self.last_interaction_time # 新增:添加最后交互时间到状态
# 可以选择性地暴露 last_increase_amount 给状态,方便调试 # 可以选择性地暴露 last_increase_amount 给状态,方便调试
# "last_increase_amount": round(self.last_increase_amount, 2) # "last_increase_amount": round(self.last_increase_amount, 2)
} }
def should_evaluate_reply(self) -> bool:
"""
判断是否应该触发一次回复评估。
首先更新概率状态,然后根据当前概率进行随机判断。
"""
current_time = time.time()
# 确保概率是基于最新兴趣值计算的
self._update_reply_probability(current_time)
# 更新兴趣衰减(如果需要,取决于逻辑,这里保持和 get_interest 一致)
self._calculate_decay(current_time)
self.last_update_time = current_time # 更新时间戳
if self.is_above_threshold and self.current_reply_probability > 0:
# 只有在阈值之上且概率大于0时才有可能触发
trigger = random.random() < self.current_reply_probability
if trigger:
logger.info(f"Reply evaluation triggered! Probability: {self.current_reply_probability:.4f}, Threshold: {self.trigger_threshold}, Interest: {self.interest_level:.2f}")
# 可选:触发后是否重置/降低概率?根据需要决定
# self.current_reply_probability = self.base_reply_probability # 例如,触发后降回基础概率
# self.current_reply_probability *= 0.5 # 例如,触发后概率减半
else:
logger.debug(f"Reply evaluation NOT triggered. Probability: {self.current_reply_probability:.4f}, Random value: {trigger + 1e-9:.4f}") # 打印随机值用于调试
return trigger
else:
# logger.debug(f"Reply evaluation check: Below threshold or zero probability. Probability: {self.current_reply_probability:.4f}")
return False
class InterestManager: class InterestManager:
_instance = None _instance = None
@@ -156,14 +275,14 @@ class InterestManager:
"""后台清理任务的异步函数""" """后台清理任务的异步函数"""
while True: while True:
await asyncio.sleep(interval_seconds) await asyncio.sleep(interval_seconds)
logger.info(f"Running periodic cleanup (interval: {interval_seconds}s)...") logger.info(f"运行定期清理 (间隔: {interval_seconds})...")
self.cleanup_inactive_chats(threshold=threshold, max_age_seconds=max_age_seconds) self.cleanup_inactive_chats(threshold=threshold, max_age_seconds=max_age_seconds)
async def _periodic_log_task(self, interval_seconds: int): async def _periodic_log_task(self, interval_seconds: int):
"""后台日志记录任务的异步函数 (记录历史数据,包含 group_name)""" """后台日志记录任务的异步函数 (记录历史数据,包含 group_name)"""
while True: while True:
await asyncio.sleep(interval_seconds) await asyncio.sleep(interval_seconds)
logger.debug(f"Running periodic history logging (interval: {interval_seconds}s)...") logger.debug(f"运行定期历史记录 (间隔: {interval_seconds})...")
try: try:
current_timestamp = time.time() current_timestamp = time.time()
all_states = self.get_all_interest_states() # 获取当前所有状态 all_states = self.get_all_interest_states() # 获取当前所有状态
@@ -190,7 +309,11 @@ class InterestManager:
"timestamp": round(current_timestamp, 2), "timestamp": round(current_timestamp, 2),
"stream_id": stream_id, "stream_id": stream_id,
"interest_level": state.get("interest_level", 0.0), # 确保有默认值 "interest_level": state.get("interest_level", 0.0), # 确保有默认值
"group_name": group_name # *** Add group_name *** "group_name": group_name, # *** Add group_name ***
# --- 新增:记录概率相关信息 ---
"reply_probability": state.get("current_reply_probability", 0.0),
"is_above_threshold": state.get("is_above_threshold", False)
# --- 结束新增 ---
} }
# 将每个条目作为单独的 JSON 行写入 # 将每个条目作为单独的 JSON 行写入
f.write(json.dumps(log_entry, ensure_ascii=False) + '\n') f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
@@ -230,7 +353,7 @@ class InterestManager:
# logger.debug(f"Applied decay to {count} streams.") # logger.debug(f"Applied decay to {count} streams.")
async def start_background_tasks(self): async def start_background_tasks(self):
"""Starts the background cleanup, logging, and decay tasks.""" """启动清理,启动衰减,启动记录,启动启动启动启动启动"""
if self._cleanup_task is None or self._cleanup_task.done(): if self._cleanup_task is None or self._cleanup_task.done():
self._cleanup_task = asyncio.create_task( self._cleanup_task = asyncio.create_task(
self._periodic_cleanup_task( self._periodic_cleanup_task(
@@ -239,26 +362,26 @@ class InterestManager:
max_age_seconds=INACTIVE_THRESHOLD_SECONDS max_age_seconds=INACTIVE_THRESHOLD_SECONDS
) )
) )
logger.info(f"Periodic cleanup task created. Interval: {CLEANUP_INTERVAL_SECONDS}s, Inactive Threshold: {INACTIVE_THRESHOLD_SECONDS}s") logger.info(f"已创建定期清理任务。间隔时间: {CLEANUP_INTERVAL_SECONDS}, 不活跃阈值: {INACTIVE_THRESHOLD_SECONDS}")
else: else:
logger.warning("Cleanup task creation skipped: already running or exists.") logger.warning("跳过创建清理任务:任务已在运行或存在。")
if self._logging_task is None or self._logging_task.done(): if self._logging_task is None or self._logging_task.done():
self._logging_task = asyncio.create_task( self._logging_task = asyncio.create_task(
self._periodic_log_task(interval_seconds=LOG_INTERVAL_SECONDS) self._periodic_log_task(interval_seconds=LOG_INTERVAL_SECONDS)
) )
logger.info(f"Periodic logging task created. Interval: {LOG_INTERVAL_SECONDS}s") logger.info(f"已创建定期日志任务。间隔时间: {LOG_INTERVAL_SECONDS}")
else: else:
logger.warning("Logging task creation skipped: already running or exists.") logger.warning("跳过创建日志任务:任务已在运行或存在。")
# 启动新的衰减任务 # 启动新的衰减任务
if self._decay_task is None or self._decay_task.done(): if self._decay_task is None or self._decay_task.done():
self._decay_task = asyncio.create_task( self._decay_task = asyncio.create_task(
self._periodic_decay_task() self._periodic_decay_task()
) )
logger.info("Periodic decay task created. Interval: 1s") logger.info("已创建定期衰减任务。间隔时间: 1")
else: else:
logger.warning("Decay task creation skipped: already running or exists.") logger.warning("跳过创建衰减任务:任务已在运行或存在。")
def get_all_interest_states(self) -> dict[str, dict]: def get_all_interest_states(self) -> dict[str, dict]:
"""获取所有聊天流的当前兴趣状态""" """获取所有聊天流的当前兴趣状态"""
@@ -287,7 +410,16 @@ class InterestManager:
# with self._lock: # with self._lock:
if stream_id not in self.interest_dict: if stream_id not in self.interest_dict:
logger.debug(f"Creating new InterestChatting for stream_id: {stream_id}") logger.debug(f"Creating new InterestChatting for stream_id: {stream_id}")
self.interest_dict[stream_id] = InterestChatting() # --- 修改:创建时传入概率相关参数 (如果需要定制化,否则使用默认值) ---
self.interest_dict[stream_id] = InterestChatting(
# decay_rate=..., max_interest=..., # 可以从配置读取
trigger_threshold=REPLY_TRIGGER_THRESHOLD, # 使用全局常量
base_reply_probability=BASE_REPLY_PROBABILITY,
increase_rate=PROBABILITY_INCREASE_RATE_PER_SECOND,
decay_factor=PROBABILITY_DECAY_FACTOR_PER_SECOND,
max_probability=MAX_REPLY_PROBABILITY
)
# --- 结束修改 ---
# 首次创建时兴趣为 0由第一次消息的 activate rate 决定初始值 # 首次创建时兴趣为 0由第一次消息的 activate rate 决定初始值
return self.interest_dict[stream_id] return self.interest_dict[stream_id]
@@ -298,13 +430,13 @@ class InterestManager:
# 直接调用修改后的 get_interest不传入时间 # 直接调用修改后的 get_interest不传入时间
return interest_chatting.get_interest() return interest_chatting.get_interest()
def increase_interest(self, stream_id: str, value: float, message: MessageRecv): def increase_interest(self, stream_id: str, value: float):
"""当收到消息时,增加指定聊天流的兴趣度,并传递关联消息""" """当收到消息时,增加指定聊天流的兴趣度"""
current_time = time.time() current_time = time.time()
interest_chatting = self._get_or_create_interest_chatting(stream_id) interest_chatting = self._get_or_create_interest_chatting(stream_id)
# 调用修改后的 increase_interest传入 message # 调用修改后的 increase_interest不再传入 message
interest_chatting.increase_interest(current_time, value, message) interest_chatting.increase_interest(current_time, value)
logger.debug(f"Increased interest for stream_id: {stream_id} by {value:.2f} to {interest_chatting.interest_level:.2f}") # 更新日志 logger.debug(f"增加了聊天流 {stream_id} 的兴趣度 {value:.2f},当前值为 {interest_chatting.interest_level:.2f}") # 更新日志
def decrease_interest(self, stream_id: str, value: float): def decrease_interest(self, stream_id: str, value: float):
"""降低指定聊天流的兴趣度""" """降低指定聊天流的兴趣度"""
@@ -313,13 +445,13 @@ class InterestManager:
interest_chatting = self.get_interest_chatting(stream_id) interest_chatting = self.get_interest_chatting(stream_id)
if interest_chatting: if interest_chatting:
interest_chatting.decrease_interest(current_time, value) interest_chatting.decrease_interest(current_time, value)
logger.debug(f"Decreased interest for stream_id: {stream_id} by {value:.2f} to {interest_chatting.interest_level:.2f}") logger.debug(f"降低了聊天流 {stream_id} 的兴趣度 {value:.2f},当前值为 {interest_chatting.interest_level:.2f}")
else: else:
logger.warning(f"Attempted to decrease interest for non-existent stream_id: {stream_id}") logger.warning(f"尝试降低不存在的聊天流 {stream_id} 的兴趣度")
def cleanup_inactive_chats(self, threshold=MIN_INTEREST_THRESHOLD, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): def cleanup_inactive_chats(self, threshold=MIN_INTEREST_THRESHOLD, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
""" """
清理长时间不活跃或兴趣度过低的聊天流记录 清理长时间不活跃的聊天流记录
threshold: 低于此兴趣度的将被清理 threshold: 低于此兴趣度的将被清理
max_age_seconds: 超过此时间未更新的将被清理 max_age_seconds: 超过此时间未更新的将被清理
""" """
@@ -334,37 +466,27 @@ class InterestManager:
# 先计算当前兴趣,确保是最新的 # 先计算当前兴趣,确保是最新的
# 加锁保护 chatting 对象状态的读取和可能的修改 # 加锁保护 chatting 对象状态的读取和可能的修改
# with self._lock: # 如果 InterestChatting 内部操作不是原子的 # with self._lock: # 如果 InterestChatting 内部操作不是原子的
interest = chatting.get_interest() last_interaction = chatting.last_interaction_time # 使用最后交互时间
last_update = chatting.last_update_time
should_remove = False should_remove = False
reason = "" reason = ""
if interest < threshold:
should_remove = True
reason = f"interest ({interest:.2f}) < threshold ({threshold})"
# 只有设置了 max_age_seconds 才检查时间 # 只有设置了 max_age_seconds 才检查时间
if max_age_seconds is not None and (current_time - last_update) > max_age_seconds: if max_age_seconds is not None and (current_time - last_interaction) > max_age_seconds: # 使用 last_interaction
should_remove = True should_remove = True
reason = f"inactive time ({current_time - last_update:.0f}s) > max age ({max_age_seconds}s)" + (f", {reason}" if reason else "") # 附加之前的理由 reason = f"inactive time ({current_time - last_interaction:.0f}s) > max age ({max_age_seconds}s)" # 更新日志信息
if should_remove: if should_remove:
keys_to_remove.append(stream_id) keys_to_remove.append(stream_id)
logger.debug(f"Marking stream_id {stream_id} for removal. Reason: {reason}") logger.debug(f"Marking stream_id {stream_id} for removal. Reason: {reason}")
if keys_to_remove: if keys_to_remove:
logger.info(f"Cleanup identified {len(keys_to_remove)} inactive/low-interest streams.") logger.info(f"清理识别到 {len(keys_to_remove)} 个不活跃/低兴趣的流。")
# with self._lock: # 确保删除操作的原子性 # with self._lock: # 确保删除操作的原子性
for key in keys_to_remove: for key in keys_to_remove:
# 再次检查 key 是否存在,以防万一在迭代和删除之间状态改变 # 再次检查 key 是否存在,以防万一在迭代和删除之间状态改变
if key in self.interest_dict: if key in self.interest_dict:
del self.interest_dict[key] del self.interest_dict[key]
logger.debug(f"Removed stream_id: {key}") logger.debug(f"移除了流_id: {key}")
final_count = initial_count - len(keys_to_remove) final_count = initial_count - len(keys_to_remove)
logger.info(f"Cleanup finished. Removed {len(keys_to_remove)} streams. Current count: {final_count}") logger.info(f"清理完成。移除了 {len(keys_to_remove)} 个流。当前数量: {final_count}")
else: else:
logger.info(f"Cleanup finished. No streams met removal criteria. Current count: {initial_count}") logger.info(f"清理完成。没有流符合移除条件。当前数量: {initial_count}")
# 不再需要手动创建实例和任务
# manager = InterestManager()
# asyncio.create_task(periodic_cleanup(manager, 3600))