Merge pull request #1224 from MaiM-with-u/dev

Dev 0.10.2
This commit is contained in:
SengokuCola
2025-09-01 21:04:52 +08:00
committed by GitHub
52 changed files with 1842 additions and 1254 deletions

View File

@@ -64,6 +64,11 @@
> - QQ 机器人存在被限制风险,请自行了解,谨慎使用。 > - QQ 机器人存在被限制风险,请自行了解,谨慎使用。
> - 由于程序处于开发中,可能消耗较多 token。 > - 由于程序处于开发中,可能消耗较多 token。
## 麦麦MC项目早期开发
[让麦麦玩MC](https://github.com/MaiM-with-u/Maicraft)
交流群1058573197
## 💬 讨论 ## 💬 讨论
**技术交流群:** **技术交流群:**

2
bot.py
View File

@@ -66,7 +66,7 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
from src.plugin_system.core.events_manager import events_manager from src.plugin_system.core.events_manager import events_manager
from src.plugin_system.base.component_types import EventType from src.plugin_system.base.component_types import EventType
# 触发 ON_STOP 事件 # 触发 ON_STOP 事件
_ = await events_manager.handle_mai_events(event_type=EventType.ON_STOP) await events_manager.handle_mai_events(event_type=EventType.ON_STOP)
# 停止所有异步任务 # 停止所有异步任务
await async_task_manager.stop_and_wait_all_tasks() await async_task_manager.stop_and_wait_all_tasks()

View File

@@ -1,10 +1,25 @@
# Changelog # Changelog
## [0.10.2] - 2025-8-31
### 🌟 主要功能更改
- 精简了人格相关配置,提供更清晰,有效的自定义
- 大幅优化了聊天逻辑,更易配置,动态控制
- 现在支持提及100%回复
### 细节功能更改
- 更好的event系统
- 记忆系统优化
- 为空回复添加重试机制
- 修复tts插件可能的复读问题
## [0.10.1] - 2025-8-24 ## [0.10.1] - 2025-8-24
### 🌟 主要功能更改 ### 🌟 主要功能更改
- planner现在改为大小核结构移除激活阶段提高回复速度和动作调用精准度 - planner现在改为大小核结构移除激活阶段提高回复速度和动作调用精准度
- 优化关系的表现的效率 - 优化关系的表现的效率
### 细节功能更改
- 优化识图的表现 - 优化识图的表现
- 为planner添加单独控制的提示词 - 为planner添加单独控制的提示词
- 修复激活值计算异常的BUG - 修复激活值计算异常的BUG

View File

@@ -11,7 +11,7 @@ from src.plugin_system import (
BaseEventHandler, BaseEventHandler,
EventType, EventType,
MaiMessages, MaiMessages,
ToolParamType ToolParamType,
) )
@@ -136,12 +136,12 @@ class PrintMessage(BaseEventHandler):
handler_name = "print_message_handler" handler_name = "print_message_handler"
handler_description = "打印接收到的消息" handler_description = "打印接收到的消息"
async def execute(self, message: MaiMessages) -> Tuple[bool, bool, str | None]: async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, str | None, None]:
"""执行打印消息事件处理""" """执行打印消息事件处理"""
# 打印接收到的消息 # 打印接收到的消息
if self.get_config("print_message.enabled", False): if self.get_config("print_message.enabled", False):
print(f"接收到消息: {message.raw_message}") print(f"接收到消息: {message.raw_message if message else '无效消息'}")
return True, True, "消息已打印" return True, True, "消息已打印", None
# ===== 插件注册 ===== # ===== 插件注册 =====

View File

@@ -3,26 +3,7 @@ from src.config.config import global_config
from src.chat.frequency_control.utils import parse_stream_config_to_chat_id from src.chat.frequency_control.utils import parse_stream_config_to_chat_id
class FocusValueControl: def get_config_base_focus_value(chat_id: Optional[str] = None) -> float:
def __init__(self, chat_id: str):
self.chat_id = chat_id
self.focus_value_adjust: float = 1
def get_current_focus_value(self) -> float:
return get_current_focus_value(self.chat_id) * self.focus_value_adjust
class FocusValueControlManager:
def __init__(self):
self.focus_value_controls: dict[str, FocusValueControl] = {}
def get_focus_value_control(self, chat_id: str) -> FocusValueControl:
if chat_id not in self.focus_value_controls:
self.focus_value_controls[chat_id] = FocusValueControl(chat_id)
return self.focus_value_controls[chat_id]
def get_current_focus_value(chat_id: Optional[str] = None) -> float:
""" """
根据当前时间和聊天流获取对应的 focus_value 根据当前时间和聊天流获取对应的 focus_value
""" """
@@ -139,5 +120,3 @@ def get_global_focus_value() -> Optional[float]:
return None return None
focus_value_control = FocusValueControlManager()

View File

@@ -0,0 +1,501 @@
import time
from typing import Optional, Dict, List
from src.plugin_system.apis import message_api
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.common.logger import get_logger
from src.config.config import global_config
from src.chat.frequency_control.talk_frequency_control import get_config_base_talk_frequency
from src.chat.frequency_control.focus_value_control import get_config_base_focus_value
logger = get_logger("frequency_control")
class FrequencyControl:
"""
频率控制类,可以根据最近时间段的发言数量和发言人数动态调整频率
特点:
- 发言频率调整基于最近10分钟的数据评估单位为"消息数/10分钟"
- 专注度调整基于最近10分钟的数据评估单位为"消息数/10分钟"
- 历史基准值基于最近一周的数据按小时统计每小时都有独立的基准值需要至少50条历史消息
- 统一标准两个调整都使用10分钟窗口确保逻辑一致性和响应速度
- 双向调整:根据活跃度高低,既能提高也能降低频率和专注度
- 数据充足性检查当历史数据不足50条时不更新基准值当基准值为默认值时不进行动态调整
- 基准值更新:直接使用新计算的周均值,无平滑更新
"""
def __init__(self, chat_id: str):
self.chat_id = chat_id
self.chat_stream: ChatStream = get_chat_manager().get_stream(self.chat_id)
if not self.chat_stream:
raise ValueError(f"无法找到聊天流: {chat_id}")
self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]"
# 发言频率调整值
self.talk_frequency_adjust: float = 1.0
self.talk_frequency_external_adjust: float = 1.0
# 专注度调整值
self.focus_value_adjust: float = 1.0
self.focus_value_external_adjust: float = 1.0
# 动态调整相关参数
self.last_update_time = time.time()
self.update_interval = 60 # 每60秒更新一次
# 历史数据缓存
self._message_count_cache = 0
self._user_count_cache = 0
self._last_cache_time = 0
self._cache_duration = 30 # 缓存30秒
# 调整参数
self.min_adjust = 0.3 # 最小调整值
self.max_adjust = 2.0 # 最大调整值
# 动态基准值(将根据历史数据计算)
self.base_message_count = 5 # 默认基准消息数量,将被动态更新
self.base_user_count = 3 # 默认基准用户数量,将被动态更新
# 平滑因子
self.smoothing_factor = 0.3
# 历史数据相关参数
self._last_historical_update = 0
self._historical_update_interval = 600 # 每十分钟更新一次历史基准值
self._historical_days = 7 # 使用最近7天的数据计算基准值
# 按小时统计的历史基准值
self._hourly_baseline = {
'messages': {}, # {0-23: 平均消息数}
'users': {} # {0-23: 平均用户数}
}
# 初始化24小时的默认基准值
for hour in range(24):
self._hourly_baseline['messages'][hour] = 0.0
self._hourly_baseline['users'][hour] = 0.0
def _update_historical_baseline(self):
"""
更新基于历史数据的基准值
使用最近一周的数据,按小时统计平均消息数量和用户数量
"""
current_time = time.time()
# 检查是否需要更新历史基准值
if current_time - self._last_historical_update < self._historical_update_interval:
return
try:
# 计算一周前的时间戳
week_ago = current_time - (self._historical_days * 24 * 3600)
# 获取最近一周的消息数据
historical_messages = message_api.get_messages_by_time_in_chat(
chat_id=self.chat_stream.stream_id,
start_time=week_ago,
end_time=current_time,
filter_mai=True,
filter_command=True
)
if historical_messages and len(historical_messages) >= 50:
# 按小时统计消息数和用户数
hourly_stats = {hour: {'messages': [], 'users': set()} for hour in range(24)}
for msg in historical_messages:
# 获取消息的小时UTC时间
msg_time = time.localtime(msg.time)
msg_hour = msg_time.tm_hour
# 统计消息数
hourly_stats[msg_hour]['messages'].append(msg)
# 统计用户数
if msg.user_info and msg.user_info.user_id:
hourly_stats[msg_hour]['users'].add(msg.user_info.user_id)
# 计算每个小时的平均值(基于一周的数据)
for hour in range(24):
# 计算该小时的平均消息数(一周内该小时的总消息数 / 7天
total_messages = len(hourly_stats[hour]['messages'])
total_users = len(hourly_stats[hour]['users'])
# 只计算有消息的时段没有消息的时段设为0
if total_messages > 0:
avg_messages = total_messages / self._historical_days
avg_users = total_users / self._historical_days
self._hourly_baseline['messages'][hour] = avg_messages
self._hourly_baseline['users'][hour] = avg_users
else:
# 没有消息的时段设为0表示该时段不活跃
self._hourly_baseline['messages'][hour] = 0.0
self._hourly_baseline['users'][hour] = 0.0
# 更新整体基准值(用于兼容性)- 基于原始数据计算不受max(1.0)限制影响
overall_avg_messages = sum(len(hourly_stats[hour]['messages']) for hour in range(24)) / (24 * self._historical_days)
overall_avg_users = sum(len(hourly_stats[hour]['users']) for hour in range(24)) / (24 * self._historical_days)
self.base_message_count = overall_avg_messages
self.base_user_count = overall_avg_users
logger.info(
f"{self.log_prefix} 历史基准值更新完成: "
f"整体平均消息数={overall_avg_messages:.2f}, 整体平均用户数={overall_avg_users:.2f}"
)
# 记录几个关键时段的基准值
key_hours = [8, 12, 18, 22] # 早、中、晚、夜
for hour in key_hours:
# 计算该小时平均每10分钟的消息数和用户数
hourly_10min_messages = self._hourly_baseline['messages'][hour] / 6 # 1小时 = 6个10分钟
hourly_10min_users = self._hourly_baseline['users'][hour] / 6
logger.info(
f"{self.log_prefix} {hour}时基准值: "
f"消息数={self._hourly_baseline['messages'][hour]:.2f}/小时 "
f"({hourly_10min_messages:.2f}/10分钟), "
f"用户数={self._hourly_baseline['users'][hour]:.2f}/小时 "
f"({hourly_10min_users:.2f}/10分钟)"
)
elif historical_messages and len(historical_messages) < 50:
# 历史数据不足50条不更新基准值
logger.info(f"{self.log_prefix} 历史数据不足50条({len(historical_messages)}条),不更新基准值")
else:
# 如果没有历史数据,不更新基准值
logger.info(f"{self.log_prefix} 无历史数据,不更新基准值")
except Exception as e:
logger.error(f"{self.log_prefix} 更新历史基准值时出错: {e}")
# 出错时保持原有基准值不变
self._last_historical_update = current_time
def _get_current_hour_baseline(self) -> tuple[float, float]:
"""
获取当前小时的基准值
Returns:
tuple: (基准消息数, 基准用户数)
"""
current_hour = time.localtime().tm_hour
return (
self._hourly_baseline['messages'][current_hour],
self._hourly_baseline['users'][current_hour]
)
def get_dynamic_talk_frequency_adjust(self) -> float:
"""
获取纯动态调整值(不包含配置文件基础值)
Returns:
float: 动态调整值
"""
self._update_talk_frequency_adjust()
return self.talk_frequency_adjust
def get_dynamic_focus_value_adjust(self) -> float:
"""
获取纯动态调整值(不包含配置文件基础值)
Returns:
float: 动态调整值
"""
self._update_focus_value_adjust()
return self.focus_value_adjust
def _update_talk_frequency_adjust(self):
"""
更新发言频率调整值
适合人少话多的时候:人少但消息多,提高回复频率
"""
current_time = time.time()
# 检查是否需要更新
if current_time - self.last_update_time < self.update_interval:
return
# 先更新历史基准值
self._update_historical_baseline()
try:
# 获取最近10分钟的数据发言频率更敏感
recent_messages = message_api.get_messages_by_time_in_chat(
chat_id=self.chat_stream.stream_id,
start_time=current_time - 600, # 10分钟前
end_time=current_time,
filter_mai=True,
filter_command=True
)
# 计算消息数量和用户数量
message_count = len(recent_messages)
user_ids = set()
for msg in recent_messages:
if msg.user_info and msg.user_info.user_id:
user_ids.add(msg.user_info.user_id)
user_count = len(user_ids)
# 获取当前小时的基准值
current_hour_base_messages, current_hour_base_users = self._get_current_hour_baseline()
# 计算当前小时平均每10分钟的基准值
current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟
current_hour_10min_users = current_hour_base_users / 6
# 发言频率调整逻辑:根据活跃度双向调整
# 检查是否有足够的数据进行分析
if user_count > 0 and message_count >= 2: # 至少需要2条消息才能进行有意义的分析
# 检查历史基准值是否有效(该时段有活跃度)
if current_hour_base_messages > 0.0 and current_hour_base_users > 0.0:
# 计算人均消息数10分钟窗口
messages_per_user = message_count / user_count
# 使用当前小时每10分钟的基准人均消息数
base_messages_per_user = current_hour_10min_messages / current_hour_10min_users if current_hour_10min_users > 0 else 1.0
# 双向调整逻辑
if messages_per_user > base_messages_per_user * 1.2:
# 活跃度很高:提高回复频率
target_talk_adjust = min(self.max_adjust, messages_per_user / base_messages_per_user)
elif messages_per_user < base_messages_per_user * 0.8:
# 活跃度很低:降低回复频率
target_talk_adjust = max(self.min_adjust, messages_per_user / base_messages_per_user)
else:
# 活跃度正常:保持正常
target_talk_adjust = 1.0
else:
# 历史基准值不足,不调整
target_talk_adjust = 1.0
else:
# 数据不足:不调整
target_talk_adjust = 1.0
# 限制调整范围
target_talk_adjust = max(self.min_adjust, min(self.max_adjust, target_talk_adjust))
# 记录调整前的值
old_adjust = self.talk_frequency_adjust
# 平滑调整
self.talk_frequency_adjust = (
self.talk_frequency_adjust * (1 - self.smoothing_factor) +
target_talk_adjust * self.smoothing_factor
)
# 判断调整方向
if target_talk_adjust > 1.0:
adjust_direction = "提高"
elif target_talk_adjust < 1.0:
adjust_direction = "降低"
else:
if current_hour_base_messages <= 0.0 or current_hour_base_users <= 0.0:
adjust_direction = "不调整(该时段无活跃度)"
else:
adjust_direction = "保持"
# 计算实际变化方向
actual_change = ""
if self.talk_frequency_adjust > old_adjust:
actual_change = f"(实际提高: {old_adjust:.2f}{self.talk_frequency_adjust:.2f})"
elif self.talk_frequency_adjust < old_adjust:
actual_change = f"(实际降低: {old_adjust:.2f}{self.talk_frequency_adjust:.2f})"
else:
actual_change = f"(无变化: {self.talk_frequency_adjust:.2f})"
logger.info(
f"{self.log_prefix} 发言频率调整: "
f"当前: {message_count}消息/{user_count}用户, 人均: {message_count/user_count if user_count > 0 else 0:.2f}消息|"
f"基准: {current_hour_10min_messages:.2f}消息/{current_hour_10min_users:.2f}用户,人均:{current_hour_10min_messages/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}消息|"
f"目标调整: {adjust_direction}{target_talk_adjust:.2f}, 实际结果: {self.talk_frequency_adjust:.2f} {actual_change}"
)
except Exception as e:
logger.error(f"{self.log_prefix} 更新发言频率调整值时出错: {e}")
def _update_focus_value_adjust(self):
"""
更新专注度调整值
适合人多话多的时候人多且消息多提高专注度LLM消耗更多但回复更精准
"""
current_time = time.time()
# 检查是否需要更新
if current_time - self.last_update_time < self.update_interval:
return
try:
# 获取最近10分钟的数据与发言频率保持一致
recent_messages = message_api.get_messages_by_time_in_chat(
chat_id=self.chat_stream.stream_id,
start_time=current_time - 600, # 10分钟前
end_time=current_time,
filter_mai=True,
filter_command=True
)
# 计算消息数量和用户数量
message_count = len(recent_messages)
user_ids = set()
for msg in recent_messages:
if msg.user_info and msg.user_info.user_id:
user_ids.add(msg.user_info.user_id)
user_count = len(user_ids)
# 获取当前小时的基准值
current_hour_base_messages, current_hour_base_users = self._get_current_hour_baseline()
# 计算当前小时平均每10分钟的基准值
current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟
current_hour_10min_users = current_hour_base_users / 6
# 专注度调整逻辑:根据活跃度双向调整
# 检查是否有足够的数据进行分析
if user_count > 0 and current_hour_10min_users > 0 and message_count >= 2:
# 检查历史基准值是否有效(该时段有活跃度)
if current_hour_base_messages > 0.0 and current_hour_base_users > 0.0:
# 计算用户活跃度比率基于10分钟数据
user_ratio = user_count / current_hour_10min_users
# 计算消息活跃度比率基于10分钟数据
message_ratio = message_count / current_hour_10min_messages if current_hour_10min_messages > 0 else 1.0
# 双向调整逻辑
if user_ratio > 1.3 and message_ratio > 1.3:
# 活跃度很高提高专注度消耗更多LLM资源但回复更精准
target_focus_adjust = min(self.max_adjust, (user_ratio + message_ratio) / 2)
elif user_ratio > 1.1 and message_ratio > 1.1:
# 活跃度较高:适度提高专注度
target_focus_adjust = min(self.max_adjust, 1.0 + (user_ratio + message_ratio - 2.0) * 0.2)
elif user_ratio < 0.7 or message_ratio < 0.7:
# 活跃度很低降低专注度节省LLM资源
target_focus_adjust = max(self.min_adjust, min(user_ratio, message_ratio))
else:
# 正常情况:保持默认专注度
target_focus_adjust = 1.0
else:
# 历史基准值不足,不调整
target_focus_adjust = 1.0
else:
# 数据不足:不调整
target_focus_adjust = 1.0
# 限制调整范围
target_focus_adjust = max(self.min_adjust, min(self.max_adjust, target_focus_adjust))
# 记录调整前的值
old_focus_adjust = self.focus_value_adjust
# 平滑调整
self.focus_value_adjust = (
self.focus_value_adjust * (1 - self.smoothing_factor) +
target_focus_adjust * self.smoothing_factor
)
# 计算当前小时平均每10分钟的基准值
current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟
current_hour_10min_users = current_hour_base_users / 6
# 判断调整方向
if target_focus_adjust > 1.0:
adjust_direction = "提高"
elif target_focus_adjust < 1.0:
adjust_direction = "降低"
else:
if current_hour_base_messages <= 0.0 or current_hour_base_users <= 0.0:
adjust_direction = "不调整(该时段无活跃度)"
else:
adjust_direction = "保持"
# 计算实际变化方向
actual_change = ""
if self.focus_value_adjust > old_focus_adjust:
actual_change = f"(实际提高: {old_focus_adjust:.2f}{self.focus_value_adjust:.2f})"
elif self.focus_value_adjust < old_focus_adjust:
actual_change = f"(实际降低: {old_focus_adjust:.2f}{self.focus_value_adjust:.2f})"
else:
actual_change = f"(无变化: {self.focus_value_adjust:.2f})"
logger.info(
f"{self.log_prefix} 专注度调整(10分钟): "
f"当前: {message_count}消息/{user_count}用户,人均:{message_count/user_count if user_count > 0 else 0:.2f}消息|"
f"基准: {current_hour_10min_messages:.2f}消息/{current_hour_10min_users:.2f}用户,人均:{current_hour_10min_messages/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}消息|"
f"比率: 用户{user_count/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}x, 消息{message_count/current_hour_10min_messages if current_hour_10min_messages > 0 else 0:.2f}x, "
f"目标调整: {adjust_direction}{target_focus_adjust:.2f}, 实际结果: {self.focus_value_adjust:.2f} {actual_change}"
)
except Exception as e:
logger.error(f"{self.log_prefix} 更新专注度调整值时出错: {e}")
def get_final_talk_frequency(self) -> float:
return get_config_base_talk_frequency(self.chat_stream.stream_id) * self.get_dynamic_talk_frequency_adjust() * self.talk_frequency_external_adjust
def get_final_focus_value(self) -> float:
return get_config_base_focus_value(self.chat_stream.stream_id) * self.get_dynamic_focus_value_adjust() * self.focus_value_external_adjust
def set_adjustment_parameters(
self,
min_adjust: Optional[float] = None,
max_adjust: Optional[float] = None,
base_message_count: Optional[int] = None,
base_user_count: Optional[int] = None,
smoothing_factor: Optional[float] = None,
update_interval: Optional[int] = None,
historical_update_interval: Optional[int] = None,
historical_days: Optional[int] = None
):
"""
设置调整参数
Args:
min_adjust: 最小调整值
max_adjust: 最大调整值
base_message_count: 基准消息数量
base_user_count: 基准用户数量
smoothing_factor: 平滑因子
update_interval: 更新间隔(秒)
"""
if min_adjust is not None:
self.min_adjust = max(0.1, min_adjust)
if max_adjust is not None:
self.max_adjust = max(1.0, max_adjust)
if base_message_count is not None:
self.base_message_count = max(1, base_message_count)
if base_user_count is not None:
self.base_user_count = max(1, base_user_count)
if smoothing_factor is not None:
self.smoothing_factor = max(0.0, min(1.0, smoothing_factor))
if update_interval is not None:
self.update_interval = max(10, update_interval)
if historical_update_interval is not None:
self._historical_update_interval = max(300, historical_update_interval) # 最少5分钟
if historical_days is not None:
self._historical_days = max(1, min(30, historical_days)) # 1-30天之间
class FrequencyControlManager:
"""
频率控制管理器,管理多个聊天流的频率控制实例
"""
def __init__(self):
self.frequency_control_dict: Dict[str, FrequencyControl] = {}
def get_or_create_frequency_control(self, chat_id: str) -> FrequencyControl:
"""
获取或创建指定聊天流的频率控制实例
Args:
chat_id: 聊天流ID
Returns:
FrequencyControl: 频率控制实例
"""
if chat_id not in self.frequency_control_dict:
self.frequency_control_dict[chat_id] = FrequencyControl(chat_id)
return self.frequency_control_dict[chat_id]
# 创建全局实例
frequency_control_manager = FrequencyControlManager()

View File

@@ -3,26 +3,7 @@ from src.config.config import global_config
from src.chat.frequency_control.utils import parse_stream_config_to_chat_id from src.chat.frequency_control.utils import parse_stream_config_to_chat_id
class TalkFrequencyControl: def get_config_base_talk_frequency(chat_id: Optional[str] = None) -> float:
def __init__(self, chat_id: str):
self.chat_id = chat_id
self.talk_frequency_adjust: float = 1
def get_current_talk_frequency(self) -> float:
return get_current_talk_frequency(self.chat_id) * self.talk_frequency_adjust
class TalkFrequencyControlManager:
def __init__(self):
self.talk_frequency_controls = {}
def get_talk_frequency_control(self, chat_id: str) -> TalkFrequencyControl:
if chat_id not in self.talk_frequency_controls:
self.talk_frequency_controls[chat_id] = TalkFrequencyControl(chat_id)
return self.talk_frequency_controls[chat_id]
def get_current_talk_frequency(chat_id: Optional[str] = None) -> float:
""" """
根据当前时间和聊天流获取对应的 talk_frequency 根据当前时间和聊天流获取对应的 talk_frequency
@@ -145,4 +126,3 @@ def get_global_frequency() -> Optional[float]:
return None return None
talk_frequency_control = TalkFrequencyControlManager()

