feat: wonderful new

This commit is contained in:
SengokuCola
2025-04-17 15:29:20 +08:00
parent eeb13a8498
commit cfdaf00559
9 changed files with 1032 additions and 304 deletions

244
interest_monitor_gui.py Normal file
View File

@@ -0,0 +1,244 @@
import tkinter as tk
from tkinter import ttk
import time
import os
from datetime import datetime
import random
from collections import deque
import json # 引入 json
# --- 引入 Matplotlib ---
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.dates as mdates # 用于处理日期格式
import matplotlib # 导入 matplotlib
# --- 配置 ---
LOG_FILE_PATH = os.path.join("logs", "interest", "interest_history.log") # 指向历史日志文件
REFRESH_INTERVAL_MS = 200 # 刷新间隔 (毫秒) - 可以适当调长,因为读取文件可能耗时
WINDOW_TITLE = "Interest Monitor (Live History)"
MAX_HISTORY_POINTS = 1000 # 图表上显示的最大历史点数 (可以增加)
MAX_STREAMS_TO_DISPLAY = 15 # 最多显示多少个聊天流的折线图 (可以增加)
# *** 添加 Matplotlib 中文字体配置 ***
# 尝试使用 'SimHei' 或 'Microsoft YaHei'如果找不到matplotlib 会回退到默认字体
# 确保你的系统上安装了这些字体
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号'-'显示为方块的问题
class InterestMonitorApp:
def __init__(self, root):
self.root = root
self.root.title(WINDOW_TITLE)
self.root.geometry("1800x800") # 调整窗口大小以适应图表
# --- 数据存储 ---
# 使用 deque 来存储有限的历史数据点
# key: stream_id, value: deque([(timestamp, interest_level), ...])
self.stream_history = {}
self.stream_colors = {} # 为每个 stream 分配颜色
self.stream_display_names = {} # *** New: Store display names (group_name) ***
# --- UI 元素 ---
# 状态标签
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)
# Matplotlib 图表设置
self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111)
# 配置在 update_plot 中进行,避免重复
# 创建 Tkinter 画布嵌入 Matplotlib 图表
self.canvas = FigureCanvasTkAgg(self.fig, master=root)
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# --- 初始化和启动刷新 ---
self.update_display() # 首次加载并开始刷新循环
def get_random_color(self):
"""生成随机颜色用于区分线条"""
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
def load_and_update_history(self):
"""从 history log 文件加载数据并更新历史记录"""
if not os.path.exists(LOG_FILE_PATH):
self.set_status(f"Error: Log file not found at {LOG_FILE_PATH}", "red")
# 如果文件不存在,不清空现有数据,以便显示最后一次成功读取的状态
return
# *** Reset display names each time we reload ***
new_stream_history = {}
new_stream_display_names = {}
read_count = 0
error_count = 0
# *** Calculate the timestamp threshold for the last 30 minutes ***
current_time = time.time()
time_threshold = current_time - (15 * 60) # 30 minutes in seconds
try:
with open(LOG_FILE_PATH, 'r', encoding='utf-8') as f:
for line in f:
read_count += 1
try:
log_entry = json.loads(line.strip())
timestamp = log_entry.get("timestamp")
# *** Add time filtering ***
if timestamp is None or float(timestamp) < time_threshold:
continue # Skip old or invalid entries
stream_id = log_entry.get("stream_id")
interest_level = log_entry.get("interest_level")
group_name = log_entry.get("group_name", stream_id) # *** Get group_name, fallback to stream_id ***
# *** Check other required fields AFTER time filtering ***
if stream_id is None or interest_level is None:
error_count += 1
continue # 跳过无效行
# 如果是第一次读到这个 stream_id则创建 deque
if stream_id not in new_stream_history:
new_stream_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS)
# 检查是否已有颜色,没有则分配
if stream_id not in self.stream_colors:
self.stream_colors[stream_id] = self.get_random_color()
# *** Store the latest display name found for this stream_id ***
new_stream_display_names[stream_id] = group_name
# 添加数据点
new_stream_history[stream_id].append((float(timestamp), float(interest_level)))
except json.JSONDecodeError:
error_count += 1
# logger.warning(f"Skipping invalid JSON line: {line.strip()}")
continue # 跳过无法解析的行
except (TypeError, ValueError) as e:
error_count += 1
# logger.warning(f"Skipping line due to data type error ({e}): {line.strip()}")
continue # 跳过数据类型错误的行
# 读取完成后,用新数据替换旧数据
self.stream_history = new_stream_history
self.stream_display_names = new_stream_display_names # *** Update display names ***
status_msg = f"Data loaded at {datetime.now().strftime('%H:%M:%S')}. Lines read: {read_count}."
if error_count > 0:
status_msg += f" Skipped {error_count} invalid lines."
self.set_status(status_msg, "orange")
else:
self.set_status(status_msg, "green")
except IOError as e:
self.set_status(f"Error reading file {LOG_FILE_PATH}: {e}", "red")
except Exception as e:
self.set_status(f"An unexpected error occurred during loading: {e}", "red")
def update_plot(self):
"""更新 Matplotlib 图表"""
self.ax.clear() # 清除旧图
# *** 设置中文标题和标签 ***
self.ax.set_title("兴趣度随时间变化图")
self.ax.set_xlabel("时间")
self.ax.set_ylabel("兴趣度")
self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
self.ax.grid(True)
self.ax.set_ylim(0, 10) # 固定 Y 轴范围 0-10
# 只绘制最新的 N 个 stream (按最后记录的兴趣度排序)
# 注意:现在是基于文件读取的快照排序,可能不是实时最新
active_streams = sorted(
self.stream_history.items(),
key=lambda item: item[1][-1][1] if item[1] else 0, # 按最后兴趣度排序
reverse=True
)[:MAX_STREAMS_TO_DISPLAY]
all_times = [] # 用于确定 X 轴范围
for stream_id, history in active_streams:
if not history:
continue
timestamps, interests = zip(*history)
# 将 time.time() 时间戳转换为 matplotlib 可识别的日期格式
try:
mpl_dates = [datetime.fromtimestamp(ts) for ts in timestamps]
all_times.extend(mpl_dates) # 收集所有时间点
# *** Use display name for label ***
display_label = self.stream_display_names.get(stream_id, stream_id)
self.ax.plot(
mpl_dates,
interests,
label=display_label, # *** Use display_label ***
color=self.stream_colors.get(stream_id, 'grey'),
marker='.',
markersize=3,
linestyle='-',
linewidth=1
)
except ValueError as e:
print(f"Skipping plot for {stream_id} due to invalid timestamp: {e}")
continue
if all_times:
# 根据数据动态调整 X 轴范围,留一点边距
min_time = min(all_times)
max_time = max(all_times)
# delta = max_time - min_time
# self.ax.set_xlim(min_time - delta * 0.05, max_time + delta * 0.05)
self.ax.set_xlim(min_time, max_time)
# 自动格式化X轴标签
self.fig.autofmt_xdate()
else:
# 如果没有数据,设置一个默认的时间范围,例如最近一小时
now = datetime.now()
one_hour_ago = now - timedelta(hours=1)
self.ax.set_xlim(one_hour_ago, now)
# 添加图例
if active_streams:
# 调整图例位置和大小
# 字体已通过全局 matplotlib.rcParams 设置
self.ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0., fontsize='x-small')
# 调整布局,确保图例不被裁剪
self.fig.tight_layout(rect=[0, 0, 0.85, 1]) # 右侧留出空间给图例
self.canvas.draw() # 重绘画布
def update_display(self):
"""主更新循环"""
try:
self.load_and_update_history() # 从文件加载数据并更新内部状态
self.update_plot() # 根据内部状态更新图表
except Exception as e:
# 提供更详细的错误信息
import traceback
error_msg = f"Error during update: {e}\n{traceback.format_exc()}"
self.set_status(error_msg, "red")
print(error_msg) # 打印详细错误到控制台
# 安排下一次刷新
self.root.after(REFRESH_INTERVAL_MS, self.update_display)
def set_status(self, message: str, color: str = "grey"):
"""更新状态栏标签"""
# 限制状态栏消息长度
max_len = 150
display_message = (message[:max_len] + '...') if len(message) > max_len else message
self.status_label.config(text=display_message, fg=color)
if __name__ == "__main__":
# 导入 timedelta 用于默认时间范围
from datetime import timedelta
root = tk.Tk()
app = InterestMonitorApp(root)
root.mainloop()

