This commit is contained in:
雅诺狐
2025-09-13 23:20:45 +08:00
43 changed files with 2026 additions and 1882 deletions

1
.gitignore vendored
View File

@@ -341,3 +341,4 @@ rust_video/Cargo.lock
.claude/settings.local.json
package-lock.json
package.json
src/chat/planner_actions/新建 文本文档.txt

9
bot.py
View File

@@ -7,17 +7,9 @@ import time
import platform
import traceback
from pathlib import Path
from dotenv import load_dotenv
from rich.traceback import install
from colorama import init, Fore
if os.path.exists(".env"):
load_dotenv(".env", override=True)
print("成功加载环境变量配置")
else:
print("未找到.env文件请确保程序所需的环境变量被正确设置")
raise FileNotFoundError(".env 文件不存在,请创建并配置所需的环境变量")
# maim_message imports for console input
# 最早期初始化日志系统,确保所有后续模块都使用正确的日志格式
@@ -45,7 +37,6 @@ logger.info(f"已设置工作目录为: {script_dir}")
confirm_logger = get_logger("confirm")
# 获取没有加载env时的环境变量
env_mask = {key: os.getenv(key) for key in os.environ}
uvicorn_server = None
driver = None

View File

@@ -84,7 +84,7 @@ class CycleProcessor:
# 获取用户信息并生成回复提示
person_id = person_info_manager.get_person_id(
platform,
action_message.get("user_id", ""),
action_message.get("chat_info_user_id", ""),
)
person_name = await person_info_manager.get_value(person_id, "person_name")
action_prompt_display = f"你对{person_name}进行了回复:{reply_text}"
@@ -180,7 +180,7 @@ class CycleProcessor:
cycle_timers, thinking_id = self.cycle_tracker.start_cycle()
logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考")
if ENABLE_S4U:
if ENABLE_S4U and self.context.chat_stream and self.context.chat_stream.user_info:
await send_typing(self.context.chat_stream.user_info.user_id)
loop_start_time = time.time()
@@ -194,30 +194,17 @@ class CycleProcessor:
logger.error(f"{self.context.log_prefix} 动作修改失败: {e}")
available_actions = {}
# 执行planner
planner_info = self.action_planner.get_necessary_info()
prompt_info = await self.action_planner.build_planner_prompt(
is_group_chat=planner_info[0],
chat_target_info=planner_info[1],
current_available_actions=planner_info[2],
)
# 规划动作
from src.plugin_system.core.event_manager import event_manager
from src.plugin_system import EventType
# 触发规划前事件
result = await event_manager.trigger_event(
EventType.ON_PLAN, permission_group="SYSTEM", stream_id=self.context.chat_stream
)
if not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成")
# 规划动作
with Timer("规划器", cycle_timers):
actions, _ = await self.action_planner.plan(
mode=mode,
loop_start_time=loop_start_time,
available_actions=available_actions,
)
actions, _ = await self.action_planner.plan(mode=mode)
async def execute_action(action_info):
"""执行单个动作的通用函数"""
@@ -312,6 +299,8 @@ class CycleProcessor:
logger.info(f"{self.log_prefix} 正在执行文本回复...")
for action in reply_actions:
target_user_id = action.get("action_message",{}).get("chat_info_user_id","")
action_message_test =action.get("action_message",{})
logger.info(f"如果你探到这条日志请把它复制下来发到Q群里,如果你探到这条日志请把它复制下来发到Q群里,如果你探到这条日志请把它复制下来发到Q群里,调试内容:{action_message_test}")
if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self:
logger.warning("选取的reply的目标为bot自己跳过reply action")
continue

View File

@@ -5,6 +5,7 @@ from src.person_info.relationship_builder_manager import RelationshipBuilder
from src.chat.express.expression_learner import ExpressionLearner
from src.chat.planner_actions.action_manager import ActionManager
from src.chat.chat_loop.hfc_utils import CycleDetail
from src.config.config import global_config
if TYPE_CHECKING:
from .sleep_manager.wakeup_manager import WakeUpManager
@@ -64,7 +65,8 @@ class HfcContext:
self.energy_manager: Optional["EnergyManager"] = None
self.sleep_manager: Optional["SleepManager"] = None
self.focus_energy = 1
# 从聊天流获取focus_energy,如果没有则使用配置文件中的值
self.focus_energy = getattr(self.chat_stream, "focus_energy", global_config.chat.focus_value)
self.no_reply_consecutive = 0
self.total_interest = 0.0
# breaking形式下的累积兴趣值

View File

@@ -1,243 +0,0 @@
"""
事件驱动的智能调度器
基于asyncio的精确定时事件调度系统替代轮询机制
"""
import asyncio
import time
import traceback
from datetime import datetime, timedelta
from typing import Dict, Callable, Any, Optional
from dataclasses import dataclass
from src.common.logger import get_logger
logger = get_logger("event_scheduler")
@dataclass
class ScheduledEvent:
"""调度事件数据类"""
event_id: str
trigger_time: datetime
callback: Callable
metadata: Dict[str, Any]
task: Optional[asyncio.Task] = None
class EventDrivenScheduler:
"""事件驱动的调度器"""
def __init__(self):
self.scheduled_events: Dict[str, ScheduledEvent] = {}
self._shutdown = False
async def schedule_event(
self,
event_id: str,
trigger_time: datetime,
callback: Callable,
metadata: Dict[str, Any] = None
) -> bool:
"""
调度一个事件在指定时间触发
Args:
event_id: 事件唯一标识
trigger_time: 触发时间
callback: 回调函数
metadata: 事件元数据
Returns:
bool: 调度成功返回True
"""
try:
if metadata is None:
metadata = {}
# 如果事件已存在,先取消
if event_id in self.scheduled_events:
await self.cancel_event(event_id)
# 计算延迟时间
now = datetime.now()
delay = (trigger_time - now).total_seconds()
if delay <= 0:
logger.warning(f"事件 {event_id} 的触发时间已过,立即执行")
# 立即执行
asyncio.create_task(self._execute_callback(event_id, callback, metadata))
return True
# 创建调度事件
scheduled_event = ScheduledEvent(
event_id=event_id,
trigger_time=trigger_time,
callback=callback,
metadata=metadata
)
# 创建异步任务
scheduled_event.task = asyncio.create_task(
self._wait_and_execute(scheduled_event)
)
self.scheduled_events[event_id] = scheduled_event
logger.info(f"调度事件 {event_id} 将在 {trigger_time} 触发 (延迟 {delay:.1f} 秒)")
return True
except Exception as e:
logger.error(f"调度事件失败: {e}")
return False
async def _wait_and_execute(self, event: ScheduledEvent):
"""等待并执行事件"""
try:
now = datetime.now()
delay = (event.trigger_time - now).total_seconds()
if delay > 0:
await asyncio.sleep(delay)
# 检查是否被取消
if self._shutdown or event.event_id not in self.scheduled_events:
return
# 执行回调
await self._execute_callback(event.event_id, event.callback, event.metadata)
except asyncio.CancelledError:
logger.info(f"事件 {event.event_id} 被取消")
except Exception as e:
logger.error(f"执行事件 {event.event_id} 时出错: {e}")
finally:
# 清理已完成的事件
if event.event_id in self.scheduled_events:
del self.scheduled_events[event.event_id]
async def _execute_callback(self, event_id: str, callback: Callable, metadata: Dict[str, Any]):
"""执行回调函数"""
try:
logger.info(f"执行调度事件: {event_id}")
# 根据回调函数签名调用
if asyncio.iscoroutinefunction(callback):
await callback(metadata)
else:
callback(metadata)
except Exception as e:
logger.error(f"执行回调函数失败: {e}")
logger.error(traceback.format_exc())
async def cancel_event(self, event_id: str) -> bool:
"""
取消一个调度事件
Args:
event_id: 事件ID
Returns:
bool: 取消成功返回True
"""
try:
if event_id in self.scheduled_events:
event = self.scheduled_events[event_id]
if event.task and not event.task.done():
event.task.cancel()
del self.scheduled_events[event_id]
logger.info(f"取消调度事件: {event_id}")
return True
return False
except Exception as e:
logger.error(f"取消事件失败: {e}")
return False
async def shutdown(self):
"""关闭调度器,取消所有事件"""
self._shutdown = True
for event_id in list(self.scheduled_events.keys()):
await self.cancel_event(event_id)
logger.info("事件调度器已关闭")
def get_scheduled_events(self) -> Dict[str, ScheduledEvent]:
"""获取所有调度事件"""
return self.scheduled_events.copy()
def get_event_count(self) -> int:
"""获取调度事件数量"""
return len(self.scheduled_events)
# 全局事件调度器实例
event_scheduler = EventDrivenScheduler()
# 便捷函数
async def schedule_reminder(
reminder_id: str,
reminder_time: datetime,
chat_id: str,
reminder_content: str,
callback: Callable
):
"""
调度提醒事件的便捷函数
Args:
reminder_id: 提醒唯一标识
reminder_time: 提醒时间
chat_id: 聊天ID
reminder_content: 提醒内容
callback: 回调函数
"""
metadata = {
"type": "reminder",
"chat_id": chat_id,
"content": reminder_content,
"created_at": datetime.now().isoformat()
}
return await event_scheduler.schedule_event(
event_id=reminder_id,
trigger_time=reminder_time,
callback=callback,
metadata=metadata
)
async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str, original_message: str = None):
"""执行提醒回调函数"""
try:
# 获取对应的subheartflow实例
from src.chat.heart_flow.heartflow import heartflow
subflow = await heartflow.get_or_create_subheartflow(subheartflow_id)
if not subflow:
logger.error(f"无法获取subheartflow实例: {subheartflow_id}")
return
# 创建主动思考事件,触发完整的思考流程
from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent
# 使用原始消息来构造reason如果没有原始消息则使用处理后的内容
reason_content = original_message if original_message else reminder_text
event = ProactiveTriggerEvent(
source="reminder_system",
reason=f"定时提醒:{reason_content}", # 这里传递完整的原始消息
metadata={
"reminder_text": reminder_text,
"original_message": original_message,
"trigger_time": datetime.now().isoformat()
}
)
# 通过subflow的HeartFChatting实例触发主动思考
await subflow.heart_fc_instance.proactive_thinker.think(event)
logger.info(f"已触发提醒的主动思考,内容: {reminder_text},没有传递那条消息吗?{original_message}")
except Exception as e:
logger.error(f"执行提醒回调时发生错误: {e}")
import traceback
traceback.print_exc()

View File