View File

@@ -18,8 +18,7 @@ from src.chat.planner_actions.action_modifier import ActionModifier
from src.chat.planner_actions.action_manager import ActionManager from src.chat.planner_actions.action_manager import ActionManager
from src.chat.heart_flow.hfc_utils import CycleDetail from src.chat.heart_flow.hfc_utils import CycleDetail
from src.chat.heart_flow.hfc_utils import send_typing, stop_typing from src.chat.heart_flow.hfc_utils import send_typing, stop_typing
from src.chat.frequency_control.talk_frequency_control import talk_frequency_control from src.chat.frequency_control.frequency_control import frequency_control_manager
from src.chat.frequency_control.focus_value_control import focus_value_control
from src.chat.express.expression_learner import expression_learner_manager from src.chat.express.expression_learner import expression_learner_manager
from src.person_info.person_info import Person from src.person_info.person_info import Person
from src.plugin_system.base.component_types import ChatMode, EventType, ActionInfo from src.plugin_system.base.component_types import ChatMode, EventType, ActionInfo
@@ -85,8 +84,7 @@ class HeartFChatting:
self.expression_learner = expression_learner_manager.get_expression_learner(self.stream_id) self.expression_learner = expression_learner_manager.get_expression_learner(self.stream_id)
self.talk_frequency_control = talk_frequency_control.get_talk_frequency_control(self.stream_id) self.frequency_control = frequency_control_manager.get_or_create_frequency_control(self.stream_id)
self.focus_value_control = focus_value_control.get_focus_value_control(self.stream_id)
self.action_manager = ActionManager() self.action_manager = ActionManager()
self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager) self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager)
@@ -101,15 +99,8 @@ class HeartFChatting:
self._cycle_counter = 0 self._cycle_counter = 0
self._current_cycle_detail: CycleDetail = None # type: ignore self._current_cycle_detail: CycleDetail = None # type: ignore
self.reply_timeout_count = 0
self.plan_timeout_count = 0
self.last_read_time = time.time() - 10 self.last_read_time = time.time() - 10
self.focus_energy = 1
self.no_action_consecutive = 0
# 最近三次no_action的新消息兴趣度记录
self.recent_interest_records: deque = deque(maxlen=3)
async def start(self): async def start(self):
"""检查是否需要启动主循环,如果未激活则启动。""" """检查是否需要启动主循环,如果未激活则启动。"""
@@ -188,86 +179,13 @@ class HeartFChatting:
f"选择动作: {action_type}" + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") f"选择动作: {action_type}" + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "")
) )
def _determine_form_type(self) -> None: async def caculate_interest_value(self, recent_messages_list: List["DatabaseMessages"]) -> float:
"""判断使用哪种形式的no_action"""
# 如果连续no_action次数少于3次使用waiting形式
if self.no_action_consecutive <= 3:
self.focus_energy = 1
else:
# 计算最近三次记录的兴趣度总和
total_recent_interest = sum(self.recent_interest_records)
# 计算调整后的阈值
adjusted_threshold = 1 / self.talk_frequency_control.get_current_talk_frequency()
logger.info(
f"{self.log_prefix} 最近三次兴趣度总和: {total_recent_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}"
)
# 如果兴趣度总和小于阈值进入breaking形式
if total_recent_interest < adjusted_threshold:
logger.info(f"{self.log_prefix} 兴趣度不足,进入休息")
self.focus_energy = random.randint(3, 6)
else:
logger.info(f"{self.log_prefix} 兴趣度充足,等待新消息")
self.focus_energy = 1
async def _should_process_messages(self, new_message: List["DatabaseMessages"]) -> tuple[bool, float]:
"""
判断是否应该处理消息
Args:
new_message: 新消息列表
mode: 当前聊天模式
Returns:
bool: 是否应该处理消息
"""
new_message_count = len(new_message)
talk_frequency = self.talk_frequency_control.get_current_talk_frequency()
modified_exit_count_threshold = self.focus_energy * 0.5 / talk_frequency
modified_exit_interest_threshold = 1.5 / talk_frequency
total_interest = 0.0 total_interest = 0.0
for msg in new_message: for msg in recent_messages_list:
interest_value = msg.interest_value interest_value = msg.interest_value
if interest_value is not None and msg.processed_plain_text: if interest_value is not None and msg.processed_plain_text:
total_interest += float(interest_value) total_interest += float(interest_value)
return total_interest / len(recent_messages_list)
if new_message_count >= modified_exit_count_threshold:
self.recent_interest_records.append(total_interest)
logger.info(
f"{self.log_prefix} 累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold:.1f}),结束等待"
)
# logger.info(self.last_read_time)
# logger.info(new_message)
return True, total_interest / new_message_count if new_message_count > 0 else 0.0
# 检查累计兴趣值
if new_message_count > 0:
# 只在兴趣值变化时输出log
if not hasattr(self, "_last_accumulated_interest") or total_interest != self._last_accumulated_interest:
logger.info(
f"{self.log_prefix} 休息中,新消息:{new_message_count}条,累计兴趣值: {total_interest:.2f}, 活跃度: {talk_frequency:.1f}"
)
self._last_accumulated_interest = total_interest
if total_interest >= modified_exit_interest_threshold:
# 记录兴趣度到列表
self.recent_interest_records.append(total_interest)
logger.info(
f"{self.log_prefix} 累计兴趣值达到{total_interest:.2f}(>{modified_exit_interest_threshold:.1f}),结束等待"
)
return True, total_interest / new_message_count if new_message_count > 0 else 0.0
# 每10秒输出一次等待状态
if int(time.time() - self.last_read_time) > 0 and int(time.time() - self.last_read_time) % 15 == 0:
logger.debug(
f"{self.log_prefix} 已等待{time.time() - self.last_read_time:.0f}秒,累计{new_message_count}条消息,累计兴趣{total_interest:.1f},继续等待..."
)
await asyncio.sleep(0.5)
return False, 0.0
async def _loopbody(self): async def _loopbody(self):
recent_messages_list = message_api.get_messages_by_time_in_chat( recent_messages_list = message_api.get_messages_by_time_in_chat(
@@ -279,16 +197,13 @@ class HeartFChatting:
filter_mai=True, filter_mai=True,
filter_command=True, filter_command=True,
) )
# 统一的消息处理逻辑
should_process, interest_value = await self._should_process_messages(recent_messages_list)
if should_process: if recent_messages_list:
self.last_read_time = time.time() self.last_read_time = time.time()
await self._observe(interest_value=interest_value) await self._observe(interest_value=await self.caculate_interest_value(recent_messages_list),recent_messages_list=recent_messages_list)
else: else:
# Normal模式消息数量不足等待 # Normal模式消息数量不足等待
await asyncio.sleep(0.5) await asyncio.sleep(0.2)
return True return True
return True return True
@@ -342,8 +257,7 @@ class HeartFChatting:
return loop_info, reply_text, cycle_timers return loop_info, reply_text, cycle_timers
async def _observe(self, interest_value: float = 0.0) -> bool: async def _observe(self, interest_value: float = 0.0,recent_messages_list: List["DatabaseMessages"] = []) -> bool:
action_type = "no_action"
reply_text = "" # 初始化reply_text变量避免UnboundLocalError reply_text = "" # 初始化reply_text变量避免UnboundLocalError
# 使用sigmoid函数将interest_value转换为概率 # 使用sigmoid函数将interest_value转换为概率
@@ -362,22 +276,28 @@ class HeartFChatting:
normal_mode_probability = ( normal_mode_probability = (
calculate_normal_mode_probability(interest_value) calculate_normal_mode_probability(interest_value)
* 2 * 2
* self.talk_frequency_control.get_current_talk_frequency() * self.frequency_control.get_final_talk_frequency()
) )
# 根据概率决定使用哪种模式 #对呼唤名字进行增幅
for msg in recent_messages_list:
if msg.reply_probability_boost is not None and msg.reply_probability_boost > 0.0:
normal_mode_probability += msg.reply_probability_boost
if global_config.chat.mentioned_bot_reply and msg.is_mentioned:
normal_mode_probability += global_config.chat.mentioned_bot_reply
if global_config.chat.at_bot_inevitable_reply and msg.is_at:
normal_mode_probability += global_config.chat.at_bot_inevitable_reply
# 根据概率决定使用直接回复
interest_triggerd = False
focus_triggerd = False
if random.random() < normal_mode_probability: if random.random() < normal_mode_probability:
mode = ChatMode.NORMAL interest_triggerd = True
logger.info( logger.info(
f"{self.log_prefix}兴趣({interest_value:.2f}),在{normal_mode_probability * 100:.0f}%概率下选择回复" f"{self.log_prefix}新消息,在{normal_mode_probability * 100:.0f}%概率下选择回复"
) )
else:
mode = ChatMode.FOCUS
# 创建新的循环信息
cycle_timers, thinking_id = self.start_cycle()
logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考")
if s4u_config.enable_s4u: if s4u_config.enable_s4u:
await send_typing() await send_typing()
@@ -385,30 +305,28 @@ class HeartFChatting:
async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()):
await self.expression_learner.trigger_learning_for_chat() await self.expression_learner.trigger_learning_for_chat()
# # 记忆构建为当前chat_id构建记忆
# try:
# await hippocampus_manager.build_memory_for_chat(self.stream_id)
# except Exception as e:
# logger.error(f"{self.log_prefix} 记忆构建失败: {e}")
available_actions: Dict[str, ActionInfo] = {} available_actions: Dict[str, ActionInfo] = {}
if random.random() > self.focus_value_control.get_current_focus_value() and mode == ChatMode.FOCUS:
# 如果激活度没有激活并且聊天活跃度低有可能不进行plan相当于不在电脑前不进行认真思考 #如果兴趣度不足以激活
action_to_use_info = [ if not interest_triggerd:
ActionPlannerInfo( #看看专注值够不够
action_type="no_action", if random.random() < self.frequency_control.get_final_focus_value():
reasoning="专注不足", #专注值足够,仍然进入正式思考
action_data={}, focus_triggerd = True #都没触发,路边
)
]
else: # 任意一种触发都行
if interest_triggerd or focus_triggerd:
# 进入正式思考模式
cycle_timers, thinking_id = self.start_cycle()
logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考")
# 第一步:动作检查 # 第一步:动作检查
with Timer("动作检查", cycle_timers): try:
try: await self.action_modifier.modify_actions()
await self.action_modifier.modify_actions() available_actions = self.action_manager.get_using_actions()
available_actions = self.action_manager.get_using_actions() except Exception as e:
except Exception as e: logger.error(f"{self.log_prefix} 动作修改失败: {e}")
logger.error(f"{self.log_prefix} 动作修改失败: {e}")
# 执行planner # 执行planner
is_group_chat, chat_target_info, _ = self.action_planner.get_necessary_info() is_group_chat, chat_target_info, _ = self.action_planner.get_necessary_info()
@@ -439,103 +357,93 @@ class HeartFChatting:
): ):
return False return False
with Timer("规划器", cycle_timers): with Timer("规划器", cycle_timers):
# 根据不同触发进入不同plan
if focus_triggerd:
mode = ChatMode.FOCUS
else:
mode = ChatMode.NORMAL
action_to_use_info, _ = await self.action_planner.plan( action_to_use_info, _ = await self.action_planner.plan(
mode=mode, mode=mode,
loop_start_time=self.last_read_time, loop_start_time=self.last_read_time,
available_actions=available_actions, available_actions=available_actions,
) )
for action in action_to_use_info: # 3. 并行执行所有动作
print(action.action_type) action_tasks = [
asyncio.create_task(
self._execute_action(action, action_to_use_info, thinking_id, available_actions, cycle_timers)
)
for action in action_to_use_info
]
# 3. 并行执行所有动作 # 并行执行所有任务
action_tasks = [ results = await asyncio.gather(*action_tasks, return_exceptions=True)
asyncio.create_task(
self._execute_action(action, action_to_use_info, thinking_id, available_actions, cycle_timers)
)
for action in action_to_use_info
]
# 并行执行所有任务 # 处理执行结果
results = await asyncio.gather(*action_tasks, return_exceptions=True) reply_loop_info = None
reply_text_from_reply = ""
action_success = False
action_reply_text = ""
action_command = ""
# 处理执行结果 for i, result in enumerate(results):
reply_loop_info = None if isinstance(result, BaseException):
reply_text_from_reply = "" logger.error(f"{self.log_prefix} 动作执行异常: {result}")
action_success = False continue
action_reply_text = ""
action_command = ""
for i, result in enumerate(results): _cur_action = action_to_use_info[i]
if isinstance(result, BaseException): if result["action_type"] != "reply":
logger.error(f"{self.log_prefix} 动作执行异常: {result}") action_success = result["success"]
continue action_reply_text = result["reply_text"]
action_command = result.get("command", "")
elif result["action_type"] == "reply":
if result["success"]:
reply_loop_info = result["loop_info"]
reply_text_from_reply = result["reply_text"]
else:
logger.warning(f"{self.log_prefix} 回复动作执行失败")
_cur_action = action_to_use_info[i] # 构建最终的循环信息
if result["action_type"] != "reply": if reply_loop_info:
action_success = result["success"] # 如果有回复信息使用回复的loop_info作为基础
action_reply_text = result["reply_text"] loop_info = reply_loop_info
action_command = result.get("command", "") # 更新动作执行信息
elif result["action_type"] == "reply": loop_info["loop_action_info"].update(
if result["success"]: {
reply_loop_info = result["loop_info"] "action_taken": action_success,
reply_text_from_reply = result["reply_text"] "command": action_command,
else: "taken_time": time.time(),
logger.warning(f"{self.log_prefix} 回复动作执行失败") }
)
# 构建最终的循环信息 reply_text = reply_text_from_reply
if reply_loop_info: else:
# 如果有回复信息,使用回复的loop_info作为基础 # 有回复信息,构建纯动作的loop_info
loop_info = reply_loop_info loop_info = {
# 更新动作执行信息 "loop_plan_info": {
loop_info["loop_action_info"].update( "action_result": action_to_use_info,
{ },
"action_taken": action_success, "loop_action_info": {
"command": action_command, "action_taken": action_success,
"taken_time": time.time(), "reply_text": action_reply_text,
"command": action_command,
"taken_time": time.time(),
},
} }
) reply_text = action_reply_text
reply_text = reply_text_from_reply
else:
# 没有回复信息构建纯动作的loop_info
loop_info = {
"loop_plan_info": {
"action_result": action_to_use_info,
},
"loop_action_info": {
"action_taken": action_success,
"reply_text": action_reply_text,
"command": action_command,
"taken_time": time.time(),
},
}
reply_text = action_reply_text
if s4u_config.enable_s4u:
await stop_typing()
await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text)
self.end_cycle(loop_info, cycle_timers) self.end_cycle(loop_info, cycle_timers)
self.print_cycle_info(cycle_timers) self.print_cycle_info(cycle_timers)
# await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", "")) """S4U内容暂时保留"""
if s4u_config.enable_s4u:
await stop_typing()
await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text)
"""S4U内容暂时保留"""
action_type = action_to_use_info[0].action_type if action_to_use_info else "no_action"
# 管理no_action计数器当执行了非no_action动作时重置计数器
if action_type != "no_action":
# no_action逻辑已集成到heartFC_chat.py中直接重置计数器
self.recent_interest_records.clear()
self.no_action_consecutive = 0
logger.debug(f"{self.log_prefix} 执行了{action_type}动作重置no_action计数器")
return True return True
if action_type == "no_action":
self.no_action_consecutive += 1
self._determine_form_type()
return True
async def _main_chat_loop(self): async def _main_chat_loop(self):
"""主循环,持续进行计划并可能回复消息,直到被外部取消。""" """主循环,持续进行计划并可能回复消息,直到被外部取消。"""
try: try:

View File

@@ -32,10 +32,10 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]:
Returns: Returns:
Tuple[float, bool, list[str]]: (兴趣度, 是否被提及, 关键词) Tuple[float, bool, list[str]]: (兴趣度, 是否被提及, 关键词)
""" """
if message.is_picid: if message.is_picid or message.is_emoji:
return 0.0, [] return 0.0, []
is_mentioned, _ = is_mentioned_bot_in_message(message) is_mentioned,is_at,reply_probability_boost = is_mentioned_bot_in_message(message)
interested_rate = 0.0 interested_rate = 0.0
with Timer("记忆激活"): with Timer("记忆激活"):
@@ -79,17 +79,13 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]:
# 确保在范围内 # 确保在范围内
base_interest = min(max(base_interest, 0.01), 0.3) base_interest = min(max(base_interest, 0.01), 0.3)
interested_rate += base_interest
if is_mentioned: message.interest_value = base_interest
interest_increase_on_mention = 2
interested_rate += interest_increase_on_mention
message.interest_value = interested_rate
message.is_mentioned = is_mentioned message.is_mentioned = is_mentioned
message.is_at = is_at
message.reply_probability_boost = reply_probability_boost
return interested_rate, keywords return base_interest, keywords
class HeartFCMessageReceiver: class HeartFCMessageReceiver:

View File