View File

@@ -17,6 +17,7 @@ from .common.logger import get_module_logger
from .plugins.remote import heartbeat_thread # noqa: F401 from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality from .individuality.individuality import Individuality
from .common.server import global_server from .common.server import global_server
from .plugins.chat_module.heartFC_chat.interest import InterestManager
logger = get_module_logger("main") logger = get_module_logger("main")
@@ -110,6 +111,15 @@ class MainSystem:
asyncio.create_task(heartflow.heartflow_start_working()) asyncio.create_task(heartflow.heartflow_start_working())
logger.success("心流系统启动成功") logger.success("心流系统启动成功")
# 启动 InterestManager 的后台任务
interest_manager = InterestManager() # 获取单例
await interest_manager.start_background_tasks()
logger.success("InterestManager 后台任务启动成功")
# 启动 HeartFC_Chat 的后台任务(例如兴趣监控)
await chat_bot.heartFC_chat.start()
logger.success("HeartFC_Chat 模块启动成功")
init_time = int(1000 * (time.time() - init_start_time)) init_time = int(1000 * (time.time() - init_start_time))
logger.success(f"初始化完成,神经元放电{init_time}") logger.success(f"初始化完成,神经元放电{init_time}")
except Exception as e: except Exception as e:

View File

@@ -8,6 +8,8 @@ from ..chat_module.only_process.only_message_process import MessageProcessor
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ..chat_module.think_flow_chat.think_flow_chat import ThinkFlowChat from ..chat_module.think_flow_chat.think_flow_chat import ThinkFlowChat
from ..chat_module.reasoning_chat.reasoning_chat import ReasoningChat from ..chat_module.reasoning_chat.reasoning_chat import ReasoningChat
from ..chat_module.heartFC_chat.heartFC_chat import HeartFC_Chat
from ..chat_module.heartFC_chat.heartFC_processor import HeartFC_Processor
from ..utils.prompt_builder import Prompt, global_prompt_manager from ..utils.prompt_builder import Prompt, global_prompt_manager
import traceback import traceback
@@ -30,6 +32,8 @@ class ChatBot:
self.mood_manager.start_mood_update() # 启动情绪更新 self.mood_manager.start_mood_update() # 启动情绪更新
self.think_flow_chat = ThinkFlowChat() self.think_flow_chat = ThinkFlowChat()
self.reasoning_chat = ReasoningChat() self.reasoning_chat = ReasoningChat()
self.heartFC_chat = HeartFC_Chat()
self.heartFC_processor = HeartFC_Processor(self.heartFC_chat)
self.only_process_chat = MessageProcessor() self.only_process_chat = MessageProcessor()
# 创建初始化PFC管理器的任务会在_ensure_started时执行 # 创建初始化PFC管理器的任务会在_ensure_started时执行
@@ -117,7 +121,12 @@ class ChatBot:
if groupinfo.group_id in global_config.talk_allowed_groups: if groupinfo.group_id in global_config.talk_allowed_groups:
# logger.debug(f"开始群聊模式{str(message_data)[:50]}...") # logger.debug(f"开始群聊模式{str(message_data)[:50]}...")
if global_config.response_mode == "heart_flow": if global_config.response_mode == "heart_flow":
await self.think_flow_chat.process_message(message_data) # logger.info(f"启动最新最好的思维流FC模式{str(message_data)[:50]}...")
await self.heartFC_processor.process_message(message_data)
elif global_config.response_mode == "reasoning": elif global_config.response_mode == "reasoning":
# logger.debug(f"开始推理模式{str(message_data)[:50]}...") # logger.debug(f"开始推理模式{str(message_data)[:50]}...")
await self.reasoning_chat.process_message(message_data) await self.reasoning_chat.process_message(message_data)

View File