@@ -1,7 +1,5 @@
import time
import traceback
import orjson
import re
from typing import TYPE_CHECKING, Dict, Any
from src.common.logger import get_logger
@@ -12,7 +10,6 @@ from src.plugin_system.apis import generator_api
from src.plugin_system.apis.generator_api import process_human_text
from src.schedule.schedule_manager import schedule_manager
from src.plugin_system import tool_api
from src.plugin_system.base.component_types import ComponentType
from src.config.config import global_config
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages_with_id
from src.mood.mood_manager import mood_manager
@@ -120,64 +117,6 @@ class ProactiveThinker:
trigger_event (ProactiveTriggerEvent): 触发事件。
"""
try:
# 如果是提醒事件直接使用当前上下文执行at_user动作
if trigger_event.source == "reminder_system":
# 1. 获取上下文信息
metadata = trigger_event.metadata or {}
reminder_content = trigger_event.reason.replace("定时提醒:", "").strip()
# 2. 使用LLM智能解析目标用户名
target_user_name = None
# 首先尝试从完整的原始信息中解析(如果有的话)
full_content = trigger_event.reason
logger.info(f"{self.context.log_prefix} 解析提醒内容: '{full_content}'")
sender_name = metadata.get("sender_name")
target_user_name = await self._extract_target_user_with_llm(full_content, sender_name)
if not target_user_name:
logger.warning(f"无法从提醒 '{reminder_content}' 中确定目标用户,回退")
# 回退到生成普通提醒消息
fallback_action = {
"action_type": "proactive_reply",
"action_data": {"topic": f"定时提醒:{reminder_content}"},
"action_message": metadata
}
await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content)
return
# 3. 直接使用当前上下文的cycle_processor执行at_user动作
try:
success, _, _ = await self.cycle_processor._handle_action(
action="at_user",
reasoning="执行定时提醒",
action_data={
"user_name": target_user_name,
"at_message": reminder_content
},
cycle_timers={},
thinking_id="",
action_message=metadata,
)
if success:
logger.info(f"{self.context.log_prefix} 成功执行定时提醒艾特用户 {target_user_name}")
return
else:
raise Exception("at_user action failed")
except Exception as e:
logger.warning(f"{self.context.log_prefix} at_user动作执行失败: {e},回退到专用提醒回复")
# 回退到专用的定时提醒回复
fallback_action = {
"action_type": "proactive_reply",
"action_data": {"topic": f"定时提醒:{reminder_content}"},
"action_message": metadata
}
await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content)
return
else:
# 对于其他来源的主动思考,正常调用规划器
actions, _ = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE)
action_result = actions[0] if actions else {}
action_type = action_result.get("action_type")
@@ -199,163 +138,7 @@ class ProactiveThinker:
except Exception as e:
logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}")
logger.error(traceback.format_exc())
async def _extract_target_user_with_llm(self, reminder_content: str, sender_name: str) -> str:
"""
使用LLM从提醒内容中提取目标用户名
Args:
reminder_content: 完整的提醒内容
sender_name: 消息发送者的昵称
Returns:
提取出的用户名如果找不到则返回None
"""
try:
from src.llm_models.utils_model import LLMRequest
from src.config.config import model_config
bot_name = global_config.bot.nickname
user_extraction_prompt = f'''
从以下提醒消息中提取需要被提醒的目标用户名。
**重要认知**:
- 你的名字是"{bot_name}"。当消息中提到"{bot_name}"时,通常是在称呼你。
- 消息的发送者是"{sender_name}"。当消息中出现""""等第一人称代词时,指代的就是"{sender_name}"
提醒消息: "{reminder_content}"
规则:
1. 分析消息,找出真正需要被提醒的人。
2. 如果提醒目标是第一人称(如""),那么目标就是发送者"{sender_name}"
3. **绝对不能**提取你自己的名字("{bot_name}")作为目标。
4. 只提取最关键的人名,不要包含多余的词语。
5. 如果没有明确的提醒目标(既不是其他人,也不是发送者自己),请回答""
示例:
- 消息: "定时提醒:{bot_name}10分钟后提醒我去打深渊" -> "{sender_name}"
- 消息: "定时提醒:{bot_name},提醒阿范一分钟后去写模组" -> "阿范"
- 消息: "定时提醒:一分钟后提醒一闪喝水" -> "一闪"
- 消息: "定时提醒:喝水" -> ""
- 消息: "定时提醒:{bot_name},记得休息" -> ""
请直接输出提取到的用户名,如果不存在则输出""
'''
llm_request = LLMRequest(
model_set=model_config.model_task_config.utils_small,
request_type="reminder_user_extraction"
)
response, _ = await llm_request.generate_response_async(prompt=user_extraction_prompt)
if response and response.strip() != "":
logger.info(f"LLM成功提取目标用户: '{response.strip()}'")
return response.strip()
else:
logger.warning(f"LLM未能从 '{reminder_content}' 中提取目标用户")
return None
except Exception as e:
logger.error(f"使用LLM提取用户名时出错: {e}")
return None
async def _generate_reminder_proactive_reply(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent, reminder_content: str):
"""
为定时提醒事件生成专用的主动回复
Args:
action_result: 动作结果
trigger_event: 触发事件
reminder_content: 提醒内容
"""
try:
logger.info(f"{self.context.log_prefix} 生成定时提醒专用回复: '{reminder_content}'")
# 获取基本信息
bot_name = global_config.bot.nickname
personality = global_config.personality
identity_block = (
f"你的名字是{bot_name}\n"
f"关于你:{personality.personality_core},并且{personality.personality_side}\n"
f"你的身份是{personality.identity},平时说话风格是{personality.reply_style}"
)
mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}"
# 获取日程信息
schedule_block = "你今天没有日程安排。"
if global_config.planning_system.schedule_enable:
if current_activity := schedule_manager.get_current_activity():
schedule_block = f"你当前正在:{current_activity}"
# 为定时提醒定制的专用提示词
reminder_prompt = f"""
## 你的角色
{identity_block}
## 你的心情
{mood_block}
## 你今天的日程安排
{schedule_block}
## 定时提醒任务
你收到了一个定时提醒:"{reminder_content}"
这是一个自动触发的提醒事件,你需要根据提醒内容发送一条友好的提醒消息。
## 任务要求
- 这是一个定时提醒,要体现出你的贴心和关怀
- 根据提醒内容的具体情况(如"喝水""休息"等)给出相应的提醒
- 保持你一贯的温暖、俏皮风格
- 可以加上一些鼓励或关心的话语
- 直接输出提醒消息,不要解释为什么要提醒
请生成一条温暖贴心的提醒消息。
"""
response_text = await generator_api.generate_response_custom(
chat_stream=self.context.chat_stream,
prompt=reminder_prompt,
request_type="chat.replyer.reminder",
)
if response_text:
response_set = process_human_text(
content=response_text,
enable_splitter=global_config.response_splitter.enable,
enable_chinese_typo=global_config.chinese_typo.enable,
)
await self.cycle_processor.response_handler.send_response(
response_set, time.time(), action_result.get("action_message")
)
await store_action_info(
chat_stream=self.context.chat_stream,
action_name="reminder_reply",
action_data={"reminder_content": reminder_content, "response": response_text},
action_prompt_display=f"定时提醒回复: {reminder_content}",
action_done=True,
)
logger.info(f"{self.context.log_prefix} 成功发送定时提醒回复: {response_text}")
else:
logger.error(f"{self.context.log_prefix} 定时提醒回复生成失败。")
except Exception as e:
logger.error(f"{self.context.log_prefix} 生成定时提醒回复时异常: {e}")
logger.error(traceback.format_exc())
async def _get_reminder_context(self, message_id: str) -> str:
"""获取提醒消息的上下文"""
try:
# 只获取那一条消息
message_record = await db_get(Messages, {"message_id": message_id}, single_result=True)
if message_record:
# 使用 build_readable_messages_with_id 来格式化单条消息
chat_context_block, _ = build_readable_messages_with_id(messages=[message_record])
return chat_context_block
return "无法加载相关的聊天记录。"
except Exception as e:
logger.error(f"{self.context.log_prefix} 获取提醒上下文失败: {e}")
return "无法加载相关的聊天记录。"
async def _generate_proactive_content_and_send(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent):
"""
@@ -380,10 +163,10 @@ class ProactiveThinker:
web_search_tool = tool_api.get_tool_instance("web_search")
if web_search_tool:
try:
search_result_dict = await web_search_tool.execute(search_query=topic, max_results=10)
search_result_dict = await web_search_tool.execute(function_args={"keyword": topic, "max_results": 10})
except TypeError:
try:
search_result_dict = await web_search_tool.execute(keyword=topic, max_results=10)
search_result_dict = await web_search_tool.execute(function_args={"keyword": topic, "max_results": 10})
except TypeError:
logger.warning(f"{self.context.log_prefix} 网络搜索工具参数不匹配,跳过搜索")
news_block = "跳过网络搜索。"
@@ -397,10 +180,6 @@ class ProactiveThinker:
logger.warning(f"{self.context.log_prefix} 未找到 web_search 工具实例。")
except Exception as e:
logger.error(f"{self.context.log_prefix} 主动思考时网络搜索失败: {e}")
if trigger_event.source == "reminder_system" and trigger_event.related_message_id:
chat_context_block = await self._get_reminder_context(trigger_event.related_message_id)
else:
message_list = get_raw_msg_before_timestamp_with_chat(
chat_id=self.context.stream_id,
timestamp=time.time(),

View File

@@ -1,260 +0,0 @@
"""
智能提醒分析器
使用LLM分析用户消息识别提醒请求并提取时间和内容信息
"""
import re
import json
from datetime import datetime, timedelta
from typing import Optional
from src.common.logger import get_logger
from src.llm_models.utils_model import LLMRequest
from src.config.config import model_config
logger = get_logger("smart_reminder")
class ReminderEvent:
"""提醒事件数据类"""
def __init__(self, user_id: str, reminder_time: datetime, content: str, confidence: float):
self.user_id = user_id
self.reminder_time = reminder_time
self.content = content
self.confidence = confidence
def __repr__(self):
return f"ReminderEvent(user_id={self.user_id}, time={self.reminder_time}, content={self.content}, confidence={self.confidence})"
def to_dict(self):
return {
'user_id': self.user_id,
'reminder_time': self.reminder_time.isoformat(),
'content': self.content,
'confidence': self.confidence
}
class SmartReminderAnalyzer:
"""智能提醒分析器"""
def __init__(self):
self.confidence_threshold = 0.7
# 使用规划器模型进行分析
self.analyzer_llm = LLMRequest(
model_set=model_config.model_task_config.utils_small,
request_type="reminder_analyzer"
)
async def analyze_message(self, user_id: str, message: str) -> Optional[ReminderEvent]:
"""分析消息是否包含提醒请求
Args:
user_id: 用户ID
message: 用户消息内容
Returns:
ReminderEvent对象如果没有检测到提醒请求则返回None
"""
if not message or len(message.strip()) == 0:
return None
logger.debug(f"分析消息中的提醒请求: {message}")
# 使用LLM分析消息
analysis_result = await self._analyze_with_llm(message)
if not analysis_result or analysis_result.get('confidence', 0) < 0.5: # 降低置信度阈值
return None
try:
# 解析时间
reminder_time = self._parse_relative_time(analysis_result['relative_time'])
if not reminder_time:
return None
# 创建提醒事件
reminder_event = ReminderEvent(
user_id=user_id,
reminder_time=reminder_time,
content=analysis_result.get('content', '提醒'),
confidence=analysis_result['confidence']
)
logger.info(f"检测到提醒请求: {reminder_event}")
return reminder_event
except Exception as e:
logger.error(f"创建提醒事件失败: {e}")
return None
async def _analyze_with_llm(self, message: str) -> Optional[dict]:
"""使用LLM分析消息中的提醒请求"""
try:
prompt = f"""分析以下消息是否包含提醒请求。
消息: {message}
当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
请判断用户是否想要设置提醒,如果是,请提取:
1. 是否包含提醒请求 (has_reminder: true/false)
2. 置信度 (confidence: 0.0-1.0)
3. 相对时间表达 (relative_time: 标准化的时间表达,例如将'半小时后'转换为'30分钟后', '明天下午三点'转换为'明天15点')
4. 提醒内容 (content: 提醒的具体内容)
5. 分析原因 (reasoning: 判断理由)
请以JSON格式输出:
{{
"has_reminder": true/false,
"confidence": 0.0-1.0,
"relative_time": "标准化的时间表达 (例如 '30分钟后', '2小时后')",
"content": "提醒内容",
"reasoning": "判断理由"
}}"""
response, _ = await self.analyzer_llm.generate_response_async(prompt=prompt)
if not response:
return None
# 解析JSON响应处理可能的markdown包装
try:
# 清理响应文本
cleaned_response = response.strip()
# 移除markdown代码块包装
if cleaned_response.startswith('```json'):
cleaned_response = cleaned_response[7:] # 移除 ```json
elif cleaned_response.startswith('```'):
cleaned_response = cleaned_response[3:] # 移除 ```
if cleaned_response.endswith('```'):
cleaned_response = cleaned_response[:-3] # 移除结尾的 ```
cleaned_response = cleaned_response.strip()
# 解析JSON
result = json.loads(cleaned_response)
if result.get('has_reminder', False):
logger.info(f"LLM分析结果: {result}")
return result
except json.JSONDecodeError as e:
logger.error(f"LLM响应JSON解析失败: {response}, Error: {e}")
# 尝试使用更宽松的JSON修复
try:
import re
# 提取JSON部分的正则表达式
json_match = re.search(r'\{.*\}', cleaned_response, re.DOTALL)
if json_match:
json_str = json_match.group()
result = json.loads(json_str)
if result.get('has_reminder', False):
logger.info(f"备用解析成功: {result}")
return result
except Exception as fallback_error:
logger.error(f"备用JSON解析也失败了: {fallback_error}")
except Exception as e:
logger.error(f"LLM分析失败: {e}")
return None
def _parse_relative_time(self, time_expr: str) -> Optional[datetime]:
"""解析时间表达式(支持相对时间和绝对时间)"""
try:
now = datetime.now()
# 1. 匹配相对时间X分钟后包括中文数字
# 先尝试匹配阿拉伯数字
minutes_match = re.search(r'(\d+)\s*分钟后', time_expr)
if minutes_match:
minutes = int(minutes_match.group(1))
result = now + timedelta(minutes=minutes)
logger.info(f"相对时间解析结果: timedelta(minutes={minutes}) -> {result}")
return result
# 匹配中文数字分钟
chinese_minutes_patterns = [
(r'一分钟后', 1), (r'二分钟后', 2), (r'两分钟后', 2), (r'三分钟后', 3), (r'四分钟后', 4), (r'五分钟后', 5),
(r'六分钟后', 6), (r'七分钟后', 7), (r'八分钟后', 8), (r'九分钟后', 9), (r'十分钟后', 10),
(r'十一分钟后', 11), (r'十二分钟后', 12), (r'十三分钟后', 13), (r'十四分钟后', 14), (r'十五分钟后', 15),
(r'二十分钟后', 20), (r'三十分钟后', 30), (r'四十分钟后', 40), (r'五十分钟后', 50), (r'六十分钟后', 60)
]
for pattern, minutes in chinese_minutes_patterns:
if re.search(pattern, time_expr):
result = now + timedelta(minutes=minutes)
logger.info(f"中文时间解析结果: {pattern} -> {minutes}分钟 -> {result}")
return result
# 2. 匹配相对时间X小时后
hours_match = re.search(r'(\d+)\s*小时后', time_expr)
if hours_match:
hours = int(hours_match.group(1))
result = now + timedelta(hours=hours)
logger.info(f"相对时间解析结果: timedelta(hours={hours})")
return result
# 3. 匹配相对时间X秒后
seconds_match = re.search(r'(\d+)\s*秒后', time_expr)
if seconds_match:
seconds = int(seconds_match.group(1))
result = now + timedelta(seconds=seconds)
logger.info(f"相对时间解析结果: timedelta(seconds={seconds})")
return result
# 4. 匹配明天+具体时间明天下午2点、明天上午10点
tomorrow_match = re.search(r'明天.*?(\d{1,2})\s*[点时]', time_expr)
if tomorrow_match:
hour = int(tomorrow_match.group(1))
# 如果是下午且小于12加12小时
if '下午' in time_expr and hour < 12:
hour += 12
elif '上午' in time_expr and hour == 12:
hour = 0
tomorrow = now + timedelta(days=1)
result = tomorrow.replace(hour=hour, minute=0, second=0, microsecond=0)
logger.info(f"绝对时间解析结果: 明天{hour}")
return result
# 5. 匹配今天+具体时间今天下午3点、今天晚上8点
today_match = re.search(r'今天.*?(\d{1,2})\s*[点时]', time_expr)
if today_match:
hour = int(today_match.group(1))
# 如果是下午且小于12加12小时
if '下午' in time_expr and hour < 12:
hour += 12
elif '晚上' in time_expr and hour < 12:
hour += 12
elif '上午' in time_expr and hour == 12:
hour = 0
result = now.replace(hour=hour, minute=0, second=0, microsecond=0)
# 如果时间已过,设为明天
if result <= now:
result += timedelta(days=1)
logger.info(f"绝对时间解析结果: 今天{hour}")
return result
# 6. 匹配纯数字时间14点、2点
pure_time_match = re.search(r'(\d{1,2})\s*[点时]', time_expr)
if pure_time_match:
hour = int(pure_time_match.group(1))
result = now.replace(hour=hour, minute=0, second=0, microsecond=0)
# 如果时间已过,设为明天
if result <= now:
result += timedelta(days=1)
logger.info(f"绝对时间解析结果: {hour}")
return result
except Exception as e:
logger.error(f"时间解析失败: {time_expr}, Error: {e}")
return None
# 全局智能提醒分析器实例
smart_reminder_analyzer = SmartReminderAnalyzer()

View File

@@ -1,5 +1,4 @@
import time
import orjson
import random
from typing import Dict, Any, Tuple

View File

@@ -1,4 +1,3 @@
import asyncio
from src.common.logger import get_logger
from ..hfc_context import HfcContext

View File

@@ -1,7 +1,7 @@
import asyncio
import random
from datetime import datetime, timedelta, date
from typing import Optional, TYPE_CHECKING, List, Dict, Any
from typing import Optional, TYPE_CHECKING
from src.common.logger import get_logger
from src.config.config import global_config

View File

@@ -91,7 +91,7 @@ class FrequencyBasedTrigger:
# 6. 直接调用 proactive_thinker
event = ProactiveTriggerEvent(
source="frequency_analyzer",
reason=f"User is in a high-frequency chat period."
reason="User is in a high-frequency chat period."
)
await sub_heartflow.heart_fc_instance.proactive_thinker.think(event)

View File

@@ -17,7 +17,6 @@ from src.chat.utils.chat_message_builder import replace_user_references_sync
from src.common.logger import get_logger
from src.person_info.relationship_manager import get_relationship_manager
from src.mood.mood_manager import mood_manager
from src.chat.message_receive.chat_stream import get_chat_manager
if TYPE_CHECKING:
from src.chat.heart_flow.sub_heartflow import SubHeartflow
@@ -118,8 +117,7 @@ class HeartFCMessageReceiver:
主要流程:
1. 消息解析与初始化
2. 智能提醒分析
3. 消息缓冲处理
2. 消息缓冲处理
4. 过滤检查
5. 兴趣度计算
6. 关系处理
@@ -132,99 +130,7 @@ class HeartFCMessageReceiver:
userinfo = message.message_info.user_info
chat = message.chat_stream
# 2. 智能提醒分析 - 检查用户是否请求提醒
from src.chat.chat_loop.proactive.smart_reminder_analyzer import smart_reminder_analyzer
from src.chat.chat_loop.proactive.event_scheduler import event_scheduler
try:
reminder_event = await smart_reminder_analyzer.analyze_message(
userinfo.user_id, # type: ignore
message.processed_plain_text
)
if reminder_event:
logger.info(f"检测到提醒请求: {reminder_event}")
# 创建提醒回调函数
async def reminder_callback(metadata):
"""提醒执行回调函数 - 触发完整的主动思考流程"""
try:
# 获取对应的subheartflow实例
from src.chat.heart_flow.heartflow import heartflow
subflow = await heartflow.get_or_create_subheartflow(metadata.get("chat_id"))
if not subflow:
logger.error(f"无法获取subheartflow实例: {metadata.get('chat_id')}")
return
# 创建主动思考事件,触发完整的思考流程
from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent
reminder_content = metadata.get('content', '提醒时间到了')
# 使用原始消息内容作为reason如果没有则使用处理后的内容
original_message = metadata.get('original_message', '')
reason_content = original_message if original_message else reminder_content
event = ProactiveTriggerEvent(
source="reminder_system",
reason=f"定时提醒:{reason_content}",
metadata=metadata,
related_message_id=metadata.get("original_message_id")
)
# 通过subflow的HeartFChatting实例触发主动思考
await subflow.heart_fc_instance.proactive_thinker.think(event)
logger.info(f"已触发提醒的主动思考,内容: {reminder_content}")
except Exception as callback_error:
logger.error(f"执行提醒回调失败: {callback_error}")
import traceback
logger.error(traceback.format_exc())
# Fallback: 如果主动思考失败,直接发送提醒消息
try:
from src.plugin_system.apis.send_api import text_to_stream
reminder_content = metadata.get('content', '提醒时间到了')
await text_to_stream(
text=f"⏰ 提醒:{reminder_content}",
stream_id=metadata.get("chat_id"),
typing=False
)
logger.info(f"Fallback提醒消息已发送: {reminder_content}")
except Exception as fallback_error:
logger.error(f"Fallback提醒也失败了: {fallback_error}")
# 调度提醒事件
event_id = f"reminder_{reminder_event.user_id}_{int(reminder_event.reminder_time.timestamp())}"
metadata = {
"type": "reminder",
"user_id": reminder_event.user_id,
"sender_name": userinfo.user_nickname, # 添加发送者昵称
"platform": chat.platform,
"chat_id": chat.stream_id,
"content": reminder_event.content,
"confidence": reminder_event.confidence,
"created_at": datetime.now().isoformat(),
"original_message_id": message.message_info.message_id,
"original_message": message.processed_plain_text # 保存完整的原始消息
}
success = await event_scheduler.schedule_event(
event_id=event_id,
trigger_time=reminder_event.reminder_time,
callback=reminder_callback,
metadata=metadata
)
if success:
logger.info(f"提醒事件调度成功: {event_id}")
else:
logger.error(f"提醒事件调度失败: {event_id}")
except Exception as e:
logger.error(f"智能提醒分析失败: {e}")
# 3. 兴趣度计算与更新
# 2. 兴趣度计算与更新
interested_rate, is_mentioned, keywords = await _calculate_interest(message)
message.interest_value = interested_rate
message.is_mentioned = is_mentioned

View File

@@ -30,8 +30,13 @@ DATA_PATH = os.path.join(ROOT_PATH, "data")
qa_manager = None
inspire_manager = None
# 检查LPMM知识库是否启用
if global_config.lpmm_knowledge.enable:
def initialize_lpmm_knowledge():
"""初始化LPMM知识库"""
global qa_manager, inspire_manager
# 检查LPMM知识库是否启用
if global_config.lpmm_knowledge.enable:
logger.info("正在初始化Mai-LPMM")
logger.info("创建LLM客户端")
@@ -74,6 +79,6 @@ if global_config.lpmm_knowledge.enable:
# embed_manager,
# llm_client_list[global_config["embedding"]["provider"]],
# )
else:
else:
logger.info("LPMM知识库已禁用跳过初始化")
# 创建空的占位符对象,避免导入错误

View File

@@ -83,7 +83,8 @@ class ChatStream:
self.sleep_pressure = data.get("sleep_pressure", 0.0) if data else 0.0
self.saved = False
self.context: ChatMessageContext = None # type: ignore # 用于存储该聊天的上下文信息
self.focus_energy = 1
# 从配置文件中读取focus_value如果没有则使用默认值1.0
self.focus_energy = data.get("focus_energy", global_config.chat.focus_value) if data else global_config.chat.focus_value
self.no_reply_consecutive = 0
self.breaking_accumulated_interest = 0.0
@@ -98,6 +99,7 @@ class ChatStream:
"last_active_time": self.last_active_time,
"energy_value": self.energy_value,
"sleep_pressure": self.sleep_pressure,
"focus_energy": self.focus_energy,
"breaking_accumulated_interest": self.breaking_accumulated_interest,
}
@@ -360,6 +362,7 @@ class ChatManager:
"group_name": group_info_d["group_name"] if group_info_d else "",
"energy_value": s_data_dict.get("energy_value", 5.0),
"sleep_pressure": s_data_dict.get("sleep_pressure", 0.0),
"focus_energy": s_data_dict.get("focus_energy", global_config.chat.focus_value),
}
# 根据数据库类型选择插入语句
@@ -421,6 +424,7 @@ class ChatManager:
"last_active_time": model_instance.last_active_time,
"energy_value": model_instance.energy_value,
"sleep_pressure": model_instance.sleep_pressure,
"focus_energy": getattr(model_instance, "focus_energy", global_config.chat.focus_value),
}
loaded_streams_data.append(data_for_from_dict)
session.commit()

View File

@@ -0,0 +1,57 @@
"""
PlanExecutor: 接收 Plan 对象并执行其中的所有动作。
"""
from src.chat.planner_actions.action_manager import ActionManager
from src.common.data_models.info_data_model import Plan
from src.common.logger import get_logger
logger = get_logger("plan_executor")
class PlanExecutor:
"""
负责接收一个 Plan 对象,并执行其中最终确定的所有动作。
这个类是规划流程的最后一步,将规划结果转化为实际的动作执行。
Attributes:
action_manager (ActionManager): 用于实际执行各种动作的管理器实例。
"""
def __init__(self, action_manager: ActionManager):
"""
初始化 PlanExecutor。
Args:
action_manager (ActionManager): 一个 ActionManager 实例,用于执行动作。
"""
self.action_manager = action_manager
async def execute(self, plan: Plan):
"""
遍历并执行 Plan 对象中 `decided_actions` 列表里的所有动作。
如果动作类型为 "no_action",则会记录原因并跳过。
否则,它将调用 ActionManager 来执行相应的动作。
Args:
plan (Plan): 包含待执行动作列表的 Plan 对象。
"""
if not plan.decided_actions:
logger.info("没有需要执行的动作。")
return
for action_info in plan.decided_actions:
if action_info.action_type == "no_action":
logger.info(f"规划器决策不执行动作,原因: {action_info.reasoning}")
continue
# TODO: 对接 ActionManager 的执行方法
# 这是一个示例调用,需要根据 ActionManager 的最终实现进行调整
logger.info(f"执行动作: {action_info.action_type}, 原因: {action_info.reasoning}")
# await self.action_manager.execute_action(
# action_name=action_info.action_type,
# action_data=action_info.action_data,
# reasoning=action_info.reasoning,
# action_message=action_info.action_message,
# )

View File

@@ -0,0 +1,350 @@
"""
PlanFilter: 接收 Plan 对象,根据不同模式的逻辑进行筛选,决定最终要执行的动作。
"""
import orjson
import time
import traceback
from datetime import datetime
from typing import Any, Dict, List, Optional
from json_repair import repair_json
from src.chat.memory_system.Hippocampus import hippocampus_manager
from src.chat.utils.chat_message_builder import (
build_readable_actions,
build_readable_messages_with_id,
get_actions_by_timestamp_with_chat,
)
from src.chat.utils.prompt import global_prompt_manager
from src.common.data_models.info_data_model import ActionPlannerInfo, Plan
from src.common.logger import get_logger
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
from src.mood.mood_manager import mood_manager
from src.plugin_system.base.component_types import ActionInfo, ChatMode
from src.schedule.schedule_manager import schedule_manager
logger = get_logger("plan_filter")
class PlanFilter:
"""
根据 Plan 中的模式和信息,筛选并决定最终的动作。
"""
def __init__(self):
self.planner_llm = LLMRequest(
model_set=model_config.model_task_config.planner, request_type="planner"
)
self.last_obs_time_mark = 0.0
async def filter(self, plan: Plan) -> Plan:
"""
执行筛选逻辑,并填充 Plan 对象的 decided_actions 字段。
"""
try:
prompt, used_message_id_list = await self._build_prompt(plan)
plan.llm_prompt = prompt
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
if llm_content:
logger.debug(f"LLM a原始返回: {llm_content}")
parsed_json = orjson.loads(repair_json(llm_content))
if isinstance(parsed_json, dict):
parsed_json = [parsed_json]
if isinstance(parsed_json, list):
final_actions = []
reply_action_added = False
# 定义回复类动作的集合,方便扩展
reply_action_types = {"reply", "proactive_reply"}
for item in parsed_json:
if not isinstance(item, dict):
continue
# 预解析 action_type 来进行判断
action_type = item.get("action", "no_action")
if action_type in reply_action_types:
if not reply_action_added:
final_actions.extend(
await self._parse_single_action(
item, used_message_id_list, plan
)
)
reply_action_added = True
else:
# 非回复类动作直接添加
final_actions.extend(
await self._parse_single_action(
item, used_message_id_list, plan
)
)
plan.decided_actions = self._filter_no_actions(final_actions)
except Exception as e:
logger.error(f"筛选 Plan 时出错: {e}\n{traceback.format_exc()}")
plan.decided_actions = [
ActionPlannerInfo(action_type="no_action", reasoning=f"筛选时出错: {e}")
]
return plan
async def _build_prompt(self, plan: Plan) -> tuple[str, list]:
"""
根据 Plan 对象构建提示词。
"""
try:
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
bot_name = global_config.bot.nickname
bot_nickname = (
f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else ""
)
bot_core_personality = global_config.personality.personality_core
identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}"
schedule_block = ""
if global_config.planning_system.schedule_enable:
if current_activity := schedule_manager.get_current_activity():
schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。"
mood_block = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(plan.chat_id)
mood_block = f"你现在的心情是:{chat_mood.mood_state}"
if plan.mode == ChatMode.PROACTIVE:
long_term_memory_block = await self._get_long_term_memory_context()
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=[msg.flatten() for msg in plan.chat_history],
timestamp_mode="normal",
truncate=False,
show_actions=False,
)
prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt")
actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=plan.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
prompt = prompt_template.format(
time_block=time_block,
identity_block=identity_block,
schedule_block=schedule_block,
mood_block=mood_block,
long_term_memory_block=long_term_memory_block,
chat_content_block=chat_content_block or "最近没有聊天内容。",
actions_before_now_block=actions_before_now_block,
)
return prompt, message_id_list
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=[msg.flatten() for msg in plan.chat_history],
timestamp_mode="normal",
read_mark=self.last_obs_time_mark,
truncate=True,
show_actions=True,
)
actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=plan.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
self.last_obs_time_mark = time.time()
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你"
if plan.mode == ChatMode.FOCUS:
no_action_block = """
动作no_action
动作描述:不选择任何动作
{{
"action": "no_action",
"reason":"不动作的原因"
}}
动作no_reply
动作描述:不进行回复,等待合适的回复时机
- 当你刚刚发送了消息没有人回复时选择no_reply
- 当你一次发送了太多消息为了避免打扰聊天节奏选择no_reply
{{
"action": "no_reply",
"reason":"不回复的原因"
}}
"""
else: # NORMAL Mode
no_action_block = """重要说明:
- 'reply' 表示只进行普通聊天回复,不执行任何额外动作
- 其他action表示在普通回复的基础上执行相应的额外动作
{{
"action": "reply",
"target_message_id":"触发action的消息id",
"reason":"回复的原因"
}}"""
is_group_chat = plan.target_info.platform == "group" if plan.target_info else True
chat_context_description = "你现在正在一个群聊中"
if not is_group_chat and plan.target_info:
chat_target_name = plan.target_info.person_name or plan.target_info.user_nickname or "对方"
chat_context_description = f"你正在和 {chat_target_name} 私聊"
action_options_block = await self._build_action_options(plan.available_actions)
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
custom_prompt_block = ""
if global_config.custom_prompt.planner_custom_prompt_content:
custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content
users_in_chat_str = "" # TODO: Re-implement user list fetching if needed
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
prompt = planner_prompt_template.format(
schedule_block=schedule_block,
mood_block=mood_block,
time_block=time_block,
chat_context_description=chat_context_description,
chat_content_block=chat_content_block,
actions_before_now_block=actions_before_now_block,
mentioned_bonus=mentioned_bonus,
no_action_block=no_action_block,
action_options_text=action_options_block,
moderation_prompt=moderation_prompt_block,
identity_block=identity_block,
custom_prompt_block=custom_prompt_block,
bot_name=bot_name,
users_in_chat=users_in_chat_str
)
return prompt, message_id_list
except Exception as e:
logger.error(f"构建 Planner 提示词时出错: {e}")
logger.error(traceback.format_exc())
return "构建 Planner Prompt 时出错", []
async def _parse_single_action(
self, action_json: dict, message_id_list: list, plan: Plan
) -> List[ActionPlannerInfo]:
parsed_actions = []
try:
action = action_json.get("action", "no_action")
reasoning = action_json.get("reason", "未提供原因")
action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]}
target_message_obj = None
if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]:
if target_message_id := action_json.get("target_message_id"):
target_message_dict = self._find_message_by_id(target_message_id, message_id_list)
else:
# 如果LLM没有指定target_message_id我们就默认选择最新的一条消息
target_message_dict = self._get_latest_message(message_id_list)
if target_message_dict:
from src.common.data_models.database_data_model import DatabaseMessages
target_message_obj = DatabaseMessages(**target_message_dict)
available_action_names = list(plan.available_actions.keys())
if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names:
reasoning = f"LLM 返回了当前不可用的动作 '{action}'。原始理由: {reasoning}"
action = "no_action"
parsed_actions.append(
ActionPlannerInfo(
action_type=action,
reasoning=reasoning,
action_data=action_data,
action_message=target_message_obj,
available_actions=plan.available_actions,
)
)
except Exception as e:
logger.error(f"解析单个action时出错: {e}")
parsed_actions.append(
ActionPlannerInfo(
action_type="no_action",
reasoning=f"解析action时出错: {e}",
)
)
return parsed_actions
def _filter_no_actions(
self, action_list: List[ActionPlannerInfo]
) -> List[ActionPlannerInfo]:
non_no_actions = [a for a in action_list if a.action_type not in ["no_action", "no_reply"]]
if non_no_actions:
return non_no_actions
return action_list[:1] if action_list else []
async def _get_long_term_memory_context(self) -> str:
try:
now = datetime.now()
keywords = ["今天", "日程", "计划"]
if 5 <= now.hour < 12:
keywords.append("早上")
elif 12 <= now.hour < 18:
keywords.append("中午")
else:
keywords.append("晚上")
retrieved_memories = await hippocampus_manager.get_memory_from_topic(
valid_keywords=keywords, max_memory_num=5, max_memory_length=1
)
if not retrieved_memories:
return "最近没有什么特别的记忆。"
memory_statements = [f"关于'{topic}', 你记得'{memory_item}'" for topic, memory_item in retrieved_memories]
return " ".join(memory_statements)
except Exception as e:
logger.error(f"获取长期记忆时出错: {e}")
return "回忆时出现了一些问题。"
async def _build_action_options(self, current_available_actions: Dict[str, ActionInfo]) -> str:
action_options_block = ""
for action_name, action_info in current_available_actions.items():
param_text = ""
if action_info.action_parameters:
param_text = "\n" + "\n".join(
f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items()
)
require_text = "\n".join(f"- {req}" for req in action_info.action_require)
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
action_options_block += using_action_prompt.format(
action_name=action_name,
action_description=action_info.description,
action_parameters=param_text,
action_require=require_text,
)
return action_options_block
def _find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]:
if message_id.isdigit():
message_id = f"m{message_id}"
for item in message_id_list:
if item.get("id") == message_id:
return item.get("message")
return None
def _get_latest_message(self, message_id_list: list) -> Optional[Dict[str, Any]]:
if not message_id_list:
return None
return message_id_list[-1].get("message")

View File

@@ -0,0 +1,110 @@
"""
PlanGenerator: 负责搜集和汇总所有决策所需的信息,生成一个未经筛选的“原始计划” (Plan)。
"""
import time
from typing import Dict, Optional, Tuple
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
from src.chat.utils.utils import get_chat_type_and_target_info
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.data_models.info_data_model import Plan, TargetPersonInfo
from src.config.config import global_config
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType
from src.plugin_system.core.component_registry import component_registry
class PlanGenerator:
"""
PlanGenerator 负责在规划流程的初始阶段收集所有必要信息。
它会汇总以下信息来构建一个“原始”的 Plan 对象,该对象后续会由 PlanFilter 进行筛选:
- 当前聊天信息 (ID, 目标用户)
- 当前可用的动作列表
- 最近的聊天历史记录
Attributes:
chat_id (str): 当前聊天的唯一标识符。
action_manager (ActionManager): 用于获取可用动作列表的管理器。
"""
def __init__(self, chat_id: str):
"""
初始化 PlanGenerator。
Args:
chat_id (str): 当前聊天的 ID。
"""
from src.chat.planner_actions.action_manager import ActionManager
self.chat_id = chat_id
# 注意ActionManager 可能需要根据实际情况初始化
self.action_manager = ActionManager()
async def generate(self, mode: ChatMode) -> Plan:
"""
收集所有信息,生成并返回一个初始的 Plan 对象。
这个 Plan 对象包含了决策所需的所有上下文信息。
Args:
mode (ChatMode): 当前的聊天模式。
Returns:
Plan: 一个填充了初始上下文信息的 Plan 对象。
"""
_is_group_chat, chat_target_info_dict = get_chat_type_and_target_info(self.chat_id)
target_info = None
if chat_target_info_dict:
target_info = TargetPersonInfo(**chat_target_info_dict)
available_actions = self._get_available_actions()
chat_history_raw = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size),
)
chat_history = [DatabaseMessages(**msg) for msg in chat_history_raw]
plan = Plan(
chat_id=self.chat_id,
mode=mode,
available_actions=available_actions,
chat_history=chat_history,
target_info=target_info,
)
return plan
def _get_available_actions(self) -> Dict[str, "ActionInfo"]:
"""
从 ActionManager 和组件注册表中获取当前所有可用的动作。
它会合并已注册的动作和系统级动作(如 "no_reply"
并以字典形式返回。
Returns:
Dict[str, "ActionInfo"]: 一个字典,键是动作名称,值是 ActionInfo 对象。
"""
current_available_actions_dict = self.action_manager.get_using_actions()
all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore
ComponentType.ACTION
)
current_available_actions = {}
for action_name in current_available_actions_dict:
if action_name in all_registered_actions:
current_available_actions[action_name] = all_registered_actions[action_name]
no_reply_info = ActionInfo(
name="no_reply",
component_type=ComponentType.ACTION,
description="系统级动作:选择不回复消息的决策",
action_parameters={},
activation_keywords=[],
plugin_name="SYSTEM",
enabled=True,
parallel_action=False,
)
current_available_actions["no_reply"] = no_reply_info
return current_available_actions

View File

@@ -1,698 +1,87 @@
import orjson
import time
import traceback
import asyncio
import math
import random
import json
from typing import Dict, Any, Optional, Tuple, List, TYPE_CHECKING
from rich.traceback import install
from datetime import datetime
from json_repair import repair_json
"""
主规划器入口,负责协调 PlanGenerator, PlanFilter, 和 PlanExecutor。
"""
from dataclasses import asdict
from typing import Dict, List, Optional, Tuple
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config
from src.common.logger import get_logger
from src.chat.utils.prompt import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import (
build_readable_actions,
get_actions_by_timestamp_with_chat,
build_readable_messages_with_id,
get_raw_msg_before_timestamp_with_chat,
)
from src.chat.utils.utils import get_chat_type_and_target_info
from src.chat.planner_actions.action_manager import ActionManager
from src.chat.message_receive.chat_stream import get_chat_manager
from src.plugin_system.base.component_types import (
ActionInfo,
ChatMode,
ComponentType,
ActionActivationType,
)
from src.plugin_system.core.component_registry import component_registry
from src.schedule.schedule_manager import schedule_manager
from src.mood.mood_manager import mood_manager
from src.chat.memory_system.Hippocampus import hippocampus_manager
from src.chat.planner_actions.plan_executor import PlanExecutor
from src.chat.planner_actions.plan_filter import PlanFilter
from src.chat.planner_actions.plan_generator import PlanGenerator
from src.common.data_models.info_data_model import ActionPlannerInfo
from src.common.logger import get_logger
from src.plugin_system.base.component_types import ChatMode
if TYPE_CHECKING:
pass
# 导入提示词模块以确保其被初始化
from . import planner_prompts
logger = get_logger("planner")
install(extra_lines=3)
def init_prompt():
Prompt(
"""
{schedule_block}
{mood_block}
{time_block}
{identity_block}
{users_in_chat}
{custom_prompt_block}
{chat_context_description},以下是具体的聊天内容。
{chat_content_block}
{moderation_prompt}
**任务: 构建一个完整的响应**
你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成:
1. **主要动作**: 这是响应的核心,通常是 `reply`(文本回复)。
2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。
**决策流程:**
1. 首先,决定是否要进行 `reply`。
2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。
3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。
4. 如果用户明确要求了某个动作,请务必优先满足。
**可用动作:**
{actions_before_now_block}
{no_action_block}
动作reply
动作描述:参与聊天回复,发送文本进行表达
- 你想要闲聊或者随便附和
- {mentioned_bonus}
- 如果你刚刚进行了回复,不要对同一个话题重复回应
- 不要回复自己发送的消息
{{
"action": "reply",
"target_message_id": "触发action的消息id",
"reason": "回复的原因"
}}
{action_options_text}
**输出格式:**
你必须以严格的 JSON 格式输出返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作返回一个空列表[]。
**单动作示例 (仅回复):**
[
{{
"action": "reply",
"target_message_id": "m123",
"reason": "回答用户的问题"
}}
]
**组合动作示例 (回复 + 表情包):**
[
{{
"action": "reply",
"target_message_id": "m123",
"reason": "回答用户的问题"
}},
{{
"action": "emoji",
"target_message_id": "m123",
"reason": "用一个可爱的表情来缓和气氛"
}}
]
不要输出markdown格式```json等内容直接输出且仅包含 JSON 列表内容:
""",
"planner_prompt",
)
Prompt(
"""
# 主动思考决策
## 你的内部状态
{time_block}
{identity_block}
{schedule_block}
{mood_block}
## 长期记忆摘要
{long_term_memory_block}
## 最近的聊天内容
{chat_content_block}
## 最近的动作历史
{actions_before_now_block}
## 任务
你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。
请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考:
- 是否想起了什么之前提到的事情,想问问后来怎么样了?
- 是否注意到朋友提到了什么值得关心的事情?
- 是否有什么话题突然想到,觉得现在聊聊很合适?
- 或者觉得现在保持沉默更好?
## 可用动作
动作proactive_reply
动作描述:主动发起对话,可以是关心朋友、询问近况、延续之前的话题,或分享想法。
- 当你突然想起之前的话题,想询问进展时
- 当你想关心朋友的情况时
- 当你有什么想法想分享时
- 当你觉得现在是个合适的聊天时机时
{{
"action": "proactive_reply",
"reason": "你决定主动发言的具体原因",
"topic": "你想说的内容主题(简洁描述)"
}}
动作do_nothing
动作描述:保持沉默,不主动发起对话。
- 当你觉得现在不是合适的时机时
- 当最近已经说得够多了时
- 当对话氛围不适合插入时
{{
"action": "do_nothing",
"reason": "决定保持沉默的原因"
}}
你必须从上面列出的可用action中选择一个。要像真人一样自然地思考和决策。
请以严格的 JSON 格式输出,且仅包含 JSON 内容:
""",
"proactive_planner_prompt",
)
Prompt(
"""
动作:{action_name}
动作描述:{action_description}
{action_require}
{{
"action": "{action_name}",
"target_message_id": "触发action的消息id",
"reason": "触发action的原因"{action_parameters}
}}
""",
"action_prompt",
)
class ActionPlanner:
"""
ActionPlanner 是规划系统的核心协调器。
它负责整合规划流程的三个主要阶段:
1. **生成 (Generate)**: 使用 PlanGenerator 创建一个初始的行动计划。
2. **筛选 (Filter)**: 使用 PlanFilter 对生成的计划进行审查和优化。
3. **执行 (Execute)**: 使用 PlanExecutor 执行最终确定的行动。
Attributes:
chat_id (str): 当前聊天的唯一标识符。
action_manager (ActionManager): 用于执行具体动作的管理器。
generator (PlanGenerator): 负责生成初始计划。
filter (PlanFilter): 负责筛选和优化计划。
executor (PlanExecutor): 负责执行最终计划。
"""
def __init__(self, chat_id: str, action_manager: ActionManager):
"""
初始化 ActionPlanner。
Args:
chat_id (str): 当前聊天的 ID。
action_manager (ActionManager): 一个 ActionManager 实例。
"""
self.chat_id = chat_id
self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]"
self.action_manager = action_manager
# LLM规划器配置
# --- 大脑 ---
self.planner_llm = LLMRequest(
model_set=model_config.model_task_config.planner, request_type="planner"
)
self.last_obs_time_mark = 0.0
async def _get_long_term_memory_context(self) -> str:
"""
获取长期记忆上下文
"""
try:
# 1. 生成时间相关的关键词
now = datetime.now()
keywords = ["今天", "日程", "计划"]
if 5 <= now.hour < 12:
keywords.append("早上")
elif 12 <= now.hour < 18:
keywords.append("中午")
else:
keywords.append("晚上")
# TODO: 添加与聊天对象相关的关键词
# 2. 调用 hippocampus_manager 检索记忆
retrieved_memories = await hippocampus_manager.get_memory_from_topic(
valid_keywords=keywords, max_memory_num=5, max_memory_length=1
)
if not retrieved_memories:
return "最近没有什么特别的记忆。"
# 3. 格式化记忆
memory_statements = []
for topic, memory_item in retrieved_memories:
memory_statements.append(f"关于'{topic}', 你记得'{memory_item}'")
return " ".join(memory_statements)
except Exception as e:
logger.error(f"获取长期记忆时出错: {e}")
return "回忆时出现了一些问题。"
async def _build_action_options(
self,
current_available_actions: Dict[str, ActionInfo],
mode: ChatMode,
target_prompt: str = "",
) -> str:
"""
构建动作选项
"""
action_options_block = ""
for action_name, action_info in current_available_actions.items():
# TODO: 增加一个字段来判断action是否支持在PROACTIVE模式下使用
param_text = ""
if action_info.action_parameters:
param_text = "\n" + "\n".join(
f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items()
)
require_text = "\n".join(f"- {req}" for req in action_info.action_require)
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
action_options_block += using_action_prompt.format(
action_name=action_name,
action_description=action_info.description,
action_parameters=param_text,
action_require=require_text,
)
return action_options_block
def find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]:
# sourcery skip: use-next
"""
根据message_id从message_id_list中查找对应的原始消息
Args:
message_id: 要查找的消息ID
message_id_list: 消息ID列表格式为[{'id': str, 'message': dict}, ...]
Returns:
找到的原始消息字典如果未找到则返回None
"""
# 检测message_id 是否为纯数字
if message_id.isdigit():
message_id = f"m{message_id}"
for item in message_id_list:
if item.get("id") == message_id:
return item.get("message")
return None
def get_latest_message(self, message_id_list: list) -> Optional[Dict[str, Any]]:
"""
获取消息列表中的最新消息
Args:
message_id_list: 消息ID列表格式为[{'id': str, 'message': dict}, ...]
Returns:
最新的消息字典如果列表为空则返回None
"""
if not message_id_list:
return None
# 假设消息列表是按时间顺序排列的,最后一个是最新的
return message_id_list[-1].get("message")
async def _parse_single_action(
self,
action_json: dict,
message_id_list: list, # 使用 planner.py 的 list of dict
current_available_actions: list, # 使用 planner.py 的 list of tuple
) -> List[Dict[str, Any]]:
"""
[注释] 解析单个LLM返回的action JSON并将其转换为标准化的字典。
"""
parsed_actions = []
try:
action = action_json.get("action", "no_action")
reasoning = action_json.get("reason", "未提供原因")
action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]}
target_message = None
if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]:
if target_message_id := action_json.get("target_message_id"):
target_message = self.find_message_by_id(target_message_id, message_id_list)
if target_message is None:
logger.warning(f"{self.log_prefix}无法找到target_message_id '{target_message_id}'")
target_message = self.get_latest_message(message_id_list)
else:
logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id")
available_action_names = [name for name, _ in current_available_actions]
if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names:
logger.warning(
f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'"
)
reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {available_action_names})。原始理由: {reasoning}"
action = "no_action"
# 将列表转换为字典格式以供将来使用
available_actions_dict = dict(current_available_actions)
parsed_actions.append(
{
"action_type": action,
"reasoning": reasoning,
"action_data": action_data,
"action_message": target_message,
"available_actions": available_actions_dict,
}
)
# 如果是at_user动作且只有user_name尝试转换为user_id
if action == "at_user" and "user_name" in action_data and "user_id" not in action_data:
user_name = action_data["user_name"]
from src.person_info.person_info import get_person_info_manager
user_info = await get_person_info_manager().get_person_info_by_name(user_name)
if user_info and user_info.get("user_id"):
action_data["user_id"] = user_info["user_id"]
logger.info(f"成功将用户名 '{user_name}' 解析为 user_id '{user_info['user_id']}'")
else:
logger.warning(f"无法将用户名 '{user_name}' 解析为 user_id")
except Exception as e:
logger.error(f"{self.log_prefix}解析单个action时出错: {e}")
parsed_actions.append(
{
"action_type": "no_action",
"reasoning": f"解析action时出错: {e}",
"action_data": {},
"action_message": None,
"available_actions": dict(current_available_actions),
}
)
return parsed_actions
def _filter_no_actions(self, action_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
[注释] 从一个action字典列表中过滤掉所有的 'no_action'
如果过滤后列表为空, 则返回一个空的列表, 或者根据需要返回一个默认的no_action字典。
"""
non_no_actions = [a for a in action_list if a.get("action_type") not in ["no_action", "no_reply"]]
if non_no_actions:
return non_no_actions
# 如果都是 no_action则返回一个包含第一个 no_action 的列表,以保留 reason
return action_list[:1] if action_list else []
self.generator = PlanGenerator(chat_id)
self.filter = PlanFilter()
self.executor = PlanExecutor(action_manager)
async def plan(
self,
mode: ChatMode = ChatMode.FOCUS,
loop_start_time: float = 0.0,
available_actions: Optional[Dict[str, ActionInfo]] = None,
pseudo_message: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
self, mode: ChatMode = ChatMode.FOCUS
) -> Tuple[List[Dict], Optional[Dict]]:
"""
[注释] "大脑"规划器
统一决策是否进行聊天回复(reply)以及执行哪些actions。
执行从生成到执行的完整规划流程
这个方法按顺序协调生成、筛选和执行三个阶段。
Args:
mode (ChatMode): 当前的聊天模式,默认为 FOCUS。
Returns:
Tuple[List[Dict], Optional[Dict]]: 一个元组,包含:
- final_actions_dict (List[Dict]): 最终确定的动作列表(字典格式)。
- final_target_message_dict (Optional[Dict]): 最终的目标消息(字典格式),如果没有则为 None。
这与旧版 planner 的返回值保持兼容。
"""
# --- 1. 准备上下文信息 ---
is_group_chat, chat_target_info, current_available_actions = self.get_necessary_info()
if available_actions is None:
available_actions = current_available_actions
# 1. 生成初始 Plan
initial_plan = await self.generator.generate(mode)
# --- 2. 大脑统一决策 ---
final_actions: List[Dict[str, Any]] = []
try:
prompt, used_message_id_list = await self.build_planner_prompt(
is_group_chat=is_group_chat,
chat_target_info=chat_target_info,
current_available_actions=available_actions,
mode=mode,
)
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
# 2. 筛选 Plan
filtered_plan = await self.filter.filter(initial_plan)
if llm_content:
parsed_json = orjson.loads(repair_json(llm_content))
# 3. 执行 Plan(临时引爆因为它暂时还跑不了)
#await self.executor.execute(filtered_plan)
# 确保处理的是列表
if isinstance(parsed_json, dict):
parsed_json = [parsed_json]
if isinstance(parsed_json, list):
for item in parsed_json:
if isinstance(item, dict):
final_actions.extend(await self._parse_single_action(item, used_message_id_list, list(available_actions.items())))
# 如果是私聊且开启了强制回复并且没有任何回复性action则强制添加reply
if not is_group_chat and global_config.chat.force_reply_private:
has_reply_action = any(a.get("action_type") == "reply" for a in final_actions)
if not has_reply_action:
final_actions.append({
"action_type": "reply",
"reasoning": "私聊强制回复",
"action_data": {},
"action_message": self.get_latest_message(used_message_id_list),
"available_actions": available_actions,
})
logger.info(f"{self.log_prefix}私聊强制回复已触发,添加 'reply' 动作")
logger.info(f"{self.log_prefix}大脑决策: {[a.get('action_type') for a in final_actions]}")
except Exception as e:
logger.error(f"{self.log_prefix}大脑处理过程中发生意外错误: {e}\n{traceback.format_exc()}")
final_actions.append({"action_type": "no_action", "reasoning": f"大脑处理错误: {e}"})
# --- 3. 后处理 ---
final_actions = self._filter_no_actions(final_actions)
# === 概率模式后处理:根据配置决定是否强制添加 emoji 动作 ===
if global_config.emoji.emoji_activate_type == 'random':
has_reply_action = any(a.get("action_type") == "reply" for a in final_actions)
if has_reply_action:
# 检查此动作是否已被选择
is_already_chosen = any(a.get("action_type") == 'emoji' for a in final_actions)
if not is_already_chosen:
if random.random() < global_config.emoji.emoji_chance:
logger.info(f"{self.log_prefix}根据概率 '{global_config.emoji.emoji_chance}' 添加 emoji 动作")
final_actions.append({
"action_type": 'emoji',
"reasoning": f"根据概率 {global_config.emoji.emoji_chance} 自动添加",
"action_data": {},
"action_message": self.get_latest_message(used_message_id_list),
"available_actions": available_actions,
})
if not final_actions:
final_actions = [
{
"action_type": "no_action",
"reasoning": "规划器选择不执行动作",
"action_data": {}, "action_message": None, "available_actions": available_actions
}
]
final_target_message = next((act.get("action_message") for act in final_actions if act.get("action_message")), None)
# 记录每个动作的原因
for action_info in final_actions:
action_type = action_info.get("action_type", "N/A")
reasoning = action_info.get("reasoning", "")
logger.info(f"{self.log_prefix}决策: [{action_type}],原因: {reasoning}")
actions_str = ", ".join([a.get('action_type', 'N/A') for a in final_actions])
logger.info(f"{self.log_prefix}最终执行动作 ({len(final_actions)}): [{actions_str}]")
return final_actions, final_target_message
async def build_planner_prompt(
self,
is_group_chat: bool,
chat_target_info: Optional[dict],
current_available_actions: Dict[str, ActionInfo],
mode: ChatMode = ChatMode.FOCUS,
refresh_time: bool = False, # 添加缺失的参数
) -> tuple[str, list]:
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try:
# --- 通用信息获取 ---
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
bot_name = global_config.bot.nickname
bot_nickname = (
f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else ""
)
bot_core_personality = global_config.personality.personality_core
identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}"
schedule_block = ""
if global_config.planning_system.schedule_enable:
if current_activity := schedule_manager.get_current_activity():
schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。"
mood_block = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id)
mood_block = f"你现在的心情是:{chat_mood.mood_state}"
# --- 根据模式构建不同的Prompt ---
if mode == ChatMode.PROACTIVE:
long_term_memory_block = await self._get_long_term_memory_context()
# 获取最近的聊天记录用于主动思考决策
message_list_short = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.2), # 主动思考时只看少量最近消息
)
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=message_list_short,
timestamp_mode="normal",
truncate=False,
show_actions=False,
# 4. 返回结果 (与旧版 planner 的返回值保持兼容)
final_actions = filtered_plan.decided_actions or []
final_target_message = next(
(act.action_message for act in final_actions if act.action_message), None
)
prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt")
actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=self.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
final_actions_dict = [asdict(act) for act in final_actions]
final_target_message_dict = asdict(final_target_message) if final_target_message else None
prompt = prompt_template.format(
time_block=time_block,
identity_block=identity_block,
schedule_block=schedule_block,
mood_block=mood_block,
long_term_memory_block=long_term_memory_block,
chat_content_block=chat_content_block or "最近没有聊天内容。",
actions_before_now_block=actions_before_now_block,
)
return prompt, message_id_list
# --- FOCUS 和 NORMAL 模式的逻辑 ---
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.6),
)
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=message_list_before_now,
timestamp_mode="normal",
read_mark=self.last_obs_time_mark,
truncate=True,
show_actions=True,
)
actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=self.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
if refresh_time:
self.last_obs_time_mark = time.time()
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你"
if mode == ChatMode.FOCUS:
no_action_block = """
动作no_action
动作描述:不选择任何动作
{{
"action": "no_action",
"reason":"不动作的原因"
}}
动作no_reply
动作描述:不进行回复,等待合适的回复时机
- 当你刚刚发送了消息没有人回复时选择no_reply
- 当你一次发送了太多消息为了避免打扰聊天节奏选择no_reply
{{
"action": "no_reply",
"reason":"不回复的原因"
}}
"""
else: # NORMAL Mode
no_action_block = """重要说明:
- 'reply' 表示只进行普通聊天回复,不执行任何额外动作
- 其他action表示在普通回复的基础上执行相应的额外动作
{{
"action": "reply",
"target_message_id":"触发action的消息id",
"reason":"回复的原因"
}}"""
chat_context_description = "你现在正在一个群聊中"
chat_target_name = None
if not is_group_chat and chat_target_info:
chat_target_name = (
chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方"
)
chat_context_description = f"你正在和 {chat_target_name} 私聊"
action_options_block = await self._build_action_options(current_available_actions, mode)
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
custom_prompt_block = ""
if global_config.custom_prompt.planner_custom_prompt_content:
custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content
from src.person_info.person_info import get_person_info_manager
users_in_chat_str = ""
if is_group_chat and chat_target_info and chat_target_info.get("group_id"):
user_list = await get_person_info_manager().get_specific_value_list("person_name", lambda x: x is not None)
if user_list:
users_in_chat_str = "当前聊天中的用户列表(用于@\n" + "\n".join([f"- {name} (ID: {pid})" for pid, name in user_list.items()]) + "\n"
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
prompt = planner_prompt_template.format(
schedule_block=schedule_block,
mood_block=mood_block,
time_block=time_block,
chat_context_description=chat_context_description,
chat_content_block=chat_content_block,
actions_before_now_block=actions_before_now_block,
mentioned_bonus=mentioned_bonus,
no_action_block=no_action_block,
action_options_text=action_options_block,
moderation_prompt=moderation_prompt_block,
identity_block=identity_block,
custom_prompt_block=custom_prompt_block,
bot_name=bot_name,
users_in_chat=users_in_chat_str
)
return prompt, message_id_list
except Exception as e:
logger.error(f"构建 Planner 提示词时出错: {e}")
logger.error(traceback.format_exc())
return "构建 Planner Prompt 时出错", []
def get_necessary_info(self) -> Tuple[bool, Optional[dict], Dict[str, ActionInfo]]:
"""
获取 Planner 需要的必要信息
"""
is_group_chat = True
is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id)
logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}")
current_available_actions_dict = self.action_manager.get_using_actions()
# 获取完整的动作信息
all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore
ComponentType.ACTION
)
current_available_actions = {}
for action_name in current_available_actions_dict:
if action_name in all_registered_actions:
current_available_actions[action_name] = all_registered_actions[action_name]
else:
logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到")
# 将no_reply作为系统级特殊动作添加到可用动作中
# no_reply虽然是系统级决策但需要让规划器认为它是可用的
no_reply_info = ActionInfo(
name="no_reply",
component_type=ComponentType.ACTION,
description="系统级动作:选择不回复消息的决策",
action_parameters={},
activation_keywords=[],
plugin_name="SYSTEM",
enabled=True, # 始终启用
parallel_action=False,
)
current_available_actions["no_reply"] = no_reply_info
return is_group_chat, chat_target_info, current_available_actions
init_prompt()
return final_actions_dict, final_target_message_dict

View File

@@ -0,0 +1,172 @@
"""
本文件集中管理所有与规划器Planner相关的提示词Prompt模板。
通过将提示词与代码逻辑分离,可以更方便地对模型的行为进行迭代和优化,
而无需修改核心代码。
"""
from src.chat.utils.prompt import Prompt
def init_prompts():
"""
初始化并向 Prompt 注册系统注册所有规划器相关的提示词。
这个函数会在模块加载时自动调用,确保所有提示词在系统启动时都已准备就绪。
"""
# 核心规划器提示词,用于在接收到新消息时决定如何回应。
# 它构建了一个复杂的上下文,包括历史记录、可用动作、角色设定等,
# 并要求模型以 JSON 格式输出一个或多个动作组合。
Prompt(
"""
{schedule_block}
{mood_block}
{time_block}
{identity_block}
{users_in_chat}
{custom_prompt_block}
{chat_context_description},以下是具体的聊天内容。
{chat_content_block}
{moderation_prompt}
**任务: 构建一个完整的响应**
你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成:
1. **主要动作**: 这是响应的核心,通常是 `reply`(文本回复)。
2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。
**决策流程:**
1. 首先,决定是否要进行 `reply`。
2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。
3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。
4. 如果用户明确要求了某个动作,请务必优先满足。
**可用动作:**
{actions_before_now_block}
{no_action_block}
动作reply
动作描述:参与聊天回复,发送文本进行表达
- 你想要闲聊或者随便附和
- {mentioned_bonus}
- 如果你刚刚进行了回复,不要对同一个话题重复回应
- 不要回复自己发送的消息
{{
"action": "reply",
"target_message_id": "触发action的消息id",
"reason": "回复的原因"
}}
{action_options_text}
**输出格式:**
你必须以严格的 JSON 格式输出返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作返回一个空列表[]。
**单动作示例 (仅回复):**
[
{{
"action": "reply",
"target_message_id": "m123",
"reason": "回答用户的问题"
}}
]
**组合动作示例 (回复 + 表情包):**
[
{{
"action": "reply",
"target_message_id": "m123",
"reason": "回答用户的问题"
}},
{{
"action": "emoji",
"target_message_id": "m123",
"reason": "用一个可爱的表情来缓和气氛"
}}
]
不要输出markdown格式```json等内容直接输出且仅包含 JSON 列表内容:
""",
"planner_prompt",
)
# 主动思考规划器提示词,用于在没有新消息时决定是否要主动发起对话。
# 它模拟了人类的自发性思考,允许模型根据长期记忆和最近的对话来决定是否开启新话题。
Prompt(
"""
# 主动思考决策
## 你的内部状态
{time_block}
{identity_block}
{schedule_block}
{mood_block}
## 长期记忆摘要
{long_term_memory_block}
## 最近的聊天内容
{chat_content_block}
## 最近的动作历史
{actions_before_now_block}
## 任务
你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。
请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考:
- 是否想起了什么之前提到的事情,想问问后来怎么样了?
- 是否注意到朋友提到了什么值得关心的事情?
- 是否有什么话题突然想到,觉得现在聊聊很合适?
- 或者觉得现在保持沉默更好?
## 可用动作
动作proactive_reply
动作描述:主动发起对话,可以是关心朋友、询问近况、延续之前的话题,或分享想法。
- 当你突然想起之前的话题,想询问进展时
- 当你想关心朋友的情况时
- 当你有什么想法想分享时
- 当你觉得现在是个合适的聊天时机时
{{
"action": "proactive_reply",
"reason": "你决定主动发言的具体原因",
"topic": "你想说的内容主题(简洁描述)"
}}
动作do_nothing
动作描述:保持沉默,不主动发起对话。
- 当你觉得现在不是合适的时机时
- 当最近已经说得够多了时
- 当对话氛围不适合插入时
{{
"action": "do_nothing",
"reason": "决定保持沉默的原因"
}}
你必须从上面列出的可用action中选择一个。要像真人一样自然地思考和决策。
请以严格的 JSON 格式输出,且仅包含 JSON 内容:
""",
"proactive_planner_prompt",
)
# 单个动作的格式化提示词模板。
# 用于将每个可用动作的信息格式化后,插入到主提示词的 {action_options_text} 占位符中。
Prompt(
"""
动作:{action_name}
动作描述:{action_description}
{action_require}
{{
"action": "{action_name}",
"target_message_id": "触发action的消息id",
"reason": "触发action的原因"{action_parameters}
}}
""",
"action_prompt",
)
# 在模块加载时自动调用,完成提示词的注册。
init_prompts()

View File

@@ -86,7 +86,6 @@ def init_prompt():
### 当前群聊中的所有人的聊天记录:
{background_dialogue_prompt}
### 其他群聊中的聊天记录
{cross_context_block}
### 当前群聊中正在与你对话的聊天记录
@@ -97,14 +96,10 @@ def init_prompt():
{reply_style}
{keywords_reaction_prompt}
- (如果有)你可以参考以下你在聊天中学到的表达方式:
{expression_habits_block}
## 工具信息
(如果有)你可以参考以下可能有帮助的工具返回的信息:
{tool_info_block}
## 知识库信息
(如果有)你可以参考以下可能有帮助的知识库中的信息:
{knowledge_prompt}
## 其他信息
@@ -114,8 +109,8 @@ def init_prompt():
{action_descriptions}
## 任务
### 梗概
- 你正在一个QQ群里聊天你需要理解整个群的聊天动态和话题走向并做出自然的回应。
*你正在一个QQ群里聊天你需要理解整个群的聊天动态和话题走向并做出自然的回应。*
### 核心任务
- 你现在的主要任务是和 {sender_name} 聊天。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。
@@ -123,6 +118,7 @@ def init_prompt():
- {reply_target_block} ,你需要生成一段紧密相关且能推动对话的回复。
## 规则
{safety_guidelines_block}
在回应之前,首先分析消息的针对性:
1. **直接针对你**@你、回复你、明确询问你 → 必须回应
2. **间接相关**:涉及你感兴趣的话题但未直接问你 → 谨慎参与
@@ -139,8 +135,6 @@ def init_prompt():
--------------------------------
{time_block}
{reply_target_block}
注意不要复读你前面发过的内容,意思相近也不行。
请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。
@@ -268,7 +262,7 @@ class DefaultReplyer:
available_actions = {}
llm_response = None
try:
# 3. 构建 Prompt
# 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_reply_context(
reply_to=reply_to,
@@ -832,16 +826,22 @@ class DefaultReplyer:
reply_message.get("user_id"), # type: ignore
)
person_name = await person_info_manager.get_value(person_id, "person_name")
# 检查是否是bot自己的名字如果是则替换为"(你)"
bot_user_id = str(global_config.bot.qq_account)
current_user_id = person_info_manager.get_value_sync(person_id, "user_id")
current_platform = reply_message.get("chat_info_platform")
if current_user_id == bot_user_id and current_platform == global_config.bot.platform:
sender = f"{person_name}(你)"
else:
# 如果不是bot自己直接使用person_name
sender = person_name
target = reply_message.get("processed_plain_text")
person_info_manager = get_person_info_manager()
person_id = person_info_manager.get_person_id_by_person_name(sender)
user_id = person_info_manager.get_value_sync(person_id, "user_id")
platform = chat_stream.platform
if user_id == global_config.bot.qq_account and platform == global_config.bot.platform:
logger.warning("选取了自身作为回复对象跳过构建prompt")
return ""
target = replace_user_references_sync(target, chat_stream.platform, replace_bot_name=True)
@@ -942,6 +942,16 @@ class DefaultReplyer:
identity_block = await get_individuality().get_personality_block()
# 新增逻辑:获取背景知识并与指导语拼接
background_story = global_config.personality.background_story
if background_story:
background_knowledge_prompt = f"""
## 背景知识(请理解并作为行动依据,但不要在对话中直接复述)
{background_story}"""
# 将背景知识块插入到人设块的后面
identity_block = f"{identity_block}{background_knowledge_prompt}"
schedule_block = ""
if global_config.planning_system.schedule_enable:
from src.schedule.schedule_manager import schedule_manager
@@ -953,6 +963,17 @@ class DefaultReplyer:
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
)
# 新增逻辑:构建安全准则块
safety_guidelines = global_config.personality.safety_guidelines
safety_guidelines_block = ""
if safety_guidelines:
guidelines_text = "\n".join(f"{i+1}. {line}" for i, line in enumerate(safety_guidelines))
safety_guidelines_block = f"""### 安全与互动底线
在任何情况下,你都必须遵守以下由你的设定者为你定义的原则:
{guidelines_text}
如果遇到违反上述原则的请求,请在保持你核心人设的同时,巧妙地拒绝或转移话题。
"""
if sender and target:
if is_group_chat:
if sender:
@@ -1005,6 +1026,7 @@ class DefaultReplyer:
identity_block=identity_block,
schedule_block=schedule_block,
moderation_prompt_block=moderation_prompt_block,
safety_guidelines_block=safety_guidelines_block,
reply_target_block=reply_target_block,
mood_prompt=mood_prompt,
action_descriptions=action_descriptions,
@@ -1038,10 +1060,8 @@ class DefaultReplyer:
**任务**: 请结合你的智慧和人设,自然地决定是否需要分段。如果需要,请在最恰当的位置插入 `[SPLIT]` 标记。
"""
# 在 "现在,你说:" 之前插入
parts = prompt_text.rsplit("现在,你说:", 1)
if len(parts) == 2:
prompt_text = f"{parts[0]}{split_instruction}\n现在,你说:{parts[1]}"
# 将分段指令添加到提示词顶部
prompt_text = f"{split_instruction}\n{prompt_text}"
return prompt_text

View File

@@ -35,6 +35,9 @@ def replace_user_references_sync(
Returns:
str: 处理后的内容字符串
"""
if not content:
return ""
if name_resolver is None:
person_info_manager = get_person_info_manager()
@@ -817,8 +820,8 @@ def build_pic_mapping_info(pic_id_mapping: Dict[str, str]) -> str:
description = "[图片内容未知]" # 默认描述
try:
with get_db_session() as session:
image = session.execute(select(Images).where(Images.image_id == pic_id)).scalar()
if image and image.description:
image = session.execute(select(Images).where(Images.image_id == pic_id)).scalar_one_or_none()
if image and image.description: # type: ignore
description = image.description
except Exception:
# 如果查询失败,保持默认描述

View File

@@ -71,6 +71,7 @@ class PromptParameters:
identity_block: str = ""
schedule_block: str = ""
moderation_prompt_block: str = ""
safety_guidelines_block: str = ""
reply_target_block: str = ""
mood_prompt: str = ""
action_descriptions: str = ""
@@ -312,16 +313,15 @@ class Prompt:
except asyncio.TimeoutError as e:
logger.error(f"构建Prompt超时: {e}")
raise TimeoutError(f"构建Prompt超时: {e}")
raise TimeoutError(f"构建Prompt超时: {e}") from e
except Exception as e:
logger.error(f"构建Prompt失败: {e}")
raise RuntimeError(f"构建Prompt失败: {e}")
raise RuntimeError(f"构建Prompt失败: {e}") from e
async def _build_context_data(self) -> Dict[str, Any]:
"""构建智能上下文数据"""
# 并行执行所有构建任务
start_time = time.time()
timing_logs = {}
try:
# 准备构建任务
@@ -381,7 +381,6 @@ class Prompt:
results = []
for i in range(0, len(tasks), max_concurrent_tasks):
batch_tasks = tasks[i : i + max_concurrent_tasks]
batch_names = task_names[i : i + max_concurrent_tasks]
batch_results = await asyncio.wait_for(
asyncio.gather(*batch_tasks, return_exceptions=True), timeout=timeout_seconds
@@ -520,12 +519,95 @@ class Prompt:
async def _build_expression_habits(self) -> Dict[str, Any]:
"""构建表达习惯"""
# 简化的实现,完整实现需要导入相关模块
if not global_config.expression.enable_expression:
return {"expression_habits_block": ""}
try:
from src.chat.express.expression_selector import ExpressionSelector
# 获取聊天历史用于表情选择
chat_history = ""
if self.parameters.message_list_before_now_long:
recent_messages = self.parameters.message_list_before_now_long[-10:]
chat_history = build_readable_messages(
recent_messages,
replace_bot_name=True,
timestamp_mode="normal",
truncate=True
)
# 创建表情选择器
expression_selector = ExpressionSelector(self.parameters.chat_id)
# 选择合适的表情
selected_expressions = await expression_selector.select_suitable_expressions_llm(
chat_history=chat_history,
current_message=self.parameters.target,
emotional_tone="neutral",
topic_type="general"
)
# 构建表达习惯块
if selected_expressions:
style_habits_str = "\n".join([f"- {expr}" for expr in selected_expressions])
expression_habits_block = f"- 你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:\n{style_habits_str}"
else:
expression_habits_block = ""
return {"expression_habits_block": expression_habits_block}
except Exception as e:
logger.error(f"构建表达习惯失败: {e}")
return {"expression_habits_block": ""}
async def _build_memory_block(self) -> Dict[str, Any]:
"""构建记忆块"""
# 简化的实现
if not global_config.memory.enable_memory:
return {"memory_block": ""}
try:
from src.chat.memory_system.memory_activator import MemoryActivator
from src.chat.memory_system.async_instant_memory_wrapper import get_async_instant_memory
# 获取聊天历史
chat_history = ""
if self.parameters.message_list_before_now_long:
recent_messages = self.parameters.message_list_before_now_long[-20:]
chat_history = build_readable_messages(
recent_messages,
replace_bot_name=True,
timestamp_mode="normal",
truncate=True
)
# 激活长期记忆
memory_activator = MemoryActivator()
running_memories = await memory_activator.activate_memory_with_chat_history(
target_message=self.parameters.target,
chat_history_prompt=chat_history
)
# 获取即时记忆
async_memory_wrapper = get_async_instant_memory(self.parameters.chat_id)
instant_memory = await async_memory_wrapper.get_memory_with_fallback(self.parameters.target)
# 构建记忆块
memory_parts = []
if running_memories:
memory_parts.append("以下是当前在聊天中,你回忆起的记忆:")
for memory in running_memories:
memory_parts.append(f"- {memory['content']}")
if instant_memory:
memory_parts.append(f"- {instant_memory}")
memory_block = "\n".join(memory_parts) if memory_parts else ""
return {"memory_block": memory_block}
except Exception as e:
logger.error(f"构建记忆块失败: {e}")
return {"memory_block": ""}
async def _build_relation_info(self) -> Dict[str, Any]:
@@ -539,12 +621,105 @@ class Prompt:
async def _build_tool_info(self) -> Dict[str, Any]:
"""构建工具信息"""
# 简化的实现
if not global_config.tool.enable_tool:
return {"tool_info_block": ""}
try:
from src.plugin_system.core.tool_use import ToolExecutor
# 获取聊天历史
chat_history = ""
if self.parameters.message_list_before_now_long:
recent_messages = self.parameters.message_list_before_now_long[-15:]
chat_history = build_readable_messages(
recent_messages,
replace_bot_name=True,
timestamp_mode="normal",
truncate=True
)
# 创建工具执行器
tool_executor = ToolExecutor(chat_id=self.parameters.chat_id)
# 执行工具获取信息
tool_results, _, _ = await tool_executor.execute_from_chat_message(
sender=self.parameters.sender,
target_message=self.parameters.target,
chat_history=chat_history,
return_details=False
)
# 构建工具信息块
if tool_results:
tool_info_parts = ["## 工具信息","以下是你通过工具获取到的实时信息:"]
for tool_result in tool_results:
tool_name = tool_result.get("tool_name", "unknown")
content = tool_result.get("content", "")
result_type = tool_result.get("type", "tool_result")
tool_info_parts.append(f"- 【{tool_name}{result_type}: {content}")
tool_info_parts.append("以上是你获取到的实时信息,请在回复时参考这些信息。")
tool_info_block = "\n".join(tool_info_parts)
else:
tool_info_block = ""
return {"tool_info_block": tool_info_block}
except Exception as e:
logger.error(f"构建工具信息失败: {e}")
return {"tool_info_block": ""}
async def _build_knowledge_info(self) -> Dict[str, Any]:
"""构建知识信息"""
# 简化的实现
if not global_config.lpmm_knowledge.enable:
return {"knowledge_prompt": ""}
try:
from src.chat.knowledge.knowledge_lib import QAManager
# 获取问题文本(当前消息)
question = self.parameters.target or ""
if not question:
return {"knowledge_prompt": ""}
# 创建QA管理器
qa_manager = QAManager()
# 搜索相关知识
knowledge_results = await qa_manager.get_knowledge(
question=question,
chat_id=self.parameters.chat_id,
max_results=5,
min_similarity=0.5
)
# 构建知识块
if knowledge_results and knowledge_results.get("knowledge_items"):
knowledge_parts = ["## 知识库信息","以下是与你当前对话相关的知识信息:"]
for item in knowledge_results["knowledge_items"]:
content = item.get("content", "")
source = item.get("source", "")
relevance = item.get("relevance", 0.0)
if content:
if source:
knowledge_parts.append(f"- [{relevance:.2f}] {content} (来源: {source})")
else:
knowledge_parts.append(f"- [{relevance:.2f}] {content}")
if knowledge_results.get("summary"):
knowledge_parts.append(f"\n知识总结: {knowledge_results['summary']}")
knowledge_prompt = "\n".join(knowledge_parts)
else:
knowledge_prompt = ""
return {"knowledge_prompt": knowledge_prompt}
except Exception as e:
logger.error(f"构建知识信息失败: {e}")
return {"knowledge_prompt": ""}
async def _build_cross_context(self) -> Dict[str, Any]:
@@ -591,6 +766,7 @@ class Prompt:
"reply_style": global_config.personality.reply_style,
"keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""),
"moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""),
"safety_guidelines_block": self.parameters.safety_guidelines_block or context_data.get("safety_guidelines_block", ""),
}
def _prepare_normal_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]:
@@ -614,6 +790,7 @@ class Prompt:
"mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""),
"keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""),
"moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""),
"safety_guidelines_block": self.parameters.safety_guidelines_block or context_data.get("safety_guidelines_block", ""),
}
def _prepare_default_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]:
@@ -633,6 +810,7 @@ class Prompt:
"reply_style": global_config.personality.reply_style,
"keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""),
"moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""),
"safety_guidelines_block": self.parameters.safety_guidelines_block or context_data.get("safety_guidelines_block", ""),
}
def format(self, *args, **kwargs) -> str:

View File

@@ -0,0 +1,53 @@
import copy
from typing import Any
class BaseDataModel:
def deepcopy(self):
return copy.deepcopy(self)
def temporarily_transform_class_to_dict(obj: Any) -> Any:
# sourcery skip: assign-if-exp, reintroduce-else
"""
将对象或容器中的 BaseDataModel 子类(类对象)或 BaseDataModel 实例
递归转换为普通 dict不修改原对象。
- 对于类对象isinstance(value, type) 且 issubclass(..., BaseDataModel)
读取类的 __dict__ 中非 dunder 项并递归转换。
- 对于实例isinstance(value, BaseDataModel)),读取 vars(instance) 并递归转换。
"""
def _transform(value: Any) -> Any:
# 值是类对象且为 BaseDataModel 的子类
if isinstance(value, type) and issubclass(value, BaseDataModel):
return {k: _transform(v) for k, v in value.__dict__.items() if not k.startswith("__") and not callable(v)}
# 值是 BaseDataModel 的实例
if isinstance(value, BaseDataModel):
return {k: _transform(v) for k, v in vars(value).items()}
# 常见容器类型,递归处理
if isinstance(value, dict):
return {k: _transform(v) for k, v in value.items()}
if isinstance(value, list):
return [_transform(v) for v in value]
if isinstance(value, tuple):
return tuple(_transform(v) for v in value)
if isinstance(value, set):
return {_transform(v) for v in value}
# 基本类型,直接返回
return value
result = _transform(obj)
def flatten(target_dict: dict):
flat_dict = {}
for k, v in target_dict.items():
if isinstance(v, dict):
# 递归扁平化子字典
sub_flat = flatten(v)
flat_dict.update(sub_flat)
else:
flat_dict[k] = v
return flat_dict
return flatten(result) if isinstance(result, dict) else result

View File

@@ -0,0 +1,235 @@
import json
from typing import Optional, Any, Dict
from dataclasses import dataclass, field
from . import BaseDataModel
@dataclass
class DatabaseUserInfo(BaseDataModel):
platform: str = field(default_factory=str)
user_id: str = field(default_factory=str)
user_nickname: str = field(default_factory=str)
user_cardname: Optional[str] = None
# def __post_init__(self):
# assert isinstance(self.platform, str), "platform must be a string"
# assert isinstance(self.user_id, str), "user_id must be a string"
# assert isinstance(self.user_nickname, str), "user_nickname must be a string"
# assert isinstance(self.user_cardname, str) or self.user_cardname is None, (
# "user_cardname must be a string or None"
# )
@dataclass
class DatabaseGroupInfo(BaseDataModel):
group_id: str = field(default_factory=str)
group_name: str = field(default_factory=str)
group_platform: Optional[str] = None
# def __post_init__(self):
# assert isinstance(self.group_id, str), "group_id must be a string"
# assert isinstance(self.group_name, str), "group_name must be a string"
# assert isinstance(self.group_platform, str) or self.group_platform is None, (
# "group_platform must be a string or None"
# )
@dataclass
class DatabaseChatInfo(BaseDataModel):
stream_id: str = field(default_factory=str)
platform: str = field(default_factory=str)
create_time: float = field(default_factory=float)
last_active_time: float = field(default_factory=float)
user_info: DatabaseUserInfo = field(default_factory=DatabaseUserInfo)
group_info: Optional[DatabaseGroupInfo] = None
# def __post_init__(self):
# assert isinstance(self.stream_id, str), "stream_id must be a string"
# assert isinstance(self.platform, str), "platform must be a string"
# assert isinstance(self.create_time, float), "create_time must be a float"
# assert isinstance(self.last_active_time, float), "last_active_time must be a float"
# assert isinstance(self.user_info, DatabaseUserInfo), "user_info must be a DatabaseUserInfo instance"
# assert isinstance(self.group_info, DatabaseGroupInfo) or self.group_info is None, (
# "group_info must be a DatabaseGroupInfo instance or None"
# )
@dataclass(init=False)
class DatabaseMessages(BaseDataModel):
def __init__(
self,
message_id: str = "",
time: float = 0.0,
chat_id: str = "",
reply_to: Optional[str] = None,
interest_value: Optional[float] = None,
key_words: Optional[str] = None,
key_words_lite: Optional[str] = None,
is_mentioned: Optional[bool] = None,
is_at: Optional[bool] = None,
reply_probability_boost: Optional[float] = None,
processed_plain_text: Optional[str] = None,
display_message: Optional[str] = None,
priority_mode: Optional[str] = None,
priority_info: Optional[str] = None,
additional_config: Optional[str] = None,
is_emoji: bool = False,
is_picid: bool = False,
is_command: bool = False,
is_notify: bool = False,
selected_expressions: Optional[str] = None,
user_id: str = "",
user_nickname: str = "",
user_cardname: Optional[str] = None,
user_platform: str = "",
chat_info_group_id: Optional[str] = None,
chat_info_group_name: Optional[str] = None,
chat_info_group_platform: Optional[str] = None,
chat_info_user_id: str = "",
chat_info_user_nickname: str = "",
chat_info_user_cardname: Optional[str] = None,
chat_info_user_platform: str = "",
chat_info_stream_id: str = "",
chat_info_platform: str = "",
chat_info_create_time: float = 0.0,
chat_info_last_active_time: float = 0.0,
**kwargs: Any,
):
self.message_id = message_id
self.time = time
self.chat_id = chat_id
self.reply_to = reply_to
self.interest_value = interest_value
self.key_words = key_words
self.key_words_lite = key_words_lite
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.display_message = display_message
self.priority_mode = priority_mode
self.priority_info = priority_info
self.additional_config = additional_config
self.is_emoji = is_emoji
self.is_picid = is_picid
self.is_command = is_command
self.is_notify = is_notify
self.selected_expressions = selected_expressions
self.group_info: Optional[DatabaseGroupInfo] = None
self.user_info = DatabaseUserInfo(
user_id=user_id,
user_nickname=user_nickname,
user_cardname=user_cardname,
platform=user_platform,
)
if chat_info_group_id and chat_info_group_name:
self.group_info = DatabaseGroupInfo(
group_id=chat_info_group_id,
group_name=chat_info_group_name,
group_platform=chat_info_group_platform,
)
self.chat_info = DatabaseChatInfo(
stream_id=chat_info_stream_id,
platform=chat_info_platform,
create_time=chat_info_create_time,
last_active_time=chat_info_last_active_time,
user_info=DatabaseUserInfo(
user_id=chat_info_user_id,
user_nickname=chat_info_user_nickname,
user_cardname=chat_info_user_cardname,
platform=chat_info_user_platform,
),
group_info=self.group_info,
)
if kwargs:
for key, value in kwargs.items():
setattr(self, key, value)
# def __post_init__(self):
# assert isinstance(self.message_id, str), "message_id must be a string"
# assert isinstance(self.time, float), "time must be a float"
# assert isinstance(self.chat_id, str), "chat_id must be a string"
# assert isinstance(self.reply_to, str) or self.reply_to is None, "reply_to must be a string or None"
# assert isinstance(self.interest_value, float) or self.interest_value is None, (
# "interest_value must be a float or None"
# )
def flatten(self) -> Dict[str, Any]:
"""
将消息数据模型转换为字典格式,便于存储或传输
"""
return {
"message_id": self.message_id,
"time": self.time,
"chat_id": self.chat_id,
"reply_to": self.reply_to,
"interest_value": self.interest_value,
"key_words": self.key_words,
"key_words_lite": self.key_words_lite,
"is_mentioned": self.is_mentioned,
"is_at": self.is_at,
"reply_probability_boost": self.reply_probability_boost,
"processed_plain_text": self.processed_plain_text,
"display_message": self.display_message,
"priority_mode": self.priority_mode,
"priority_info": self.priority_info,
"additional_config": self.additional_config,
"is_emoji": self.is_emoji,
"is_picid": self.is_picid,
"is_command": self.is_command,
"is_notify": self.is_notify,
"selected_expressions": self.selected_expressions,
"user_id": self.user_info.user_id,
"user_nickname": self.user_info.user_nickname,
"user_cardname": self.user_info.user_cardname,
"user_platform": self.user_info.platform,
"chat_info_group_id": self.group_info.group_id if self.group_info else None,
"chat_info_group_name": self.group_info.group_name if self.group_info else None,
"chat_info_group_platform": self.group_info.group_platform if self.group_info else None,
"chat_info_stream_id": self.chat_info.stream_id,
"chat_info_platform": self.chat_info.platform,
"chat_info_create_time": self.chat_info.create_time,
"chat_info_last_active_time": self.chat_info.last_active_time,
"chat_info_user_platform": self.chat_info.user_info.platform,
"chat_info_user_id": self.chat_info.user_info.user_id,
"chat_info_user_nickname": self.chat_info.user_info.user_nickname,
"chat_info_user_cardname": self.chat_info.user_info.user_cardname,
}
@dataclass(init=False)
class DatabaseActionRecords(BaseDataModel):
def __init__(
self,
action_id: str,
time: float,
action_name: str,
action_data: str,
action_done: bool,
action_build_into_prompt: bool,
action_prompt_display: str,
chat_id: str,
chat_info_stream_id: str,
chat_info_platform: str,
):
self.action_id = action_id
self.time = time
self.action_name = action_name
if isinstance(action_data, str):
self.action_data = json.loads(action_data)
else:
raise ValueError("action_data must be a JSON string")
self.action_done = action_done
self.action_build_into_prompt = action_build_into_prompt
self.action_prompt_display = action_prompt_display
self.chat_id = chat_id
self.chat_info_stream_id = chat_info_stream_id
self.chat_info_platform = chat_info_platform

View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass, field
from typing import Optional, Dict, List, TYPE_CHECKING
from . import BaseDataModel
if TYPE_CHECKING:
from .database_data_model import DatabaseMessages
from src.plugin_system.base.component_types import ActionInfo, ChatMode
@dataclass
class TargetPersonInfo(BaseDataModel):
platform: str = field(default_factory=str)
user_id: str = field(default_factory=str)
user_nickname: str = field(default_factory=str)
person_id: Optional[str] = None
person_name: Optional[str] = None
@dataclass
class ActionPlannerInfo(BaseDataModel):
action_type: str = field(default_factory=str)
reasoning: Optional[str] = None
action_data: Optional[Dict] = None
action_message: Optional["DatabaseMessages"] = None
available_actions: Optional[Dict[str, "ActionInfo"]] = None
@dataclass
class Plan(BaseDataModel):
"""
统一规划数据模型
"""
chat_id: str
mode: "ChatMode"
# Generator 填充
available_actions: Dict[str, "ActionInfo"] = field(default_factory=dict)
chat_history: List["DatabaseMessages"] = field(default_factory=list)
target_info: Optional[TargetPersonInfo] = None
# Filter 填充
llm_prompt: Optional[str] = None
decided_actions: Optional[List[ActionPlannerInfo]] = None

View File

@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import Optional, List, Tuple, TYPE_CHECKING, Any
from . import BaseDataModel
if TYPE_CHECKING:
from src.llm_models.payload_content.tool_option import ToolCall
@dataclass
class LLMGenerationDataModel(BaseDataModel):
content: Optional[str] = None
reasoning: Optional[str] = None
model: Optional[str] = None
tool_calls: Optional[List["ToolCall"]] = None
prompt: Optional[str] = None
selected_expressions: Optional[List[int]] = None
reply_set: Optional[List[Tuple[str, Any]]] = None

View File

@@ -0,0 +1,36 @@
from typing import Optional, TYPE_CHECKING
from dataclasses import dataclass, field
from . import BaseDataModel
if TYPE_CHECKING:
from .database_data_model import DatabaseMessages
@dataclass
class MessageAndActionModel(BaseDataModel):
chat_id: str = field(default_factory=str)
time: float = field(default_factory=float)
user_id: str = field(default_factory=str)
user_platform: str = field(default_factory=str)
user_nickname: str = field(default_factory=str)
user_cardname: Optional[str] = None
processed_plain_text: Optional[str] = None
display_message: Optional[str] = None
chat_info_platform: str = field(default_factory=str)
is_action_record: bool = field(default=False)
action_name: Optional[str] = None
@classmethod
def from_DatabaseMessages(cls, message: "DatabaseMessages"):
return cls(
chat_id=message.chat_id,
time=message.time,
user_id=message.user_info.user_id,
user_platform=message.user_info.platform,
user_nickname=message.user_info.user_nickname,
user_cardname=message.user_info.user_cardname,
processed_plain_text=message.processed_plain_text,
display_message=message.display_message,
chat_info_platform=message.chat_info.platform,
)

View File

@@ -53,6 +53,7 @@ class ChatStreams(Base):
user_cardname = Column(Text, nullable=True)
energy_value = Column(Float, nullable=True, default=5.0)
sleep_pressure = Column(Float, nullable=True, default=0.0)
focus_energy = Column(Float, nullable=True, default=1.0)
__table_args__ = (
Index("idx_chatstreams_stream_id", "stream_id"),

View File

@@ -1,5 +1,4 @@
from src.common.server import get_global_server
import os
import importlib.metadata
from maim_message import MessageServer
from src.common.logger import get_logger
@@ -24,8 +23,8 @@ def get_global_api() -> MessageServer: # sourcery skip: extract-method
# 设置基本参数
kwargs = {
"host": os.environ["HOST"],
"port": int(os.environ["PORT"]),
"host": global_config.server.host,
"port": int(global_config.server.port),
"app": get_global_server().get_app(),
}

View File

@@ -2,7 +2,7 @@ from fastapi import FastAPI, APIRouter
from fastapi.middleware.cors import CORSMiddleware # 新增导入
from typing import Optional
from uvicorn import Config, Server as UvicornServer
import os
from src.config.config import global_config
from rich.traceback import install
install(extra_lines=3)
@@ -98,5 +98,5 @@ def get_global_server() -> Server:
"""获取全局服务器实例"""
global global_server
if global_server is None:
global_server = Server(host=os.environ["HOST"], port=int(os.environ["PORT"]))
global_server = Server(host=global_config.server.host,port=int(global_config.server.port),)
return global_server

View File

@@ -43,8 +43,8 @@ from src.config.official_configs import (
CrossContextConfig,
PermissionConfig,
CommandConfig,
MaizoneIntercomConfig,
PlanningSystemConfig,
ServerConfig,
)
from .api_ada_configs import (
@@ -399,9 +399,7 @@ class Config(ValidatedConfigBase):
cross_context: CrossContextConfig = Field(
default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置"
)
maizone_intercom: MaizoneIntercomConfig = Field(
default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置"
)
server: ServerConfig = Field(default_factory=lambda: ServerConfig(), description="主服务器配置")
class APIAdapterConfig(ValidatedConfigBase):

View File

@@ -51,6 +51,8 @@ class PersonalityConfig(ValidatedConfigBase):
personality_core: str = Field(..., description="核心人格")
personality_side: str = Field(..., description="人格侧写")
identity: str = Field(default="", description="身份特征")
background_story: str = Field(default="", description="世界观背景故事这部分内容会作为背景知识LLM被指导不应主动复述")
safety_guidelines: List[str] = Field(default_factory=list, description="安全与互动底线Bot在任何情况下都必须遵守的原则")
reply_style: str = Field(default="", description="表达风格")
prompt_mode: Literal["s4u", "normal"] = Field(default="s4u", description="Prompt模式")
compress_personality: bool = Field(default=True, description="是否压缩人格")
@@ -341,31 +343,11 @@ class ExpressionConfig(ValidatedConfigBase):
# 如果都没有匹配,返回默认值
return True, True, 1.0
class ToolHistoryConfig(ValidatedConfigBase):
"""工具历史记录配置类"""
enable_history: bool = True
"""是否启用工具历史记录"""
enable_prompt_history: bool = True
"""是否在提示词中加入工具历史记录"""
max_history: int = 5
"""注入到提示词中的最大工具历史记录数量"""
data_dir: str = "data/tool_history"
"""历史记录保存目录"""
class ToolConfig(ValidatedConfigBase):
"""工具配置类"""
enable_tool: bool = Field(default=False, description="启用工具")
history: ToolHistoryConfig = Field(default_factory=ToolHistoryConfig)
"""工具历史记录配置"""
class VoiceConfig(ValidatedConfigBase):
"""语音识别配置类"""
@@ -385,6 +367,7 @@ class EmojiConfig(ValidatedConfigBase):
content_filtration: bool = Field(default=False, description="内容过滤")
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")
enable_emotion_analysis: bool = Field(default=True, description="启用情感分析")
emoji_selection_mode: Literal["emotion", "description"] = Field(default="emotion", description="表情选择模式")
max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量0为全部")
@@ -494,6 +477,13 @@ class ExperimentalConfig(ValidatedConfigBase):
pfc_chatting: bool = Field(default=False, description="启用PFC聊天")
class ServerConfig(ValidatedConfigBase):
"""主服务器配置类"""
host: str = Field(default="127.0.0.1", description="主服务器监听地址")
port: int = Field(default=8080, description="主服务器监听端口")
class MaimMessageConfig(ValidatedConfigBase):
"""maim_message配置类"""
@@ -653,9 +643,6 @@ class SleepSystemConfig(ValidatedConfigBase):
)
max_sleep_delay_minutes: int = Field(default=60, description="单日最大延迟入睡分钟数")
enable_pre_sleep_notification: bool = Field(default=True, description="是否启用睡前消息")
pre_sleep_notification_groups: List[str] = Field(
default_factory=list, description='接收睡前消息的群号列表, 格式: ["platform:group_id1", "platform:group_id2"]'
)
pre_sleep_prompt: str = Field(
default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示"
)
@@ -676,15 +663,6 @@ class CrossContextConfig(ValidatedConfigBase):
enable: bool = Field(default=False, description="是否启用跨群聊上下文共享功能")
groups: List[ContextGroup] = Field(default_factory=list, description="上下文共享组列表")
class MaizoneIntercomConfig(ValidatedConfigBase):
"""Maizone互通组配置"""
enable: bool = Field(default=False, description="是否启用Maizone互通组功能")
groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表")
class CommandConfig(ValidatedConfigBase):
"""命令系统配置类"""

View File

@@ -117,9 +117,9 @@ class MainSystem:
# 停止消息重组器
from src.plugin_system.core.event_manager import event_manager
from src.plugin_system import EventType
import asyncio
asyncio.run(event_manager.trigger_event(EventType.ON_STOP,permission_group="SYSTEM"))
from src.utils.message_chunker import reassembler
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
@@ -251,6 +251,11 @@ MoFox_Bot(第三方修改版)
self.hippocampus_manager.initialize()
logger.info("记忆系统初始化成功")
# 初始化LPMM知识库
from src.chat.knowledge.knowledge_lib import initialize_lpmm_knowledge
initialize_lpmm_knowledge()
logger.info("LPMM知识库初始化成功")
# 初始化异步记忆管理器
try:
from src.chat.memory_system.async_memory_optimizer import async_memory_manager

View File

@@ -117,7 +117,7 @@ async def build_cross_context_s4u(
if not cross_context_messages:
return ""
return "# 跨上下文参考\n" + "\n\n".join(cross_context_messages) + "\n"
return "### 其他群聊中的聊天记录\n" + "\n\n".join(cross_context_messages) + "\n"
async def get_chat_history_by_group_name(group_name: str) -> str:

View File

@@ -381,6 +381,61 @@ class BaseAction(ABC):
logger.error(f"{self.log_prefix} 发送命令时出错: {e}")
return False
async def call_action(self, action_name: str, action_data: Optional[dict] = None) -> Tuple[bool, str]:
"""
在当前Action中调用另一个Action。
Args:
action_name (str): 要调用的Action的名称。
action_data (Optional[dict], optional): 传递给被调用Action的动作数据。如果为None则使用当前Action的action_data。
Returns:
Tuple[bool, str]: 被调用Action的执行结果 (is_success, message)。
"""
log_prefix = f"{self.log_prefix} [call_action -> {action_name}]"
logger.info(f"{log_prefix} 尝试调用Action: {action_name}")
try:
# 1. 从注册中心获取Action类
from src.plugin_system.core.component_registry import component_registry
action_class = component_registry.get_component_class(action_name, ComponentType.ACTION)
if not action_class:
logger.error(f"{log_prefix} 未找到Action: {action_name}")
return False, f"未找到Action: {action_name}"
# 2. 准备实例化参数
# 复用当前Action的大部分上下文信息
called_action_data = action_data if action_data is not None else self.action_data
component_info = component_registry.get_component_info(action_name, ComponentType.ACTION)
if not component_info:
logger.warning(f"{log_prefix} 未找到Action组件信息: {action_name}")
return False, f"未找到Action组件信息: {action_name}"
plugin_config = component_registry.get_plugin_config(component_info.plugin_name)
# 3. 实例化被调用的Action
action_instance = action_class(
action_data=called_action_data,
reasoning=f"Called by {self.action_name}",
cycle_timers=self.cycle_timers,
thinking_id=self.thinking_id,
chat_stream=self.chat_stream,
log_prefix=log_prefix,
plugin_config=plugin_config,
action_message=self.action_message,
)
# 4. 执行Action
logger.debug(f"{log_prefix} 开始执行...")
result = await action_instance.execute()
logger.info(f"{log_prefix} 执行完成,结果: {result}")
return result
except Exception as e:
logger.error(f"{log_prefix} 调用时发生错误: {e}", exc_info=True)
return False, f"调用Action '{action_name}' 时发生错误: {e}"
@classmethod
def get_action_info(cls) -> "ActionInfo":
"""从类属性生成ActionInfo

View File

@@ -21,7 +21,7 @@ class AtAction(BaseAction):
# === 基本信息(必须填写)===
action_name = "at_user"
action_description = "发送艾特消息"
activation_type = ActionActivationType.LLM_JUDGE
activation_type = ActionActivationType.LLM_JUDGE # 消息接收时激活(?)
parallel_action = False
chat_type_allow = ChatType.GROUP
@@ -48,144 +48,122 @@ class AtAction(BaseAction):
if not user_name or not at_message:
logger.warning("艾特用户的动作缺少必要参数。")
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送消息: {at_message},失败了,因为没有提供必要参数",
action_done=False,
)
return False, "缺少必要参数"
from src.plugin_system.apis import send_api
from fuzzywuzzy import process
group_id = self.chat_stream.group_info.group_id
if not group_id:
return False, "无法获取群组ID"
response = await send_api.adapter_command_to_stream(
action="get_group_member_list",
params={"group_id": group_id},
stream_id=self.chat_id,
)
if response.get("status") != "ok":
return False, f"获取群成员列表失败: {response.get('message')}"
member_list = response.get("data", [])
if not member_list:
return False, "群成员列表为空"
# 优化用户匹配逻辑
best_match = None
user_id = None
# 1. 完全精确匹配
for member in member_list:
card = member.get("card", "")
nickname = member.get("nickname", "")
if user_name == card or user_name == nickname:
best_match = card if user_name == card else nickname
user_id = member["user_id"]
logger.info(f"找到完全精确匹配: '{user_name}' -> '{best_match}' (ID: {user_id})")
break
# 2. 包含关系匹配
if not best_match:
containing_matches = []
for member in member_list:
card = member.get("card", "")
nickname = member.get("nickname", "")
if user_name in card:
containing_matches.append((card, member["user_id"]))
elif user_name in nickname:
containing_matches.append((nickname, member["user_id"]))
if containing_matches:
# 选择最短的匹配项,因为通常更精确
best_match, user_id = min(containing_matches, key=lambda x: len(x[0]))
logger.info(f"找到包含关系匹配: '{user_name}' -> '{best_match}' (ID: {user_id})")
# 3. 模糊匹配作为兜底
if not best_match:
choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list}
fuzzy_match, score = process.extractOne(user_name, choices.keys())
if score >= 60: # 维持较高的阈值
best_match = fuzzy_match
user_id = choices[best_match]
logger.info(f"找到模糊匹配: '{user_name}' -> '{best_match}' (ID: {user_id}, Score: {score})")
if not best_match:
logger.warning(f"所有匹配策略都未能找到用户: '{user_name}'")
user_info = await get_person_info_manager().get_person_info_by_name(user_name)
if not user_info or not user_info.get("user_id"):
logger.info(f"找不到名为 '{user_name}' 的用户。")
return False, "用户不存在"
user_info = {"user_id": user_id, "user_nickname": best_match}
try:
# 使用回复器生成艾特回复,而不是直接发送命令
from src.chat.replyer.default_generator import DefaultReplyer
from src.chat.message_receive.chat_stream import get_chat_manager
# 获取当前聊天流
chat_manager = get_chat_manager()
chat_stream = chat_manager.get_stream(self.chat_id)
chat_stream = self.chat_stream or chat_manager.get_stream(self.chat_id)
if not chat_stream:
logger.error(f"找不到聊天流: {self.stream_id}")
logger.error(f"找不到聊天流: {self.chat_stream}")
return False, "聊天流不存在"
# 创建回复器实例
replyer = DefaultReplyer(chat_stream)
# 优化提示词,消除记忆割裂感
reminder_task = at_message.replace("定时提醒:", "").strip()
extra_info = f"""你之前记下了一个提醒任务:'{reminder_task}'
现在时间到了,你需要去提醒用户 '{user_name}'
**重要规则**
- 你的任务**只**是生成提醒的**内容**。
- **绝对不要**在你的回复中包含任何`@`符号或者目标用户的名字。真正的@操作会由系统自动完成。
- 像一个朋友一样,自然地完成这个提醒,而不是生硬地复述任务。
# 构建回复对象,将艾特消息作为回复目标
reply_to = f"{user_name}:{at_message}"
extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}"
请直接输出提醒的**内容**。"""
success, llm_response, _ = await replyer.generate_reply_with_context(
reply_to=f"是时候提醒'{user_name}'", # 内部上下文,更符合执行任务的语境
# 使用回复器生成回复
success, llm_response, prompt = await replyer.generate_reply_with_context(
reply_to=reply_to,
extra_info=extra_info,
enable_tool=False,
from_plugin=True # 标记为插件调用以便LLM更好地理解上下文
enable_tool=False, # 艾特回复通常不需要工具调用
from_plugin=False
)
if not success or not llm_response:
logger.error("回复器生成回复失败")
return False, "回复生成失败"
final_message_raw = llm_response.get("content", "")
if not final_message_raw:
logger.warning("回复器生成了空内容")
return False, "回复内容为空"
# 对LLM生成的内容进行后处理解析[SPLIT]标记并将分段消息合并
from src.chat.utils.utils import process_llm_response
final_message_segments = process_llm_response(final_message_raw, enable_splitter=True, enable_chinese_typo=False)
final_message = " ".join(final_message_segments)
if success and llm_response:
# 获取生成回复内容
reply_content = llm_response.get("content", "")
if reply_content:
# 获取用户QQ号发送真正的艾特消息
user_id = user_info.get("user_id")
# 发送真正的艾特命令,使用回复器生成的智能内容
await self.send_command(
"SEND_AT_MESSAGE",
args={"group_id": self.chat_stream.group_info.group_id, "qq_id": user_id, "text": final_message},
display_message=f"艾特用户 {user_name} 并发送消息: {final_message}",
args={"qq_id": user_id, "text": reply_content},
display_message=f"艾特用户 {user_name} 并发送智能回复: {reply_content}",
)
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送消息: {final_message}",
action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送智能回复: {reply_content}",
action_done=True,
)
logger.info(f"成功发送艾特消息给 {user_name}: {final_message}")
return True, "艾特消息发送成功"
logger.info(f"成功通过回复器生成智能内容并发送真正的艾特消息给 {user_name}: {reply_content}")
return True, "智能艾特消息发送成功"
else:
logger.warning("回复器生成了空内容")
return False, "回复内容为空"
else:
logger.error("回复器生成回复失败")
return False, "回复生成失败"
except Exception as e:
logger.error(f"执行艾特用户动作时发生异常: {e}", exc_info=True)
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display=f"执行艾特用户动作失败:{str(e)}",
action_done=False,
)
return False, f"执行失败: {str(e)}"
class AtCommand(BaseCommand):
command_name: str = "at_user"
description: str = "通过名字艾特用户"
command_pattern: str = r"/at\s+@?(?P<name>[\S]+)(?:\s+(?P<text>.*))?"
async def execute(self) -> Tuple[bool, str, bool]:
name = self.matched_groups.get("name")
text = self.matched_groups.get("text", "")
if not name:
await self.send_text("请指定要艾特的用户名称。")
return False, "缺少用户名称", True
person_info_manager = get_person_info_manager()
user_info = await person_info_manager.get_person_info_by_name(name)
if not user_info or not user_info.get("user_id"):
await self.send_text(f"找不到名为 '{name}' 的用户。")
return False, "用户不存在", True
user_id = user_info.get("user_id")
await self.send_command(
"SEND_AT_MESSAGE",
args={"qq_id": user_id, "text": text},
display_message=f"艾特用户 {name} 并发送消息: {text}",
)
return True, "艾特消息已发送", True
@register_plugin
class AtUserPlugin(BasePlugin):
plugin_name: str = "at_user_plugin"
enable_plugin: bool = True
dependencies: list[str] = []
python_dependencies: list[str] = ["fuzzywuzzy", "python-Levenshtein"]
python_dependencies: list[str] = []
config_file_name: str = "config.toml"
config_schema: dict = {}

View File

@@ -1,7 +1,5 @@
import random
from typing import Tuple
from collections import deque
import json
# 导入新插件系统
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
@@ -22,7 +20,6 @@ logger = get_logger("emoji")
class EmojiAction(BaseAction):
"""表情动作 - 发送表情包"""
# --- 类级别属性 ---
# 激活设置
if global_config.emoji.emoji_activate_type == "llm":
activation_type = ActionActivationType.LLM_JUDGE
@@ -37,9 +34,6 @@ class EmojiAction(BaseAction):
action_name = "emoji"
action_description = "发送表情包辅助表达情绪"
# 最近发送表情的历史记录
_sent_emoji_history = deque(maxlen=4)
# LLM判断提示词
llm_judge_prompt = """
判定是否需要使用表情动作的条件:
@@ -80,22 +74,40 @@ class EmojiAction(BaseAction):
logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包")
return False, "无法获取任何带有描述的有效表情包"
# 3. 根据新配置项决定抽样数量
sample_size = global_config.emoji.max_context_emojis
if sample_size > 0 and len(all_emojis_obj) > sample_size:
sampled_emojis = random.sample(all_emojis_obj, sample_size)
# 3. 准备情感数据和后备列表
emotion_map = {}
all_emojis_data = []
for emoji in all_emojis_obj:
b64 = image_path_to_base64(emoji.full_path)
if not b64:
continue
desc = emoji.description
emotions = emoji.emotion
all_emojis_data.append((b64, desc))
for emo in emotions:
if emo not in emotion_map:
emotion_map[emo] = []
emotion_map[emo].append((b64, desc))
if not all_emojis_data:
logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据")
return False, "无法加载任何有效的表情包数据"
available_emotions = list(emotion_map.keys())
emoji_base64, emoji_description = "", ""
chosen_emotion = "表情包" # 默认描述,避免变量未定义错误
# 4. 根据配置选择不同的表情选择模式
if global_config.emoji.emoji_selection_mode == "emotion":
# --- 情感标签选择模式 ---
if not available_emotions:
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
emoji_base64, emoji_description = random.choice(all_emojis_data)
else:
sampled_emojis = all_emojis_obj # 0表示全部
# 4. 为抽样的表情包创建带编号的描述列表
prompt_emoji_list = []
for i, emoji in enumerate(sampled_emojis):
prompt_emoji_list.append(f"{i + 1}. {emoji.description}")
prompt_emoji_str = "\n".join(prompt_emoji_list)
chosen_emoji_obj: MaiEmoji = None
# 5. 获取最近的5条消息内容用于判断
# 获取最近的5条消息内容用于判断
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
messages_text = ""
if recent_messages:
@@ -106,73 +118,134 @@ class EmojiAction(BaseAction):
show_actions=False,
)
# 6. 构建prompt让LLM选择编号
# 构建prompt让LLM选择情感
prompt = f"""
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个
这是最近的聊天记录:
{messages_text}
这是理由:“{reason}
这里是可用的表情包详细描述列表:
{prompt_emoji_str}
请直接返回一个包含3个最匹配表情包编号的有序JSON列表例如[10, 2, 5],不要进行任何解释或添加其他多余的文字。
这里是可用的情感标签:{available_emotions}
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
"""
# 7. 调用LLM
if global_config.debug.show_prompt:
logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
else:
logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
# 调用LLM
models = llm_api.get_available_models()
chat_model_config = models.get("planner")
if not chat_model_config:
logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置无法调用LLM")
return False, "未找到 'planner' 模型配置"
logger.error(f"{self.log_prefix} 未找到'planner'模型配置无法调用LLM")
return False, "未找到'planner'模型配置"
success, chosen_indices_str, _, _ = await llm_api.generate_with_model(
prompt, model_config=chat_model_config, request_type="emoji_selection"
success, chosen_emotion, _, _ = await llm_api.generate_with_model(
prompt, model_config=chat_model_config, request_type="emoji"
)
selected_emoji_obj = None
if success:
try:
chosen_indices = json.loads(chosen_indices_str)
if isinstance(chosen_indices, list):
logger.info(f"{self.log_prefix} LLM选择的表情编号候选项: {chosen_indices}")
for index in chosen_indices:
if isinstance(index, int) and 1 <= index <= len(sampled_emojis):
candidate_emoji = sampled_emojis[index - 1]
if candidate_emoji.hash not in self._sent_emoji_history:
selected_emoji_obj = candidate_emoji
break
else:
logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}")
except (json.JSONDecodeError, TypeError):
logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}")
if selected_emoji_obj:
chosen_emoji_obj = selected_emoji_obj
logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}")
else:
if not success:
logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包")
logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包")
emoji_base64, emoji_description = random.choice(all_emojis_data)
else:
logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择")
chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "")
logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}")
selectable_emojis = [e for e in all_emojis_obj if e.hash not in self._sent_emoji_history]
if not selectable_emojis:
selectable_emojis = all_emojis_obj
chosen_emoji_obj = random.choice(selectable_emojis)
# 使用模糊匹配来查找最相关的情感标签
matched_key = next((key for key in emotion_map if chosen_emotion in key), None)
# 8. 发送表情包并更新历史记录
if chosen_emoji_obj:
emoji_base64 = image_path_to_base64(chosen_emoji_obj.full_path)
if emoji_base64:
send_success = await self.send_emoji(emoji_base64)
if send_success:
self._sent_emoji_history.append(chosen_emoji_obj.hash)
logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_obj.description}")
logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}")
return True, f"发送表情包: {chosen_emoji_obj.description}"
if matched_key:
emoji_base64, emoji_description = random.choice(emotion_map[matched_key])
logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' (匹配到: '{matched_key}') 的表情包: {emoji_description}")
else:
logger.warning(
f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包"
)
emoji_base64, emoji_description = random.choice(all_emojis_data)
elif global_config.emoji.emoji_selection_mode == "description":
# --- 详细描述选择模式 ---
# 获取最近的5条消息内容用于判断
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
messages_text = ""
if recent_messages:
messages_text = message_api.build_readable_messages(
messages=recent_messages,
timestamp_mode="normal_no_YMD",
truncate=False,
show_actions=False,
)
# 准备表情描述列表
emoji_descriptions = [desc for _, desc in all_emojis_data]
# 构建prompt让LLM选择描述
prompt = f"""
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个表情包描述列表中选择最匹配的一个。
这是最近的聊天记录:
{messages_text}
这是理由:“{reason}
这里是可用的表情包描述:{emoji_descriptions}
请直接返回最匹配的那个表情包描述,不要进行任何解释或添加其他多余的文字。
"""
logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
# 调用LLM
models = llm_api.get_available_models()
chat_model_config = models.get("planner")
if not chat_model_config:
logger.error(f"{self.log_prefix} 未找到'planner'模型配置无法调用LLM")
return False, "未找到'planner'模型配置"
success, chosen_description, _, _ = await llm_api.generate_with_model(
prompt, model_config=chat_model_config, request_type="emoji"
)
if not success:
logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_description}, 将随机选择一个表情包")
emoji_base64, emoji_description = random.choice(all_emojis_data)
else:
chosen_description = chosen_description.strip().replace('"', "").replace("'", "")
chosen_emotion = chosen_description # 在描述模式下,用描述作为情感标签
logger.info(f"{self.log_prefix} LLM选择的描述: {chosen_description}")
# 简单关键词匹配
matched_emoji = next((item for item in all_emojis_data if chosen_description.lower() in item[1].lower() or item[1].lower() in chosen_description.lower()), None)
# 如果包含匹配失败,尝试关键词匹配
if not matched_emoji:
keywords = ['惊讶', '困惑', '呆滞', '震惊', '', '无语', '', '可爱']
for keyword in keywords:
if keyword in chosen_description:
for item in all_emojis_data:
if any(k in item[1] for k in ['', '', '', '困惑', '无语']):
matched_emoji = item
break
if matched_emoji:
break
if matched_emoji:
emoji_base64, emoji_description = matched_emoji
logger.info(f"{self.log_prefix} 找到匹配描述的表情包: {emoji_description}")
else:
logger.warning(f"{self.log_prefix} LLM选择的描述无法匹配任何表情包, 将随机选择")
emoji_base64, emoji_description = random.choice(all_emojis_data)
else:
logger.error(f"{self.log_prefix} 无效的表情选择模式: {global_config.emoji.emoji_selection_mode}")
return False, "无效的表情选择模式"
# 7. 发送表情包
success = await self.send_emoji(emoji_base64)
if not success:
logger.error(f"{self.log_prefix} 表情包发送失败")
await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个表情包,但失败了",action_done= False)
return False, "表情包发送失败"
await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个表情包",action_done= True)
return True, f"发送表情包: {emoji_description}"
except Exception as e:
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True)

View File

@@ -346,8 +346,8 @@ class QZoneService:
def _load_local_images(self, image_dir: str) -> List[bytes]:
"""随机加载本地图片(不删除文件)"""
images = []
if not os.path.exists(image_dir):
logger.warning(f"图片目录不存在: {image_dir}")
if not image_dir or not os.path.exists(image_dir):
logger.warning(f"图片目录不存在或未配置: {image_dir}")
return images
try:

View File

@@ -1,4 +1,3 @@
import orjson
from src.plugin_system import BaseEventHandler
from src.plugin_system.base.base_event import HandlerResult

View File

@@ -318,7 +318,7 @@ class NapcatAdapterPlugin(BasePlugin):
"maibot_server": {
"host": ConfigField(type=str, default="localhost", description="麦麦在.env文件中设置的主机地址即HOST字段"),
"port": ConfigField(type=int, default=8000, description="麦麦在.env文件中设置的端口即PORT字段"),
"platform_name": ConfigField(type=str, default="napcat", description="平台名称,用于消息路由"),
"platform_name": ConfigField(type=str, default="qq", description="平台名称,用于消息路由"),
},
"voice": {
"use_tts": ConfigField(type=bool, default=False, description="是否使用tts语音请确保你配置了tts并有对应的adapter"),

View File

@@ -0,0 +1,9 @@
{
"manifest_version": 1,
"name": "智能提醒插件",
"version": "1.0.0",
"description": "一个能从对话中智能识别并设置定时提醒的插件。",
"author": {
"name": "墨墨"
}
}

View File

@@ -0,0 +1,198 @@
import asyncio
from datetime import datetime
from typing import List, Tuple, Type
from dateutil.parser import parse as parse_datetime
from src.common.logger import get_logger
from src.manager.async_task_manager import AsyncTask, async_task_manager
from src.person_info.person_info import get_person_info_manager
from src.plugin_system import (
BaseAction,
ActionInfo,
BasePlugin,
register_plugin,
ActionActivationType,
)
from src.plugin_system.apis import send_api
from src.plugin_system.base.component_types import ChatType
logger = get_logger(__name__)
# ============================ AsyncTask ============================
class ReminderTask(AsyncTask):
def __init__(self, delay: float, stream_id: str, is_group: bool, target_user_id: str, target_user_name: str, event_details: str, creator_name: str):
super().__init__(task_name=f"ReminderTask_{target_user_id}_{datetime.now().timestamp()}")
self.delay = delay
self.stream_id = stream_id
self.is_group = is_group
self.target_user_id = target_user_id
self.target_user_name = target_user_name
self.event_details = event_details
self.creator_name = creator_name
async def run(self):
try:
if self.delay > 0:
logger.info(f"等待 {self.delay:.2f} 秒后执行提醒...")
await asyncio.sleep(self.delay)
logger.info(f"执行提醒任务: 给 {self.target_user_name} 发送关于 '{self.event_details}' 的提醒")
reminder_text = f"叮咚!这是 {self.creator_name} 让我准时提醒你的事情:\n\n{self.event_details}"
if self.is_group:
# 在群聊中,构造 @ 消息段并发送
group_id = self.stream_id.split('_')[-1] if '_' in self.stream_id else self.stream_id
message_payload = [
{"type": "at", "data": {"qq": self.target_user_id}},
{"type": "text", "data": {"text": f" {reminder_text}"}}
]
await send_api.adapter_command_to_stream(
action="send_group_msg",
params={"group_id": group_id, "message": message_payload},
stream_id=self.stream_id
)
else:
# 在私聊中,直接发送文本
await send_api.text_to_stream(text=reminder_text, stream_id=self.stream_id)
logger.info(f"提醒任务 {self.task_name} 成功完成。")
except Exception as e:
logger.error(f"执行提醒任务 {self.task_name} 时出错: {e}", exc_info=True)
# =============================== Actions ===============================
class RemindAction(BaseAction):
"""一个能从对话中智能识别并设置定时提醒的动作。"""
# === 基本信息 ===
action_name = "set_reminder"
action_description = "根据用户的对话内容,智能地设置一个未来的提醒事项。"
activation_type = ActionActivationType.LLM_JUDGE
chat_type_allow = ChatType.ALL
# === LLM 判断与参数提取 ===
llm_judge_prompt = """
判断用户是否意图设置一个未来的提醒。
- 必须包含明确的时间点或时间段如“十分钟后”、“明天下午3点”、“周五”
- 必须包含一个需要被提醒的事件。
- 可能会包含需要提醒的特定人物。
- 如果只是普通的聊天或询问时间,则不应触发。
示例:
- "半小时后提醒我开会" -> 是
- "明天下午三点叫张三来一下" -> 是
- "别忘了周五把报告交了" -> 是
- "现在几点了?" -> 否
- "我明天下午有空" -> 否
请只回答""""
"""
action_parameters = {
"user_name": "需要被提醒的人的称呼或名字,如果没有明确指定给某人,则默认为'自己'",
"remind_time": "描述提醒时间的自然语言字符串,例如'十分钟后''明天下午3点'",
"event_details": "需要提醒的具体事件内容"
}
action_require = [
"当用户请求在未来的某个时间点提醒他/她或别人某件事时使用",
"适用于包含明确时间信息和事件描述的对话",
"例如:'10分钟后提醒我收快递''明天早上九点喊一下李四参加晨会'"
]
async def execute(self) -> Tuple[bool, str]:
"""执行设置提醒的动作"""
user_name = self.action_data.get("user_name")
remind_time_str = self.action_data.get("remind_time")
event_details = self.action_data.get("event_details")
if not all([user_name, remind_time_str, event_details]):
missing_params = [p for p, v in {"user_name": user_name, "remind_time": remind_time_str, "event_details": event_details}.items() if not v]
error_msg = f"缺少必要的提醒参数: {', '.join(missing_params)}"
logger.warning(f"[ReminderPlugin] LLM未能提取完整参数: {error_msg}")
return False, error_msg
# 1. 解析时间
try:
assert isinstance(remind_time_str, str)
target_time = parse_datetime(remind_time_str, fuzzy=True)
except Exception as e:
logger.error(f"[ReminderPlugin] 无法解析时间字符串 '{remind_time_str}': {e}")
await self.send_text(f"抱歉,我无法理解您说的时间 '{remind_time_str}',提醒设置失败。")
return False, f"无法解析时间 '{remind_time_str}'"
now = datetime.now()
if target_time <= now:
await self.send_text("提醒时间必须是一个未来的时间点哦,提醒设置失败。")
return False, "提醒时间必须在未来"
delay_seconds = (target_time - now).total_seconds()
# 2. 解析用户
person_manager = get_person_info_manager()
user_id_to_remind = None
user_name_to_remind = ""
assert isinstance(user_name, str)
if user_name.strip() in ["自己", "", "me"]:
user_id_to_remind = self.user_id
user_name_to_remind = self.user_nickname
else:
user_info = await person_manager.get_person_info_by_name(user_name)
if not user_info or not user_info.get("user_id"):
logger.warning(f"[ReminderPlugin] 找不到名为 '{user_name}' 的用户")
await self.send_text(f"抱歉,我的联系人里找不到叫做 '{user_name}' 的人,提醒设置失败。")
return False, f"用户 '{user_name}' 不存在"
user_id_to_remind = user_info.get("user_id")
user_name_to_remind = user_name
# 3. 创建并调度异步任务
try:
assert user_id_to_remind is not None
assert event_details is not None
reminder_task = ReminderTask(
delay=delay_seconds,
stream_id=self.chat_id,
is_group=self.is_group,
target_user_id=str(user_id_to_remind),
target_user_name=str(user_name_to_remind),
event_details=str(event_details),
creator_name=str(self.user_nickname)
)
await async_task_manager.add_task(reminder_task)
# 4. 发送确认消息
confirm_message = f"好的,我记下了。\n将在 {target_time.strftime('%Y-%m-%d %H:%M:%S')} 提醒 {user_name_to_remind}\n{event_details}"
await self.send_text(confirm_message)
return True, "提醒设置成功"
except Exception as e:
logger.error(f"[ReminderPlugin] 创建提醒任务时出错: {e}", exc_info=True)
await self.send_text("抱歉,设置提醒时发生了一点内部错误。")
return False, "设置提醒时发生内部错误"
# =============================== Plugin ===============================
@register_plugin
class ReminderPlugin(BasePlugin):
"""一个能从对话中智能识别并设置定时提醒的插件。"""
# --- 插件基础信息 ---
plugin_name = "reminder_plugin"
enable_plugin = True
dependencies = []
python_dependencies = []
config_file_name = "config.toml"
config_schema = {}
def get_plugin_components(self) -> List[Tuple[ActionInfo, Type[BaseAction]]]:
"""注册插件的所有功能组件。"""
return [
(RemindAction.get_action_info(), RemindAction)
]

View File

@@ -1,5 +1,5 @@
[inner]
version = "6.8.4"
version = "6.8.6"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值
@@ -64,9 +64,21 @@ personality_side = "用一句话或几句话描述人格的侧面特质"
# 可以描述外貌,性别,身高,职业,属性等等描述
identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发"
# 此处用于填写详细的世界观、背景故事、复杂人际关系等。
# 这部分内容将作为Bot的“背景知识”Bot被指导不应在对话中主动或频繁地复述这些设定。
background_story = ""
# 描述MoFox-Bot说话的表达风格表达习惯如要修改可以酌情新增内容
reply_style = "回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。"
# 安全与互动底线 (Bot在任何情况下都必须遵守的原则)
# 你可以在这里定义Bot的行为红线例如如何回应不恰当的问题。
safety_guidelines = [
"拒绝任何包含骚扰、冒犯、暴力、色情或危险内容的请求。",
"在拒绝时,请使用符合你人设的、坚定的语气。",
"不要执行任何可能被用于恶意目的的指令。"
]
#回复的Prompt模式选择s4u为原有s4u样式normal为0.9之前的模式
prompt_mode = "s4u" # 可选择 "s4u" 或 "normal"
@@ -246,6 +258,10 @@ steal_emoji = true # 是否偷取表情包让MoFox-Bot可以将一些表情
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
# 表情选择模式, 可选值为 "emotion" 或 "description"
# emotion: 让大模型从情感标签中选择
# description: 让大模型从详细描述中选择
emoji_selection_mode = "emotion"
max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量0为全部
[memory]
@@ -472,8 +488,6 @@ max_sleep_delay_minutes = 60
# 是否在进入“准备入睡”状态时发送一条消息通知。
enable_pre_sleep_notification = false
# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"]
pre_sleep_notification_groups = []
# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。
pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。"
insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围(分钟)
@@ -481,6 +495,9 @@ insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围
# 入睡后,经过一段延迟后触发失眠判定的延迟时间(分钟),设置为范围以增加随机性
insomnia_trigger_delay_minutes = [15, 45]
[server]
host = "127.0.0.1"
port = 8000
[cross_context] # 跨群聊/私聊上下文共享配置
# 这是总开关,用于一键启用或禁用此功能