@@ -18,6 +18,7 @@ from src.config.config import global_config, model_config
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
from src.common.database.database_model import GraphNodes, GraphEdges # Peewee Models导入 from src.common.database.database_model import GraphNodes, GraphEdges # Peewee Models导入
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.utils.utils import cut_key_words
from src.chat.utils.chat_message_builder import ( from src.chat.utils.chat_message_builder import (
build_readable_messages, build_readable_messages,
get_raw_msg_by_timestamp_with_chat_inclusive, get_raw_msg_by_timestamp_with_chat_inclusive,
@@ -98,19 +99,23 @@ class MemoryGraph:
current_weight = self.G.nodes[concept].get("weight", 0.0) current_weight = self.G.nodes[concept].get("weight", 0.0)
self.G.nodes[concept]["weight"] = current_weight + 1.0 self.G.nodes[concept]["weight"] = current_weight + 1.0
logger.debug(f"节点 {concept} 记忆整合成功,权重增加到 {current_weight + 1.0}") logger.debug(f"节点 {concept} 记忆整合成功,权重增加到 {current_weight + 1.0}")
logger.info(f"节点 {concept} 记忆内容已更新:{integrated_memory}")
except Exception as e: except Exception as e:
logger.error(f"LLM整合记忆失败: {e}") logger.error(f"LLM整合记忆失败: {e}")
# 降级到简单连接 # 降级到简单连接
new_memory_str = f"{existing_memory} | {memory}" new_memory_str = f"{existing_memory} | {memory}"
self.G.nodes[concept]["memory_items"] = new_memory_str self.G.nodes[concept]["memory_items"] = new_memory_str
logger.info(f"节点 {concept} 记忆内容已简单拼接并更新:{new_memory_str}")
else: else:
new_memory_str = str(memory) new_memory_str = str(memory)
self.G.nodes[concept]["memory_items"] = new_memory_str self.G.nodes[concept]["memory_items"] = new_memory_str
logger.info(f"节点 {concept} 记忆内容已直接更新:{new_memory_str}")
else: else:
self.G.nodes[concept]["memory_items"] = str(memory) self.G.nodes[concept]["memory_items"] = str(memory)
# 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time
if "created_time" not in self.G.nodes[concept]: if "created_time" not in self.G.nodes[concept]:
self.G.nodes[concept]["created_time"] = current_time self.G.nodes[concept]["created_time"] = current_time
logger.info(f"节点 {concept} 创建新记忆:{str(memory)}")
# 更新最后修改时间 # 更新最后修改时间
self.G.nodes[concept]["last_modified"] = current_time self.G.nodes[concept]["last_modified"] = current_time
else: else:
@@ -122,6 +127,7 @@ class MemoryGraph:
created_time=current_time, # 添加创建时间 created_time=current_time, # 添加创建时间
last_modified=current_time, last_modified=current_time,
) # 添加最后修改时间 ) # 添加最后修改时间
logger.info(f"新节点 {concept} 已添加,记忆内容已写入:{str(memory)}")
def get_dot(self, concept): def get_dot(self, concept):
# 检查节点是否存在于图中 # 检查节点是否存在于图中
@@ -402,9 +408,7 @@ class Hippocampus:
text_length = len(text) text_length = len(text)
topic_num: int | list[int] = 0 topic_num: int | list[int] = 0
words = jieba.cut(text) keywords_lite = cut_key_words(text)
keywords_lite = [word for word in words if len(word) > 1]
keywords_lite = list(set(keywords_lite))
if keywords_lite: if keywords_lite:
logger.debug(f"提取关键词极简版: {keywords_lite}") logger.debug(f"提取关键词极简版: {keywords_lite}")
@@ -1113,6 +1117,7 @@ class ParahippocampalGyrus:
# 4. 创建所有话题的摘要生成任务 # 4. 创建所有话题的摘要生成任务
tasks: List[Tuple[str, Coroutine[Any, Any, Tuple[str, Tuple[str, str, List | None]]]]] = [] tasks: List[Tuple[str, Coroutine[Any, Any, Tuple[str, Tuple[str, str, List | None]]]]] = []
topic_what_prompt: str = ""
for topic in filtered_topics: for topic in filtered_topics:
# 调用修改后的 topic_what不再需要 time_info # 调用修改后的 topic_what不再需要 time_info
topic_what_prompt = self.hippocampus.topic_what(input_text, topic) topic_what_prompt = self.hippocampus.topic_what(input_text, topic)
@@ -1159,6 +1164,131 @@ class ParahippocampalGyrus:
return compressed_memory, similar_topics_dict return compressed_memory, similar_topics_dict
def get_similar_topics_from_keywords(
self,
keywords: list[str] | str,
top_k: int = 3,
threshold: float = 0.7,
) -> dict[str, list[tuple[str, float]]]:
"""基于输入的关键词,返回每个关键词对应的相似主题列表。
Args:
keywords: 关键词列表或以逗号/空格/顿号分隔的字符串。
top_k: 每个关键词返回的相似主题数量上限。
threshold: 相似度阈值,低于该值的主题将被过滤。
Returns:
dict[str, list[tuple[str, float]]]: {keyword: [(topic, similarity), ...]}
"""
# 规范化输入为列表[str]
if isinstance(keywords, str):
# 支持中英文逗号、顿号、空格分隔
parts = (
keywords.replace("", ",").replace("", ",").replace(" ", ",").strip(", ")
)
keyword_list = [p.strip() for p in parts.split(",") if p.strip()]
else:
keyword_list = [k.strip() for k in keywords if isinstance(k, str) and k.strip()]
if not keyword_list:
return {}
existing_topics = list(self.memory_graph.G.nodes())
result: dict[str, list[tuple[str, float]]] = {}
for kw in keyword_list:
kw_words = set(jieba.cut(kw))
similar_topics: list[tuple[str, float]] = []
for topic in existing_topics:
topic_words = set(jieba.cut(topic))
all_words = kw_words | topic_words
if not all_words:
continue
v1 = [1 if w in kw_words else 0 for w in all_words]
v2 = [1 if w in topic_words else 0 for w in all_words]
sim = cosine_similarity(v1, v2)
if sim >= threshold:
similar_topics.append((topic, sim))
similar_topics.sort(key=lambda x: x[1], reverse=True)
result[kw] = similar_topics[:top_k]
return result
async def add_memory_with_similar(
self,
memory_item: str,
similar_topics_dict: dict[str, list[tuple[str, float]]],
) -> bool:
"""将单条记忆内容与相似主题写入记忆网络并同步数据库。
按 build_memory_for_chat 的方式:为 similar_topics_dict 的每个键作为主题添加节点内容,
并与其相似主题建立连接,连接强度为 int(similarity * 10)。
Args:
memory_item: 记忆内容字符串,将作为每个主题节点的 memory_items。
similar_topics_dict: {topic: [(similar_topic, similarity), ...]}
Returns:
bool: 是否成功执行添加与同步。
"""
try:
if not memory_item or not isinstance(memory_item, str):
return False
if not similar_topics_dict or not isinstance(similar_topics_dict, dict):
return False
current_time = time.time()
# 为每个主题写入节点
for topic, similar_list in similar_topics_dict.items():
if not topic or not isinstance(topic, str):
continue
await self.hippocampus.memory_graph.add_dot(topic, memory_item, self.hippocampus)
# 连接相似主题
if isinstance(similar_list, list):
for item in similar_list:
try:
similar_topic, similarity = item
except Exception:
continue
if not isinstance(similar_topic, str):
continue
if topic == similar_topic:
continue
# 强度按 build_memory_for_chat 的规则
strength = int(max(0.0, float(similarity)) * 10) if similarity is not None else 0
if strength <= 0:
continue
# 确保相似主题节点存在如果没有也可以只建立边networkx会创建节点但需初始化属性
if similar_topic not in self.memory_graph.G:
# 创建一个空的相似主题节点避免悬空边memory_items 为空字符串
self.memory_graph.G.add_node(
similar_topic,
memory_items="",
weight=1.0,
created_time=current_time,
last_modified=current_time,
)
self.memory_graph.G.add_edge(
topic,
similar_topic,
strength=strength,
created_time=current_time,
last_modified=current_time,
)
# 同步数据库
await self.hippocampus.entorhinal_cortex.sync_memory_to_db()
return True
except Exception as e:
logger.error(f"添加记忆节点失败: {e}")
return False
async def operation_forget_topic(self, percentage=0.005): async def operation_forget_topic(self, percentage=0.005):
start_time = time.time() start_time = time.time()
logger.info("[遗忘] 开始检查数据库...") logger.info("[遗忘] 开始检查数据库...")
@@ -1325,7 +1455,6 @@ class HippocampusManager:
logger.info(f""" logger.info(f"""
-------------------------------- --------------------------------
记忆系统参数配置: 记忆系统参数配置:
构建频率: {global_config.memory.memory_build_frequency}秒|压缩率: {global_config.memory.memory_compress_rate}
遗忘间隔: {global_config.memory.forget_memory_interval}秒|遗忘比例: {global_config.memory.memory_forget_percentage}|遗忘: {global_config.memory.memory_forget_time}小时之后 遗忘间隔: {global_config.memory.forget_memory_interval}秒|遗忘比例: {global_config.memory.memory_forget_percentage}|遗忘: {global_config.memory.memory_forget_time}小时之后
记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count} 记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count}
--------------------------------""") # noqa: E501 --------------------------------""") # noqa: E501
@@ -1343,61 +1472,6 @@ class HippocampusManager:
raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法")
return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage)
async def build_memory_for_chat(self, chat_id: str):
"""为指定chat_id构建记忆在heartFC_chat.py中调用"""
if not self._initialized:
raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法")
try:
# 检查是否需要构建记忆
logger.info(f"{chat_id} 构建记忆")
if memory_segment_manager.check_and_build_memory_for_chat(chat_id):
logger.info(f"{chat_id} 构建记忆,需要构建记忆")
messages = memory_segment_manager.get_messages_for_memory_build(chat_id, 50)
build_probability = 0.3 * global_config.memory.memory_build_frequency
if messages and random.random() < build_probability:
logger.info(f"{chat_id} 构建记忆,消息数量: {len(messages)}")
# 调用记忆压缩和构建
(
compressed_memory,
similar_topics_dict,
) = await self._hippocampus.parahippocampal_gyrus.memory_compress(
messages, global_config.memory.memory_compress_rate
)
# 添加记忆节点
current_time = time.time()
for topic, memory in compressed_memory:
await self._hippocampus.memory_graph.add_dot(topic, memory, self._hippocampus)
# 连接相似主题
if topic in similar_topics_dict:
similar_topics = similar_topics_dict[topic]
for similar_topic, similarity in similar_topics:
if topic != similar_topic:
strength = int(similarity * 10)
self._hippocampus.memory_graph.G.add_edge(
topic,
similar_topic,
strength=strength,
created_time=current_time,
last_modified=current_time,
)
# 同步到数据库
await self._hippocampus.entorhinal_cortex.sync_memory_to_db()
logger.info(f"{chat_id} 构建记忆完成")
return True
except Exception as e:
logger.error(f"{chat_id} 构建记忆失败: {e}")
return False
return False
async def get_memory_from_topic( async def get_memory_from_topic(
self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3 self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3
) -> list: ) -> list:
@@ -1441,89 +1515,3 @@ class HippocampusManager:
# 创建全局实例 # 创建全局实例
hippocampus_manager = HippocampusManager() hippocampus_manager = HippocampusManager()
# 在Hippocampus类中添加新的记忆构建管理器
class MemoryBuilder:
"""记忆构建器
为每个chat_id维护消息缓存和触发机制类似ExpressionLearner
"""
def __init__(self, chat_id: str):
self.chat_id = chat_id
self.last_update_time: float = time.time()
self.last_processed_time: float = 0.0
def should_trigger_memory_build(self) -> bool:
# sourcery skip: assign-if-exp, boolean-if-exp-identity, reintroduce-else
"""检查是否应该触发记忆构建"""
current_time = time.time()
# 检查时间间隔
time_diff = current_time - self.last_update_time
if time_diff < 600 / global_config.memory.memory_build_frequency:
return False
# 检查消息数量
recent_messages = get_raw_msg_by_timestamp_with_chat_inclusive(
chat_id=self.chat_id,
timestamp_start=self.last_update_time,
timestamp_end=current_time,
)
logger.info(f"最近消息数量: {len(recent_messages)},间隔时间: {time_diff}")
if not recent_messages or len(recent_messages) < 30 / global_config.memory.memory_build_frequency:
return False
return True
def get_messages_for_memory_build(self, threshold: int = 25) -> List[DatabaseMessages]:
"""获取用于记忆构建的消息"""
current_time = time.time()
messages = get_raw_msg_by_timestamp_with_chat_inclusive(
chat_id=self.chat_id,
timestamp_start=self.last_update_time,
timestamp_end=current_time,
limit=threshold,
)
if messages:
# 更新最后处理时间
self.last_processed_time = current_time
self.last_update_time = current_time
return messages or []
class MemorySegmentManager:
"""记忆段管理器
管理所有chat_id的MemoryBuilder实例自动检查和触发记忆构建
"""
def __init__(self):
self.builders: Dict[str, MemoryBuilder] = {}
def get_or_create_builder(self, chat_id: str) -> MemoryBuilder:
"""获取或创建指定chat_id的MemoryBuilder"""
if chat_id not in self.builders:
self.builders[chat_id] = MemoryBuilder(chat_id)
return self.builders[chat_id]
def check_and_build_memory_for_chat(self, chat_id: str) -> bool:
"""检查指定chat_id是否需要构建记忆如果需要则返回True"""
builder = self.get_or_create_builder(chat_id)
return builder.should_trigger_memory_build()
def get_messages_for_memory_build(self, chat_id: str, threshold: int = 25) -> List[DatabaseMessages]:
"""获取指定chat_id用于记忆构建的消息"""
if chat_id not in self.builders:
return []
return self.builders[chat_id].get_messages_for_memory_build(threshold)
# 创建全局实例
memory_segment_manager = MemorySegmentManager()

View File

@@ -1,254 +0,0 @@
# -*- coding: utf-8 -*-
import time
import re
import json
import ast
import traceback
from json_repair import repair_json
from datetime import datetime, timedelta
from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger
from src.common.database.database_model import Memory # Peewee Models导入
from src.config.config import model_config, global_config
logger = get_logger(__name__)
class MemoryItem:
def __init__(self, memory_id: str, chat_id: str, memory_text: str, keywords: list[str]):
self.memory_id = memory_id
self.chat_id = chat_id
self.memory_text: str = memory_text
self.keywords: list[str] = keywords
self.create_time: float = time.time()
self.last_view_time: float = time.time()
class MemoryManager:
def __init__(self):
# self.memory_items:list[MemoryItem] = []
pass
class InstantMemory:
def __init__(self, chat_id):
self.chat_id = chat_id
self.last_view_time = time.time()
self.summary_model = LLMRequest(
model_set=model_config.model_task_config.utils,
request_type="memory.summary",
)
async def if_need_build(self, text: str):
prompt = f"""
请判断以下内容中是否有值得记忆的信息如果有请输出1否则输出0
{text}
请只输出1或0就好
"""
try:
response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5)
if global_config.debug.show_prompt:
print(prompt)
print(response)
return "1" in response
except Exception as e:
logger.error(f"判断是否需要记忆出现错误:{str(e)} {traceback.format_exc()}")
return False
async def build_memory(self, text):
prompt = f"""
以下内容中存在值得记忆的信息,请你从中总结出一段值得记忆的信息,并输出
{text}
请以json格式输出一段概括的记忆内容和关键词
{{
"memory_text": "记忆内容",
"keywords": "关键词,用/划分"
}}
"""
try:
response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5)
# print(prompt)
# print(response)
if not response:
return None
try:
repaired = repair_json(response)
result = json.loads(repaired)
memory_text = result.get("memory_text", "")
keywords = result.get("keywords", "")
if isinstance(keywords, str):
keywords_list = [k.strip() for k in keywords.split("/") if k.strip()]
elif isinstance(keywords, list):
keywords_list = keywords
else:
keywords_list = []
return {"memory_text": memory_text, "keywords": keywords_list}
except Exception as parse_e:
logger.error(f"解析记忆json失败{str(parse_e)} {traceback.format_exc()}")
return None
except Exception as e:
logger.error(f"构建记忆出现错误:{str(e)} {traceback.format_exc()}")
return None
async def create_and_store_memory(self, text: str):
if_need = await self.if_need_build(text)
if if_need:
logger.info(f"需要记忆:{text}")
memory = await self.build_memory(text)
if memory and memory.get("memory_text"):
memory_id = f"{self.chat_id}_{time.time()}"
memory_item = MemoryItem(
memory_id=memory_id,
chat_id=self.chat_id,
memory_text=memory["memory_text"],
keywords=memory.get("keywords", []),
)
await self.store_memory(memory_item)
else:
logger.info(f"不需要记忆:{text}")
async def store_memory(self, memory_item: MemoryItem):
memory = Memory(
memory_id=memory_item.memory_id,
chat_id=memory_item.chat_id,
memory_text=memory_item.memory_text,
keywords=memory_item.keywords,
create_time=memory_item.create_time,
last_view_time=memory_item.last_view_time,
)
memory.save()
async def get_memory(self, target: str):
from json_repair import repair_json
prompt = f"""
请根据以下发言内容,判断是否需要提取记忆
{target}
请用json格式输出包含以下字段
其中time的要求是
可以选择具体日期时间格式为YYYY-MM-DD HH:MM:SS或者大致时间格式为YYYY-MM-DD
可以选择相对时间例如今天昨天前天5天前1个月前
可以选择留空进行模糊搜索
{{
"need_memory": 1,
"keywords": "希望获取的记忆关键词,用/划分",
"time": "希望获取的记忆大致时间"
}}
请只输出json格式不要输出其他多余内容
"""
try:
response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5)
if global_config.debug.show_prompt:
print(prompt)
print(response)
if not response:
return None
try:
repaired = repair_json(response)
result = json.loads(repaired)
# 解析keywords
keywords = result.get("keywords", "")
if isinstance(keywords, str):
keywords_list = [k.strip() for k in keywords.split("/") if k.strip()]
elif isinstance(keywords, list):
keywords_list = keywords
else:
keywords_list = []
# 解析time为时间段
time_str = result.get("time", "").strip()
start_time, end_time = self._parse_time_range(time_str)
logger.info(f"start_time: {start_time}, end_time: {end_time}")
# 检索包含关键词的记忆
memories_set = set()
if start_time and end_time:
start_ts = start_time.timestamp()
end_ts = end_time.timestamp()
query = Memory.select().where(
(Memory.chat_id == self.chat_id)
& (Memory.create_time >= start_ts) # type: ignore
& (Memory.create_time < end_ts) # type: ignore
)
else:
query = Memory.select().where(Memory.chat_id == self.chat_id)
for mem in query:
# 对每条记忆
mem_keywords = mem.keywords or ""
parsed = ast.literal_eval(mem_keywords)
if isinstance(parsed, list):
mem_keywords = [str(k).strip() for k in parsed if str(k).strip()]
else:
mem_keywords = []
# logger.info(f"mem_keywords: {mem_keywords}")
# logger.info(f"keywords_list: {keywords_list}")
for kw in keywords_list:
# logger.info(f"kw: {kw}")
# logger.info(f"kw in mem_keywords: {kw in mem_keywords}")
if kw in mem_keywords:
# logger.info(f"mem.memory_text: {mem.memory_text}")
memories_set.add(mem.memory_text)
break
return list(memories_set)
except Exception as parse_e:
logger.error(f"解析记忆json失败{str(parse_e)} {traceback.format_exc()}")
return None
except Exception as e:
logger.error(f"获取记忆出现错误:{str(e)} {traceback.format_exc()}")
return None
def _parse_time_range(self, time_str):
# sourcery skip: extract-duplicate-method, use-contextlib-suppress
"""
支持解析如下格式:
- 具体日期时间YYYY-MM-DD HH:MM:SS
- 具体日期YYYY-MM-DD
- 相对时间今天昨天前天N天前N个月前
- 空字符串:返回(None, None)
"""
now = datetime.now()
if not time_str:
return 0, now
time_str = time_str.strip()
# 具体日期时间
try:
dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
return dt, dt + timedelta(hours=1)
except Exception:
pass
# 具体日期
try:
dt = datetime.strptime(time_str, "%Y-%m-%d")
return dt, dt + timedelta(days=1)
except Exception:
pass
# 相对时间
if time_str == "今天":
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=1)
return start, end
if time_str == "昨天":
start = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=1)
return start, end
if time_str == "前天":
start = (now - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=1)
return start, end
if m := re.match(r"(\d+)天前", time_str):
days = int(m.group(1))
start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=1)
return start, end
if m := re.match(r"(\d+)个月前", time_str):
months = int(m.group(1))
# 近似每月30天
start = (now - timedelta(days=months * 30)).replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=1)
return start, end
# 其他无法解析
return 0, now

View File

@@ -84,7 +84,7 @@ class Message(MessageBase):
return await self._process_single_segment(segment) # type: ignore return await self._process_single_segment(segment) # type: ignore
@abstractmethod @abstractmethod
async def _process_single_segment(self, segment): async def _process_single_segment(self, segment) -> str:
pass pass
@@ -108,6 +108,8 @@ class MessageRecv(Message):
self.has_picid = False self.has_picid = False
self.is_voice = False self.is_voice = False
self.is_mentioned = None self.is_mentioned = None
self.is_at = False
self.reply_probability_boost = 0.0
self.is_notify = False self.is_notify = False
self.is_command = False self.is_command = False
@@ -353,44 +355,44 @@ class MessageProcessBase(Message):
self.thinking_time = round(time.time() - self.thinking_start_time, 2) self.thinking_time = round(time.time() - self.thinking_start_time, 2)
return self.thinking_time return self.thinking_time
async def _process_single_segment(self, seg: Seg) -> str | None: async def _process_single_segment(self, segment: Seg) -> str:
"""处理单个消息段 """处理单个消息段
Args: Args:
seg: 要处理的消息段 segment: 要处理的消息段
Returns: Returns:
str: 处理后的文本 str: 处理后的文本
""" """
try: try:
if seg.type == "text": if segment.type == "text":
return seg.data # type: ignore return segment.data # type: ignore
elif seg.type == "image": elif segment.type == "image":
# 如果是base64图片数据 # 如果是base64图片数据
if isinstance(seg.data, str): if isinstance(segment.data, str):
return await get_image_manager().get_image_description(seg.data) return await get_image_manager().get_image_description(segment.data)
return "[图片,网卡了加载不出来]" return "[图片,网卡了加载不出来]"
elif seg.type == "emoji": elif segment.type == "emoji":
if isinstance(seg.data, str): if isinstance(segment.data, str):
return await get_image_manager().get_emoji_tag(seg.data) return await get_image_manager().get_emoji_tag(segment.data)
return "[表情,网卡了加载不出来]" return "[表情,网卡了加载不出来]"
elif seg.type == "voice": elif segment.type == "voice":
if isinstance(seg.data, str): if isinstance(segment.data, str):
return await get_voice_text(seg.data) return await get_voice_text(segment.data)
return "[发了一段语音,网卡了加载不出来]" return "[发了一段语音,网卡了加载不出来]"
elif seg.type == "at": elif segment.type == "at":
return f"[@{seg.data}]" return f"[@{segment.data}]"
elif seg.type == "reply": elif segment.type == "reply":
if self.reply and hasattr(self.reply, "processed_plain_text"): if self.reply and hasattr(self.reply, "processed_plain_text"):
# print(f"self.reply.processed_plain_text: {self.reply.processed_plain_text}") # print(f"self.reply.processed_plain_text: {self.reply.processed_plain_text}")
# print(f"reply: {self.reply}") # print(f"reply: {self.reply}")
return f"[回复<{self.reply.message_info.user_info.user_nickname}:{self.reply.message_info.user_info.user_id}> 的消息:{self.reply.processed_plain_text}]" # type: ignore return f"[回复<{self.reply.message_info.user_info.user_nickname}:{self.reply.message_info.user_info.user_id}> 的消息:{self.reply.processed_plain_text}]" # type: ignore
return None return ""
else: else:
return f"[{seg.type}:{str(seg.data)}]" return f"[{segment.type}:{str(segment.data)}]"
except Exception as e: except Exception as e:
logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") logger.error(f"处理消息段失败: {str(e)}, 类型: {segment.type}, 数据: {segment.data}")
return f"[处理失败的{seg.type}消息]" return f"[处理失败的{segment.type}消息]"
def _generate_detailed_text(self) -> str: def _generate_detailed_text(self) -> str:
"""生成详细文本,包含时间和用户信息""" """生成详细文本,包含时间和用户信息"""

View File

@@ -56,6 +56,8 @@ class MessageStorage:
filtered_display_message = "" filtered_display_message = ""
interest_value = 0 interest_value = 0
is_mentioned = False is_mentioned = False
is_at = False
reply_probability_boost = 0.0
reply_to = message.reply_to reply_to = message.reply_to
priority_mode = "" priority_mode = ""
priority_info = {} priority_info = {}
@@ -70,6 +72,8 @@ class MessageStorage:
filtered_display_message = "" filtered_display_message = ""
interest_value = message.interest_value interest_value = message.interest_value
is_mentioned = message.is_mentioned is_mentioned = message.is_mentioned
is_at = message.is_at
reply_probability_boost = message.reply_probability_boost
reply_to = "" reply_to = ""
priority_mode = message.priority_mode priority_mode = message.priority_mode
priority_info = message.priority_info priority_info = message.priority_info
@@ -100,6 +104,8 @@ class MessageStorage:
# Flattened chat_info # Flattened chat_info
reply_to=reply_to, reply_to=reply_to,
is_mentioned=is_mentioned, is_mentioned=is_mentioned,
is_at=is_at,
reply_probability_boost=reply_probability_boost,
chat_info_stream_id=chat_info_dict.get("stream_id"), chat_info_stream_id=chat_info_dict.get("stream_id"),
chat_info_platform=chat_info_dict.get("platform"), chat_info_platform=chat_info_dict.get("platform"),
chat_info_user_platform=user_info_from_chat.get("platform"), chat_info_user_platform=user_info_from_chat.get("platform"),

View File

@@ -84,7 +84,7 @@ class ActionManager:
log_prefix=log_prefix, log_prefix=log_prefix,
shutting_down=shutting_down, shutting_down=shutting_down,
plugin_config=plugin_config, plugin_config=plugin_config,
action_message=action_message.flatten() if action_message else None, action_message=action_message,
) )
logger.debug(f"创建Action实例成功: {action_name}") logger.debug(f"创建Action实例成功: {action_name}")

View File