@@ -1,27 +1,23 @@
import time import time
from random import random from random import random
import traceback import traceback
from typing import List from typing import List, Optional
from ...memory_system.Hippocampus import HippocampusManager import asyncio
from ...moods.moods import MoodManager from ...moods.moods import MoodManager
from ....config.config import global_config from ....config.config import global_config
from ...chat.emoji_manager import emoji_manager from ...chat.emoji_manager import emoji_manager
from .heartFC__generator import ResponseGenerator from .heartFC__generator import ResponseGenerator
from ...chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet from ...chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from .messagesender import MessageManager from .messagesender import MessageManager
from ...storage.storage import MessageStorage
from ...chat.utils import is_mentioned_bot_in_message, get_recent_group_detailed_plain_text
from ...chat.utils_image import image_path_to_base64 from ...chat.utils_image import image_path_to_base64
from ...willing.willing_manager import willing_manager
from ...message import UserInfo, Seg from ...message import UserInfo, Seg
from src.heart_flow.heartflow import heartflow from src.heart_flow.heartflow import heartflow
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ...chat.chat_stream import chat_manager
from ...person_info.relationship_manager import relationship_manager from ...person_info.relationship_manager import relationship_manager
from ...chat.message_buffer import message_buffer
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager 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
# 定义日志配置 # 定义日志配置
chat_config = LogConfig( chat_config = LogConfig(
@@ -29,19 +25,103 @@ chat_config = LogConfig(
file_format=CHAT_STYLE_CONFIG["file_format"], file_format=CHAT_STYLE_CONFIG["file_format"],
) )
logger = get_module_logger("think_flow_chat", config=chat_config) logger = get_module_logger("heartFC_chat", config=chat_config)
# 新增常量
INTEREST_LEVEL_REPLY_THRESHOLD = 4.0
INTEREST_MONITOR_INTERVAL_SECONDS = 1
class ThinkFlowChat: class HeartFC_Chat:
def __init__(self): def __init__(self):
self.storage = MessageStorage()
self.gpt = ResponseGenerator() self.gpt = ResponseGenerator()
self.mood_manager = MoodManager.get_instance() self.mood_manager = MoodManager.get_instance()
self.mood_manager.start_mood_update() self.mood_manager.start_mood_update()
self.tool_user = ToolUser() self.tool_user = ToolUser()
self.interest_manager = InterestManager()
self._interest_monitor_task: Optional[asyncio.Task] = None
async def _create_thinking_message(self, message, chat, userinfo, messageinfo): async def start(self):
"""创建思考消息""" """Starts asynchronous tasks like the interest monitor."""
logger.info("HeartFC_Chat starting asynchronous tasks...")
await self.interest_manager.start_background_tasks()
self._initialize_monitor_task()
logger.info("HeartFC_Chat asynchronous tasks started.")
def _initialize_monitor_task(self):
"""启动后台兴趣监控任务"""
if self._interest_monitor_task is None or self._interest_monitor_task.done():
try:
loop = asyncio.get_running_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}")
except RuntimeError:
logger.error("Failed to create interest monitor task: No running event loop.")
raise
else:
logger.warning("Interest monitor task creation skipped: already running or exists.")
async def _interest_monitor_loop(self):
"""后台任务,定期检查兴趣度变化并触发回复"""
logger.info("Interest monitor loop starting...")
await asyncio.sleep(0.3)
while True:
await asyncio.sleep(INTEREST_MONITOR_INTERVAL_SECONDS)
try:
interest_items_snapshot: List[tuple[str, InterestChatting]] = []
stream_ids = list(self.interest_manager.interest_dict.keys())
for stream_id in 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:
triggering_message = chatting_instance.last_triggering_message
current_interest = chatting_instance.get_interest()
# 添加调试日志,检查触发条件
# logger.debug(f"[兴趣监控][{stream_id}] 当前兴趣: {current_interest:.2f}, 阈值: {INTEREST_LEVEL_REPLY_THRESHOLD}, 触发消息存在: {triggering_message is not None}")
if current_interest > INTEREST_LEVEL_REPLY_THRESHOLD and triggering_message is not None:
logger.info(f"[{stream_id}] 检测到高兴趣度 ({current_interest:.2f} > {INTEREST_LEVEL_REPLY_THRESHOLD}). 基于消息 ID: {triggering_message.message_info.message_id} 的上下文触发回复") # 更新日志信息使其更清晰
chatting_instance.reset_trigger_info()
logger.debug(f"[{stream_id}] Trigger info reset before starting reply task.")
asyncio.create_task(self._process_triggered_reply(stream_id, triggering_message))
except asyncio.CancelledError:
logger.info("Interest monitor loop cancelled.")
break
except Exception as e:
logger.error(f"Error in interest monitor loop: {e}")
logger.error(traceback.format_exc())
await asyncio.sleep(5)
async def _process_triggered_reply(self, stream_id: str, triggering_message: MessageRecv):
"""Helper coroutine to handle the processing of a triggered reply based on interest level."""
try:
logger.info(f"[{stream_id}] Starting level-triggered reply generation for message ID: {triggering_message.message_info.message_id}...")
await self.trigger_reply_generation(triggering_message)
# 在回复处理后降低兴趣度,降低固定值:新阈值的一半
decrease_value = INTEREST_LEVEL_REPLY_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 fixed value {decrease_value:.2f} (LevelThreshold/2) after processing level-triggered reply. Current interest: {post_trigger_interest:.2f}")
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(traceback.format_exc())
async def _create_thinking_message(self, message: MessageRecv):
"""创建思考消息 (从 message 获取信息)"""
chat = message.chat_stream
if not chat:
logger.error(f"Cannot create thinking message, chat_stream is None for message ID: {message.message_info.message_id}")
return None
userinfo = message.message_info.user_info # 发起思考的用户(即原始消息发送者)
messageinfo = 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,
@@ -53,8 +133,8 @@ class ThinkFlowChat:
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_user_info=bot_user_info, # 思考消息的发出者是 bot
reply=message, reply=message, # 回复的是原始消息
thinking_start_time=thinking_time_point, thinking_start_time=thinking_time_point,
) )
@@ -62,24 +142,21 @@ class ThinkFlowChat:
return thinking_id return thinking_id
async def _send_response_messages(self, message, chat, response_set: List[str], thinking_id) -> MessageSending: async def _send_response_messages(self, message: MessageRecv, response_set: List[str], thinking_id) -> MessageSending:
"""发送回复消息""" chat = 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:
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
thinking_message = msg thinking_message = msg
container.messages.remove(msg) container.messages.remove(msg)
break break
if not thinking_message: if not thinking_message:
logger.warning("未找到对应的思考消息,可能已超时被移除") logger.warning("未找到对应的思考消息,可能已超时被移除")
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 in response_set:
@@ -90,11 +167,11 @@ class ThinkFlowChat:
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=message.message_info.platform, # 从传入的 message 获取 platform
), ),
sender_info=message.message_info.user_info, sender_info=message.message_info.user_info, # 发送给谁
message_segment=message_segment, message_segment=message_segment,
reply=message, reply=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,
@@ -102,24 +179,22 @@ class ThinkFlowChat:
if not mark_head: if not mark_head:
mark_head = True mark_head = True
first_bot_msg = bot_message first_bot_msg = bot_message
# print(f"thinking_start_time:{bot_message.thinking_start_time}")
message_set.add_message(bot_message) message_set.add_message(bot_message)
MessageManager().add_message(message_set) MessageManager().add_message(message_set)
return first_bot_msg return first_bot_msg
async def _handle_emoji(self, message, chat, response, send_emoji=""): async def _handle_emoji(self, message: MessageRecv, response_set, send_emoji=""):
"""处理表情包""" """处理表情包 (从 message 获取信息)"""
chat = 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_raw = await emoji_manager.get_emoji_for_text(response) emoji_text_source = "".join(response_set) if response_set else ""
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(message.message_info.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="mt" + str(thinking_time_point),
@@ -129,13 +204,12 @@ class ThinkFlowChat:
user_nickname=global_config.BOT_NICKNAME, user_nickname=global_config.BOT_NICKNAME,
platform=message.message_info.platform, platform=message.message_info.platform,
), ),
sender_info=message.message_info.user_info, sender_info=message.message_info.user_info, # 发送给谁
message_segment=message_segment, message_segment=message_segment,
reply=message, reply=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, message: MessageRecv, response_set):
@@ -147,281 +221,144 @@ class ThinkFlowChat:
) )
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 process_message(self, message_data: str) -> None: async def trigger_reply_generation(self, message: MessageRecv):
"""处理消息并生成回复""" """根据意愿阈值触发的实际回复生成和发送逻辑 (V3 - 简化参数)"""
timing_results = {} chat = message.chat_stream
response_set = None
message = MessageRecv(message_data)
groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info userinfo = message.message_info.user_info
messageinfo = message.message_info messageinfo = message.message_info
# 消息加入缓冲池 timing_results = {}
await message_buffer.start_caching_messages(message) response_set = None
thinking_id = None
info_catcher = None
# 创建聊天流 try:
chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform,
user_info=userinfo,
group_info=groupinfo,
)
message.update_chat_stream(chat)
# 创建心流与chat的观察
heartflow.create_subheartflow(chat.stream_id)
await message.process()
logger.trace(f"消息处理成功{message.processed_plain_text}")
# 过滤词/正则表达式过滤
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
message.raw_message, chat, userinfo
):
return
logger.trace(f"过滤词/正则表达式过滤成功{message.processed_plain_text}")
await self.storage.store_message(message, chat)
logger.trace(f"存储成功{message.processed_plain_text}")
# 记忆激活
with Timer("记忆激活", timing_results):
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
message.processed_plain_text, fast_retrieval=True
)
logger.trace(f"记忆激活: {interested_rate}")
# 查询缓冲器结果会整合前面跳过的消息改变processed_plain_text
buffer_result = await message_buffer.query_buffer_result(message)
# 处理提及
is_mentioned, reply_probability = is_mentioned_bot_in_message(message)
# 意愿管理器设置当前message信息
willing_manager.setup(message, chat, is_mentioned, interested_rate)
# 处理缓冲器结果
if not buffer_result:
await willing_manager.bombing_buffer_message_handle(message.message_info.message_id)
willing_manager.delete(message.message_info.message_id)
F_type = "seglist"
if message.message_segment.type != "seglist":
F_type =message.message_segment.type
else:
if (isinstance(message.message_segment.data, list)
and all(isinstance(x, Seg) for x in message.message_segment.data)
and len(message.message_segment.data) == 1):
F_type = message.message_segment.data[0].type
if F_type == "text":
logger.info(f"触发缓冲,已炸飞消息:{message.processed_plain_text}")
elif F_type == "image":
logger.info("触发缓冲,已炸飞表情包/图片")
elif F_type == "seglist":
logger.info("触发缓冲,已炸飞消息列")
return
# 获取回复概率
is_willing = False
if reply_probability != 1:
is_willing = True
reply_probability = await willing_manager.get_reply_probability(message.message_info.message_id)
if message.message_info.additional_config:
if "maimcore_reply_probability_gain" in message.message_info.additional_config.keys():
reply_probability += message.message_info.additional_config["maimcore_reply_probability_gain"]
# 打印消息信息
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
willing_log = f"[回复意愿:{await willing_manager.get_willing(chat.stream_id):.2f}]" if is_willing else ""
logger.info(
f"[{current_time}][{mes_name}]"
f"{chat.user_info.user_nickname}:"
f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]"
)
do_reply = False
if random() < reply_probability:
try: try:
do_reply = True with Timer("观察", timing_results):
sub_hf = heartflow.get_subheartflow(chat.stream_id)
if not sub_hf:
logger.warning(f"尝试观察时未找到 stream_id {chat.stream_id} 的 subheartflow")
return
await sub_hf.do_observe()
except Exception as e:
logger.error(f"心流观察失败: {e}")
logger.error(traceback.format_exc())
# 回复前处理 container = MessageManager().get_container(chat.stream_id)
await willing_manager.before_generate_reply_handle(message.message_info.message_id) thinking_count = container.count_thinking_messages()
max_thinking_messages = getattr(global_config, 'max_concurrent_thinking_messages', 3)
if thinking_count >= max_thinking_messages:
logger.warning(f"聊天流 {chat.stream_id} 已有 {thinking_count} 条思考消息,取消回复。触发消息: {message.processed_plain_text[:30]}...")
return
# 创建思考消息 try:
try: with Timer("创建思考消息", timing_results):
with Timer("创建思考消息", timing_results): thinking_id = await self._create_thinking_message(message)
thinking_id = await self._create_thinking_message(message, chat, userinfo, messageinfo) except Exception as e:
except Exception as e: logger.error(f"心流创建思考消息失败: {e}")
logger.error(f"心流创建思考消息失败: {e}") return
if not thinking_id:
logger.error("未能成功创建思考消息 ID无法继续回复流程。")
return
logger.trace(f"创建捕捉器thinking_id:{thinking_id}") logger.trace(f"创建捕捉器thinking_id:{thinking_id}")
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
info_catcher.catch_decide_to_response(message)
info_catcher = info_catcher_manager.get_info_catcher(thinking_id) get_mid_memory_id = []
info_catcher.catch_decide_to_response(message) tool_result_info = {}
send_emoji = ""
# 观察 try:
try: with Timer("思考前使用工具", timing_results):
with Timer("观察", timing_results): tool_result = await self.tool_user.use_tool(
await heartflow.get_subheartflow(chat.stream_id).do_observe() message.processed_plain_text,
except Exception as e: userinfo.user_nickname,
logger.error(f"心流观察失败: {e}") chat,
logger.error(traceback.format_exc()) heartflow.get_subheartflow(chat.stream_id),
info_catcher.catch_after_observe(timing_results["观察"])
# 思考前使用工具
update_relationship = ""
get_mid_memory_id = []
tool_result_info = {}
send_emoji = ""
try:
with Timer("思考前使用工具", timing_results):
tool_result = await self.tool_user.use_tool(
message.processed_plain_text,
message.message_info.user_info.user_nickname,
chat,
heartflow.get_subheartflow(chat.stream_id),
)
# 如果工具被使用且获得了结果,将收集到的信息合并到思考中
# collected_info = ""
if tool_result.get("used_tools", False):
if "structured_info" in tool_result:
tool_result_info = tool_result["structured_info"]
# collected_info = ""
get_mid_memory_id = []
update_relationship = ""
# 动态解析工具结果
for tool_name, tool_data in tool_result_info.items():
# tool_result_info += f"\n{tool_name} 相关信息:\n"
# for item in tool_data:
# tool_result_info += f"- {item['name']}: {item['content']}\n"
# 特殊判定mid_chat_mem
if tool_name == "mid_chat_mem":
for mid_memory in tool_data:
get_mid_memory_id.append(mid_memory["content"])
# 特殊判定change_mood
if tool_name == "change_mood":
for mood in tool_data:
self.mood_manager.update_mood_from_emotion(
mood["content"], global_config.mood_intensity_factor
)
# 特殊判定change_relationship
if tool_name == "change_relationship":
update_relationship = tool_data[0]["content"]
if tool_name == "send_emoji":
send_emoji = tool_data[0]["content"]
except Exception as e:
logger.error(f"思考前工具调用失败: {e}")
logger.error(traceback.format_exc())
# 处理关系更新
if update_relationship:
stance, emotion = await self.gpt._get_emotion_tags_with_reason(
"你还没有回复", message.processed_plain_text, update_relationship
)
await relationship_manager.calculate_update_relationship_value(
chat_stream=message.chat_stream, label=emotion, stance=stance
) )
if tool_result.get("used_tools", False):
if "structured_info" in tool_result:
tool_result_info = tool_result["structured_info"]
get_mid_memory_id = []
for tool_name, tool_data in tool_result_info.items():
if tool_name == "mid_chat_mem":
for mid_memory in tool_data:
get_mid_memory_id.append(mid_memory["content"])
if tool_name == "send_emoji":
send_emoji = tool_data[0]["content"]
except Exception as e:
logger.error(f"思考前工具调用失败: {e}")
logger.error(traceback.format_exc())
# 思考前脑内状态 current_mind, past_mind = "", ""
try: try:
with Timer("思考前脑内状态", timing_results): with Timer("思考前脑内状态", timing_results):
current_mind, past_mind = await heartflow.get_subheartflow( sub_hf = heartflow.get_subheartflow(chat.stream_id)
chat.stream_id if sub_hf:
).do_thinking_before_reply( current_mind, past_mind = await sub_hf.do_thinking_before_reply(
message_txt=message.processed_plain_text, message_txt=message.processed_plain_text,
sender_info=message.message_info.user_info, sender_info=userinfo,
chat_stream=chat, chat_stream=chat,
obs_id=get_mid_memory_id, obs_id=get_mid_memory_id,
extra_info=tool_result_info, extra_info=tool_result_info,
) )
except Exception as e: else:
logger.error(f"心流思考前脑内状态失败: {e}") logger.warning(f"尝试思考前状态时未找到 stream_id {chat.stream_id} 的 subheartflow")
logger.error(traceback.format_exc()) except Exception as e:
# 确保变量被定义,即使在错误情况下 logger.error(f"心流思考前脑内状态失败: {e}")
current_mind = "" logger.error(traceback.format_exc())
past_mind = "" if info_catcher:
info_catcher.catch_afer_shf_step(timing_results.get("思考前脑内状态"), past_mind, current_mind)
info_catcher.catch_afer_shf_step(timing_results["思考前脑内状态"], past_mind, current_mind) try:
# 生成回复
with Timer("生成回复", timing_results): with Timer("生成回复", timing_results):
response_set = await self.gpt.generate_response(message, thinking_id) response_set = await self.gpt.generate_response(message, thinking_id)
except Exception as e:
logger.error(f"GPT 生成回复失败: {e}")
logger.error(traceback.format_exc())
if info_catcher: info_catcher.done_catch()
return
if info_catcher:
info_catcher.catch_after_generate_response(timing_results.get("生成回复"))
if not response_set:
logger.info("回复生成失败,返回为空")
if info_catcher: info_catcher.done_catch()
return
info_catcher.catch_after_generate_response(timing_results["生成回复"]) first_bot_msg = None
try:
if not response_set: with Timer("发送消息", timing_results):
logger.info("回复生成失败,返回为空") first_bot_msg = await self._send_response_messages(message, response_set, thinking_id)
return except Exception as e:
logger.error(f"心流发送消息失败: {e}")
# 发送消息 if info_catcher:
try: info_catcher.catch_after_response(timing_results.get("发送消息"), response_set, first_bot_msg)
with Timer("发送消息", timing_results):
first_bot_msg = await self._send_response_messages(message, chat, response_set, thinking_id)
except Exception as e:
logger.error(f"心流发送消息失败: {e}")
info_catcher.catch_after_response(timing_results["发送消息"], response_set, first_bot_msg)
info_catcher.done_catch() info_catcher.done_catch()
# 处理表情包 try:
try: with Timer("处理表情包", timing_results):
with Timer("处理表情包", timing_results): if send_emoji:
if send_emoji: logger.info(f"麦麦决定发送表情包{send_emoji}")
logger.info(f"麦麦决定发送表情包{send_emoji}") await self._handle_emoji(message, response_set, send_emoji)
await self._handle_emoji(message, chat, response_set, send_emoji)
except Exception as e:
logger.error(f"心流处理表情包失败: {e}")
# 回复后处理
await willing_manager.after_generate_reply_handle(message.message_info.message_id)
except Exception as e: except Exception as e:
logger.error(f"心流处理消息失败: {e}") logger.error(f"心流处理表情包失败: {e}")
logger.error(traceback.format_exc())
# 输出性能计时结果
if do_reply:
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 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"回复任务完成: 触发消息: {trigger_msg[:20]}... | 思维消息: {response_msg[:20]}... | 性能计时: {timing_str}")
else:
# 不回复处理
await willing_manager.not_reply_handle(message.message_info.message_id)
# 意愿管理器注销当前message信息 if first_bot_msg:
willing_manager.delete(message.message_info.message_id) try:
with Timer("更新关系情绪", timing_results):
await self._update_relationship(message, response_set)
except Exception as e:
logger.error(f"更新关系情绪失败: {e}")
logger.error(traceback.format_exc())
def _check_ban_words(self, text: str, chat, userinfo) -> bool: except Exception as e:
"""检查消息中是否包含过滤词""" logger.error(f"回复生成任务失败 (trigger_reply_generation V3): {e}")
for word in global_config.ban_words: logger.error(traceback.format_exc())
if word in text:
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
)
logger.info(f"[过滤词识别]消息中含有{word}filtered")
return True
return False
def _check_ban_regex(self, text: str, chat, userinfo) -> bool: finally:
"""检查消息是否匹配过滤正则表达式""" pass
for pattern in global_config.ban_msgs_regex:
if pattern.search(text):
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
)
logger.info(f"[正则表达式过滤]消息匹配到{pattern}filtered")
return True
return False

View File

@@ -0,0 +1,170 @@
import time
import traceback
import asyncio
from ...memory_system.Hippocampus import HippocampusManager
from ....config.config import global_config
from ...chat.message import MessageRecv
from ...storage.storage import MessageStorage
from ...chat.utils import is_mentioned_bot_in_message
from ...message import UserInfo, Seg
from src.heart_flow.heartflow import heartflow
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ...chat.chat_stream import chat_manager
from ...chat.message_buffer import message_buffer
from ...utils.timer_calculater import Timer
from .interest import InterestManager
from .heartFC_chat import HeartFC_Chat # 导入 HeartFC_Chat 以调用回复生成
# 定义日志配置
processor_config = LogConfig(
console_format=CHAT_STYLE_CONFIG["console_format"],
file_format=CHAT_STYLE_CONFIG["file_format"],
)
logger = get_module_logger("heartFC_processor", config=processor_config)
# # 定义兴趣度增加触发回复的阈值 (移至 InterestManager)
# INTEREST_INCREASE_THRESHOLD = 0.5
class HeartFC_Processor:
def __init__(self, chat_instance: HeartFC_Chat):
self.storage = MessageStorage()
self.interest_manager = InterestManager() # TODO: 可能需要传递 chat_instance 给 InterestManager 或修改其方法签名
self.chat_instance = chat_instance # 持有 HeartFC_Chat 实例
async def process_message(self, message_data: str) -> None:
"""处理接收到的消息,更新状态,并将回复决策委托给 InterestManager"""
timing_results = {} # 初始化 timing_results
message = None
try:
message = MessageRecv(message_data)
groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info
messageinfo = message.message_info
# 消息加入缓冲池
await message_buffer.start_caching_messages(message)
# 创建聊天流
chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform,
user_info=userinfo,
group_info=groupinfo,
)
if not chat:
logger.error(f"无法为消息创建或获取聊天流: user {userinfo.user_id}, group {groupinfo.group_id if groupinfo else 'None'}")
return
message.update_chat_stream(chat)
# 创建心流与chat的观察 (在接收消息时创建,以便后续观察和思考)
heartflow.create_subheartflow(chat.stream_id)
await message.process()
logger.trace(f"消息处理成功: {message.processed_plain_text}")
# 过滤词/正则表达式过滤
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
message.raw_message, chat, userinfo
):
return
logger.trace(f"过滤词/正则表达式过滤成功: {message.processed_plain_text}")
# 查询缓冲器结果
buffer_result = await message_buffer.query_buffer_result(message)
# 处理缓冲器结果 (Bombing logic)
if not buffer_result:
F_type = "seglist"
if message.message_segment.type != "seglist":
F_type = message.message_segment.type
else:
if (isinstance(message.message_segment.data, list)
and all(isinstance(x, Seg) for x in message.message_segment.data)
and len(message.message_segment.data) == 1):
F_type = message.message_segment.data[0].type
if F_type == "text":
logger.debug(f"触发缓冲,消息:{message.processed_plain_text}")
elif F_type == "image":
logger.debug("触发缓冲,表情包/图片等待中")
elif F_type == "seglist":
logger.debug("触发缓冲,消息列表等待中")
return # 被缓冲器拦截,不生成回复
# ---- 只有通过缓冲的消息才进行存储和后续处理 ----
# 存储消息 (使用可能被缓冲器更新过的 message)
try:
await self.storage.store_message(message, chat)
logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}")
except Exception as e:
logger.error(f"存储消息失败: {e}")
logger.error(traceback.format_exc())
# 存储失败可能仍需考虑是否继续,暂时返回
return
# 激活度计算 (使用可能被缓冲器更新过的 message.processed_plain_text)
is_mentioned, _ = is_mentioned_bot_in_message(message)
interested_rate = 0.0 # 默认值
try:
with Timer("记忆激活", timing_results):
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
message.processed_plain_text, fast_retrieval=True # 使用更新后的文本
)
logger.trace(f"记忆激活率 (通过缓冲后): {interested_rate:.2f}")
except Exception as e:
logger.error(f"计算记忆激活率失败: {e}")
logger.error(traceback.format_exc())
if is_mentioned:
interested_rate += 0.8
# 更新兴趣度
try:
self.interest_manager.increase_interest(chat.stream_id, value=interested_rate, message=message)
current_interest = self.interest_manager.get_interest(chat.stream_id) # 获取更新后的值用于日志
logger.trace(f"使用激活率 {interested_rate:.2f} 更新后 (通过缓冲后),当前兴趣度: {current_interest:.2f}")
except Exception as e:
logger.error(f"更新兴趣度失败: {e}") # 调整日志消息
logger.error(traceback.format_exc())
# ---- 兴趣度计算和更新结束 ----
# 打印消息接收和处理信息
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
logger.info(
f"[{current_time}][{mes_name}]"
f"{chat.user_info.user_nickname}:"
f"{message.processed_plain_text}"
f"兴趣度: {current_interest:.2f}"
)
# 回复触发逻辑已移至 HeartFC_Chat 的监控任务
except Exception as e:
logger.error(f"消息处理失败 (process_message V3): {e}")
logger.error(traceback.format_exc())
if message: # 记录失败的消息内容
logger.error(f"失败消息原始内容: {message.raw_message}")
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
"""检查消息中是否包含过滤词"""
for word in global_config.ban_words:
if word in text:
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
)
logger.info(f"[过滤词识别]消息中含有{word}filtered")
return True
return False
def _check_ban_regex(self, text: str, chat, userinfo) -> bool:
"""检查消息是否匹配过滤正则表达式"""
for pattern in global_config.ban_msgs_regex:
if pattern.search(text):
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
)
logger.info(f"[正则表达式过滤]消息匹配到{pattern}filtered")
return True
return False