@@ -40,41 +40,39 @@ def init_prompt():
""" """
{time_block} {time_block}
{name_block} {name_block}
你现在需要根据聊天内容选择的合适的action来参与聊天。
请你根据以下行事风格来决定action:
{plan_style}
{chat_context_description},以下是具体的聊天内容 {chat_context_description},以下是具体的聊天内容
**聊天内容**
{chat_content_block} {chat_content_block}
{moderation_prompt} **动作记录**
现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息:
{actions_before_now_block} {actions_before_now_block}
动作no_action **回复标准**
动作描述:不进行动作,等待合适的时机 请你根据聊天内容和用户的最新消息选择合适回复或者沉默:
- 当你刚刚发送了消息没有人回复时选择no_action 1.你可以选择呼叫了你的名字,但是你没有做出回应的消息进行回复
- 当你一次发送了太多消息,为了避免过于烦人,可以不回复 2.你可以自然的顺着正在进行的聊天内容进行回复或自然的提出一个问题
3.你的兴趣是:{interest}
4.如果你刚刚进行了回复,不要对同一个话题重复回应
5.请控制你的发言频率,不要太过频繁的发言,当你刚刚发送了消息没有人回复时选择no_action
6.如果有人对你感到厌烦,请减少回复
7.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对
8.最好不要选择图片和表情包作为回复对象
{moderation_prompt}
**动作**
保持沉默no_action
{{ {{
"action": "no_action", "action": "no_action",
"reason":"动作的原因" "reason":"回复的原因"
}} }}
动作reply 进行回复reply
动作描述:参与聊天回复,发送文本进行表达
- 你想要闲聊或者随便附和
- 有人提到了你,但是你还没有回应
- {mentioned_bonus}
- 如果你刚刚进行了回复,不要对同一个话题重复回应
{{ {{
"action": "reply", "action": "reply",
"target_message_id":"想要回复的消息id", "target_message_id":"想要回复的消息id",
"reason":"回复的原因" "reason":"回复的原因"
}} }}
你必须从上面列出的可用action中选择一个并说明触发action的消息id不是消息原文和选择该action的原因。消息id格式:m+数字 你必须从上面列出的可用action中选择一个并说明触发action的消息id不是消息原文和选择该action的原因。消息id格式:m+数字
请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
""", """,
"planner_prompt", "planner_prompt",
@@ -85,27 +83,29 @@ def init_prompt():
{time_block} {time_block}
{name_block} {name_block}
{chat_context_description},以下是具体的聊天内容 {chat_context_description}
**聊天内容**
{chat_content_block} {chat_content_block}
{moderation_prompt} **动作记录**
现在,最新的聊天消息引起了你的兴趣,你想要对其中的消息进行回复,回复标准如下:
- 你想要闲聊或者随便附和
- 有人提到了你,但是你还没有回应
- {mentioned_bonus}
- 如果你刚刚进行了回复,不要对同一个话题重复回应
你之前的动作记录:
{actions_before_now_block} {actions_before_now_block}
**回复标准**
请你选择合适的消息进行回复:
1.你可以选择呼叫了你的名字,但是你没有做出回应的消息进行回复
2.你可以自然的顺着正在进行的聊天内容进行回复,或者自然的提出一个问题
3.你的兴趣是{interest}
4.如果有人对你感到厌烦,请你不要太积极的提问或是表达,可以进行顺从
5.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对
6.最好不要选择图片和表情包作为回复对象
7.{moderation_prompt}
请你从新消息中选出一条需要回复的消息并输出其id,输出格式如下: 请你从新消息中选出一条需要回复的消息并输出其id,输出格式如下:
{{ {{
"action": "reply", "action": "reply",
"target_message_id":"想要回复的消息id消息id格式:m+数字", "target_message_id":"想要回复的消息id消息id格式:m+数字",
"reason":"回复的原因" "reason":"回复的原因"
}} }}
请根据示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: 请根据示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
""", """,
"planner_reply_prompt", "planner_reply_prompt",
@@ -129,12 +129,18 @@ def init_prompt():
""" """
{name_block} {name_block}
{chat_context_description}{time_block}现在请你根据以下聊天内容选择一个或多个action来参与聊天。如果没有合适的action请选择no_action。, {chat_context_description}{time_block},现在请你根据以下聊天内容,选择一个或多个合适的action。如果没有合适的action请选择no_action。,
{chat_content_block} {chat_content_block}
{moderation_prompt} **要求**
现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息: 1.action必须符合使用条件如果符合条件就选择
2.如果聊天内容不适合使用action即使符合条件也不要使用
3.{moderation_prompt}
4.请注意如果相同的内容已经被执行,请不要重复执行
这是你最近执行过的动作:
{actions_before_now_block}
**可用的action**
no_action不选择任何动作 no_action不选择任何动作
{{ {{
@@ -144,9 +150,6 @@ no_action不选择任何动作
{action_options_text} {action_options_text}
这是你最近执行过的动作,请注意如果相同的内容已经被执行,请不要重复执行:
{actions_before_now_block}
请选择并说明触发action的消息id和选择该action的原因。消息id格式:m+数字 请选择并说明触发action的消息id和选择该action的原因。消息id格式:m+数字
请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
""", """,
@@ -465,7 +468,7 @@ class ActionPlanner:
) )
) )
logger.info(f"{self.log_prefix}副规划器返回了{len(action_planner_infos)}个action") logger.debug(f"{self.log_prefix}副规划器返回了{len(action_planner_infos)}个action")
return action_planner_infos return action_planner_infos
async def plan( async def plan(
@@ -510,7 +513,7 @@ class ActionPlanner:
) )
self.last_obs_time_mark = time.time() self.last_obs_time_mark = time.time()
all_sub_planner_results: List[ActionPlannerInfo] = [] # 防止Unbound
try: try:
sub_planner_actions: Dict[str, ActionInfo] = {} sub_planner_actions: Dict[str, ActionInfo] = {}
@@ -553,7 +556,7 @@ class ActionPlanner:
for i, (action_name, action_info) in enumerate(action_items): for i, (action_name, action_info) in enumerate(action_items):
sub_planner_lists[i % sub_planner_num].append((action_name, action_info)) sub_planner_lists[i % sub_planner_num].append((action_name, action_info))
logger.info( logger.debug(
f"{self.log_prefix}成功将{sub_planner_actions_num}个actions分配到{sub_planner_num}个子列表中" f"{self.log_prefix}成功将{sub_planner_actions_num}个actions分配到{sub_planner_num}个子列表中"
) )
for i, action_list in enumerate(sub_planner_lists): for i, action_list in enumerate(sub_planner_lists):
@@ -581,11 +584,10 @@ class ActionPlanner:
sub_plan_results = await asyncio.gather(*sub_plan_tasks) sub_plan_results = await asyncio.gather(*sub_plan_tasks)
# 收集所有结果 # 收集所有结果
all_sub_planner_results: List[ActionPlannerInfo] = []
for sub_result in sub_plan_results: for sub_result in sub_plan_results:
all_sub_planner_results.extend(sub_result) all_sub_planner_results.extend(sub_result)
logger.info(f"{self.log_prefix}所有副规划器共返回了{len(all_sub_planner_results)}action") logger.info(f"{self.log_prefix}小脑决定执行{len(all_sub_planner_results)}动作")
# --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- # --- 构建提示词 (调用修改后的 PromptBuilder 方法) ---
prompt, message_id_list = await self.build_planner_prompt( prompt, message_id_list = await self.build_planner_prompt(
@@ -596,6 +598,7 @@ class ActionPlanner:
chat_content_block=chat_content_block, chat_content_block=chat_content_block,
# actions_before_now_block=actions_before_now_block, # actions_before_now_block=actions_before_now_block,
message_id_list=message_id_list, message_id_list=message_id_list,
interest=global_config.personality.interest,
) )
# --- 调用 LLM (普通文本生成) --- # --- 调用 LLM (普通文本生成) ---
@@ -724,11 +727,9 @@ class ActionPlanner:
] ]
action_str = "" action_str = ""
for action in actions: for action_planner_info in actions:
action_str += f"{action.action_type} " action_str += f"{action_planner_info.action_type} "
logger.info( logger.info(f"{self.log_prefix}大脑小脑决定执行{len(actions)}个动作: {action_str}")
f"{self.log_prefix}大脑小脑决定执行{len(actions)}个动作: {action_str}"
)
else: else:
# 如果为假,只返回副规划器的结果 # 如果为假,只返回副规划器的结果
actions = self._filter_no_actions(all_sub_planner_results) actions = self._filter_no_actions(all_sub_planner_results)
@@ -758,6 +759,7 @@ class ActionPlanner:
mode: ChatMode = ChatMode.FOCUS, mode: ChatMode = ChatMode.FOCUS,
# actions_before_now_block :str = "", # actions_before_now_block :str = "",
chat_content_block: str = "", chat_content_block: str = "",
interest: str = "",
) -> tuple[str, List[Tuple[str, "DatabaseMessages"]]]: # sourcery skip: use-join ) -> tuple[str, List[Tuple[str, "DatabaseMessages"]]]: # sourcery skip: use-join
"""构建 Planner LLM 的提示词 (获取模板并填充数据)""" """构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try: try:
@@ -777,12 +779,6 @@ class ActionPlanner:
else: else:
actions_before_now_block = "" actions_before_now_block = ""
mentioned_bonus = ""
if global_config.chat.mentioned_bot_inevitable_reply:
mentioned_bonus = "\n- 有人提到你"
if global_config.chat.at_bot_inevitable_reply:
mentioned_bonus = "\n- 有人提到你或者at你"
chat_context_description = "你现在正在一个群聊中" chat_context_description = "你现在正在一个群聊中"
chat_target_name = None chat_target_name = None
if not is_group_chat and chat_target_info: if not is_group_chat and chat_target_info:
@@ -838,11 +834,10 @@ class ActionPlanner:
chat_context_description=chat_context_description, chat_context_description=chat_context_description,
chat_content_block=chat_content_block, chat_content_block=chat_content_block,
actions_before_now_block=actions_before_now_block, actions_before_now_block=actions_before_now_block,
mentioned_bonus=mentioned_bonus,
# action_options_text=action_options_block, # action_options_text=action_options_block,
moderation_prompt=moderation_prompt_block, moderation_prompt=moderation_prompt_block,
name_block=name_block, name_block=name_block,
plan_style=global_config.personality.plan_style, interest=interest,
) )
else: else:
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_reply_prompt") planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_reply_prompt")
@@ -850,10 +845,10 @@ class ActionPlanner:
time_block=time_block, time_block=time_block,
chat_context_description=chat_context_description, chat_context_description=chat_context_description,
chat_content_block=chat_content_block, chat_content_block=chat_content_block,
mentioned_bonus=mentioned_bonus,
moderation_prompt=moderation_prompt_block, moderation_prompt=moderation_prompt_block,
name_block=name_block, name_block=name_block,
actions_before_now_block=actions_before_now_block, actions_before_now_block=actions_before_now_block,
interest=interest,
) )
return prompt, message_id_list return prompt, message_id_list
except Exception as e: except Exception as e:

View File

@@ -26,7 +26,6 @@ from src.chat.utils.chat_message_builder import (
) )
from src.chat.express.expression_selector import expression_selector from src.chat.express.expression_selector import expression_selector
from src.chat.memory_system.memory_activator import MemoryActivator from src.chat.memory_system.memory_activator import MemoryActivator
from src.chat.memory_system.instant_memory import InstantMemory
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
from src.person_info.person_info import Person, is_person_known from src.person_info.person_info import Person, is_person_known
from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.base.component_types import ActionInfo, EventType
@@ -52,11 +51,14 @@ def init_prompt():
{chat_info} {chat_info}
{identity} {identity}
你正在{chat_target_2},{reply_target_block}
对这句话,你想表达,原句:{raw_reply},原因是:{reason}。你现在要思考怎么组织回复
你现在的心情是:{mood_state} 你现在的心情是:{mood_state}
你正在{chat_target_2},{reply_target_block}
你想要对上述的发言进行回复,回复的具体内容(原句)是:{raw_reply}
原因是:{reason}
现在请你将这条具体内容改写成一条适合在群聊中发送的回复消息。
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯 你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯
{reply_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。 {reply_style}
你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
{keywords_reaction_prompt} {keywords_reaction_prompt}
{moderation_prompt} {moderation_prompt}
不要输出多余内容(包括前后缀冒号和引号括号表情包emoji,at或 @等 ),只输出一条回复就好。 不要输出多余内容(包括前后缀冒号和引号括号表情包emoji,at或 @等 ),只输出一条回复就好。
@@ -67,44 +69,39 @@ def init_prompt():
# s4u 风格的 prompt 模板 # s4u 风格的 prompt 模板
Prompt( Prompt(
""" """{identity}
{expression_habits_block}{tool_info_block} 你正在群聊中聊天,你想要回复 {sender_name} 的发言。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。
{knowledge_prompt}{memory_block}{relation_info_block}
{extra_info_block}
{identity}
{action_descriptions}
{time_block}
你现在的主要任务是和 {sender_name} 聊天。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。
{time_block}
{background_dialogue_prompt} {background_dialogue_prompt}
{core_dialogue_prompt} {core_dialogue_prompt}
{expression_habits_block}{tool_info_block}
{knowledge_prompt}{memory_block}{relation_info_block}
{extra_info_block}
{reply_target_block} {reply_target_block}
你的心情:{mood_state}
你现在的心情是:{mood_state}
{reply_style} {reply_style}
注意不要复读你说过的话 注意不要复读你说过的话
{keywords_reaction_prompt} {keywords_reaction_prompt}
请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。 请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。
{moderation_prompt} {moderation_prompt}
不要输出多余内容(包括前后缀,冒号和引号,括号()表情包emoji,at或 @等 )。只输出一条回复就好 不要输出多余内容(包括前后缀,冒号和引号,括号()表情包emoji,at或 @等 )。只输出一条回复就好
现在,你说: 现在,你说:""",
""",
"replyer_prompt", "replyer_prompt",
) )
Prompt( Prompt(
""" """{identity}
{expression_habits_block}{tool_info_block}
{knowledge_prompt}{memory_block}{relation_info_block}
{extra_info_block}
{identity}
{action_descriptions}
{time_block} {time_block}
你现在正在一个QQ群里聊天以下是正在进行的聊天内容 你现在正在一个QQ群里聊天以下是正在进行的聊天内容
{background_dialogue_prompt} {background_dialogue_prompt}
{expression_habits_block}{tool_info_block}
{knowledge_prompt}{memory_block}{relation_info_block}
{extra_info_block}
你现在想补充说明你刚刚自己的发言内容:{target},原因是{reason} 你现在想补充说明你刚刚自己的发言内容:{target},原因是{reason}
请你根据聊天内容,组织一条新回复。注意,{target} 是刚刚你自己的发言,你要在这基础上进一步发言,请按照你自己的角度来继续进行回复。 请你根据聊天内容,组织一条新回复。注意,{target} 是刚刚你自己的发言,你要在这基础上进一步发言,请按照你自己的角度来继续进行回复。
注意保持上下文的连贯性。 注意保持上下文的连贯性。
@@ -147,7 +144,6 @@ class DefaultReplyer:
self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id) self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id)
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
self.memory_activator = MemoryActivator() self.memory_activator = MemoryActivator()
self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id)
from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor不然会循环依赖 from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor不然会循环依赖
@@ -375,20 +371,11 @@ class DefaultReplyer:
instant_memory = None instant_memory = None
# running_memories = await self.memory_activator.activate_memory_with_chat_history( running_memories = await self.memory_activator.activate_memory_with_chat_history(
# target_message=target, chat_history=chat_history target_message=target, chat_history=chat_history
# ) )
running_memories = None running_memories = None
if global_config.memory.enable_instant_memory:
chat_history_str = build_readable_messages(
messages=chat_history, replace_bot_name=True, timestamp_mode="normal"
)
asyncio.create_task(self.instant_memory.create_and_store_memory(chat_history_str))
instant_memory = await self.instant_memory.get_memory(target)
logger.info(f"即时记忆:{instant_memory}")
if not running_memories: if not running_memories:
return "" return ""
@@ -649,9 +636,12 @@ class DefaultReplyer:
"""构建动作提示""" """构建动作提示"""
action_descriptions = "" action_descriptions = ""
skip_names = ["emoji","build_memory","build_relation","reply"]
if available_actions: if available_actions:
action_descriptions = "除了进行回复之外,你可以做以下这些动作,不过这些动作由另一个模型决定,:\n" action_descriptions = "除了进行回复之外,你可以做以下这些动作,不过这些动作由另一个模型决定,:\n"
for action_name, action_info in available_actions.items(): for action_name, action_info in available_actions.items():
if action_name in skip_names:
continue
action_description = action_info.description action_description = action_info.description
action_descriptions += f"- {action_name}: {action_description}\n" action_descriptions += f"- {action_name}: {action_description}\n"
action_descriptions += "\n" action_descriptions += "\n"
@@ -660,11 +650,13 @@ class DefaultReplyer:
if chosen_actions_info: if chosen_actions_info:
for action_plan_info in chosen_actions_info: for action_plan_info in chosen_actions_info:
action_name = action_plan_info.action_type action_name = action_plan_info.action_type
if action_name == "reply": if action_name in skip_names:
continue continue
action_description: str = "无描述"
reasoning: str = "无原因"
if action := available_actions.get(action_name): if action := available_actions.get(action_name):
action_description = action.description or "无描述" action_description = action.description or action_description
reasoning = action_plan_info.reasoning or "无原因" reasoning = action_plan_info.reasoning or reasoning
chosen_action_descriptions += f"- {action_name}: {action_description},原因:{reasoning}\n" chosen_action_descriptions += f"- {action_name}: {action_description},原因:{reasoning}\n"
@@ -682,18 +674,18 @@ class DefaultReplyer:
bot_nickname = "" bot_nickname = ""
prompt_personality = ( prompt_personality = (
f"{global_config.personality.personality_core};{global_config.personality.personality_side}" f"{global_config.personality.personality};"
) )
return f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" return f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
async def build_prompt_reply_context( async def build_prompt_reply_context(
self, self,
reply_message: DatabaseMessages,
extra_info: str = "", extra_info: str = "",
reply_reason: str = "", reply_reason: str = "",
available_actions: Optional[Dict[str, ActionInfo]] = None, available_actions: Optional[Dict[str, ActionInfo]] = None,
chosen_actions: Optional[List[ActionPlannerInfo]] = None, chosen_actions: Optional[List[ActionPlannerInfo]] = None,
enable_tool: bool = True, enable_tool: bool = True,
reply_message: Optional[DatabaseMessages] = None,
) -> Tuple[str, List[int]]: ) -> Tuple[str, List[int]]:
""" """
构建回复器上下文 构建回复器上下文
@@ -716,24 +708,25 @@ class DefaultReplyer:
is_group_chat = bool(chat_stream.group_info) is_group_chat = bool(chat_stream.group_info)
platform = chat_stream.platform platform = chat_stream.platform
user_id = "用户ID"
person_name = "用户"
sender = "用户"
target = "消息"
if reply_message: if reply_message:
user_id = reply_message.user_info.user_id user_id = reply_message.user_info.user_id
person = Person(platform=platform, user_id=user_id) person = Person(platform=platform, user_id=user_id)
person_name = person.person_name or user_id person_name = person.person_name or user_id
sender = person_name sender = person_name
target = reply_message.processed_plain_text target = reply_message.processed_plain_text
else:
person_name = "用户"
sender = "用户"
target = "消息"
mood_prompt: str = ""
if global_config.mood.enable_mood: if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(chat_id) chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
mood_prompt = chat_mood.mood_state mood_prompt = chat_mood.mood_state
else:
mood_prompt = ""
target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) target = replace_user_references(target, chat_stream.platform, replace_bot_name=True)
target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target)
message_list_before_now_long = get_raw_msg_before_timestamp_with_chat( message_list_before_now_long = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id, chat_id=chat_id,
@@ -789,14 +782,14 @@ class DefaultReplyer:
for name, result, duration in task_results: for name, result, duration in task_results:
results_dict[name] = result results_dict[name] = result
chinese_name = task_name_mapping.get(name, name) chinese_name = task_name_mapping.get(name, name)
if duration < 0.01: if duration < 0.1:
almost_zero_str += f"{chinese_name}," almost_zero_str += f"{chinese_name},"
continue continue
timing_logs.append(f"{chinese_name}: {duration:.1f}s") timing_logs.append(f"{chinese_name}: {duration:.1f}s")
if duration > 8: if duration > 8:
logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s请使用更快的模型") logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s请使用更快的模型")
logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.01s") logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s")
expression_habits_block, selected_expressions = results_dict["expression_habits"] expression_habits_block, selected_expressions = results_dict["expression_habits"]
expression_habits_block: str expression_habits_block: str
@@ -818,6 +811,11 @@ class DefaultReplyer:
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
if sender: if sender:
if is_group_chat: if is_group_chat:
reply_target_block = ( reply_target_block = (
@@ -888,6 +886,8 @@ class DefaultReplyer:
is_group_chat = bool(chat_stream.group_info) is_group_chat = bool(chat_stream.group_info)
sender, target = self._parse_reply_target(reply_to) sender, target = self._parse_reply_target(reply_to)
target = replace_user_references(target, chat_stream.platform, replace_bot_name=True)
target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target)
# 添加情绪状态获取 # 添加情绪状态获取
if global_config.mood.enable_mood: if global_config.mood.enable_mood:
@@ -950,9 +950,7 @@ class DefaultReplyer:
else: else:
chat_target_name = "对方" chat_target_name = "对方"
if self.chat_target_info: if self.chat_target_info:
chat_target_name = ( chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方"
self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方"
)
chat_target_1 = await global_prompt_manager.format_prompt( chat_target_1 = await global_prompt_manager.format_prompt(
"chat_target_private1", sender_name=chat_target_name "chat_target_private1", sender_name=chat_target_name
) )
@@ -1019,6 +1017,8 @@ class DefaultReplyer:
# 直接使用已初始化的模型实例 # 直接使用已初始化的模型实例
logger.info(f"使用模型集生成回复: {', '.join(map(str, self.express_model.model_for_task.model_list))}") logger.info(f"使用模型集生成回复: {', '.join(map(str, self.express_model.model_for_task.model_list))}")
logger.info(f"\n{prompt}\n")
if global_config.debug.show_prompt: if global_config.debug.show_prompt:
logger.info(f"\n{prompt}\n") logger.info(f"\n{prompt}\n")
else: else:

View File

@@ -203,18 +203,21 @@ def get_actions_by_timestamp_with_chat(
query = query.order_by(ActionRecords.time.asc()) query = query.order_by(ActionRecords.time.asc())
actions = list(query) actions = list(query)
return [DatabaseActionRecords( return [
action_id=action.action_id, DatabaseActionRecords(
time=action.time, action_id=action.action_id,
action_name=action.action_name, time=action.time,
action_data=action.action_data, action_name=action.action_name,
action_done=action.action_done, action_data=action.action_data,
action_build_into_prompt=action.action_build_into_prompt, action_done=action.action_done,
action_prompt_display=action.action_prompt_display, action_build_into_prompt=action.action_build_into_prompt,
chat_id=action.chat_id, action_prompt_display=action.action_prompt_display,
chat_info_stream_id=action.chat_info_stream_id, chat_id=action.chat_id,
chat_info_platform=action.chat_info_platform, chat_info_stream_id=action.chat_info_stream_id,
) for action in actions] chat_info_platform=action.chat_info_platform,
)
for action in actions
]
def get_actions_by_timestamp_with_chat_inclusive( def get_actions_by_timestamp_with_chat_inclusive(
@@ -474,7 +477,7 @@ def _build_readable_messages_internal(
truncated_content = content truncated_content = content
if 0 < limit < original_len: if 0 < limit < original_len:
truncated_content = f"{content[:limit]}{replace_content}" truncated_content = f"{content[:limit]}{replace_content}" # pyright: ignore[reportPossiblyUnboundVariable]
detailed_message.append((timestamp, name, truncated_content, is_action)) detailed_message.append((timestamp, name, truncated_content, is_action))
else: else:
@@ -544,7 +547,7 @@ def build_pic_mapping_info(pic_id_mapping: Dict[str, str]) -> str:
return "\n".join(mapping_lines) return "\n".join(mapping_lines)
def build_readable_actions(actions: List[DatabaseActionRecords],mode:str="relative") -> str: def build_readable_actions(actions: List[DatabaseActionRecords], mode: str = "relative") -> str:
""" """
将动作列表转换为可读的文本格式。 将动作列表转换为可读的文本格式。
格式: 在()分钟前,你使用了(action_name)具体内容是action_prompt_display 格式: 在()分钟前,你使用了(action_name)具体内容是action_prompt_display
@@ -585,6 +588,8 @@ def build_readable_actions(actions: List[DatabaseActionRecords],mode:str="relati
action_time_struct = time.localtime(action_time) action_time_struct = time.localtime(action_time)
time_str = time.strftime("%H:%M:%S", action_time_struct) time_str = time.strftime("%H:%M:%S", action_time_struct)
time_ago_str = f"{time_str}" time_ago_str = f"{time_str}"
else:
raise ValueError(f"Unsupported mode: {mode}")
line = f"{time_ago_str},你使用了“{action_name}”,具体内容是:“{action_prompt_display}" line = f"{time_ago_str},你使用了“{action_name}”,具体内容是:“{action_prompt_display}"
output_lines.append(line) output_lines.append(line)

View File

@@ -728,7 +728,7 @@ class StatisticOutputTask(AsyncTask):
f"<td>{stat_data[STD_TIME_COST_BY_MODEL][model_name]:.1f} 秒</td>" f"<td>{stat_data[STD_TIME_COST_BY_MODEL][model_name]:.1f} 秒</td>"
f"</tr>" f"</tr>"
for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items()) for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items())
] ] if stat_data[REQ_CNT_BY_MODEL] else ["<tr><td colspan='8' style='text-align: center; color: #999;'>暂无数据</td></tr>"]
) )
# 按请求类型分类统计 # 按请求类型分类统计
type_rows = "\n".join( type_rows = "\n".join(
@@ -744,7 +744,7 @@ class StatisticOutputTask(AsyncTask):
f"<td>{stat_data[STD_TIME_COST_BY_TYPE][req_type]:.1f} 秒</td>" f"<td>{stat_data[STD_TIME_COST_BY_TYPE][req_type]:.1f} 秒</td>"
f"</tr>" f"</tr>"
for req_type, count in sorted(stat_data[REQ_CNT_BY_TYPE].items()) for req_type, count in sorted(stat_data[REQ_CNT_BY_TYPE].items())
] ] if stat_data[REQ_CNT_BY_TYPE] else ["<tr><td colspan='8' style='text-align: center; color: #999;'>暂无数据</td></tr>"]
) )
# 按模块分类统计 # 按模块分类统计
module_rows = "\n".join( module_rows = "\n".join(
@@ -760,7 +760,7 @@ class StatisticOutputTask(AsyncTask):
f"<td>{stat_data[STD_TIME_COST_BY_MODULE][module_name]:.1f} 秒</td>" f"<td>{stat_data[STD_TIME_COST_BY_MODULE][module_name]:.1f} 秒</td>"
f"</tr>" f"</tr>"
for module_name, count in sorted(stat_data[REQ_CNT_BY_MODULE].items()) for module_name, count in sorted(stat_data[REQ_CNT_BY_MODULE].items())
] ] if stat_data[REQ_CNT_BY_MODULE] else ["<tr><td colspan='8' style='text-align: center; color: #999;'>暂无数据</td></tr>"]
) )
# 聊天消息统计 # 聊天消息统计
@@ -768,7 +768,7 @@ class StatisticOutputTask(AsyncTask):
[ [
f"<tr><td>{self.name_mapping[chat_id][0]}</td><td>{count}</td></tr>" f"<tr><td>{self.name_mapping[chat_id][0]}</td><td>{count}</td></tr>"
for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items()) for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items())
] ] if stat_data[MSG_CNT_BY_CHAT] else ["<tr><td colspan='2' style='text-align: center; color: #999;'>暂无数据</td></tr>"]
) )
# 生成HTML # 生成HTML
return f""" return f"""
@@ -820,145 +820,192 @@ class StatisticOutputTask(AsyncTask):
</tbody> </tbody>
</table> </table>
<h2>数据分布图表</h2>
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-top: 20px;">
<div style="flex: 1; min-width: 300px;">
<h3>模型花费分布</h3>
<canvas id="modelPieChart_{div_id}" width="300" height="300"></canvas>
</div>
<div style="flex: 1; min-width: 300px;">
<h3>模块花费分布</h3>
<canvas id="modulePieChart_{div_id}" width="300" height="300"></canvas>
</div>
<div style="flex: 1; min-width: 300px;">
<h3>请求类型花费分布</h3>
<canvas id="typePieChart_{div_id}" width="300" height="300"></canvas>
</div>
<div style="flex: 1; min-width: 300px;">
<h3>聊天消息分布</h3>
<canvas id="chatPieChart_{div_id}" width="300" height="300"></canvas>
</div>
</div>
<script>
// 为当前统计卡片创建饼图 // 为当前统计卡片创建饼图
createPieCharts_{div_id}(); document.addEventListener('DOMContentLoaded', function() {{
createPieCharts_{div_id}();
}});
function createPieCharts_{div_id}() {{ function createPieCharts_{div_id}() {{
const colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#34495e', '#e67e22', '#95a5a6', '#f1c40f']; const colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#34495e', '#e67e22', '#95a5a6', '#f1c40f'];
// 模型调用次数饼图 // 模型花费分布饼图
const modelData = {{ const modelLabels = {list(sorted(stat_data[COST_BY_MODEL].keys())) if stat_data[COST_BY_MODEL] else []};
labels: {[f'"{model_name}"' for model_name in sorted(stat_data[REQ_CNT_BY_MODEL].keys())]}, if (modelLabels.length > 0) {{
datasets: [{{ const modelData = {{
data: {[stat_data[REQ_CNT_BY_MODEL][model_name] for model_name in sorted(stat_data[REQ_CNT_BY_MODEL].keys())]}, labels: modelLabels,
backgroundColor: colors[:len(stat_data[REQ_CNT_BY_MODEL])], datasets: [{{
borderColor: colors[:len(stat_data[REQ_CNT_BY_MODEL])], data: {[stat_data[COST_BY_MODEL][model_name] for model_name in sorted(stat_data[COST_BY_MODEL].keys())] if stat_data[COST_BY_MODEL] else []},
borderWidth: 2 backgroundColor: colors.slice(0, {len(stat_data[COST_BY_MODEL]) if stat_data[COST_BY_MODEL] else 0}),
}}] borderColor: colors.slice(0, {len(stat_data[COST_BY_MODEL]) if stat_data[COST_BY_MODEL] else 0}),
}}; borderWidth: 2
}}]
}};
new Chart(document.getElementById('modelPieChart_{div_id}'), {{ new Chart(document.getElementById('modelPieChart_{div_id}'), {{
type: 'pie', type: 'pie',
data: modelData, data: modelData,
options: {{ options: {{
responsive: true, responsive: true,
plugins: {{ plugins: {{
legend: {{ legend: {{
position: 'bottom' position: 'bottom'
}}, }},
tooltip: {{ tooltip: {{
callbacks: {{ callbacks: {{
label: function(context) {{ label: function(context) {{
const total = context.dataset.data.reduce((a, b) => a + b, 0); const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((context.parsed / total) * 100).toFixed(1); const percentage = ((context.parsed / total) * 100).toFixed(1);
return context.label + ': ' + context.parsed + ' (' + percentage + '%)'; return context.label + ': ¥' + context.parsed.toFixed(2) + ' (' + percentage + '%)';
}}
}} }}
}} }}
}} }}
}} }}
}} }});
}}); }} else {{
document.getElementById('modelPieChart_{div_id}').style.display = 'none';
document.querySelector('#modelPieChart_{div_id}').parentElement.querySelector('h3').textContent = '模型花费分布 (无数据)';
}}
// 模块调用次数饼图 // 模块花费分布饼图
const moduleData = {{ const moduleLabels = {list(sorted(stat_data[COST_BY_MODULE].keys())) if stat_data[COST_BY_MODULE] else []};
labels: {[f'"{module_name}"' for module_name in sorted(stat_data[REQ_CNT_BY_MODULE].keys())]}, if (moduleLabels.length > 0) {{
datasets: [{{ const moduleData = {{
data: {[stat_data[REQ_CNT_BY_MODULE][module_name] for module_name in sorted(stat_data[REQ_CNT_BY_MODULE].keys())]}, labels: moduleLabels,
backgroundColor: colors[:len(stat_data[REQ_CNT_BY_MODULE])], datasets: [{{
borderColor: colors[:len(stat_data[REQ_CNT_BY_MODULE])], data: {[stat_data[COST_BY_MODULE][module_name] for module_name in sorted(stat_data[COST_BY_MODULE].keys())] if stat_data[COST_BY_MODULE] else []},
borderWidth: 2 backgroundColor: colors.slice(0, {len(stat_data[COST_BY_MODULE]) if stat_data[COST_BY_MODULE] else 0}),
}}] borderColor: colors.slice(0, {len(stat_data[COST_BY_MODULE]) if stat_data[COST_BY_MODULE] else 0}),
}}; borderWidth: 2
}}]
}};
new Chart(document.getElementById('modulePieChart_{div_id}'), {{ new Chart(document.getElementById('modulePieChart_{div_id}'), {{
type: 'pie', type: 'pie',
data: moduleData, data: moduleData,
options: {{ options: {{
responsive: true, responsive: true,
plugins: {{ plugins: {{
legend: {{ legend: {{
position: 'bottom' position: 'bottom'
}}, }},
tooltip: {{ tooltip: {{
callbacks: {{ callbacks: {{
label: function(context) {{ label: function(context) {{
const total = context.dataset.data.reduce((a, b) => a + b, 0); const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((context.parsed / total) * 100).toFixed(1); const percentage = ((context.parsed / total) * 100).toFixed(1);
return context.label + ': ' + context.parsed + ' (' + percentage + '%)'; return context.label + ': ¥' + context.parsed.toFixed(2) + ' (' + percentage + '%)';
}}
}} }}
}} }}
}} }}
}} }}
}} }});
}}); }} else {{
document.getElementById('modulePieChart_{div_id}').style.display = 'none';
document.querySelector('#modulePieChart_{div_id}').parentElement.querySelector('h3').textContent = '模块花费分布 (无数据)';
}}
// 请求类型分布饼图 // 请求类型花费分布饼图
const typeData = {{ const typeLabels = {list(sorted(stat_data[COST_BY_TYPE].keys())) if stat_data[COST_BY_TYPE] else []};
labels: {[f'"{req_type}"' for req_type in sorted(stat_data[REQ_CNT_BY_TYPE].keys())]}, if (typeLabels.length > 0) {{
datasets: [{{ const typeData = {{
data: {[stat_data[REQ_CNT_BY_TYPE][req_type] for req_type in sorted(stat_data[REQ_CNT_BY_TYPE].keys())]}, labels: typeLabels,
backgroundColor: colors[:len(stat_data[REQ_CNT_BY_TYPE])], datasets: [{{
borderColor: colors[:len(stat_data[REQ_CNT_BY_TYPE])], data: {[stat_data[COST_BY_TYPE][req_type] for req_type in sorted(stat_data[COST_BY_TYPE].keys())] if stat_data[COST_BY_TYPE] else []},
borderWidth: 2 backgroundColor: colors.slice(0, {len(stat_data[COST_BY_TYPE]) if stat_data[COST_BY_TYPE] else 0}),
}}] borderColor: colors.slice(0, {len(stat_data[COST_BY_TYPE]) if stat_data[COST_BY_TYPE] else 0}),
}}; borderWidth: 2
}}]
}};
new Chart(document.getElementById('typePieChart_{div_id}'), {{ new Chart(document.getElementById('typePieChart_{div_id}'), {{
type: 'pie', type: 'pie',
data: typeData, data: typeData,
options: {{ options: {{
responsive: true, responsive: true,
plugins: {{ plugins: {{
legend: {{ legend: {{
position: 'bottom' position: 'bottom'
}}, }},
tooltip: {{ tooltip: {{
callbacks: {{ callbacks: {{
label: function(context) {{ label: function(context) {{
const total = context.dataset.data.reduce((a, b) => a + b, 0); const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((context.parsed / total) * 100).toFixed(1); const percentage = ((context.parsed / total) * 100).toFixed(1);
return context.label + ': ' + context.parsed + ' (' + percentage + '%)'; return context.label + ': ¥' + context.parsed.toFixed(2) + ' (' + percentage + '%)';
}}
}} }}
}} }}
}} }}
}} }}
}} }});
}}); }} else {{
document.getElementById('typePieChart_{div_id}').style.display = 'none';
document.querySelector('#typePieChart_{div_id}').parentElement.querySelector('h3').textContent = '请求类型花费分布 (无数据)';
}}
// 聊天消息分布饼图 // 聊天消息分布饼图
const chatData = {{ const chatLabels = {[self.name_mapping[chat_id][0] for chat_id in sorted(stat_data[MSG_CNT_BY_CHAT].keys())] if stat_data[MSG_CNT_BY_CHAT] else []};
labels: {[f'"{self.name_mapping[chat_id][0]}"' for chat_id in sorted(stat_data[MSG_CNT_BY_CHAT].keys())]}, if (chatLabels.length > 0) {{
datasets: [{{ const chatData = {{
data: {[stat_data[MSG_CNT_BY_CHAT][chat_id] for chat_id in sorted(stat_data[MSG_CNT_BY_CHAT].keys())]}, labels: chatLabels,
backgroundColor: colors[:len(stat_data[MSG_CNT_BY_CHAT])], datasets: [{{
borderColor: colors[:len(stat_data[MSG_CNT_BY_CHAT])], data: {[stat_data[MSG_CNT_BY_CHAT][chat_id] for chat_id in sorted(stat_data[MSG_CNT_BY_CHAT].keys())] if stat_data[MSG_CNT_BY_CHAT] else []},
borderWidth: 2 backgroundColor: colors.slice(0, {len(stat_data[MSG_CNT_BY_CHAT]) if stat_data[MSG_CNT_BY_CHAT] else 0}),
}}] borderColor: colors.slice(0, {len(stat_data[MSG_CNT_BY_CHAT]) if stat_data[MSG_CNT_BY_CHAT] else 0}),
}}; borderWidth: 2
}}]
}};
new Chart(document.getElementById('chatPieChart_{div_id}'), {{ new Chart(document.getElementById('chatPieChart_{div_id}'), {{
type: 'pie', type: 'pie',
data: chatData, data: chatData,
options: {{ options: {{
responsive: true, responsive: true,
plugins: {{ plugins: {{
legend: {{ legend: {{
position: 'bottom' position: 'bottom'
}}, }},
tooltip: {{ tooltip: {{
callbacks: {{ callbacks: {{
label: function(context) {{ label: function(context) {{
const total = context.dataset.data.reduce((a, b) => a + b, 0); const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((context.parsed / total) * 100).toFixed(1); const percentage = ((context.parsed / total) * 100).toFixed(1);
return context.label + ': ' + context.parsed + ' (' + percentage + '%)'; return context.label + ': ' + context.parsed + ' (' + percentage + '%)';
}}
}} }}
}} }}
}} }}
}} }}
}} }});
}}); }} else {{
document.getElementById('chatPieChart_{div_id}').style.display = 'none';
document.querySelector('#chatPieChart_{div_id}').parentElement.querySelector('h3').textContent = '聊天消息分布 (无数据)';
}}
}} }}
</script>
</div> </div>
""" """

View File

@@ -43,15 +43,15 @@ def db_message_to_str(message_dict: dict) -> str:
return result return result
def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, bool, float]:
"""检查消息是否提到了机器人""" """检查消息是否提到了机器人"""
keywords = [global_config.bot.nickname] keywords = [global_config.bot.nickname] + list(global_config.bot.alias_names)
nicknames = global_config.bot.alias_names
reply_probability = 0.0 reply_probability = 0.0
is_at = False is_at = False
is_mentioned = False is_mentioned = False
if message.is_mentioned is not None:
return bool(message.is_mentioned), message.is_mentioned # 这部分怎么处理啊啊啊啊
#我觉得可以给消息加一个 reply_probability_boost字段
if ( if (
message.message_info.additional_config is not None message.message_info.additional_config is not None
and message.message_info.additional_config.get("is_mentioned") is not None and message.message_info.additional_config.get("is_mentioned") is not None
@@ -59,18 +59,15 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
try: try:
reply_probability = float(message.message_info.additional_config.get("is_mentioned")) # type: ignore reply_probability = float(message.message_info.additional_config.get("is_mentioned")) # type: ignore
is_mentioned = True is_mentioned = True
return is_mentioned, reply_probability return is_mentioned, is_at, reply_probability
except Exception as e: except Exception as e:
logger.warning(str(e)) logger.warning(str(e))
logger.warning( logger.warning(
f"消息中包含不合理的设置 is_mentioned: {message.message_info.additional_config.get('is_mentioned')}" f"消息中包含不合理的设置 is_mentioned: {message.message_info.additional_config.get('is_mentioned')}"
) )
if global_config.bot.nickname in message.processed_plain_text: for keyword in keywords:
is_mentioned = True if keyword in message.processed_plain_text:
for alias_name in global_config.bot.alias_names:
if alias_name in message.processed_plain_text:
is_mentioned = True is_mentioned = True
# 判断是否被@ # 判断是否被@
@@ -78,10 +75,6 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
is_at = True is_at = True
is_mentioned = True is_mentioned = True
# print(f"message.processed_plain_text: {message.processed_plain_text}")
# print(f"is_mentioned: {is_mentioned}")
# print(f"is_at: {is_at}")
if is_at and global_config.chat.at_bot_inevitable_reply: if is_at and global_config.chat.at_bot_inevitable_reply:
reply_probability = 1.0 reply_probability = 1.0
logger.debug("被@回复概率设置为100%") logger.debug("被@回复概率设置为100%")
@@ -104,13 +97,10 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
for keyword in keywords: for keyword in keywords:
if keyword in message_content: if keyword in message_content:
is_mentioned = True is_mentioned = True
for nickname in nicknames: if is_mentioned and global_config.chat.mentioned_bot_reply:
if nickname in message_content:
is_mentioned = True
if is_mentioned and global_config.chat.mentioned_bot_inevitable_reply:
reply_probability = 1.0 reply_probability = 1.0
logger.debug("被提及回复概率设置为100%") logger.debug("被提及回复概率设置为100%")
return is_mentioned, reply_probability return is_mentioned, is_at, reply_probability
async def get_embedding(text, request_type="embedding") -> Optional[List[float]]: async def get_embedding(text, request_type="embedding") -> Optional[List[float]]:
@@ -834,3 +824,79 @@ def parse_keywords_string(keywords_input) -> list[str]:
# 如果没有分隔符,返回单个关键词 # 如果没有分隔符,返回单个关键词
return [keywords_str] if keywords_str else [] return [keywords_str] if keywords_str else []
def cut_key_words(concept_name: str) -> list[str]:
"""对概念名称进行jieba分词并过滤掉关键词列表中的关键词"""
concept_name_tokens = list(jieba.cut(concept_name))
# 定义常见连词、停用词与标点
conjunctions = {
"", "", "", "", "以及", "并且", "而且", "", "或者", ""
}
stop_words = {
"", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "而且", "或者", "", "以及"
}
chinese_punctuations = set(",。!?、;:()【】《》“”‘’—…·-——,.!?;:()[]<>'\"/\\")
# 清理空白并初步过滤纯标点
cleaned_tokens = []
for tok in concept_name_tokens:
t = tok.strip()
if not t:
continue
# 去除纯标点
if all(ch in chinese_punctuations for ch in t):
continue
cleaned_tokens.append(t)
# 合并连词两侧的词(仅当两侧都存在且不是标点/停用词时)
merged_tokens = []
i = 0
n = len(cleaned_tokens)
while i < n:
tok = cleaned_tokens[i]
if tok in conjunctions and merged_tokens and i + 1 < n:
left = merged_tokens[-1]
right = cleaned_tokens[i + 1]
# 左右都需要是有效词
if left and right \
and left not in conjunctions and right not in conjunctions \
and left not in stop_words and right not in stop_words \
and not all(ch in chinese_punctuations for ch in left) \
and not all(ch in chinese_punctuations for ch in right):
# 合并为一个新词,并替换掉左侧与跳过右侧
combined = f"{left}{tok}{right}"
merged_tokens[-1] = combined
i += 2
continue
# 常规推进
merged_tokens.append(tok)
i += 1
# 二次过滤:去除停用词、单字符纯标点与无意义项
result_tokens = []
seen = set()
# ban_words = set(getattr(global_config.memory, "memory_ban_words", []) or [])
for tok in merged_tokens:
if tok in conjunctions:
# 独立连词丢弃
continue
if tok in stop_words:
continue
# if tok in ban_words:
# continue
if all(ch in chinese_punctuations for ch in tok):
continue
if tok.strip() == "":
continue
if tok not in seen:
seen.add(tok)
result_tokens.append(tok)
filtered_concept_name_tokens = result_tokens
return filtered_concept_name_tokens

View File

@@ -4,7 +4,6 @@ import time
import hashlib import hashlib
import uuid import uuid
import io import io
import asyncio
import numpy as np import numpy as np
from typing import Optional, Tuple from typing import Optional, Tuple
@@ -177,7 +176,7 @@ class ImageManager:
emotion_prompt, temperature=0.3, max_tokens=50 emotion_prompt, temperature=0.3, max_tokens=50
) )
if emotion_result is None: if not emotion_result:
logger.warning("LLM未能生成情感标签使用详细描述的前几个词") logger.warning("LLM未能生成情感标签使用详细描述的前几个词")
# 降级处理:从详细描述中提取关键词 # 降级处理:从详细描述中提取关键词
import jieba import jieba

View File

@@ -67,6 +67,8 @@ class DatabaseMessages(BaseDataModel):
key_words: Optional[str] = None, key_words: Optional[str] = None,
key_words_lite: Optional[str] = None, key_words_lite: Optional[str] = None,
is_mentioned: Optional[bool] = None, is_mentioned: Optional[bool] = None,
is_at: Optional[bool] = None,
reply_probability_boost: Optional[float] = None,
processed_plain_text: Optional[str] = None, processed_plain_text: Optional[str] = None,
display_message: Optional[str] = None, display_message: Optional[str] = None,
priority_mode: Optional[str] = None, priority_mode: Optional[str] = None,
@@ -104,6 +106,9 @@ class DatabaseMessages(BaseDataModel):
self.key_words_lite = key_words_lite self.key_words_lite = key_words_lite
self.is_mentioned = is_mentioned self.is_mentioned = is_mentioned
self.is_at = is_at
self.reply_probability_boost = reply_probability_boost
self.processed_plain_text = processed_plain_text self.processed_plain_text = processed_plain_text
self.display_message = display_message self.display_message = display_message
@@ -171,6 +176,8 @@ class DatabaseMessages(BaseDataModel):
"key_words": self.key_words, "key_words": self.key_words,
"key_words_lite": self.key_words_lite, "key_words_lite": self.key_words_lite,
"is_mentioned": self.is_mentioned, "is_mentioned": self.is_mentioned,
"is_at": self.is_at,
"reply_probability_boost": self.reply_probability_boost,
"processed_plain_text": self.processed_plain_text, "processed_plain_text": self.processed_plain_text,
"display_message": self.display_message, "display_message": self.display_message,
"priority_mode": self.priority_mode, "priority_mode": self.priority_mode,

View File

@@ -137,7 +137,8 @@ class Messages(BaseModel):
key_words_lite = TextField(null=True) key_words_lite = TextField(null=True)
is_mentioned = BooleanField(null=True) is_mentioned = BooleanField(null=True)
is_at = BooleanField(null=True)
reply_probability_boost = DoubleField(null=True)
# 从 chat_info 扁平化而来的字段 # 从 chat_info 扁平化而来的字段
chat_info_stream_id = TextField() chat_info_stream_id = TextField()
chat_info_platform = TextField() chat_info_platform = TextField()
@@ -298,19 +299,6 @@ class GroupInfo(BaseModel):
# database = db # 继承自 BaseModel # database = db # 继承自 BaseModel
table_name = "group_info" table_name = "group_info"
class Memory(BaseModel):
memory_id = TextField(index=True)
chat_id = TextField(null=True)
memory_text = TextField(null=True)
keywords = TextField(null=True)
create_time = FloatField(null=True)
last_view_time = FloatField(null=True)
class Meta:
table_name = "memory"
class Expression(BaseModel): class Expression(BaseModel):
""" """
用于存储表达风格的模型。 用于存储表达风格的模型。
@@ -377,7 +365,6 @@ def create_tables():
Expression, Expression,
GraphNodes, # 添加图节点表 GraphNodes, # 添加图节点表
GraphEdges, # 添加图边表 GraphEdges, # 添加图边表
Memory,
ActionRecords, # 添加 ActionRecords 到初始化列表 ActionRecords, # 添加 ActionRecords 到初始化列表
] ]
) )
@@ -403,7 +390,6 @@ def initialize_database(sync_constraints=False):
OnlineTime, OnlineTime,
PersonInfo, PersonInfo,
Expression, Expression,
Memory,
GraphNodes, GraphNodes,
GraphEdges, GraphEdges,
ActionRecords, # 添加 ActionRecords 到初始化列表 ActionRecords, # 添加 ActionRecords 到初始化列表
@@ -501,7 +487,6 @@ def sync_field_constraints():
OnlineTime, OnlineTime,
PersonInfo, PersonInfo,
Expression, Expression,
Memory,
GraphNodes, GraphNodes,
GraphEdges, GraphEdges,
ActionRecords, ActionRecords,
@@ -680,7 +665,6 @@ def check_field_constraints():
OnlineTime, OnlineTime,
PersonInfo, PersonInfo,
Expression, Expression,
Memory,
GraphNodes, GraphNodes,
GraphEdges, GraphEdges,
ActionRecords, ActionRecords,

View File

@@ -355,6 +355,7 @@ MODULE_COLORS = {
# 核心模块 # 核心模块
"main": "\033[1;97m", # 亮白色+粗体 (主程序) "main": "\033[1;97m", # 亮白色+粗体 (主程序)
"memory": "\033[38;5;34m", # 天蓝色
"config": "\033[93m", # 亮黄色 "config": "\033[93m", # 亮黄色
"common": "\033[95m", # 亮紫色 "common": "\033[95m", # 亮紫色
@@ -366,10 +367,9 @@ MODULE_COLORS = {
"llm_models": "\033[36m", # 青色 "llm_models": "\033[36m", # 青色
"remote": "\033[38;5;242m", # 深灰色,更不显眼 "remote": "\033[38;5;242m", # 深灰色,更不显眼
"planner": "\033[36m", "planner": "\033[36m",
"memory": "\033[38;5;117m", # 天蓝色
"hfc": "\033[38;5;81m", # 稍微暗一些的青色,保持可读
"action_manager": "\033[38;5;208m", # 橙色不与replyer重复
# 关系系统
"relation": "\033[38;5;139m", # 柔和的紫色,不刺眼 "relation": "\033[38;5;139m", # 柔和的紫色,不刺眼
# 聊天相关模块 # 聊天相关模块
"normal_chat": "\033[38;5;81m", # 亮蓝绿色 "normal_chat": "\033[38;5;81m", # 亮蓝绿色

View File

@@ -56,7 +56,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
# 对该字段的更新请严格参照语义化版本规范https://semver.org/lang/zh-CN/ # 对该字段的更新请严格参照语义化版本规范https://semver.org/lang/zh-CN/
MMC_VERSION = "0.10.1" MMC_VERSION = "0.10.2-snapshot.3"
def get_key_comment(toml_table, key): def get_key_comment(toml_table, key):

View File

@@ -35,21 +35,15 @@ class BotConfig(ConfigBase):
class PersonalityConfig(ConfigBase): class PersonalityConfig(ConfigBase):
"""人格配置类""" """人格配置类"""
personality_core: str personality: str
"""核心人格""" """人格"""
personality_side: str emotion_style: str
"""人格侧写""" """情感特征"""
identity: str = ""
"""身份特征"""
reply_style: str = "" reply_style: str = ""
"""表达风格""" """表达风格"""
plan_style: str = ""
"""行为风格"""
interest: str = "" interest: str = ""
"""兴趣""" """兴趣"""
@@ -60,9 +54,6 @@ class RelationshipConfig(ConfigBase):
enable_relationship: bool = True enable_relationship: bool = True
"""是否启用关系系统""" """是否启用关系系统"""
relation_frequency: int = 1
"""关系频率,麦麦构建关系的速度"""
@dataclass @dataclass
class ChatConfig(ConfigBase): class ChatConfig(ConfigBase):
@@ -74,14 +65,14 @@ class ChatConfig(ConfigBase):
interest_rate_mode: Literal["fast", "accurate"] = "fast" interest_rate_mode: Literal["fast", "accurate"] = "fast"
"""兴趣值计算模式fast为快速计算accurate为精确计算""" """兴趣值计算模式fast为快速计算accurate为精确计算"""
mentioned_bot_inevitable_reply: bool = False mentioned_bot_reply: float = 1
"""提及 bot 必然回复""" """提及 bot 必然回复1为100%回复0为不额外增幅"""
planner_size: float = 1.5 planner_size: float = 1.5
"""副规划器大小越小麦麦的动作执行能力越精细但是消耗更多token调大可以缓解429类错误""" """副规划器大小越小麦麦的动作执行能力越精细但是消耗更多token调大可以缓解429类错误"""
at_bot_inevitable_reply: bool = False at_bot_inevitable_reply: float = 1
"""@bot 必然回复""" """@bot 必然回复1为100%回复0为不额外增幅"""
talk_frequency: float = 0.5 talk_frequency: float = 0.5
"""回复频率阈值""" """回复频率阈值"""
@@ -337,13 +328,7 @@ class MemoryConfig(ConfigBase):
enable_memory: bool = True enable_memory: bool = True
"""是否启用记忆系统""" """是否启用记忆系统"""
memory_build_frequency: int = 1 forget_memory_interval: int = 1500
"""记忆构建频率(秒)"""
memory_compress_rate: float = 0.1
"""记忆压缩率"""
forget_memory_interval: int = 1000
"""记忆遗忘间隔(秒)""" """记忆遗忘间隔(秒)"""
memory_forget_time: int = 24 memory_forget_time: int = 24
@@ -355,9 +340,6 @@ class MemoryConfig(ConfigBase):
memory_ban_words: list[str] = field(default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"]) memory_ban_words: list[str] = field(default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"])
"""不允许记忆的词列表""" """不允许记忆的词列表"""
enable_instant_memory: bool = True
"""是否启用即时记忆"""
@dataclass @dataclass
class MoodConfig(ConfigBase): class MoodConfig(ConfigBase):

View File

@@ -96,3 +96,14 @@ class PermissionDeniedException(Exception):
def __str__(self): def __str__(self):
return self.message return self.message
class EmptyResponseException(Exception):
"""响应内容为空"""
def __init__(self, message: str = "响应内容为空,这可能是一个临时性问题"):
super().__init__(message)
self.message = message
def __str__(self):
return self.message

View File

@@ -37,6 +37,7 @@ from ..exceptions import (
NetworkConnectionError, NetworkConnectionError,
RespNotOkException, RespNotOkException,
ReqAbortException, ReqAbortException,
EmptyResponseException,
) )
from ..payload_content.message import Message, RoleType from ..payload_content.message import Message, RoleType
from ..payload_content.resp_format import RespFormat, RespFormatType from ..payload_content.resp_format import RespFormat, RespFormatType
@@ -85,6 +86,8 @@ def _convert_messages(
role = "model" role = "model"
elif message.role == RoleType.User: elif message.role == RoleType.User:
role = "user" role = "user"
else:
raise ValueError(f"Unsupported role: {message.role}")
# 添加Content # 添加Content
if isinstance(message.content, str): if isinstance(message.content, str):
@@ -224,6 +227,9 @@ def _build_stream_api_resp(
resp.tool_calls.append(ToolCall(call_id, function_name, arguments)) resp.tool_calls.append(ToolCall(call_id, function_name, arguments))
if not resp.content and not resp.tool_calls:
raise EmptyResponseException()
return resp return resp
@@ -284,26 +290,27 @@ def _default_normal_response_parser(
""" """
api_response = APIResponse() api_response = APIResponse()
if not hasattr(resp, "candidates") or not resp.candidates: # 解析思考内容
raise RespParseException(resp, "响应解析失败缺失candidates字段")
try: try:
if resp.candidates[0].content and resp.candidates[0].content.parts: if candidates := resp.candidates:
for part in resp.candidates[0].content.parts: if candidates[0].content and candidates[0].content.parts:
if not part.text: for part in candidates[0].content.parts:
continue if not part.text:
if part.thought: continue
api_response.reasoning_content = ( if part.thought:
api_response.reasoning_content + part.text if api_response.reasoning_content else part.text api_response.reasoning_content = (
) api_response.reasoning_content + part.text if api_response.reasoning_content else part.text
)
except Exception as e: except Exception as e:
logger.warning(f"解析思考内容时发生错误: {e},跳过解析") logger.warning(f"解析思考内容时发生错误: {e},跳过解析")
if resp.text: # 解析响应内容
api_response.content = resp.text api_response.content = resp.text
if resp.function_calls: # 解析工具调用
if function_calls := resp.function_calls:
api_response.tool_calls = [] api_response.tool_calls = []
for call in resp.function_calls: for call in function_calls:
try: try:
if not isinstance(call.args, dict): if not isinstance(call.args, dict):
raise RespParseException(resp, "响应解析失败,工具调用参数无法解析为字典类型") raise RespParseException(resp, "响应解析失败,工具调用参数无法解析为字典类型")
@@ -313,17 +320,22 @@ def _default_normal_response_parser(
except Exception as e: except Exception as e:
raise RespParseException(resp, "响应解析失败,无法解析工具调用参数") from e raise RespParseException(resp, "响应解析失败,无法解析工具调用参数") from e
if resp.usage_metadata: # 解析使用情况
if usage_metadata := resp.usage_metadata:
_usage_record = ( _usage_record = (
resp.usage_metadata.prompt_token_count or 0, usage_metadata.prompt_token_count or 0,
(resp.usage_metadata.candidates_token_count or 0) + (resp.usage_metadata.thoughts_token_count or 0), (usage_metadata.candidates_token_count or 0) + (usage_metadata.thoughts_token_count or 0),
resp.usage_metadata.total_token_count or 0, usage_metadata.total_token_count or 0,
) )
else: else:
_usage_record = None _usage_record = None
api_response.raw_data = resp api_response.raw_data = resp
# 最终的、唯一的空响应检查
if not api_response.content and not api_response.tool_calls:
raise EmptyResponseException("响应中既无文本内容也无工具调用")
return api_response, _usage_record return api_response, _usage_record

View File

@@ -30,6 +30,7 @@ from ..exceptions import (
NetworkConnectionError, NetworkConnectionError,
RespNotOkException, RespNotOkException,
ReqAbortException, ReqAbortException,
EmptyResponseException,
) )
from ..payload_content.message import Message, RoleType from ..payload_content.message import Message, RoleType
from ..payload_content.resp_format import RespFormat from ..payload_content.resp_format import RespFormat
@@ -235,6 +236,9 @@ def _build_stream_api_resp(
resp.tool_calls.append(ToolCall(call_id, function_name, arguments)) resp.tool_calls.append(ToolCall(call_id, function_name, arguments))
if not resp.content and not resp.tool_calls:
raise EmptyResponseException()
return resp return resp
@@ -332,7 +336,7 @@ def _default_normal_response_parser(
api_response = APIResponse() api_response = APIResponse()
if not hasattr(resp, "choices") or len(resp.choices) == 0: if not hasattr(resp, "choices") or len(resp.choices) == 0:
raise RespParseException(resp, "响应解析失败缺失choices字段") raise EmptyResponseException("响应解析失败缺失choices字段或choices列表为空")
message_part = resp.choices[0].message message_part = resp.choices[0].message
if hasattr(message_part, "reasoning_content") and message_part.reasoning_content: # type: ignore if hasattr(message_part, "reasoning_content") and message_part.reasoning_content: # type: ignore
@@ -377,6 +381,9 @@ def _default_normal_response_parser(
# 将原始响应存储在原始数据中 # 将原始响应存储在原始数据中
api_response.raw_data = resp api_response.raw_data = resp
if not api_response.content and not api_response.tool_calls:
raise EmptyResponseException()
return api_response, _usage_record return api_response, _usage_record

View File

@@ -14,7 +14,13 @@ from .payload_content.resp_format import RespFormat
from .payload_content.tool_option import ToolOption, ToolCall, ToolOptionBuilder, ToolParamType from .payload_content.tool_option import ToolOption, ToolCall, ToolOptionBuilder, ToolParamType
from .model_client.base_client import BaseClient, APIResponse, client_registry from .model_client.base_client import BaseClient, APIResponse, client_registry
from .utils import compress_messages, llm_usage_recorder from .utils import compress_messages, llm_usage_recorder
from .exceptions import NetworkConnectionError, ReqAbortException, RespNotOkException, RespParseException from .exceptions import (
NetworkConnectionError,
ReqAbortException,
RespNotOkException,
RespParseException,
EmptyResponseException,
)
install(extra_lines=3) install(extra_lines=3)
@@ -174,7 +180,6 @@ class LLMRequest:
tool_options=tool_built, tool_options=tool_built,
) )
content = response.content content = response.content
reasoning_content = response.reasoning_content or "" reasoning_content = response.reasoning_content or ""
tool_calls = response.tool_calls tool_calls = response.tool_calls
@@ -193,13 +198,7 @@ class LLMRequest:
time_cost=time.time() - start_time, time_cost=time.time() - start_time,
) )
if not content: return content or "", (reasoning_content, model_info.name, tool_calls)
if raise_when_empty:
logger.warning(f"生成的响应为空, 请求类型: {self.request_type}")
raise RuntimeError("生成的响应为空")
content = "生成的响应为空,请检查模型配置或输入内容是否正确"
return content, (reasoning_content, model_info.name, tool_calls)
async def get_embedding(self, embedding_input: str) -> Tuple[List[float], str]: async def get_embedding(self, embedding_input: str) -> Tuple[List[float], str]:
"""获取嵌入向量 """获取嵌入向量
@@ -250,7 +249,7 @@ class LLMRequest:
api_provider = model_config.get_provider(model_info.api_provider) api_provider = model_config.get_provider(model_info.api_provider)
# 对于嵌入任务,强制创建新的客户端实例以避免事件循环问题 # 对于嵌入任务,强制创建新的客户端实例以避免事件循环问题
force_new_client = (self.request_type == "embedding") force_new_client = self.request_type == "embedding"
client = client_registry.get_client_class_instance(api_provider, force_new=force_new_client) client = client_registry.get_client_class_instance(api_provider, force_new=force_new_client)
logger.debug(f"选择请求模型: {model_info.name}") logger.debug(f"选择请求模型: {model_info.name}")
@@ -367,6 +366,13 @@ class LLMRequest:
can_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 连接异常,将于{retry_interval}秒后重试", can_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 连接异常,将于{retry_interval}秒后重试",
cannot_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 连接异常超过最大重试次数请检查网络连接状态或URL是否正确", cannot_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 连接异常超过最大重试次数请检查网络连接状态或URL是否正确",
) )
elif isinstance(e, EmptyResponseException): # 空响应错误
return self._check_retry(
remain_try,
retry_interval,
can_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 收到空响应,将于{retry_interval}秒后重试。原因: {e}",
cannot_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 收到空响应,超过最大重试次数,放弃请求",
)
elif isinstance(e, ReqAbortException): elif isinstance(e, ReqAbortException):
logger.warning(f"任务-'{task_name}' 模型-'{model_name}': 请求被中断,详细信息-{str(e.message)}") logger.warning(f"任务-'{task_name}' 模型-'{model_name}': 请求被中断,详细信息-{str(e.message)}")
return -1, None # 不再重试请求该模型 return -1, None # 不再重试请求该模型

View File

@@ -37,10 +37,9 @@ logger = get_logger("main")
class MainSystem: class MainSystem:
def __init__(self): def __init__(self):
# 根据配置条件性地初始化记忆系统 # 根据配置条件性地初始化记忆系统
self.hippocampus_manager = None
if global_config.memory.enable_memory: if global_config.memory.enable_memory:
self.hippocampus_manager = hippocampus_manager self.hippocampus_manager = hippocampus_manager
else:
self.hippocampus_manager = None
# 使用消息API替代直接的FastAPI实例 # 使用消息API替代直接的FastAPI实例
self.app: MessageServer = get_global_api() self.app: MessageServer = get_global_api()
@@ -117,13 +116,11 @@ class MainSystem:
await check_and_run_migrations() await check_and_run_migrations()
# 触发 ON_START 事件 # 触发 ON_START 事件
from src.plugin_system.core.events_manager import events_manager from src.plugin_system.core.events_manager import events_manager
from src.plugin_system.base.component_types import EventType from src.plugin_system.base.component_types import EventType
await events_manager.handle_mai_events(
event_type=EventType.ON_START await events_manager.handle_mai_events(event_type=EventType.ON_START)
)
# logger.info("已触发 ON_START 事件") # logger.info("已触发 ON_START 事件")
try: try:
init_time = int(1000 * (time.time() - init_start_time)) init_time = int(1000 * (time.time() - init_start_time))
@@ -162,8 +159,6 @@ class MainSystem:
logger.info("[记忆遗忘] 记忆遗忘完成") logger.info("[记忆遗忘] 记忆遗忘完成")
async def main(): async def main():
"""主函数""" """主函数"""
system = MainSystem() system = MainSystem()
@@ -175,5 +170,3 @@ async def main():
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@@ -43,9 +43,9 @@ DEFAULT_BODY_CODE = {
} }
def get_head_code() -> dict: async def get_head_code() -> dict:
"""获取头部动作代码字典""" """获取头部动作代码字典"""
head_code_str = global_prompt_manager.get_prompt("head_code_prompt") head_code_str = await global_prompt_manager.format_prompt("head_code_prompt")
if not head_code_str: if not head_code_str:
return DEFAULT_HEAD_CODE return DEFAULT_HEAD_CODE
try: try:
@@ -55,9 +55,9 @@ def get_head_code() -> dict:
return DEFAULT_HEAD_CODE return DEFAULT_HEAD_CODE
def get_body_code() -> dict: async def get_body_code() -> dict:
"""获取身体动作代码字典""" """获取身体动作代码字典"""
body_code_str = global_prompt_manager.get_prompt("body_code_prompt") body_code_str = await global_prompt_manager.format_prompt("body_code_prompt")
if not body_code_str: if not body_code_str:
return DEFAULT_BODY_CODE return DEFAULT_BODY_CODE
try: try:
@@ -143,7 +143,7 @@ class ChatAction:
async def send_action_update(self): async def send_action_update(self):
"""发送动作更新到前端""" """发送动作更新到前端"""
body_code = get_body_code().get(self.body_action, "") body_code = (await get_body_code()).get(self.body_action, "")
await send_api.custom_to_stream( await send_api.custom_to_stream(
message_type="body_action", message_type="body_action",
content=body_code, content=body_code,
@@ -178,13 +178,13 @@ class ChatAction:
else: else:
bot_nickname = "" bot_nickname = ""
prompt_personality = global_config.personality.personality_core prompt_personality = global_config.personality.personality
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
try: try:
# 冷却池处理:过滤掉冷却中的动作 # 冷却池处理:过滤掉冷却中的动作
self._update_body_action_cooldown() self._update_body_action_cooldown()
available_actions = [k for k in get_body_code().keys() if k not in self.body_action_cooldown] available_actions = [k for k in (await get_body_code()).keys() if k not in self.body_action_cooldown]
all_actions = "\n".join(available_actions) all_actions = "\n".join(available_actions)
prompt = await global_prompt_manager.format_prompt( prompt = await global_prompt_manager.format_prompt(
@@ -241,12 +241,12 @@ class ChatAction:
else: else:
bot_nickname = "" bot_nickname = ""
prompt_personality = global_config.personality.personality_core prompt_personality = global_config.personality.personality
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
try: try:
# 冷却池处理:过滤掉冷却中的动作 # 冷却池处理:过滤掉冷却中的动作
self._update_body_action_cooldown() self._update_body_action_cooldown()
available_actions = [k for k in get_body_code().keys() if k not in self.body_action_cooldown] available_actions = [k for k in (await get_body_code()).keys() if k not in self.body_action_cooldown]
all_actions = "\n".join(available_actions) all_actions = "\n".join(available_actions)
prompt = await global_prompt_manager.format_prompt( prompt = await global_prompt_manager.format_prompt(

View File

@@ -182,7 +182,7 @@ class ChatMood:
else: else:
bot_nickname = "" bot_nickname = ""
prompt_personality = global_config.personality.personality_core prompt_personality = global_config.personality.personality
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
async def _update_text_mood(): async def _update_text_mood():
@@ -261,7 +261,7 @@ class ChatMood:
else: else:
bot_nickname = "" bot_nickname = ""
prompt_personality = global_config.personality.personality_core prompt_personality = global_config.personality.personality
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
async def _regress_text_mood(): async def _regress_text_mood():

View File

@@ -25,7 +25,8 @@ def init_prompt():
你刚刚的情绪状态是:{mood_state} 你刚刚的情绪状态是:{mood_state}
现在,发送了消息,引起了你的注意,你对其进行了阅读和思考,请你输出一句话描述你新的情绪状态 现在,发送了消息,引起了你的注意,你对其进行了阅读和思考,请你输出一句话描述你新的情绪状态
请只输出情绪状态,不要输出其他内容: 你的情绪特点是:{emotion_style}
请只输出新的情绪状态,不要输出其他内容:
""", """,
"change_mood_prompt", "change_mood_prompt",
) )
@@ -38,7 +39,8 @@ def init_prompt():
你之前的情绪状态是:{mood_state} 你之前的情绪状态是:{mood_state}
距离你上次关注群里消息已经过去了一段时间,你冷静了下来,请你输出一句话描述你现在的情绪状态 距离你上次关注群里消息已经过去了一段时间,你冷静了下来,请你输出一句话描述你现在的情绪状态
请只输出情绪状态,不要输出其他内容: 你的情绪特点是:{emotion_style}
请只输出新的情绪状态,不要输出其他内容:
""", """,
"regress_mood_prompt", "regress_mood_prompt",
) )
@@ -115,14 +117,14 @@ class ChatMood:
else: else:
bot_nickname = "" bot_nickname = ""
prompt_personality = global_config.personality.personality_core identity_block = f"你的名字是{bot_name}{bot_nickname}"
identity_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
prompt = await global_prompt_manager.format_prompt( prompt = await global_prompt_manager.format_prompt(
"change_mood_prompt", "change_mood_prompt",
chat_talking_prompt=chat_talking_prompt, chat_talking_prompt=chat_talking_prompt,
identity_block=identity_block, identity_block=identity_block,
mood_state=self.mood_state, mood_state=self.mood_state,
emotion_style=global_config.personality.emotion_style,
) )
response, (reasoning_content, _, _) = await self.mood_model.generate_response_async( response, (reasoning_content, _, _) = await self.mood_model.generate_response_async(
@@ -164,14 +166,14 @@ class ChatMood:
else: else:
bot_nickname = "" bot_nickname = ""
prompt_personality = global_config.personality.personality_core identity_block = f"你的名字是{bot_name}{bot_nickname}"
identity_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
prompt = await global_prompt_manager.format_prompt( prompt = await global_prompt_manager.format_prompt(
"regress_mood_prompt", "regress_mood_prompt",
chat_talking_prompt=chat_talking_prompt, chat_talking_prompt=chat_talking_prompt,
identity_block=identity_block, identity_block=identity_block,
mood_state=self.mood_state, mood_state=self.mood_state,
emotion_style=global_config.personality.emotion_style,
) )
response, (reasoning_content, _, _) = await self.mood_model.generate_response_async( response, (reasoning_content, _, _) = await self.mood_model.generate_response_async(

View File

@@ -241,7 +241,7 @@ class Person:
self.name_reason: Optional[str] = None self.name_reason: Optional[str] = None
self.know_times = 0 self.know_times = 0
self.know_since = None self.know_since = None
self.last_know = None self.last_know: Optional[float] = None
self.memory_points = [] self.memory_points = []
# 初始化性格特征相关字段 # 初始化性格特征相关字段

View File

@@ -25,6 +25,7 @@ from .base import (
EventType, EventType,
MaiMessages, MaiMessages,
ToolParamType, ToolParamType,
CustomEventHandlerResult,
) )
# 导入工具模块 # 导入工具模块
@@ -52,6 +53,15 @@ from .apis import (
get_logger, get_logger,
) )
from src.common.data_models.database_data_model import (
DatabaseMessages,
DatabaseUserInfo,
DatabaseGroupInfo,
DatabaseChatInfo,
)
from src.common.data_models.info_data_model import TargetPersonInfo, ActionPlannerInfo
from src.common.data_models.llm_data_model import LLMGenerationDataModel
__version__ = "2.0.0" __version__ = "2.0.0"
@@ -92,6 +102,7 @@ __all__ = [
"ToolParamType", "ToolParamType",
# 消息 # 消息
"MaiMessages", "MaiMessages",
"CustomEventHandlerResult",
# 装饰器 # 装饰器
"register_plugin", "register_plugin",
"ConfigField", "ConfigField",
@@ -101,4 +112,12 @@ __all__ = [
# "ManifestGenerator", # "ManifestGenerator",
# "validate_plugin_manifest", # "validate_plugin_manifest",
# "generate_plugin_manifest", # "generate_plugin_manifest",
# 数据模型
"DatabaseMessages",
"DatabaseUserInfo",
"DatabaseGroupInfo",
"DatabaseChatInfo",
"TargetPersonInfo",
"ActionPlannerInfo",
"LLMGenerationDataModel"
] ]

View File

@@ -1,27 +1,26 @@
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.frequency_control.focus_value_control import focus_value_control from src.chat.frequency_control.frequency_control import frequency_control_manager
from src.chat.frequency_control.talk_frequency_control import talk_frequency_control
logger = get_logger("frequency_api") logger = get_logger("frequency_api")
def get_current_focus_value(chat_id: str) -> float: def get_current_focus_value(chat_id: str) -> float:
return focus_value_control.get_focus_value_control(chat_id).get_current_focus_value() return frequency_control_manager.get_or_create_frequency_control(chat_id).get_final_focus_value()
def get_current_talk_frequency(chat_id: str) -> float: def get_current_talk_frequency(chat_id: str) -> float:
return talk_frequency_control.get_talk_frequency_control(chat_id).get_current_talk_frequency() return frequency_control_manager.get_or_create_frequency_control(chat_id).get_final_talk_frequency()
def set_focus_value_adjust(chat_id: str, focus_value_adjust: float) -> None: def set_focus_value_adjust(chat_id: str, focus_value_adjust: float) -> None:
focus_value_control.get_focus_value_control(chat_id).focus_value_adjust = focus_value_adjust frequency_control_manager.get_or_create_frequency_control(chat_id).focus_value_external_adjust = focus_value_adjust
def set_talk_frequency_adjust(chat_id: str, talk_frequency_adjust: float) -> None: def set_talk_frequency_adjust(chat_id: str, talk_frequency_adjust: float) -> None:
talk_frequency_control.get_talk_frequency_control(chat_id).talk_frequency_adjust = talk_frequency_adjust frequency_control_manager.get_or_create_frequency_control(chat_id).talk_frequency_external_adjust = talk_frequency_adjust
def get_focus_value_adjust(chat_id: str) -> float: def get_focus_value_adjust(chat_id: str) -> float:
return focus_value_control.get_focus_value_control(chat_id).focus_value_adjust return frequency_control_manager.get_or_create_frequency_control(chat_id).focus_value_external_adjust
def get_talk_frequency_adjust(chat_id: str) -> float: def get_talk_frequency_adjust(chat_id: str) -> float:
return talk_frequency_control.get_talk_frequency_control(chat_id).talk_frequency_adjust return frequency_control_manager.get_or_create_frequency_control(chat_id).talk_frequency_external_adjust

View File

@@ -11,6 +11,7 @@
import time import time
from typing import List, Dict, Any, Tuple, Optional from typing import List, Dict, Any, Tuple, Optional
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
from src.common.database.database_model import Images
from src.config.config import global_config from src.config.config import global_config
from src.chat.utils.chat_message_builder import ( from src.chat.utils.chat_message_builder import (
get_raw_msg_by_timestamp, get_raw_msg_by_timestamp,
@@ -488,3 +489,15 @@ def filter_mai_messages(messages: List[DatabaseMessages]) -> List[DatabaseMessag
过滤后的消息列表 过滤后的消息列表
""" """
return [msg for msg in messages if msg.user_info.user_id != str(global_config.bot.qq_account)] return [msg for msg in messages if msg.user_info.user_id != str(global_config.bot.qq_account)]
def translate_pid_to_description(pid: str) -> str:
image = Images.get_or_none(Images.image_id == pid)
description = ""
if image and image.description:
description = image.description
else:
description = "[图片]"
return description

View File

@@ -99,6 +99,8 @@ async def _send_to_target(
# 创建消息段 # 创建消息段
message_segment = Seg(type=message_type, data=content) # type: ignore message_segment = Seg(type=message_type, data=content) # type: ignore
reply_to_platform_id = ""
anchor_message: Union["MessageRecv", None] = None
if reply_message: if reply_message:
anchor_message = message_dict_to_message_recv(reply_message.flatten()) anchor_message = message_dict_to_message_recv(reply_message.flatten())
if anchor_message: if anchor_message:
@@ -107,9 +109,6 @@ async def _send_to_target(
reply_to_platform_id = ( reply_to_platform_id = (
f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}"
) )
else:
reply_to_platform_id = ""
anchor_message = None
# 构建发送消息对象 # 构建发送消息对象
bot_message = MessageSending( bot_message = MessageSending(

View File

@@ -23,6 +23,7 @@ from .component_types import (
EventType, EventType,
MaiMessages, MaiMessages,
ToolParamType, ToolParamType,
CustomEventHandlerResult,
) )
from .config_types import ConfigField from .config_types import ConfigField
@@ -46,4 +47,5 @@ __all__ = [
"BaseEventHandler", "BaseEventHandler",
"MaiMessages", "MaiMessages",
"ToolParamType", "ToolParamType",
"CustomEventHandlerResult",
] ]

View File

@@ -39,7 +39,7 @@ class BaseAction(ABC):
chat_stream: ChatStream, chat_stream: ChatStream,
log_prefix: str = "", log_prefix: str = "",
plugin_config: Optional[dict] = None, plugin_config: Optional[dict] = None,
action_message: Optional[dict] = None, action_message: Optional["DatabaseMessages"] = None,
**kwargs, **kwargs,
): ):
# sourcery skip: hoist-similar-statement-from-if, merge-else-if-into-elif, move-assign-in-block, swap-if-else-branches, swap-nested-ifs # sourcery skip: hoist-similar-statement-from-if, merge-else-if-into-elif, move-assign-in-block, swap-if-else-branches, swap-nested-ifs
@@ -76,15 +76,19 @@ class BaseAction(ABC):
self.action_require: list[str] = getattr(self.__class__, "action_require", []).copy() self.action_require: list[str] = getattr(self.__class__, "action_require", []).copy()
# 设置激活类型实例属性(从类属性复制,提供默认值) # 设置激活类型实例属性(从类属性复制,提供默认值)
self.focus_activation_type = getattr(self.__class__, "focus_activation_type", ActionActivationType.ALWAYS) #已弃用 self.focus_activation_type = getattr(
self.__class__, "focus_activation_type", ActionActivationType.ALWAYS
) # 已弃用
"""FOCUS模式下的激活类型""" """FOCUS模式下的激活类型"""
self.normal_activation_type = getattr(self.__class__, "normal_activation_type", ActionActivationType.ALWAYS) #已弃用 self.normal_activation_type = getattr(
self.__class__, "normal_activation_type", ActionActivationType.ALWAYS
) # 已弃用
"""NORMAL模式下的激活类型""" """NORMAL模式下的激活类型"""
self.activation_type = getattr(self.__class__, "activation_type", self.focus_activation_type) self.activation_type = getattr(self.__class__, "activation_type", self.focus_activation_type)
"""激活类型""" """激活类型"""
self.random_activation_probability: float = getattr(self.__class__, "random_activation_probability", 0.0) self.random_activation_probability: float = getattr(self.__class__, "random_activation_probability", 0.0)
"""当激活类型为RANDOM时的概率""" """当激活类型为RANDOM时的概率"""
self.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "") #已弃用 self.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "") # 已弃用
"""协助LLM进行判断的Prompt""" """协助LLM进行判断的Prompt"""
self.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy() self.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy()
"""激活类型为KEYWORD时的KEYWORDS列表""" """激活类型为KEYWORD时的KEYWORDS列表"""
@@ -114,16 +118,21 @@ class BaseAction(ABC):
if self.action_message: if self.action_message:
self.has_action_message = True self.has_action_message = True
else:
self.action_message = {}
if self.has_action_message:
if self.action_name != "no_action": if self.action_name != "no_action":
self.group_id = str(self.action_message.get("chat_info_group_id", None)) self.group_id = (
self.group_name = self.action_message.get("chat_info_group_name", None) str(self.action_message.chat_info.group_info.group_id)
if self.action_message.chat_info.group_info
else None
)
self.group_name = (
self.action_message.chat_info.group_info.group_name
if self.action_message.chat_info.group_info
else None
)
self.user_id = str(self.action_message.get("user_id", None)) self.user_id = str(self.action_message.user_info.user_id)
self.user_nickname = self.action_message.get("user_nickname", None) self.user_nickname = self.action_message.user_info.user_nickname
if self.group_id: if self.group_id:
self.is_group = True self.is_group = True
self.target_id = self.group_id self.target_id = self.group_id

View File

@@ -1,8 +1,8 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Tuple, Optional, Dict from typing import Tuple, Optional, Dict, List
from src.common.logger import get_logger from src.common.logger import get_logger
from .component_types import MaiMessages, EventType, EventHandlerInfo, ComponentType from .component_types import MaiMessages, EventType, EventHandlerInfo, ComponentType, CustomEventHandlerResult
logger = get_logger("base_event_handler") logger = get_logger("base_event_handler")
@@ -13,7 +13,7 @@ class BaseEventHandler(ABC):
所有事件处理器都应该继承这个基类,提供事件处理的基本接口 所有事件处理器都应该继承这个基类,提供事件处理的基本接口
""" """
event_type: EventType = EventType.UNKNOWN event_type: EventType | str = EventType.UNKNOWN
"""事件类型,默认为未知""" """事件类型,默认为未知"""
handler_name: str = "" handler_name: str = ""
"""处理器名称""" """处理器名称"""
@@ -30,16 +30,19 @@ class BaseEventHandler(ABC):
"""对应插件名""" """对应插件名"""
self.plugin_config: Optional[Dict] = None self.plugin_config: Optional[Dict] = None
"""插件配置字典""" """插件配置字典"""
self._events_subscribed: List[EventType | str] = []
if self.event_type == EventType.UNKNOWN: if self.event_type == EventType.UNKNOWN:
raise NotImplementedError("事件处理器必须指定 event_type") raise NotImplementedError("事件处理器必须指定 event_type")
@abstractmethod @abstractmethod
async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, Optional[str]]: async def execute(
self, message: MaiMessages | None
) -> Tuple[bool, bool, Optional[str], Optional[CustomEventHandlerResult]]:
"""执行事件处理的抽象方法,子类必须实现 """执行事件处理的抽象方法,子类必须实现
Args: Args:
message (MaiMessages | None): 事件消息对象当你注册的事件为ON_START和ON_STOP时message为None message (MaiMessages | None): 事件消息对象当你注册的事件为ON_START和ON_STOP时message为None
Returns: Returns:
Tuple[bool, bool, Optional[str]]: (是否执行成功, 是否需要继续处理, 可选的返回消息) Tuple[bool, bool, Optional[str], Optional[CustomEventHandlerResult]]: (是否执行成功, 是否需要继续处理, 可选的返回消息, 可选的自定义结果)
""" """
raise NotImplementedError("子类必须实现 execute 方法") raise NotImplementedError("子类必须实现 execute 方法")

View File

@@ -1,3 +1,4 @@
import copy
from enum import Enum from enum import Enum
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -165,7 +166,7 @@ class ToolInfo(ComponentInfo):
class EventHandlerInfo(ComponentInfo): class EventHandlerInfo(ComponentInfo):
"""事件处理器组件信息""" """事件处理器组件信息"""
event_type: EventType = EventType.ON_MESSAGE # 监听事件类型 event_type: EventType | str = EventType.ON_MESSAGE # 监听事件类型
intercept_message: bool = False # 是否拦截消息处理(默认不拦截) intercept_message: bool = False # 是否拦截消息处理(默认不拦截)
weight: int = 0 # 事件处理器权重,决定执行顺序 weight: int = 0 # 事件处理器权重,决定执行顺序
@@ -281,3 +282,12 @@ class MaiMessages:
def __post_init__(self): def __post_init__(self):
if self.message_segments is None: if self.message_segments is None:
self.message_segments = [] self.message_segments = []
def deepcopy(self):
return copy.deepcopy(self)
@dataclass
class CustomEventHandlerResult:
message: str = ""
timestamp: float = 0.0
extra_info: Optional[Dict] = None

View File

@@ -124,6 +124,7 @@ class ComponentRegistry:
self._components_classes[namespaced_name] = component_class self._components_classes[namespaced_name] = component_class
# 根据组件类型进行特定注册(使用原始名称) # 根据组件类型进行特定注册(使用原始名称)
ret = False
match component_type: match component_type:
case ComponentType.ACTION: case ComponentType.ACTION:
assert isinstance(component_info, ActionInfo) assert isinstance(component_info, ActionInfo)

View File

@@ -1,11 +1,11 @@
import asyncio import asyncio
import contextlib import contextlib
from typing import List, Dict, Optional, Type, Tuple, Any, TYPE_CHECKING from typing import List, Dict, Optional, Type, Tuple, TYPE_CHECKING
from src.chat.message_receive.message import MessageRecv from src.chat.message_receive.message import MessageRecv
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.base.component_types import EventType, EventHandlerInfo, MaiMessages from src.plugin_system.base.component_types import EventType, EventHandlerInfo, MaiMessages, CustomEventHandlerResult
from src.plugin_system.base.base_events_handler import BaseEventHandler from src.plugin_system.base.base_events_handler import BaseEventHandler
from .global_announcement_manager import global_announcement_manager from .global_announcement_manager import global_announcement_manager
@@ -18,9 +18,23 @@ logger = get_logger("events_manager")
class EventsManager: class EventsManager:
def __init__(self): def __init__(self):
# 有权重的 events 订阅者注册表 # 有权重的 events 订阅者注册表
self._events_subscribers: Dict[EventType, List[BaseEventHandler]] = {event: [] for event in EventType} self._events_subscribers: Dict[EventType | str, List[BaseEventHandler]] = {}
self._handler_mapping: Dict[str, Type[BaseEventHandler]] = {} # 事件处理器映射表 self._handler_mapping: Dict[str, Type[BaseEventHandler]] = {} # 事件处理器映射表
self._handler_tasks: Dict[str, List[asyncio.Task]] = {} # 事件处理器正在处理的任务 self._handler_tasks: Dict[str, List[asyncio.Task]] = {} # 事件处理器正在处理的任务
self._events_result_history: Dict[EventType | str, List[CustomEventHandlerResult]] = {} # 事件的结果历史记录
self._history_enable_map: Dict[EventType | str, bool] = {} # 是否启用历史记录的映射表同时作为events注册表
# 事件注册(同时作为注册样例)
for event in EventType:
self.register_event(event, enable_history_result=False)
def register_event(self, event_type: EventType | str, enable_history_result: bool = False):
if event_type in self._events_subscribers:
raise ValueError(f"事件类型 {event_type} 已存在")
self._events_subscribers[event_type] = []
self._history_enable_map[event_type] = enable_history_result
if enable_history_result:
self._events_result_history[event_type] = []
def register_event_subscriber(self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler]) -> bool: def register_event_subscriber(self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler]) -> bool:
"""注册事件处理器 """注册事件处理器
@@ -32,69 +46,23 @@ class EventsManager:
Returns: Returns:
bool: 是否注册成功 bool: 是否注册成功
""" """
if not issubclass(handler_class, BaseEventHandler):
logger.error(f"{handler_class.__name__} 不是 BaseEventHandler 的子类")
return False
handler_name = handler_info.name handler_name = handler_info.name
if handler_name in self._handler_mapping: if handler_name in self._handler_mapping:
logger.warning(f"事件处理器 {handler_name} 已存在,跳过注册") logger.warning(f"事件处理器 {handler_name} 已存在,跳过注册")
return False return False
if not issubclass(handler_class, BaseEventHandler): if handler_info.event_type not in self._history_enable_map:
logger.error(f" {handler_class.__name__} 不是 BaseEventHandler 的子类") logger.error(f"事件类型 {handler_info.event_type} 未注册,无法为其注册处理器 {handler_name}")
return False return False
self._handler_mapping[handler_name] = handler_class self._handler_mapping[handler_name] = handler_class
return self._insert_event_handler(handler_class, handler_info) return self._insert_event_handler(handler_class, handler_info)
def _prepare_message(
self,
event_type: EventType,
message: Optional[MessageRecv] = None,
llm_prompt: Optional[str] = None,
llm_response: Optional["LLMGenerationDataModel"] = None,
stream_id: Optional[str] = None,
action_usage: Optional[List[str]] = None,
) -> Optional[MaiMessages]:
"""根据事件类型和输入,准备和转换消息对象。"""
if message:
return self._transform_event_message(message, llm_prompt, llm_response)
if event_type not in [EventType.ON_START, EventType.ON_STOP]:
assert stream_id, "如果没有消息,必须为非启动/关闭事件提供流ID"
if event_type in [EventType.ON_MESSAGE, EventType.ON_PLAN, EventType.POST_LLM, EventType.AFTER_LLM]:
return self._build_message_from_stream(stream_id, llm_prompt, llm_response)
else:
return self._transform_event_without_message(stream_id, llm_prompt, llm_response, action_usage)
return None # ON_START, ON_STOP事件没有消息体
def _dispatch_handler_task(self, handler: BaseEventHandler, message: Optional[MaiMessages]):
"""分发一个非阻塞(异步)的事件处理任务。"""
try:
task = asyncio.create_task(handler.execute(message))
task_name = f"{handler.plugin_name}-{handler.handler_name}"
task.set_name(task_name)
task.add_done_callback(self._task_done_callback)
self._handler_tasks.setdefault(handler.handler_name, []).append(task)
except Exception as e:
logger.error(f"创建事件处理器任务 {handler.handler_name} 时发生异常: {e}", exc_info=True)
async def _dispatch_intercepting_handler(self, handler: BaseEventHandler, message: Optional[MaiMessages]) -> bool:
"""分发并等待一个阻塞(同步)的事件处理器,返回是否应继续处理。"""
try:
success, continue_processing, result = await handler.execute(message)
if not success:
logger.error(f"EventHandler {handler.handler_name} 执行失败: {result}")
else:
logger.debug(f"EventHandler {handler.handler_name} 执行成功: {result}")
return continue_processing
except Exception as e:
logger.error(f"EventHandler {handler.handler_name} 发生异常: {e}", exc_info=True)
return True # 发生异常时默认不中断其他处理
async def handle_mai_events( async def handle_mai_events(
self, self,
event_type: EventType, event_type: EventType,
@@ -115,6 +83,8 @@ class EventsManager:
transformed_message = self._prepare_message( transformed_message = self._prepare_message(
event_type, message, llm_prompt, llm_response, stream_id, action_usage event_type, message, llm_prompt, llm_response, stream_id, action_usage
) )
if transformed_message:
transformed_message = transformed_message.deepcopy()
# 2. 获取并遍历处理器 # 2. 获取并遍历处理器
handlers = self._events_subscribers.get(event_type, []) handlers = self._events_subscribers.get(event_type, [])
@@ -137,22 +107,75 @@ class EventsManager:
handler.set_plugin_config(plugin_config) handler.set_plugin_config(plugin_config)
# 4. 根据类型分发任务 # 4. 根据类型分发任务
if handler.intercept_message: if handler.intercept_message or event_type == EventType.ON_STOP: # 让ON_STOP的所有事件处理器都发挥作用防止还没执行即被取消
# 阻塞执行,并更新 continue_flag # 阻塞执行,并更新 continue_flag
should_continue = await self._dispatch_intercepting_handler(handler, transformed_message) should_continue = await self._dispatch_intercepting_handler(handler, event_type, transformed_message)
continue_flag = continue_flag and should_continue continue_flag = continue_flag and should_continue
else: else:
# 异步执行,不阻塞 # 异步执行,不阻塞
self._dispatch_handler_task(handler, transformed_message) self._dispatch_handler_task(handler, event_type, transformed_message)
return continue_flag return continue_flag
async def cancel_handler_tasks(self, handler_name: str) -> None:
tasks_to_be_cancelled = self._handler_tasks.get(handler_name, [])
if remaining_tasks := [task for task in tasks_to_be_cancelled if not task.done()]:
for task in remaining_tasks:
task.cancel()
try:
await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5)
logger.info(f"已取消事件处理器 {handler_name} 的所有任务")
except asyncio.TimeoutError:
logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消")
except Exception as e:
logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}")
if handler_name in self._handler_tasks:
del self._handler_tasks[handler_name]
async def unregister_event_subscriber(self, handler_name: str) -> bool:
"""取消注册事件处理器"""
if handler_name not in self._handler_mapping:
logger.warning(f"事件处理器 {handler_name} 不存在,无法取消注册")
return False
await self.cancel_handler_tasks(handler_name)
handler_class = self._handler_mapping.pop(handler_name)
if not self._remove_event_handler_instance(handler_class):
return False
logger.info(f"事件处理器 {handler_name} 已成功取消注册")
return True
async def get_event_result_history(self, event_type: EventType | str) -> List[CustomEventHandlerResult]:
"""获取事件的结果历史记录"""
if event_type == EventType.UNKNOWN:
raise ValueError("未知事件类型")
if event_type not in self._history_enable_map:
raise ValueError(f"事件类型 {event_type} 未注册")
if not self._history_enable_map[event_type]:
raise ValueError(f"事件类型 {event_type} 的历史记录未启用")
return self._events_result_history[event_type]
async def clear_event_result_history(self, event_type: EventType | str) -> None:
"""清空事件的结果历史记录"""
if event_type == EventType.UNKNOWN:
raise ValueError("未知事件类型")
if event_type not in self._history_enable_map:
raise ValueError(f"事件类型 {event_type} 未注册")
if not self._history_enable_map[event_type]:
raise ValueError(f"事件类型 {event_type} 的历史记录未启用")
self._events_result_history[event_type] = []
def _insert_event_handler(self, handler_class: Type[BaseEventHandler], handler_info: EventHandlerInfo) -> bool: def _insert_event_handler(self, handler_class: Type[BaseEventHandler], handler_info: EventHandlerInfo) -> bool:
"""插入事件处理器到对应的事件类型列表中并设置其插件配置""" """插入事件处理器到对应的事件类型列表中并设置其插件配置"""
if handler_class.event_type == EventType.UNKNOWN: if handler_class.event_type == EventType.UNKNOWN:
logger.error(f"事件处理器 {handler_class.__name__} 的事件类型未知,无法注册") logger.error(f"事件处理器 {handler_class.__name__} 的事件类型未知,无法注册")
return False return False
if handler_class.event_type not in self._events_subscribers:
self._events_subscribers[handler_class.event_type] = []
handler_instance = handler_class() handler_instance = handler_class()
handler_instance.set_plugin_name(handler_info.plugin_name or "unknown") handler_instance.set_plugin_name(handler_info.plugin_name or "unknown")
self._events_subscribers[handler_class.event_type].append(handler_instance) self._events_subscribers[handler_class.event_type].append(handler_instance)
@@ -178,7 +201,10 @@ class EventsManager:
return False return False
def _transform_event_message( def _transform_event_message(
self, message: MessageRecv, llm_prompt: Optional[str] = None, llm_response: Optional["LLMGenerationDataModel"] = None self,
message: MessageRecv,
llm_prompt: Optional[str] = None,
llm_response: Optional["LLMGenerationDataModel"] = None,
) -> MaiMessages: ) -> MaiMessages:
"""转换事件消息格式""" """转换事件消息格式"""
# 直接赋值部分内容 # 直接赋值部分内容
@@ -262,52 +288,100 @@ class EventsManager:
additional_data={"response_is_processed": True}, additional_data={"response_is_processed": True},
) )
def _task_done_callback(self, task: asyncio.Task[Tuple[bool, bool, str | None]]): def _prepare_message(
self,
event_type: EventType,
message: Optional[MessageRecv] = None,
llm_prompt: Optional[str] = None,
llm_response: Optional["LLMGenerationDataModel"] = None,
stream_id: Optional[str] = None,
action_usage: Optional[List[str]] = None,
) -> Optional[MaiMessages]:
"""根据事件类型和输入,准备和转换消息对象。"""
if message:
return self._transform_event_message(message, llm_prompt, llm_response)
if event_type not in [EventType.ON_START, EventType.ON_STOP]:
assert stream_id, "如果没有消息,必须为非启动/关闭事件提供流ID"
if event_type in [EventType.ON_MESSAGE, EventType.ON_PLAN, EventType.POST_LLM, EventType.AFTER_LLM]:
return self._build_message_from_stream(stream_id, llm_prompt, llm_response)
else:
return self._transform_event_without_message(stream_id, llm_prompt, llm_response, action_usage)
return None # ON_START, ON_STOP事件没有消息体
def _dispatch_handler_task(
self, handler: BaseEventHandler, event_type: EventType | str, message: Optional[MaiMessages] = None
):
"""分发一个非阻塞(异步)的事件处理任务。"""
if event_type == EventType.UNKNOWN:
raise ValueError("未知事件类型")
try:
task = asyncio.create_task(handler.execute(message))
task_name = f"{handler.plugin_name}-{handler.handler_name}"
task.set_name(task_name)
task.add_done_callback(lambda t: self._task_done_callback(t, event_type))
self._handler_tasks.setdefault(handler.handler_name, []).append(task)
except Exception as e:
logger.error(f"创建事件处理器任务 {handler.handler_name} 时发生异常: {e}", exc_info=True)
async def _dispatch_intercepting_handler(
self, handler: BaseEventHandler, event_type: EventType | str, message: Optional[MaiMessages] = None
) -> bool:
"""分发并等待一个阻塞(同步)的事件处理器,返回是否应继续处理。"""
if event_type == EventType.UNKNOWN:
raise ValueError("未知事件类型")
if event_type not in self._history_enable_map:
raise ValueError(f"事件类型 {event_type} 未注册")
try:
success, continue_processing, return_message, custom_result = await handler.execute(message)
if not success:
logger.error(f"EventHandler {handler.handler_name} 执行失败: {return_message}")
else:
logger.debug(f"EventHandler {handler.handler_name} 执行成功: {return_message}")
if self._history_enable_map[event_type] and custom_result:
self._events_result_history[event_type].append(custom_result)
return continue_processing
except KeyError:
logger.error(f"事件 {event_type} 注册的历史记录启用情况与实际不符合")
return True
except Exception as e:
logger.error(f"EventHandler {handler.handler_name} 发生异常: {e}", exc_info=True)
return True # 发生异常时默认不中断其他处理
def _task_done_callback(
self,
task: asyncio.Task[Tuple[bool, bool, str | None, CustomEventHandlerResult | None]],
event_type: EventType | str,
):
"""任务完成回调""" """任务完成回调"""
task_name = task.get_name() or "Unknown Task" task_name = task.get_name() or "Unknown Task"
if event_type == EventType.UNKNOWN:
raise ValueError("未知事件类型")
if event_type not in self._history_enable_map:
raise ValueError(f"事件类型 {event_type} 未注册")
try: try:
success, _, result = task.result() # 忽略是否继续的标志,因为消息本身未被拦截 success, _, result, custom_result = task.result() # 忽略是否继续的标志,因为消息本身未被拦截
if success: if success:
logger.debug(f"事件处理任务 {task_name} 已成功完成: {result}") logger.debug(f"事件处理任务 {task_name} 已成功完成: {result}")
else: else:
logger.error(f"事件处理任务 {task_name} 执行失败: {result}") logger.error(f"事件处理任务 {task_name} 执行失败: {result}")
if self._history_enable_map[event_type] and custom_result:
self._events_result_history[event_type].append(custom_result)
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except KeyError:
logger.error(f"事件 {event_type} 注册的历史记录启用情况与实际不符合")
except Exception as e: except Exception as e:
logger.error(f"事件处理任务 {task_name} 发生异常: {e}") logger.error(f"事件处理任务 {task_name} 发生异常: {e}")
finally: finally:
with contextlib.suppress(ValueError, KeyError): with contextlib.suppress(ValueError, KeyError):
self._handler_tasks[task_name].remove(task) self._handler_tasks[task_name].remove(task)
async def cancel_handler_tasks(self, handler_name: str) -> None:
tasks_to_be_cancelled = self._handler_tasks.get(handler_name, [])
if remaining_tasks := [task for task in tasks_to_be_cancelled if not task.done()]:
for task in remaining_tasks:
task.cancel()
try:
await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5)
logger.info(f"已取消事件处理器 {handler_name} 的所有任务")
except asyncio.TimeoutError:
logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消")
except Exception as e:
logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}")
if handler_name in self._handler_tasks:
del self._handler_tasks[handler_name]
async def unregister_event_subscriber(self, handler_name: str) -> bool:
"""取消注册事件处理器"""
if handler_name not in self._handler_mapping:
logger.warning(f"事件处理器 {handler_name} 不存在,无法取消注册")
return False
await self.cancel_handler_tasks(handler_name)
handler_class = self._handler_mapping.pop(handler_name)
if not self._remove_event_handler_instance(handler_class):
return False
logger.info(f"事件处理器 {handler_name} 已成功取消注册")
return True
events_manager = EventsManager() events_manager = EventsManager()

View File

@@ -0,0 +1,13 @@
- [x] 自定义事件
- [ ] <del>允许handler随时订阅</del>
- [x] 允许其他组件给handler增加订阅
- [x] 允许其他组件给handler取消订阅
- [ ] <del>允许一个handler订阅多个事件</del>
- [x] event激活时给handler传递参数
- [ ] handler能拿到所有handlers的结果按照处理权重
- [x] 随时注册
- [ ] <del>删除event</del>
- [ ] 必要性?
- [ ] 能够更改prompt
- [ ] 能够更改llm_response
- [ ] 能够更改message

View File

@@ -0,0 +1,34 @@
{
"manifest_version": 1,
"name": "Memory Build组件",
"version": "1.0.0",
"description": "可以构建和管理记忆",
"author": {
"name": "Mai",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.1"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": ["memory", "build", "built-in"],
"categories": ["Memory"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "action_provider",
"components": [
{
"type": "build_memory",
"name": "build_memory",
"description": "构建记忆"
}
]
}
}

View File

@@ -0,0 +1,134 @@
from typing import Tuple
from src.common.logger import get_logger
from src.config.config import global_config
from src.chat.utils.prompt_builder import Prompt
from src.plugin_system import BaseAction, ActionActivationType
from src.chat.memory_system.Hippocampus import hippocampus_manager
from src.chat.utils.utils import cut_key_words
logger = get_logger("memory")
def init_prompt():
Prompt(
"""
以下是一些记忆条目的分类:
----------------------
{category_list}
----------------------
每一个分类条目类型代表了你对用户:"{person_name}"的印象的一个类别
现在,你有一条对 {person_name} 的新记忆内容:
{memory_point}
请判断该记忆内容是否属于上述分类,请给出分类的名称。
如果不属于上述分类,请输出一个合适的分类名称,对新记忆内容进行概括。要求分类名具有概括性。
注意分类数一般不超过5个
请严格用json格式输出不要输出任何其他内容
{{
"category": "分类名称"
}} """,
"relation_category",
)
Prompt(
"""
以下是有关{category}的现有记忆:
----------------------
{memory_list}
----------------------
现在,你有一条对 {person_name} 的新记忆内容:
{memory_point}
请判断该新记忆内容是否已经存在于现有记忆中,你可以对现有进行进行以下修改:
注意一般来说记忆内容不超过5个且记忆文本不应太长
1.新增当记忆内容不存在于现有记忆且不存在矛盾请用json格式输出
{{
"new_memory": "需要新增的记忆内容"
}}
2.加深印象如果这个新记忆已经存在于现有记忆中在内容上与现有记忆类似请用json格式输出
{{
"memory_id": 1, #请输出你认为需要加深印象的,与新记忆内容类似的,已经存在的记忆的序号
"integrate_memory": "加深后的记忆内容,合并内容类似的新记忆和旧记忆"
}}
3.整合如果这个新记忆与现有记忆产生矛盾请你结合其他记忆进行整合用json格式输出
{{
"memory_id": 1, #请输出你认为需要整合的,与新记忆存在矛盾的,已经存在的记忆的序号
"integrate_memory": "整合后的记忆内容,合并内容矛盾的新记忆和旧记忆"
}}
现在请你根据情况选出合适的修改方式并输出json不要输出其他内容
""",
"relation_category_update",
)
class BuildMemoryAction(BaseAction):
"""关系动作 - 构建关系"""
activation_type = ActionActivationType.LLM_JUDGE
parallel_action = True
# 动作基本信息
action_name = "build_memory"
action_description = "了解对于某个概念或者某件事的记忆,并存储下来,在之后的聊天中,你可以根据这条记忆来获取相关信息"
# 动作参数定义
action_parameters = {
"concept_name": "需要了解或记忆的概念或事件的名称",
"concept_description": "需要了解或记忆的概念或事件的描述,需要具体且明确",
}
# 动作使用场景
action_require = [
"了解对于某个概念或者某件事的记忆,并存储下来,在之后的聊天中,你可以根据这条记忆来获取相关信息",
"有你不了解的概念",
"有人要求你记住某个概念或者事件",
"你对某件事或概念有新的理解,或产生了兴趣",
]
# 关联类型
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
"""执行关系动作"""
try:
# 1. 获取构建关系的原因
concept_description = self.action_data.get("concept_description", "")
logger.info(f"{self.log_prefix} 添加记忆原因: {self.reasoning}")
concept_name = self.action_data.get("concept_name", "")
# 2. 获取目标用户信息
# 对 concept_name 进行jieba分词
concept_name_tokens = cut_key_words(concept_name)
# logger.info(f"{self.log_prefix} 对 concept_name 进行分词结果: {concept_name_tokens}")
filtered_concept_name_tokens = [
token for token in concept_name_tokens if all(keyword not in token for keyword in global_config.memory.memory_ban_words)
]
if not filtered_concept_name_tokens:
logger.warning(f"{self.log_prefix} 过滤后的概念名称列表为空,跳过添加记忆")
return False, "过滤后的概念名称列表为空,跳过添加记忆"
similar_topics_dict = hippocampus_manager.get_hippocampus().parahippocampal_gyrus.get_similar_topics_from_keywords(filtered_concept_name_tokens)
await hippocampus_manager.get_hippocampus().parahippocampal_gyrus.add_memory_with_similar(concept_description, similar_topics_dict)
return True, f"成功添加记忆: {concept_name}"
except Exception as e:
logger.error(f"{self.log_prefix} 构建记忆时出错: {e}")
return False, f"构建记忆时出错: {e}"
# 还缺一个关系的太多遗忘和对应的提取
init_prompt()

View File

@@ -0,0 +1,58 @@
from typing import List, Tuple, Type
# 导入新插件系统
from src.plugin_system import BasePlugin, register_plugin, ComponentInfo
from src.plugin_system.base.config_types import ConfigField
# 导入依赖的系统组件
from src.common.logger import get_logger
from src.plugins.built_in.memory.build_memory import BuildMemoryAction
logger = get_logger("relation_actions")
@register_plugin
class MemoryBuildPlugin(BasePlugin):
"""关系动作插件
系统内置插件,提供基础的聊天交互功能:
- Reply: 回复动作
- NoReply: 不回复动作
- Emoji: 表情动作
注意插件基本信息优先从_manifest.json文件中读取
"""
# 插件基本信息
plugin_name: str = "memory_build" # 内部标识符
enable_plugin: bool = True
dependencies: list[str] = [] # 插件依赖列表
python_dependencies: list[str] = [] # Python包依赖列表
config_file_name: str = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件启用配置",
"components": "核心组件启用配置",
}
# 配置Schema定义
config_schema: dict = {
"plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"),
},
"components": {
"memory_max_memory_num": ConfigField(type=int, default=10, description="记忆最大数量"),
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# --- 根据配置注册组件 ---
components = []
components.append((BuildMemoryAction.get_action_info(), BuildMemoryAction))
return components

View File

@@ -9,7 +9,7 @@
}, },
"license": "GPL-v3.0-or-later", "license": "GPL-v3.0-or-later",
"host_application": { "host_application": {
"min_version": "0.9.1" "min_version": "0.10.1"
}, },
"homepage_url": "https://github.com/MaiM-with-u/maibot", "homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot", "repository_url": "https://github.com/MaiM-with-u/maibot",

View File

@@ -1,6 +1,7 @@
import json import json
from json_repair import repair_json from json_repair import repair_json
from typing import Tuple from typing import Tuple
import time
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
@@ -79,16 +80,6 @@ class BuildRelationAction(BaseAction):
action_name = "build_relation" action_name = "build_relation"
action_description = "了解对于某人的记忆,并添加到你对对方的印象中" action_description = "了解对于某人的记忆,并添加到你对对方的印象中"
# LLM判断提示词
llm_judge_prompt = """
判定是否需要使用关系动作,添加对于某人的记忆:
1. 对方与你的交互让你对其有新记忆
2. 对方有提到其个人信息,包括喜好,身份,等等
3. 对方希望你记住对方的信息
请回答""""
"""
# 动作参数定义 # 动作参数定义
action_parameters = {"person_name": "需要了解或记忆的人的名称", "impression": "需要了解的对某人的记忆或印象"} action_parameters = {"person_name": "需要了解或记忆的人的名称", "impression": "需要了解的对某人的记忆或印象"}
@@ -109,7 +100,7 @@ class BuildRelationAction(BaseAction):
try: try:
# 1. 获取构建关系的原因 # 1. 获取构建关系的原因
impression = self.action_data.get("impression", "") impression = self.action_data.get("impression", "")
logger.info(f"{self.log_prefix} 添加记忆原因: {self.reasoning}") logger.info(f"{self.log_prefix} 添加关系印象原因: {self.reasoning}")
person_name = self.action_data.get("person_name", "") person_name = self.action_data.get("person_name", "")
# 2. 获取目标用户信息 # 2. 获取目标用户信息
person = Person(person_name=person_name) person = Person(person_name=person_name)
@@ -117,6 +108,10 @@ class BuildRelationAction(BaseAction):
logger.warning(f"{self.log_prefix} 用户 {person_name} 不存在,跳过添加记忆") logger.warning(f"{self.log_prefix} 用户 {person_name} 不存在,跳过添加记忆")
return False, f"用户 {person_name} 不存在,跳过添加记忆" return False, f"用户 {person_name} 不存在,跳过添加记忆"
person.last_know = time.time()
person.know_times += 1
person.sync_to_database()
category_list = person.get_all_category() category_list = person.get_all_category()
if not category_list: if not category_list:
category_list_str = "无分类" category_list_str = "无分类"
@@ -196,6 +191,8 @@ class BuildRelationAction(BaseAction):
person.memory_points.append(f"{category}:{new_memory}:1.0") person.memory_points.append(f"{category}:{new_memory}:1.0")
person.sync_to_database() person.sync_to_database()
logger.info(f"{self.log_prefix}{person.person_name}新增记忆点: {new_memory}")
return True, f"{person.person_name}新增记忆点: {new_memory}" return True, f"{person.person_name}新增记忆点: {new_memory}"
elif memory_id and integrate_memory: elif memory_id and integrate_memory:
# 现存或冲突记忆 # 现存或冲突记忆
@@ -204,12 +201,14 @@ class BuildRelationAction(BaseAction):
del_count = person.del_memory(category, memory_content) del_count = person.del_memory(category, memory_content)
if del_count > 0: if del_count > 0:
logger.info(f"{self.log_prefix} 删除记忆点: {memory_content}") # logger.info(f"{self.log_prefix} 删除记忆点: {memory_content}")
memory_weight = get_weight_from_memory(memory) memory_weight = get_weight_from_memory(memory)
person.memory_points.append(f"{category}:{integrate_memory}:{memory_weight + 1.0}") person.memory_points.append(f"{category}:{integrate_memory}:{memory_weight + 1.0}")
person.sync_to_database() person.sync_to_database()
logger.info(f"{self.log_prefix} 更新{person.person_name}的记忆点: {memory_content} -> {integrate_memory}")
return True, f"更新{person.person_name}的记忆点: {memory_content} -> {integrate_memory}" return True, f"更新{person.person_name}的记忆点: {memory_content} -> {integrate_memory}"
else: else:

View File

@@ -13,21 +13,16 @@ class TTSAction(BaseAction):
"""TTS语音转换动作处理类""" """TTS语音转换动作处理类"""
# 激活设置 # 激活设置
focus_activation_type = ActionActivationType.LLM_JUDGE activation_type = ActionActivationType.LLM_JUDGE
normal_activation_type = ActionActivationType.KEYWORD
parallel_action = False parallel_action = False
# 动作基本信息 # 动作基本信息
action_name = "tts_action" action_name = "tts_action"
action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景" action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景"
# 关键词配置 - Normal模式下使用关键词触发
activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"]
keyword_case_sensitive = False
# 动作参数定义 # 动作参数定义
action_parameters = { action_parameters = {
"text": "需要转换为语音的文本内容,必填,内容应当适合语音播报,语句流畅、清晰", "voice_text": "你想用语音表达的内容,这段内容将会以语音形式发出",
} }
# 动作使用场景 # 动作使用场景
@@ -46,7 +41,7 @@ class TTSAction(BaseAction):
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}") logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}")
# 获取要转换的文本 # 获取要转换的文本
text = self.action_data.get("text") text = self.action_data.get("voice_text")
if not text: if not text:
logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容") logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容")

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "6.7.1" version = "6.9.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -18,21 +18,13 @@ nickname = "麦麦" # 麦麦的昵称
alias_names = ["麦叠", "牢麦"] # 麦麦的别名 alias_names = ["麦叠", "牢麦"] # 麦麦的别名
[personality] [personality]
# 建议50字以内描述人格的核心特质 # 建议120字以内描述人格特质 和 身份特征
personality_core = "是一个女孩子" personality = "是一个女大学生,现在在读大二,会刷贴吧。有时候说话不过脑子,有时候会喜欢说一些奇怪的话。年龄为19岁,有黑色的短发。"
# 人格的细节,描述人格的一些侧面
personality_side = "有时候说话不过脑子,喜欢开玩笑, 有时候会表现得无语,有时候会喜欢说一些奇怪的话"
#アイデンティティがない 生まれないらららら #アイデンティティがない 生まれないらららら
# 可以描述外貌,性别,身高,职业,属性等等描述
identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发"
# 描述麦麦说话的表达风格,表达习惯,如要修改,可以酌情新增内容 # 描述麦麦说话的表达风格,表达习惯,如要修改,可以酌情新增内容
reply_style = "回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。不要浮夸,不要夸张修辞。" reply_style = "回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。不要浮夸,不要夸张修辞。"
# 情感特征,影响情绪的变化情况
# 描述麦麦的行为风格,会影响麦麦什么时候回复,什么时候使用动作,麦麦考虑的可就多了 emotion_style = "情绪较为稳定,但遭遇特定事件的时候起伏较大"
plan_style = "当你刚刚发送了消息没有人回复时不要选择action如果有别的动作非回复满足条件可以选择当你一次发送了太多消息为了避免打扰聊天节奏不要选择动作"
# 麦麦的兴趣,会影响麦麦对什么话题进行回复 # 麦麦的兴趣,会影响麦麦对什么话题进行回复
interest = "对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题" interest = "对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题"
@@ -59,18 +51,16 @@ expression_groups = [
[chat] #麦麦的聊天设置 [chat] #麦麦的聊天设置
talk_frequency = 0.5 talk_frequency = 0.5
# 麦麦活跃度,越高,麦麦回复越多范围0-1 # 麦麦活跃度,越高,麦麦越容易回复范围0-1
focus_value = 0.5 focus_value = 0.5
# 麦麦的专注度越高越容易持续连续对话可能消耗更多token, 范围0-1 # 麦麦的专注度越高越容易持续连续对话可能消耗更多token, 范围0-1
mentioned_bot_reply = 1 # 提及时,回复概率增幅1为100%回复0为不额外增幅
at_bot_inevitable_reply = 1 # at时,回复概率增幅1为100%回复0为不额外增幅
max_context_size = 20 # 上下文长度 max_context_size = 20 # 上下文长度
interest_rate_mode = "fast" #激活值计算模式可选fast或者accurate planner_size = 3.5 # 副规划器大小越小麦麦的动作执行能力越精细但是消耗更多token调大可以缓解429类错误
planner_size = 2.5 # 副规划器大小越小麦麦的动作执行能力越精细但是消耗更多token调大可以缓解429类错误
mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复
at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复
focus_value_adjust = [ focus_value_adjust = [
["", "8:00,1", "12:00,0.8", "18:00,1", "01:00,0.3"], ["", "8:00,1", "12:00,0.8", "18:00,1", "01:00,0.3"],
@@ -102,25 +92,11 @@ talk_frequency_adjust = [
# - 后续元素是"时间,频率"格式,表示从该时间开始使用该活跃度,直到下一个时间点 # - 后续元素是"时间,频率"格式,表示从该时间开始使用该活跃度,直到下一个时间点
# - 优先级:特定聊天流配置 > 全局配置 > 默认 talk_frequency # - 优先级:特定聊天流配置 > 全局配置 > 默认 talk_frequency
[relationship] [relationship]
enable_relationship = true # 是否启用关系系统 enable_relationship = true # 是否启用关系系统
relation_frequency = 1 # 关系频率,麦麦构建关系的频率
[message_receive]
# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息
ban_words = [
# "403","张三"
]
ban_msgs_regex = [
# 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改
#"https?://[^\\s]+", # 匹配https链接
#"\\d{4}-\\d{2}-\\d{2}", # 匹配日期
]
[tool] [tool]
enable_tool = false # 是否在普通聊天中启用工具 enable_tool = false # 是否启用回复工具
[mood] [mood]
enable_mood = true # 是否启用情绪系统 enable_mood = true # 是否启用情绪系统
@@ -138,21 +114,29 @@ filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合
[memory] [memory]
enable_memory = true # 是否启用记忆系统 enable_memory = true # 是否启用记忆系统
memory_build_frequency = 1 # 记忆构建频率 越高,麦麦学习越多 forget_memory_interval = 1500 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习
memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多
forget_memory_interval = 3000 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习
memory_forget_time = 48 #多长时间后的记忆会被遗忘 单位小时 memory_forget_time = 48 #多长时间后的记忆会被遗忘 单位小时
memory_forget_percentage = 0.008 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 memory_forget_percentage = 0.008 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认
enable_instant_memory = false # 是否启用即时记忆,测试功能,可能存在未知问题
#不希望记忆的词,已经记忆的不会受到影响,需要手动清理 #不希望记忆的词,已经记忆的不会受到影响,需要手动清理
memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ] memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ]
[voice] [voice]
enable_asr = false # 是否启用语音识别,启用后麦麦可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s enable_asr = false # 是否启用语音识别,启用后麦麦可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s
[message_receive]
# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息
ban_words = [
# "403","张三"
]
ban_msgs_regex = [
# 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改
#"https?://[^\\s]+", # 匹配https链接
#"\\d{4}-\\d{2}-\\d{2}", # 匹配日期
]
[lpmm_knowledge] # lpmm知识库配置 [lpmm_knowledge] # lpmm知识库配置
enable = false # 是否启用lpmm知识库 enable = false # 是否启用lpmm知识库
rag_synonym_search_top_k = 10 # 同义词搜索TopK rag_synonym_search_top_k = 10 # 同义词搜索TopK