View File

@@ -0,0 +1,370 @@
import time
import math
import asyncio
import threading
import json # 引入 json
import os # 引入 os
import traceback # <--- 添加导入
from typing import Optional # <--- 添加导入
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 ...chat.message import MessageRecv # 导入 MessageRecv
# 定义日志配置 (使用 loguru 格式)
interest_log_config = LogConfig(
console_format=DEFAULT_CONFIG["console_format"], # 使用默认控制台格式
file_format=DEFAULT_CONFIG["file_format"] # 使用默认文件格式
)
logger = get_module_logger("InterestManager", config=interest_log_config)
# 定义常量
DEFAULT_DECAY_RATE_PER_SECOND = 0.95 # 每秒衰减率 (兴趣保留 99%)
# DEFAULT_INCREASE_AMOUNT = 10.0 # 不再需要固定增加值
MAX_INTEREST = 10.0 # 最大兴趣值
MIN_INTEREST_THRESHOLD = 0.1 # 低于此值可能被清理 (可选)
CLEANUP_INTERVAL_SECONDS = 3600 # 清理任务运行间隔 (例如1小时)
INACTIVE_THRESHOLD_SECONDS = 3600 * 24 # 不活跃时间阈值 (例如1天)
LOG_INTERVAL_SECONDS = 3 # 日志记录间隔 (例如30秒)
LOG_DIRECTORY = "logs/interest" # 日志目录
LOG_FILENAME = "interest_log.json" # 快照日志文件名 (保留,以防其他地方用到)
HISTORY_LOG_FILENAME = "interest_history.log" # 新的历史日志文件名
# 移除阈值,将移至 HeartFC_Chat
# INTEREST_INCREASE_THRESHOLD = 0.5
class InterestChatting:
def __init__(self, decay_rate=DEFAULT_DECAY_RATE_PER_SECOND, max_interest=MAX_INTEREST):
self.interest_level: float = 0.0
self.last_update_time: float = time.time()
self.decay_rate_per_second: float = decay_rate
# self.increase_amount: float = increase_amount # 移除固定的 increase_amount
self.max_interest: float = max_interest
# 新增:用于追踪最后一次显著增加的信息,供外部监控任务使用
self.last_increase_amount: float = 0.0
self.last_triggering_message: MessageRecv | None = None
def _calculate_decay(self, current_time: float):
"""计算从上次更新到现在的衰减"""
time_delta = current_time - self.last_update_time
if time_delta > 0:
# 指数衰减: interest = interest * (decay_rate ^ time_delta)
# 添加处理极小兴趣值避免 math domain error
if self.interest_level < 1e-9:
self.interest_level = 0.0
else:
# 检查 decay_rate_per_second 是否为非正数,避免 math domain error
if self.decay_rate_per_second <= 0:
logger.warning(f"InterestChatting encountered non-positive decay rate: {self.decay_rate_per_second}. Setting interest to 0.")
self.interest_level = 0.0
# 检查 interest_level 是否为负数,虽然理论上不应发生,但以防万一
elif self.interest_level < 0:
logger.warning(f"InterestChatting encountered negative interest level: {self.interest_level}. Setting interest to 0.")
self.interest_level = 0.0
else:
try:
decay_factor = math.pow(self.decay_rate_per_second, time_delta)
self.interest_level *= decay_factor
except ValueError as e:
# 捕获潜在的 math domain error例如对负数开非整数次方虽然已加保护
logger.error(f"Math error during decay calculation: {e}. Rate: {self.decay_rate_per_second}, Delta: {time_delta}, Level: {self.interest_level}. Setting interest to 0.")
self.interest_level = 0.0
# 防止低于阈值 (如果需要)
# self.interest_level = max(self.interest_level, MIN_INTEREST_THRESHOLD)
self.last_update_time = current_time
def increase_interest(self, current_time: float, value: float, message: Optional[MessageRecv]):
"""根据传入的值增加兴趣值,并记录增加量和关联消息"""
self._calculate_decay(current_time) # 先计算衰减
# 记录这次增加的具体数值和消息,供外部判断是否触发
self.last_increase_amount = value
self.last_triggering_message = message
# 应用增加
self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest) # 不超过最大值
self.last_update_time = current_time # 更新时间戳
def decrease_interest(self, current_time: float, value: float):
"""降低兴趣值并更新时间 (确保不低于0)"""
# 注意:降低兴趣度是否需要先衰减?取决于具体逻辑,这里假设不衰减直接减
self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0) # 确保不低于0
self.last_update_time = current_time # 降低也更新时间戳
def reset_trigger_info(self):
"""重置触发相关信息,在外部任务处理后调用"""
self.last_increase_amount = 0.0
self.last_triggering_message = None
def get_interest(self) -> float:
"""获取当前兴趣值 (由后台任务更新)"""
return self.interest_level
def get_state(self) -> dict:
"""获取当前状态字典"""
# 不再需要传入 current_time 来计算,直接获取
interest = self.get_interest() # 使用修改后的 get_interest
return {
"interest_level": round(interest, 2),
"last_update_time": self.last_update_time,
# 可以选择性地暴露 last_increase_amount 给状态,方便调试
# "last_increase_amount": round(self.last_increase_amount, 2)
}
class InterestManager:
_instance = None
_lock = threading.Lock()
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
# Double-check locking
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._initialized:
with self._lock:
# 确保初始化也只执行一次
if not self._initialized:
logger.info("Initializing InterestManager singleton...")
# key: stream_id (str), value: InterestChatting instance
self.interest_dict: dict[str, InterestChatting] = {}
# 保留旧的快照文件路径变量,尽管此任务不再写入
self._snapshot_log_file_path = os.path.join(LOG_DIRECTORY, LOG_FILENAME)
# 定义新的历史日志文件路径
self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME)
self._ensure_log_directory()
self._cleanup_task = None
self._logging_task = None # 添加日志任务变量
self._initialized = True
logger.info("InterestManager initialized.") # 修改日志消息
self._decay_task = None # 新增:衰减任务变量
def _ensure_log_directory(self):
"""确保日志目录存在"""
try:
os.makedirs(LOG_DIRECTORY, exist_ok=True)
logger.info(f"Log directory '{LOG_DIRECTORY}' ensured.")
except OSError as e:
logger.error(f"Error creating log directory '{LOG_DIRECTORY}': {e}")
async def _periodic_cleanup_task(self, interval_seconds: int, threshold: float, max_age_seconds: int):
"""后台清理任务的异步函数"""
while True:
await asyncio.sleep(interval_seconds)
logger.info(f"Running periodic cleanup (interval: {interval_seconds}s)...")
self.cleanup_inactive_chats(threshold=threshold, max_age_seconds=max_age_seconds)
async def _periodic_log_task(self, interval_seconds: int):
"""后台日志记录任务的异步函数 (记录历史数据,包含 group_name)"""
while True:
await asyncio.sleep(interval_seconds)
logger.debug(f"Running periodic history logging (interval: {interval_seconds}s)...")
try:
current_timestamp = time.time()
all_states = self.get_all_interest_states() # 获取当前所有状态
# 以追加模式打开历史日志文件
with open(self._history_log_file_path, 'a', encoding='utf-8') as f:
count = 0
for stream_id, state in all_states.items():
# *** Get group name from ChatManager ***
group_name = stream_id # Default to stream_id
try:
# Use the imported chat_manager instance
chat_stream = chat_manager.get_stream(stream_id)
if chat_stream and chat_stream.group_info:
group_name = chat_stream.group_info.group_name
elif chat_stream and not chat_stream.group_info:
# Handle private chats - maybe use user nickname?
group_name = f"私聊_{chat_stream.user_info.user_nickname}" if chat_stream.user_info else stream_id
except Exception as e:
logger.warning(f"Could not get group name for stream_id {stream_id}: {e}")
# Fallback to stream_id is already handled by default value
log_entry = {
"timestamp": round(current_timestamp, 2),
"stream_id": stream_id,
"interest_level": state.get("interest_level", 0.0), # 确保有默认值
"group_name": group_name # *** Add group_name ***
}
# 将每个条目作为单独的 JSON 行写入
f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
count += 1
logger.debug(f"Successfully appended {count} interest history entries to {self._history_log_file_path}")
# 注意:不再写入快照文件 interest_log.json
# 如果需要快照文件,可以在这里单独写入 self._snapshot_log_file_path
# 例如:
# with open(self._snapshot_log_file_path, 'w', encoding='utf-8') as snap_f:
# json.dump(all_states, snap_f, indent=4, ensure_ascii=False)
# logger.debug(f"Successfully wrote snapshot to {self._snapshot_log_file_path}")
except IOError as e:
logger.error(f"Error writing interest history log to {self._history_log_file_path}: {e}")
except Exception as e:
logger.error(f"Unexpected error during periodic history logging: {e}")
async def _periodic_decay_task(self):
"""后台衰减任务的异步函数,每秒更新一次所有实例的衰减"""
while True:
await asyncio.sleep(1) # 每秒运行一次
current_time = time.time()
# logger.debug("Running periodic decay calculation...") # 调试日志,可能过于频繁
# 创建字典项的快照进行迭代,避免在迭代时修改字典的问题
items_snapshot = list(self.interest_dict.items())
count = 0
for stream_id, chatting in items_snapshot:
try:
# 调用 InterestChatting 实例的衰减方法
chatting._calculate_decay(current_time)
count += 1
except Exception as e:
logger.error(f"Error calculating decay for stream_id {stream_id}: {e}")
# if count > 0: # 仅在实际处理了项目时记录日志,避免空闲时刷屏
# logger.debug(f"Applied decay to {count} streams.")
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():
self._cleanup_task = asyncio.create_task(
self._periodic_cleanup_task(
interval_seconds=CLEANUP_INTERVAL_SECONDS,
threshold=MIN_INTEREST_THRESHOLD,
max_age_seconds=INACTIVE_THRESHOLD_SECONDS
)
)
logger.info(f"Periodic cleanup task created. Interval: {CLEANUP_INTERVAL_SECONDS}s, Inactive Threshold: {INACTIVE_THRESHOLD_SECONDS}s")
else:
logger.warning("Cleanup task creation skipped: already running or exists.")
if self._logging_task is None or self._logging_task.done():
self._logging_task = asyncio.create_task(
self._periodic_log_task(interval_seconds=LOG_INTERVAL_SECONDS)
)
logger.info(f"Periodic logging task created. Interval: {LOG_INTERVAL_SECONDS}s")
else:
logger.warning("Logging task creation skipped: already running or exists.")
# 启动新的衰减任务
if self._decay_task is None or self._decay_task.done():
self._decay_task = asyncio.create_task(
self._periodic_decay_task()
)
logger.info("Periodic decay task created. Interval: 1s")
else:
logger.warning("Decay task creation skipped: already running or exists.")
def get_all_interest_states(self) -> dict[str, dict]:
"""获取所有聊天流的当前兴趣状态"""
# 不再需要 current_time, 因为 get_state 现在不接收它
states = {}
# 创建副本以避免在迭代时修改字典
items_snapshot = list(self.interest_dict.items())
for stream_id, chatting in items_snapshot:
try:
# 直接调用 get_state它会使用内部的 get_interest 获取已更新的值
states[stream_id] = chatting.get_state()
except Exception as e:
logger.warning(f"Error getting state for stream_id {stream_id}: {e}")
return states
def get_interest_chatting(self, stream_id: str) -> Optional[InterestChatting]:
"""获取指定流的 InterestChatting 实例,如果不存在则返回 None"""
return self.interest_dict.get(stream_id)
def _get_or_create_interest_chatting(self, stream_id: str) -> InterestChatting:
"""获取或创建指定流的 InterestChatting 实例 (线程安全)"""
# 由于字典操作本身在 CPython 中大部分是原子的,
# 且主要写入发生在 __init__ 和 cleanup (由单任务执行)
# 读取和 get_or_create 主要在事件循环线程,简单场景下可能不需要锁。
# 但为保险起见或跨线程使用考虑,可加锁。
# with self._lock:
if stream_id not in self.interest_dict:
logger.debug(f"Creating new InterestChatting for stream_id: {stream_id}")
self.interest_dict[stream_id] = InterestChatting()
# 首次创建时兴趣为 0由第一次消息的 activate rate 决定初始值
return self.interest_dict[stream_id]
def get_interest(self, stream_id: str) -> float:
"""获取指定聊天流当前的兴趣度 (值由后台任务更新)"""
# current_time = time.time() # 不再需要获取当前时间
interest_chatting = self._get_or_create_interest_chatting(stream_id)
# 直接调用修改后的 get_interest不传入时间
return interest_chatting.get_interest()
def increase_interest(self, stream_id: str, value: float, message: MessageRecv):
"""当收到消息时,增加指定聊天流的兴趣度,并传递关联消息"""
current_time = time.time()
interest_chatting = self._get_or_create_interest_chatting(stream_id)
# 调用修改后的 increase_interest传入 message
interest_chatting.increase_interest(current_time, value, message)
logger.debug(f"Increased interest for stream_id: {stream_id} by {value:.2f} to {interest_chatting.interest_level:.2f}") # 更新日志
def decrease_interest(self, stream_id: str, value: float):
"""降低指定聊天流的兴趣度"""
current_time = time.time()
# 尝试获取,如果不存在则不做任何事
interest_chatting = self.get_interest_chatting(stream_id)
if interest_chatting:
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}")
else:
logger.warning(f"Attempted to decrease interest for non-existent stream_id: {stream_id}")
def cleanup_inactive_chats(self, threshold=MIN_INTEREST_THRESHOLD, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
"""
清理长时间不活跃或兴趣度过低的聊天流记录
threshold: 低于此兴趣度的将被清理
max_age_seconds: 超过此时间未更新的将被清理
"""
current_time = time.time()
keys_to_remove = []
initial_count = len(self.interest_dict)
# with self._lock: # 如果需要锁整个迭代过程
# 创建副本以避免在迭代时修改字典
items_snapshot = list(self.interest_dict.items())
for stream_id, chatting in items_snapshot:
# 先计算当前兴趣,确保是最新的
# 加锁保护 chatting 对象状态的读取和可能的修改
# with self._lock: # 如果 InterestChatting 内部操作不是原子的
interest = chatting.get_interest()
last_update = chatting.last_update_time
should_remove = False
reason = ""
if interest < threshold:
should_remove = True
reason = f"interest ({interest:.2f}) < threshold ({threshold})"
# 只有设置了 max_age_seconds 才检查时间
if max_age_seconds is not None and (current_time - last_update) > max_age_seconds:
should_remove = True
reason = f"inactive time ({current_time - last_update:.0f}s) > max age ({max_age_seconds}s)" + (f", {reason}" if reason else "") # 附加之前的理由
if should_remove:
keys_to_remove.append(stream_id)
logger.debug(f"Marking stream_id {stream_id} for removal. Reason: {reason}")
if keys_to_remove:
logger.info(f"Cleanup identified {len(keys_to_remove)} inactive/low-interest streams.")
# with self._lock: # 确保删除操作的原子性
for key in keys_to_remove:
# 再次检查 key 是否存在,以防万一在迭代和删除之间状态改变
if key in self.interest_dict:
del self.interest_dict[key]
logger.debug(f"Removed stream_id: {key}")
final_count = initial_count - len(keys_to_remove)
logger.info(f"Cleanup finished. Removed {len(keys_to_remove)} streams. Current count: {final_count}")
else:
logger.info(f"Cleanup finished. No streams met removal criteria. Current count: {initial_count}")
# 不再需要手动创建实例和任务
# manager = InterestManager()
# asyncio.create_task(periodic_cleanup(manager, 3600))

View File

@@ -5,8 +5,7 @@ from typing import Dict, List, Optional, Union
from src.common.logger import get_module_logger from src.common.logger import get_module_logger
from ....common.database import db from ....common.database import db
from ...message.api import global_api from ...message.api import global_api
from ...message import MessageSending, MessageThinking, MessageSet from ...chat.message import MessageSending, MessageThinking, MessageSet
from ...storage.storage import MessageStorage from ...storage.storage import MessageStorage
from ....config.config import global_config from ....config.config import global_config
from ...chat.utils import truncate_message, calculate_typing_time, count_messages_between from ...chat.utils import truncate_message, calculate_typing_time, count_messages_between
@@ -97,22 +96,10 @@ class MessageContainer:
self.max_size = max_size self.max_size = max_size
self.messages = [] self.messages = []
self.last_send_time = 0 self.last_send_time = 0
self.thinking_wait_timeout = 20 # 思考等待超时时间(秒)
def get_timeout_messages(self) -> List[MessageSending]: def count_thinking_messages(self) -> int:
"""获取所有超时的Message_Sending对象思考时间超过20秒按thinking_start_time排序""" """计算当前容器中思考消息的数量"""
current_time = time.time() return sum(1 for msg in self.messages if isinstance(msg, MessageThinking))
timeout_messages = []
for msg in self.messages:
if isinstance(msg, MessageSending):
if current_time - msg.thinking_start_time > self.thinking_wait_timeout:
timeout_messages.append(msg)
# 按thinking_start_time排序时间早的在前面
timeout_messages.sort(key=lambda x: x.thinking_start_time)
return timeout_messages
def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]:
"""获取thinking_start_time最早的消息对象""" """获取thinking_start_time最早的消息对象"""
@@ -224,10 +211,10 @@ class MessageManager:
if ( if (
message_earliest.is_head message_earliest.is_head
and (thinking_messages_count > 4 or thinking_messages_length > 250) and (thinking_messages_count > 3 or thinking_messages_length > 200)
and not message_earliest.is_private_message() # 避免在私聊时插入reply and not message_earliest.is_private_message() # 避免在私聊时插入reply
): ):
logger.debug(f"设置回复消息{message_earliest.processed_plain_text}") logger.debug(f"距离原始消息太长,设置回复消息{message_earliest.processed_plain_text}")
message_earliest.set_reply() message_earliest.set_reply()
await message_earliest.process() await message_earliest.process()

View File

@@ -400,7 +400,7 @@ class Hippocampus:
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
if not valid_keywords: if not valid_keywords:
logger.info("没有找到有效的关键词节点") # logger.info("没有找到有效的关键词节点")
return [] return []
logger.info(f"有效的关键词: {', '.join(valid_keywords)}") logger.info(f"有效的关键词: {', '.join(valid_keywords)}")
@@ -590,7 +590,7 @@ class Hippocampus:
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
if not valid_keywords: if not valid_keywords:
logger.info("没有找到有效的关键词节点") # logger.info("没有找到有效的关键词节点")
return 0 return 0
logger.info(f"有效的关键词: {', '.join(valid_keywords)}") logger.info(f"有效的关键词: {', '.join(valid_keywords)}")
@@ -1114,7 +1114,7 @@ class Hippocampus:
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
if not valid_keywords: if not valid_keywords:
logger.info("没有找到有效的关键词节点") # logger.info("没有找到有效的关键词节点")
return [] return []
logger.info(f"有效的关键词: {', '.join(valid_keywords)}") logger.info(f"有效的关键词: {', '.join(valid_keywords)}")
@@ -1304,7 +1304,7 @@ class Hippocampus:
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
if not valid_keywords: if not valid_keywords:
logger.info("没有找到有效的关键词节点") # logger.info("没有找到有效的关键词节点")
return 0 return 0
logger.info(f"有效的关键词: {', '.join(valid_keywords)}") logger.info(f"有效的关键词: {', '.join(valid_keywords)}")

View File

@@ -371,6 +371,7 @@ class PersonInfoManager:
"msg_interval_list", lambda x: isinstance(x, list) and len(x) >= 100 "msg_interval_list", lambda x: isinstance(x, list) and len(x) >= 100
) )
for person_id, msg_interval_list_ in msg_interval_lists.items(): for person_id, msg_interval_list_ in msg_interval_lists.items():
await asyncio.sleep(0.3)
try: try:
time_interval = [] time_interval = []
for t1, t2 in zip(msg_interval_list_, msg_interval_list_[1:]): for t1, t2 in zip(msg_interval_list_, msg_interval_list_[1:]):