Merge branch 'dev' of https://github.com/A0000Xz/MaiBot into dev

This commit is contained in:
A0000Xz
2025-07-01 18:43:17 +08:00
46 changed files with 2637 additions and 2819 deletions

View File

@@ -44,7 +44,7 @@
## 🔥 更新和安装
**最新版本: v0.7.0** ([更新日志](changelogs/changelog.md))
**最新版本: v0.8.1** ([更新日志](changelogs/changelog.md))
可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/tag/v0.1.0)下载最新启动器
**GitHub 分支说明:**
@@ -53,7 +53,7 @@
- `classical`: 旧版本(停止维护)
### 最新版本部署教程
- [从0.6升级须知](https://docs.mai-mai.org/faq/maibot/update_to_07.html)
- [从0.6/0.7升级须知](https://docs.mai-mai.org/faq/maibot/update_to_07.html)
- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于 MaiCore 的新版本部署方式(与旧版本不兼容)
> [!WARNING]
@@ -66,11 +66,11 @@
## 💬 讨论
- [](https://qm.qq.com/q/VQ3XZrWgMs) |
[](https://qm.qq.com/q/wGePTl1UyY) |
- [](https://qm.qq.com/q/wGePTl1UyY) |
[](https://qm.qq.com/q/VQ3XZrWgMs) |
[二群](https://qm.qq.com/q/RzmCiRtHEW) |
[五群](https://qm.qq.com/q/JxvHZnxyec)(已满) |
[三群](https://qm.qq.com/q/wlH5eT8OmQ)(已满)
[五群](https://qm.qq.com/q/JxvHZnxyec) |
[三群](https://qm.qq.com/q/wlH5eT8OmQ)
## 📚 文档

14
bot.py
View File

@@ -314,10 +314,16 @@ if __name__ == "__main__":
# Schedule tasks returns a future that runs forever.
# We can run console_input_loop concurrently.
main_tasks = loop.create_task(main_system.schedule_tasks())
console_task = loop.create_task(console_input_loop(main_system))
# Wait for all tasks to complete (which they won't, normally)
loop.run_until_complete(asyncio.gather(main_tasks, console_task))
# 仅在 TTY 中启用 console_input_loop
if sys.stdin.isatty():
logger.info("检测到终端环境,启用控制台输入循环")
console_task = loop.create_task(console_input_loop(main_system))
# Wait for all tasks to complete (which they won't, normally)
loop.run_until_complete(asyncio.gather(main_tasks, console_task))
else:
logger.info("非终端环境,跳过控制台输入循环")
# Wait for all tasks to complete (which they won't, normally)
loop.run_until_complete(main_tasks)
except KeyboardInterrupt:
# loop.run_until_complete(get_global_api().stop())

View File

@@ -1,5 +1,13 @@
# Changelog
## [0.8.1] - 2025-6-27
- 修复表情包配置无效问题
- 合并normal和focus的prompt构建
## [0.8.0] - 2025-6-27
MaiBot 0.8.0 现已推出!

View File

@@ -57,9 +57,9 @@ services:
ports:
- "8120:8080"
volumes:
- ./data/MaiMBot/MaiBot.db:/data/MaiBot.db
- ./data/MaiMBot:/data/MaiMBot
environment:
- SQLITE_DATABASE=MaiBot.db # 你的数据库文件
- SQLITE_DATABASE=MaiMBot/MaiBot.db # 你的数据库文件
networks:
- maim_bot
networks:

View File

@@ -25,7 +25,6 @@ class CycleDetail:
self.loop_processor_info: Dict[str, Any] = {} # 前处理器信息
self.loop_plan_info: Dict[str, Any] = {}
self.loop_action_info: Dict[str, Any] = {}
self.loop_post_processor_info: Dict[str, Any] = {} # 后处理器信息
def to_dict(self) -> Dict[str, Any]:
"""将循环信息转换为字典格式"""
@@ -80,7 +79,6 @@ class CycleDetail:
"loop_processor_info": convert_to_serializable(self.loop_processor_info),
"loop_plan_info": convert_to_serializable(self.loop_plan_info),
"loop_action_info": convert_to_serializable(self.loop_action_info),
"loop_post_processor_info": convert_to_serializable(self.loop_post_processor_info),
}
def complete_cycle(self):
@@ -135,4 +133,3 @@ class CycleDetail:
self.loop_processor_info = loop_info["loop_processor_info"]
self.loop_plan_info = loop_info["loop_plan_info"]
self.loop_action_info = loop_info["loop_action_info"]
self.loop_post_processor_info = loop_info["loop_post_processor_info"]

View File

@@ -13,40 +13,32 @@ from src.chat.heart_flow.observation.observation import Observation
from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail
from src.chat.focus_chat.info.info_base import InfoBase
from src.chat.focus_chat.info_processors.chattinginfo_processor import ChattingInfoProcessor
from src.chat.focus_chat.info_processors.relationship_processor import PersonImpressionpProcessor
from src.chat.focus_chat.info_processors.working_memory_processor import WorkingMemoryProcessor
from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation
from src.chat.heart_flow.observation.working_observation import WorkingMemoryObservation
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.heart_flow.observation.structure_observation import StructureObservation
from src.chat.heart_flow.observation.actions_observation import ActionObservation
from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor
from src.chat.focus_chat.memory_activator import MemoryActivator
from src.chat.focus_chat.info_processors.base_processor import BaseProcessor
from src.chat.focus_chat.info_processors.expression_selector_processor import ExpressionSelectorProcessor
from src.chat.focus_chat.planners.planner_factory import PlannerFactory
from src.chat.focus_chat.planners.modify_actions import ActionModifier
from src.chat.focus_chat.planners.action_manager import ActionManager
from src.config.config import global_config
from src.chat.focus_chat.hfc_performance_logger import HFCPerformanceLogger
from src.chat.focus_chat.hfc_version_manager import get_hfc_version
from src.chat.focus_chat.info.relation_info import RelationInfo
from src.chat.focus_chat.info.expression_selection_info import ExpressionSelectionInfo
from src.chat.focus_chat.info.structured_info import StructuredInfo
from src.person_info.relationship_builder_manager import relationship_builder_manager
install(extra_lines=3)
# 超时常量配置
MEMORY_ACTIVATION_TIMEOUT = 5.0 # 记忆激活任务超时时限(秒)
ACTION_MODIFICATION_TIMEOUT = 15.0 # 动作修改任务超时时限(秒)
# 注释:原来的动作修改超时常量已移除,因为改为顺序执行
# 定义观察器映射:键是观察器名称,值是 (观察器类, 初始化参数)
OBSERVATION_CLASSES = {
"ChattingObservation": (ChattingObservation, "chat_id"),
"WorkingMemoryObservation": (WorkingMemoryObservation, "observe_id"),
"HFCloopObservation": (HFCloopObservation, "observe_id"),
"StructureObservation": (StructureObservation, "observe_id"),
}
# 定义处理器映射:键是处理器名称,值是 (处理器类, 可选的配置键名)
@@ -55,13 +47,6 @@ PROCESSOR_CLASSES = {
"WorkingMemoryProcessor": (WorkingMemoryProcessor, "working_memory_processor"),
}
# 定义后期处理器映射:在规划后、动作执行前运行的处理器
POST_PLANNING_PROCESSOR_CLASSES = {
"ToolProcessor": (ToolProcessor, "tool_use_processor"),
"PersonImpressionpProcessor": (PersonImpressionpProcessor, "person_impression_processor"),
"ExpressionSelectorProcessor": (ExpressionSelectorProcessor, "expression_selector_processor"),
}
logger = get_logger("hfc") # Logger Name Changed
@@ -112,6 +97,8 @@ class HeartFChatting:
self.memory_activator = MemoryActivator()
self.relationship_builder = relationship_builder_manager.get_or_create_builder(self.stream_id)
# 新增:消息计数器和疲惫阈值
self._message_count = 0 # 发送的消息计数
# 基于exit_focus_threshold动态计算疲惫阈值
@@ -132,30 +119,11 @@ class HeartFChatting:
if not config_key or getattr(config_processor_settings, config_key, True):
self.enabled_processor_names.append(proc_name)
# 初始化后期处理器(规划后执行的处理器)
self.enabled_post_planning_processor_names = []
for proc_name, (_proc_class, config_key) in POST_PLANNING_PROCESSOR_CLASSES.items():
# 对于关系处理器,需要同时检查两个配置项
if proc_name == "PersonImpressionpProcessor":
if global_config.relationship.enable_relationship and getattr(
config_processor_settings, config_key, True
):
self.enabled_post_planning_processor_names.append(proc_name)
else:
# 其他后期处理器的逻辑
if not config_key or getattr(config_processor_settings, config_key, True):
self.enabled_post_planning_processor_names.append(proc_name)
# logger.info(f"{self.log_prefix} 将启用的处理器: {self.enabled_processor_names}")
# logger.info(f"{self.log_prefix} 将启用的后期处理器: {self.enabled_post_planning_processor_names}")
self.processors: List[BaseProcessor] = []
self._register_default_processors()
# 初始化后期处理器
self.post_planning_processors: List[BaseProcessor] = []
self._register_post_planning_processors()
self.action_manager = ActionManager()
self.action_planner = PlannerFactory.create_planner(
log_prefix=self.log_prefix, action_manager=self.action_manager
@@ -197,7 +165,7 @@ class HeartFChatting:
# 检查是否需要跳过WorkingMemoryObservation
if name == "WorkingMemoryObservation":
# 如果工作记忆处理器被禁用则跳过WorkingMemoryObservation
if not global_config.focus_chat_processor.working_memory_processor:
if not global_config.focus_chat.working_memory_processor:
logger.debug(f"{self.log_prefix} 工作记忆处理器已禁用,跳过注册观察器 {name}")
continue
@@ -222,16 +190,13 @@ class HeartFChatting:
processor_info = PROCESSOR_CLASSES.get(name) # processor_info is (ProcessorClass, config_key)
if processor_info:
processor_actual_class = processor_info[0] # 获取实际的类定义
# 根据处理器类名判断是否需要 subheartflow_id
if name in [
"WorkingMemoryProcessor",
]:
self.processors.append(processor_actual_class(subheartflow_id=self.stream_id))
elif name == "ChattingInfoProcessor":
# 根据处理器类名判断构造参数
if name == "ChattingInfoProcessor":
self.processors.append(processor_actual_class())
elif name == "WorkingMemoryProcessor":
self.processors.append(processor_actual_class(subheartflow_id=self.stream_id))
else:
# 对于PROCESSOR_CLASSES中定义但此处未明确处理构造的处理器
# (例如, 新增了一个处理器到PROCESSOR_CLASSES, 它不需要id, 也不叫ChattingInfoProcessor)
try:
self.processors.append(processor_actual_class()) # 尝试无参构造
logger.debug(f"{self.log_prefix} 注册处理器 {name} (尝试无参构造).")
@@ -250,46 +215,6 @@ class HeartFChatting:
else:
logger.warning(f"{self.log_prefix} 没有注册任何处理器。这可能是由于配置错误或所有处理器都被禁用了。")
def _register_post_planning_processors(self):
"""根据 self.enabled_post_planning_processor_names 注册后期处理器"""
self.post_planning_processors = [] # 清空已有的
for name in self.enabled_post_planning_processor_names: # 'name' is "PersonImpressionpProcessor", etc.
processor_info = POST_PLANNING_PROCESSOR_CLASSES.get(name) # processor_info is (ProcessorClass, config_key)
if processor_info:
processor_actual_class = processor_info[0] # 获取实际的类定义
# 根据处理器类名判断是否需要 subheartflow_id
if name in [
"ToolProcessor",
"PersonImpressionpProcessor",
"ExpressionSelectorProcessor",
]:
self.post_planning_processors.append(processor_actual_class(subheartflow_id=self.stream_id))
else:
# 对于POST_PLANNING_PROCESSOR_CLASSES中定义但此处未明确处理构造的处理器
# (例如, 新增了一个处理器到POST_PLANNING_PROCESSOR_CLASSES, 它不需要id, 也不叫PersonImpressionpProcessor)
try:
self.post_planning_processors.append(processor_actual_class()) # 尝试无参构造
logger.debug(f"{self.log_prefix} 注册后期处理器 {name} (尝试无参构造).")
except TypeError:
logger.error(
f"{self.log_prefix} 后期处理器 {name} 构造失败。它可能需要参数(如 subheartflow_id但未在注册逻辑中明确处理。"
)
else:
# 这理论上不应该发生,因为 enabled_post_planning_processor_names 是从 POST_PLANNING_PROCESSOR_CLASSES 的键生成的
logger.warning(
f"{self.log_prefix} 在 POST_PLANNING_PROCESSOR_CLASSES 中未找到名为 '{name}' 的处理器定义,将跳过注册。"
)
if self.post_planning_processors:
logger.info(
f"{self.log_prefix} 已注册后期处理器: {[p.__class__.__name__ for p in self.post_planning_processors]}"
)
else:
logger.warning(
f"{self.log_prefix} 没有注册任何后期处理器。这可能是由于配置错误或所有后期处理器都被禁用了。"
)
async def start(self):
"""检查是否需要启动主循环,如果未激活则启动。"""
logger.debug(f"{self.log_prefix} 开始启动 HeartFChatting")
@@ -470,27 +395,12 @@ class HeartFChatting:
("\n前处理器耗时: " + "; ".join(processor_time_strings)) if processor_time_strings else ""
)
# 新增:输出每个后处理器的耗时
post_processor_time_costs = self._current_cycle_detail.loop_post_processor_info.get(
"post_processor_time_costs", {}
)
post_processor_time_strings = []
for pname, ptime in post_processor_time_costs.items():
formatted_ptime = f"{ptime * 1000:.2f}毫秒" if ptime < 1 else f"{ptime:.2f}"
post_processor_time_strings.append(f"{pname}: {formatted_ptime}")
post_processor_time_log = (
("\n后处理器耗时: " + "; ".join(post_processor_time_strings))
if post_processor_time_strings
else ""
)
logger.info(
f"{self.log_prefix}{self._current_cycle_detail.cycle_id}次思考,"
f"耗时: {self._current_cycle_detail.end_time - self._current_cycle_detail.start_time:.1f}秒, "
f"动作: {self._current_cycle_detail.loop_plan_info.get('action_result', {}).get('action_type', '未知动作')}"
+ (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "")
+ processor_time_log
+ post_processor_time_log
)
# 记录性能数据
@@ -501,8 +411,7 @@ class HeartFChatting:
"action_type": action_result.get("action_type", "unknown"),
"total_time": self._current_cycle_detail.end_time - self._current_cycle_detail.start_time,
"step_times": cycle_timers.copy(),
"processor_time_costs": processor_time_costs, # 处理器时间
"post_processor_time_costs": post_processor_time_costs, # 后处理器时间
"processor_time_costs": processor_time_costs, # 处理器时间
"reasoning": action_result.get("reasoning", ""),
"success": self._current_cycle_detail.loop_action_info.get("action_taken", False),
}
@@ -589,10 +498,7 @@ class HeartFChatting:
processor_name = processor.__class__.log_prefix
async def run_with_timeout(proc=processor):
return await asyncio.wait_for(
proc.process_info(observations=observations),
timeout=global_config.focus_chat.processor_max_time,
)
return await asyncio.wait_for(proc.process_info(observations=observations), 30)
task = asyncio.create_task(run_with_timeout())
@@ -621,10 +527,8 @@ class HeartFChatting:
# 记录耗时
processor_time_costs[processor_name] = duration_since_parallel_start
except asyncio.TimeoutError:
logger.info(
f"{self.log_prefix} 处理器 {processor_name} 超时(>{global_config.focus_chat.processor_max_time}s已跳过"
)
processor_time_costs[processor_name] = global_config.focus_chat.processor_max_time
logger.info(f"{self.log_prefix} 处理器 {processor_name} 超时(>30s已跳过")
processor_time_costs[processor_name] = 30
except Exception as e:
logger.error(
f"{self.log_prefix} 处理器 {processor_name} 执行失败,耗时 (自并行开始): {duration_since_parallel_start:.2f}秒. 错误: {e}",
@@ -649,190 +553,6 @@ class HeartFChatting:
return all_plan_info, processor_time_costs
async def _process_post_planning_processors_with_timing(
self, observations: List[Observation], action_type: str, action_data: dict
) -> tuple[dict, dict]:
"""
处理后期处理器(规划后执行的处理器)并收集详细时间统计
包括:关系处理器、表达选择器、记忆激活器
参数:
observations: 观察器列表
action_type: 动作类型
action_data: 原始动作数据
返回:
tuple[dict, dict]: (更新后的动作数据, 后处理器时间统计)
"""
logger.info(f"{self.log_prefix} 开始执行后期处理器(带详细统计)")
# 创建所有后期任务
task_list = []
task_to_name_map = {}
task_start_times = {}
post_processor_time_costs = {}
# 添加后期处理器任务
for processor in self.post_planning_processors:
processor_name = processor.__class__.__name__
async def run_processor_with_timeout_and_timing(proc=processor, name=processor_name):
start_time = time.time()
try:
result = await asyncio.wait_for(
proc.process_info(observations=observations, action_type=action_type, action_data=action_data),
timeout=global_config.focus_chat.processor_max_time,
)
end_time = time.time()
post_processor_time_costs[name] = end_time - start_time
logger.debug(f"{self.log_prefix} 后期处理器 {name} 耗时: {end_time - start_time:.3f}")
return result
except Exception as e:
end_time = time.time()
post_processor_time_costs[name] = end_time - start_time
logger.warning(f"{self.log_prefix} 后期处理器 {name} 执行异常,耗时: {end_time - start_time:.3f}")
raise e
task = asyncio.create_task(run_processor_with_timeout_and_timing())
task_list.append(task)
task_to_name_map[task] = ("processor", processor_name)
task_start_times[task] = time.time()
logger.info(f"{self.log_prefix} 启动后期处理器任务: {processor_name}")
# 添加记忆激活器任务
async def run_memory_with_timeout_and_timing():
start_time = time.time()
try:
result = await asyncio.wait_for(
self.memory_activator.activate_memory(observations),
timeout=MEMORY_ACTIVATION_TIMEOUT,
)
end_time = time.time()
post_processor_time_costs["MemoryActivator"] = end_time - start_time
logger.debug(f"{self.log_prefix} 记忆激活器耗时: {end_time - start_time:.3f}")
return result
except Exception as e:
end_time = time.time()
post_processor_time_costs["MemoryActivator"] = end_time - start_time
logger.warning(f"{self.log_prefix} 记忆激活器执行异常,耗时: {end_time - start_time:.3f}")
raise e
memory_task = asyncio.create_task(run_memory_with_timeout_and_timing())
task_list.append(memory_task)
task_to_name_map[memory_task] = ("memory", "MemoryActivator")
task_start_times[memory_task] = time.time()
logger.info(f"{self.log_prefix} 启动记忆激活器任务")
# 如果没有任何后期任务,直接返回
if not task_list:
logger.info(f"{self.log_prefix} 没有启用的后期处理器或记忆激活器")
return action_data, {}
# 等待所有任务完成
pending_tasks = set(task_list)
all_post_plan_info = []
running_memorys = []
while pending_tasks:
done, pending_tasks = await asyncio.wait(pending_tasks, return_when=asyncio.FIRST_COMPLETED)
for task in done:
task_type, task_name = task_to_name_map[task]
try:
result = await task
if task_type == "processor":
logger.info(f"{self.log_prefix} 后期处理器 {task_name} 已完成!")
if result is not None:
all_post_plan_info.extend(result)
else:
logger.warning(f"{self.log_prefix} 后期处理器 {task_name} 返回了 None")
elif task_type == "memory":
logger.info(f"{self.log_prefix} 记忆激活器已完成!")
if result is not None:
running_memorys = result
else:
logger.warning(f"{self.log_prefix} 记忆激活器返回了 None")
running_memorys = []
except asyncio.TimeoutError:
# 对于超时任务,记录已用时间
elapsed_time = time.time() - task_start_times[task]
if task_type == "processor":
post_processor_time_costs[task_name] = elapsed_time
logger.warning(
f"{self.log_prefix} 后期处理器 {task_name} 超时(>{global_config.focus_chat.processor_max_time}s已跳过耗时: {elapsed_time:.3f}"
)
elif task_type == "memory":
post_processor_time_costs["MemoryActivator"] = elapsed_time
logger.warning(
f"{self.log_prefix} 记忆激活器超时(>{MEMORY_ACTIVATION_TIMEOUT}s已跳过耗时: {elapsed_time:.3f}"
)
running_memorys = []
except Exception as e:
# 对于异常任务,记录已用时间
elapsed_time = time.time() - task_start_times[task]
if task_type == "processor":
post_processor_time_costs[task_name] = elapsed_time
logger.error(
f"{self.log_prefix} 后期处理器 {task_name} 执行失败,耗时: {elapsed_time:.3f}秒. 错误: {e}",
exc_info=True,
)
elif task_type == "memory":
post_processor_time_costs["MemoryActivator"] = elapsed_time
logger.error(
f"{self.log_prefix} 记忆激活器执行失败,耗时: {elapsed_time:.3f}秒. 错误: {e}",
exc_info=True,
)
running_memorys = []
# 将后期处理器的结果整合到 action_data 中
updated_action_data = action_data.copy()
relation_info = ""
selected_expressions = []
structured_info = ""
for info in all_post_plan_info:
if isinstance(info, RelationInfo):
relation_info = info.get_processed_info()
elif isinstance(info, ExpressionSelectionInfo):
selected_expressions = info.get_expressions_for_action_data()
elif isinstance(info, StructuredInfo):
structured_info = info.get_processed_info()
if relation_info:
updated_action_data["relation_info_block"] = relation_info
if selected_expressions:
updated_action_data["selected_expressions"] = selected_expressions
if structured_info:
updated_action_data["structured_info"] = structured_info
# 特殊处理running_memorys
if running_memorys:
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
for running_memory in running_memorys:
memory_str += f"{running_memory['content']}\n"
updated_action_data["memory_block"] = memory_str
logger.info(f"{self.log_prefix} 添加了 {len(running_memorys)} 个激活的记忆到action_data")
if all_post_plan_info or running_memorys:
logger.info(
f"{self.log_prefix} 后期处理完成,产生了 {len(all_post_plan_info)} 个信息项和 {len(running_memorys)} 个记忆"
)
# 输出详细统计信息
if post_processor_time_costs:
stats_str = ", ".join(
[f"{name}: {time_cost:.3f}s" for name, time_cost in post_processor_time_costs.items()]
)
logger.info(f"{self.log_prefix} 后期处理器详细耗时统计: {stats_str}")
return updated_action_data, post_processor_time_costs
async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> dict:
try:
loop_start_time = time.time()
@@ -845,12 +565,12 @@ class HeartFChatting:
"observations": self.observations,
}
# 根据配置决定是否并行执行调整动作、回忆和处理器阶段
await self.relationship_builder.build_relation()
# 并行执行调整动作、回忆和处理器阶段
with Timer("并行调整动作、处理", cycle_timers):
# 创建并行任务
async def modify_actions_task():
# 顺序执行调整动作和处理器阶段
# 第一步:动作修改
with Timer("动作修改", cycle_timers):
try:
# 调用完整的动作修改流程
await self.action_modifier.modify_actions(
observations=self.observations,
@@ -858,44 +578,17 @@ class HeartFChatting:
await self.action_observation.observe()
self.observations.append(self.action_observation)
return True
# 创建两个并行任务为LLM调用添加超时保护
action_modify_task = asyncio.create_task(
asyncio.wait_for(modify_actions_task(), timeout=ACTION_MODIFICATION_TIMEOUT)
)
processor_task = asyncio.create_task(self._process_processors(self.observations))
# 等待两个任务完成,使用超时保护和详细错误处理
action_modify_result = None
all_plan_info = []
processor_time_costs = {}
try:
action_modify_result, (all_plan_info, processor_time_costs) = await asyncio.gather(
action_modify_task, processor_task, return_exceptions=True
)
# 检查各个任务的结果
if isinstance(action_modify_result, Exception):
if isinstance(action_modify_result, asyncio.TimeoutError):
logger.error(f"{self.log_prefix} 动作修改任务超时")
else:
logger.error(f"{self.log_prefix} 动作修改任务失败: {action_modify_result}")
processor_result = (all_plan_info, processor_time_costs)
if isinstance(processor_result, Exception):
if isinstance(processor_result, asyncio.TimeoutError):
logger.error(f"{self.log_prefix} 处理器任务超时")
else:
logger.error(f"{self.log_prefix} 处理器任务失败: {processor_result}")
all_plan_info = []
processor_time_costs = {}
else:
all_plan_info, processor_time_costs = processor_result
logger.debug(f"{self.log_prefix} 动作修改完成")
except Exception as e:
logger.error(f"{self.log_prefix} 并行任务gather失败: {e}")
logger.error(f"{self.log_prefix} 动作修改失败: {e}")
# 继续执行,不中断流程
# 第二步:信息处理器
with Timer("信息处理器", cycle_timers):
try:
all_plan_info, processor_time_costs = await self._process_processors(self.observations)
except Exception as e:
logger.error(f"{self.log_prefix} 信息处理器失败: {e}")
# 设置默认值以继续执行
all_plan_info = []
processor_time_costs = {}
@@ -908,14 +601,13 @@ class HeartFChatting:
logger.debug(f"{self.log_prefix} 并行阶段完成准备进入规划器plan_info数量: {len(all_plan_info)}")
with Timer("规划器", cycle_timers):
plan_result = await self.action_planner.plan(all_plan_info, [], loop_start_time)
plan_result = await self.action_planner.plan(all_plan_info, self.observations, loop_start_time)
loop_plan_info = {
"action_result": plan_result.get("action_result", {}),
"observed_messages": plan_result.get("observed_messages", ""),
}
# 修正将后期处理器从执行动作Timer中分离出来
action_type, action_data, reasoning = (
plan_result.get("action_result", {}).get("action_type", "error"),
plan_result.get("action_result", {}).get("action_data", {}),
@@ -931,22 +623,7 @@ class HeartFChatting:
logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}'")
# 添加:单独计时后期处理器,并收集详细统计
post_processor_time_costs = {}
if action_type != "no_reply":
with Timer("后期处理器", cycle_timers):
logger.debug(f"{self.log_prefix} 执行后期处理器(动作类型: {action_type}")
# 记录详细的后处理器时间
post_start_time = time.time()
action_data, post_processor_time_costs = await self._process_post_planning_processors_with_timing(
self.observations, action_type, action_data
)
post_end_time = time.time()
logger.info(f"{self.log_prefix} 后期处理器总耗时: {post_end_time - post_start_time:.3f}")
else:
logger.debug(f"{self.log_prefix} 跳过后期处理器(动作类型: {action_type}")
# 修正:纯动作执行计时
# 动作执行计时
with Timer("动作执行", cycle_timers):
success, reply_text, command = await self._handle_action(
action_type, reasoning, action_data, cycle_timers, thinking_id
@@ -959,17 +636,11 @@ class HeartFChatting:
"taken_time": time.time(),
}
# 添加后处理器统计到loop_info
loop_post_processor_info = {
"post_processor_time_costs": post_processor_time_costs,
}
loop_info = {
"loop_observation_info": loop_observation_info,
"loop_processor_info": loop_processor_info,
"loop_plan_info": loop_plan_info,
"loop_action_info": loop_action_info,
"loop_post_processor_info": loop_post_processor_info, # 新增
}
return loop_info

View File

@@ -1,71 +0,0 @@
from dataclasses import dataclass
from typing import List, Dict
from .info_base import InfoBase
@dataclass
class ExpressionSelectionInfo(InfoBase):
"""表达选择信息类
用于存储和管理选中的表达方式信息。
Attributes:
type (str): 信息类型标识符,默认为 "expression_selection"
data (Dict[str, Any]): 包含选中表达方式的数据字典
"""
type: str = "expression_selection"
def get_selected_expressions(self) -> List[Dict[str, str]]:
"""获取选中的表达方式列表
Returns:
List[Dict[str, str]]: 选中的表达方式列表
"""
return self.get_info("selected_expressions") or []
def set_selected_expressions(self, expressions: List[Dict[str, str]]) -> None:
"""设置选中的表达方式列表
Args:
expressions: 选中的表达方式列表
"""
self.data["selected_expressions"] = expressions
def get_expressions_count(self) -> int:
"""获取选中表达方式的数量
Returns:
int: 表达方式数量
"""
return len(self.get_selected_expressions())
def get_processed_info(self) -> str:
"""获取处理后的信息
Returns:
str: 处理后的信息字符串
"""
expressions = self.get_selected_expressions()
if not expressions:
return ""
# 格式化表达方式为可读文本
formatted_expressions = []
for expr in expressions:
situation = expr.get("situation", "")
style = expr.get("style", "")
expr.get("type", "")
if situation and style:
formatted_expressions.append(f"{situation}时,使用 {style}")
return "\n".join(formatted_expressions)
def get_expressions_for_action_data(self) -> List[Dict[str, str]]:
"""获取用于action_data的表达方式数据
Returns:
List[Dict[str, str]]: 格式化后的表达方式数据
"""
return self.get_selected_expressions()

View File

@@ -1,34 +0,0 @@
from typing import Dict, Any
from dataclasses import dataclass, field
from .info_base import InfoBase
@dataclass
class MindInfo(InfoBase):
"""思维信息类
用于存储和管理当前思维状态的信息。
Attributes:
type (str): 信息类型标识符,默认为 "mind"
data (Dict[str, Any]): 包含 current_mind 的数据字典
"""
type: str = "mind"
data: Dict[str, Any] = field(default_factory=lambda: {"current_mind": ""})
def get_current_mind(self) -> str:
"""获取当前思维状态
Returns:
str: 当前思维状态
"""
return self.get_info("current_mind") or ""
def set_current_mind(self, mind: str) -> None:
"""设置当前思维状态
Args:
mind: 要设置的思维状态
"""
self.data["current_mind"] = mind

View File

@@ -1,40 +0,0 @@
from dataclasses import dataclass
from .info_base import InfoBase
@dataclass
class RelationInfo(InfoBase):
"""关系信息类
用于存储和管理当前关系状态的信息。
Attributes:
type (str): 信息类型标识符,默认为 "relation"
data (Dict[str, Any]): 包含 current_relation 的数据字典
"""
type: str = "relation"
def get_relation_info(self) -> str:
"""获取当前关系状态
Returns:
str: 当前关系状态
"""
return self.get_info("relation_info") or ""
def set_relation_info(self, relation_info: str) -> None:
"""设置当前关系状态
Args:
relation_info: 要设置的关系状态
"""
self.data["relation_info"] = relation_info
def get_processed_info(self) -> str:
"""获取处理后的信息
Returns:
str: 处理后的信息
"""
return self.get_relation_info() or ""

View File

@@ -1,85 +0,0 @@
from typing import Dict, Optional, Any, List
from dataclasses import dataclass, field
@dataclass
class StructuredInfo:
"""信息基类
这是一个基础信息类,用于存储和管理各种类型的信息数据。
所有具体的信息类都应该继承自这个基类。
Attributes:
type (str): 信息类型标识符,默认为 "base"
data (Dict[str, Union[str, Dict, list]]): 存储具体信息数据的字典,
支持存储字符串、字典、列表等嵌套数据结构
"""
type: str = "structured_info"
data: Dict[str, Any] = field(default_factory=dict)
def get_type(self) -> str:
"""获取信息类型
Returns:
str: 当前信息对象的类型标识符
"""
return self.type
def get_data(self) -> Dict[str, Any]:
"""获取所有信息数据
Returns:
Dict[str, Any]: 包含所有信息数据的字典
"""
return self.data
def get_info(self, key: str) -> Optional[Any]:
"""获取特定属性的信息
Args:
key: 要获取的属性键名
Returns:
Optional[Any]: 属性值,如果键不存在则返回 None
"""
return self.data.get(key)
def get_info_list(self, key: str) -> List[Any]:
"""获取特定属性的信息列表
Args:
key: 要获取的属性键名
Returns:
List[Any]: 属性值列表,如果键不存在则返回空列表
"""
value = self.data.get(key)
if isinstance(value, list):
return value
return []
def set_info(self, key: str, value: Any) -> None:
"""设置特定属性的信息值
Args:
key: 要设置的属性键名
value: 要设置的属性值
"""
self.data[key] = value
def get_processed_info(self) -> str:
"""获取处理后的信息
Returns:
str: 处理后的信息字符串
"""
info_str = ""
# print(f"self.data: {self.data}")
for key, value in self.data.items():
# print(f"key: {key}, value: {value}")
info_str += f"信息类型:{key},信息内容:{value}\n"
return info_str

View File

@@ -1,107 +0,0 @@
import time
import random
from typing import List
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.heart_flow.observation.observation import Observation
from src.common.logger import get_logger
from src.chat.message_receive.chat_stream import get_chat_manager
from .base_processor import BaseProcessor
from src.chat.focus_chat.info.info_base import InfoBase
from src.chat.focus_chat.info.expression_selection_info import ExpressionSelectionInfo
from src.chat.express.expression_selector import expression_selector
logger = get_logger("processor")
class ExpressionSelectorProcessor(BaseProcessor):
log_prefix = "表达选择器"
def __init__(self, subheartflow_id: str):
super().__init__()
self.subheartflow_id = subheartflow_id
self.last_selection_time = 0
self.selection_interval = 10 # 40秒间隔
self.cached_expressions = [] # 缓存上一次选择的表达方式
name = get_chat_manager().get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] 表达选择器"
async def process_info(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
**kwargs,
) -> List[InfoBase]:
"""处理信息对象
Args:
observations: 观察对象列表
Returns:
List[InfoBase]: 处理后的表达选择信息列表
"""
current_time = time.time()
# 检查频率限制
if current_time - self.last_selection_time < self.selection_interval:
logger.debug(f"{self.log_prefix} 距离上次选择不足{self.selection_interval}秒,使用缓存的表达方式")
# 使用缓存的表达方式
if self.cached_expressions:
# 从缓存的15个中随机选5个
final_expressions = random.sample(self.cached_expressions, min(5, len(self.cached_expressions)))
# 创建表达选择信息
expression_info = ExpressionSelectionInfo()
expression_info.set_selected_expressions(final_expressions)
logger.info(f"{self.log_prefix} 使用缓存选择了{len(final_expressions)}个表达方式")
return [expression_info]
else:
logger.debug(f"{self.log_prefix} 没有缓存的表达方式,跳过选择")
return []
# 获取聊天内容
chat_info = ""
if observations:
for observation in observations:
if isinstance(observation, ChattingObservation):
# chat_info = observation.get_observe_info()
chat_info = observation.talking_message_str_truncate_short
break
if not chat_info:
logger.debug(f"{self.log_prefix} 没有聊天内容,跳过表达方式选择")
return []
try:
if action_type == "reply":
target_message = action_data.get("reply_to", "")
else:
target_message = ""
# LLM模式调用LLM选择5-10个然后随机选5个
selected_expressions = await expression_selector.select_suitable_expressions_llm(
self.subheartflow_id, chat_info, max_num=12, min_num=2, target_message=target_message
)
cache_size = len(selected_expressions) if selected_expressions else 0
mode_desc = f"LLM模式已缓存{cache_size}个)"
if selected_expressions:
self.cached_expressions = selected_expressions
self.last_selection_time = current_time
# 创建表达选择信息
expression_info = ExpressionSelectionInfo()
expression_info.set_selected_expressions(selected_expressions)
logger.info(f"{self.log_prefix} 为当前聊天选择了{len(selected_expressions)}个表达方式({mode_desc}")
return [expression_info]
else:
logger.debug(f"{self.log_prefix} 未选择任何表达方式")
return []
except Exception as e:
logger.error(f"{self.log_prefix} 处理表达方式选择时出错: {e}")
return []

View File

@@ -1,951 +0,0 @@
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.heart_flow.observation.observation import Observation
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
import time
import traceback
from src.common.logger import get_logger
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.message_receive.chat_stream import get_chat_manager
from src.person_info.relationship_manager import get_relationship_manager
from .base_processor import BaseProcessor
from typing import List
from typing import Dict
from src.chat.focus_chat.info.info_base import InfoBase
from src.chat.focus_chat.info.relation_info import RelationInfo
from json_repair import repair_json
from src.person_info.person_info import get_person_info_manager
import json
import asyncio
from src.chat.utils.chat_message_builder import (
get_raw_msg_by_timestamp_with_chat,
get_raw_msg_by_timestamp_with_chat_inclusive,
get_raw_msg_before_timestamp_with_chat,
num_new_messages_since,
)
import os
import pickle
# 消息段清理配置
SEGMENT_CLEANUP_CONFIG = {
"enable_cleanup": True, # 是否启用清理
"max_segment_age_days": 7, # 消息段最大保存天数
"max_segments_per_user": 10, # 每用户最大消息段数
"cleanup_interval_hours": 1, # 清理间隔(小时)
}
logger = get_logger("processor")
def init_prompt():
relationship_prompt = """
<聊天记录>
{chat_observe_info}
</聊天记录>
{name_block}
现在,你想要回复{person_name}的消息,消息内容是:{target_message}。请根据聊天记录和你要回复的消息,从你对{person_name}的了解中提取有关的信息:
1.你需要提供你想要提取的信息具体是哪方面的信息例如年龄性别对ta的印象最近发生的事等等。
2.请注意,请不要重复调取相同的信息,已经调取的信息如下:
{info_cache_block}
3.如果当前聊天记录中没有需要查询的信息,或者现有信息已经足够回复,请返回{{"none": "不需要查询"}}
请以json格式输出例如
{{
"info_type": "信息类型",
}}
请严格按照json输出格式不要输出多余内容
"""
Prompt(relationship_prompt, "relationship_prompt")
fetch_info_prompt = """
{name_block}
以下是你在之前与{person_name}的交流中,产生的对{person_name}的了解:
{person_impression_block}
{points_text_block}
请从中提取用户"{person_name}"的有关"{info_type}"信息
请以json格式输出例如
{{
{info_json_str}
}}
请严格按照json输出格式不要输出多余内容
"""
Prompt(fetch_info_prompt, "fetch_person_info_prompt")
class PersonImpressionpProcessor(BaseProcessor):
log_prefix = "关系"
def __init__(self, subheartflow_id: str):
super().__init__()
self.subheartflow_id = subheartflow_id
self.info_fetching_cache: List[Dict[str, any]] = []
self.info_fetched_cache: Dict[
str, Dict[str, any]
] = {} # {person_id: {"info": str, "ttl": int, "start_time": float}}
# 新的消息段缓存结构:
# {person_id: [{"start_time": float, "end_time": float, "last_msg_time": float, "message_count": int}, ...]}
self.person_engaged_cache: Dict[str, List[Dict[str, any]]] = {}
# 持久化存储文件路径
self.cache_file_path = os.path.join("data", "relationship", f"relationship_cache_{self.subheartflow_id}.pkl")
# 最后处理的消息时间,避免重复处理相同消息
current_time = time.time()
self.last_processed_message_time = current_time
# 最后清理时间,用于定期清理老消息段
self.last_cleanup_time = 0.0
self.llm_model = LLMRequest(
model=global_config.model.relation,
request_type="focus.relationship",
)
# 小模型用于即时信息提取
self.instant_llm_model = LLMRequest(
model=global_config.model.utils_small,
request_type="focus.relationship.instant",
)
name = get_chat_manager().get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] "
# 加载持久化的缓存
self._load_cache()
# ================================
# 缓存管理模块
# 负责持久化存储、状态管理、缓存读写
# ================================
def _load_cache(self):
"""从文件加载持久化的缓存"""
if os.path.exists(self.cache_file_path):
try:
with open(self.cache_file_path, "rb") as f:
cache_data = pickle.load(f)
# 新格式:包含额外信息的缓存
self.person_engaged_cache = cache_data.get("person_engaged_cache", {})
self.last_processed_message_time = cache_data.get("last_processed_message_time", 0.0)
self.last_cleanup_time = cache_data.get("last_cleanup_time", 0.0)
logger.info(
f"{self.log_prefix} 成功加载关系缓存,包含 {len(self.person_engaged_cache)} 个用户,最后处理时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_processed_message_time)) if self.last_processed_message_time > 0 else '未设置'}"
)
except Exception as e:
logger.error(f"{self.log_prefix} 加载关系缓存失败: {e}")
self.person_engaged_cache = {}
self.last_processed_message_time = 0.0
else:
logger.info(f"{self.log_prefix} 关系缓存文件不存在,使用空缓存")
def _save_cache(self):
"""保存缓存到文件"""
try:
os.makedirs(os.path.dirname(self.cache_file_path), exist_ok=True)
cache_data = {
"person_engaged_cache": self.person_engaged_cache,
"last_processed_message_time": self.last_processed_message_time,
"last_cleanup_time": self.last_cleanup_time,
}
with open(self.cache_file_path, "wb") as f:
pickle.dump(cache_data, f)
logger.debug(f"{self.log_prefix} 成功保存关系缓存")
except Exception as e:
logger.error(f"{self.log_prefix} 保存关系缓存失败: {e}")
# ================================
# 消息段管理模块
# 负责跟踪用户消息活动、管理消息段、清理过期数据
# ================================
def _update_message_segments(self, person_id: str, message_time: float):
"""更新用户的消息段
Args:
person_id: 用户ID
message_time: 消息时间戳
"""
if person_id not in self.person_engaged_cache:
self.person_engaged_cache[person_id] = []
segments = self.person_engaged_cache[person_id]
current_time = time.time()
# 获取该消息前5条消息的时间作为潜在的开始时间
before_messages = get_raw_msg_before_timestamp_with_chat(self.subheartflow_id, message_time, limit=5)
if before_messages:
# 由于get_raw_msg_before_timestamp_with_chat返回按时间升序排序的消息最后一个是最接近message_time的
# 我们需要第一个消息作为开始时间但应该确保至少包含5条消息或该用户之前的消息
potential_start_time = before_messages[0]["time"]
else:
# 如果没有前面的消息,就从当前消息开始
potential_start_time = message_time
# 如果没有现有消息段,创建新的
if not segments:
new_segment = {
"start_time": potential_start_time,
"end_time": message_time,
"last_msg_time": message_time,
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
person_name = get_person_info_manager().get_value_sync(person_id, "person_name") or person_id
logger.info(
f"{self.log_prefix} 眼熟用户 {person_name}{time.strftime('%H:%M:%S', time.localtime(potential_start_time))} - {time.strftime('%H:%M:%S', time.localtime(message_time))} 之间有 {new_segment['message_count']} 条消息"
)
self._save_cache()
return
# 获取最后一个消息段
last_segment = segments[-1]
# 计算从最后一条消息到当前消息之间的消息数量(不包含边界)
messages_between = self._count_messages_between(last_segment["last_msg_time"], message_time)
if messages_between <= 10:
# 在10条消息内延伸当前消息段
last_segment["end_time"] = message_time
last_segment["last_msg_time"] = message_time
# 重新计算整个消息段的消息数量
last_segment["message_count"] = self._count_messages_in_timerange(
last_segment["start_time"], last_segment["end_time"]
)
logger.debug(f"{self.log_prefix} 延伸用户 {person_id} 的消息段: {last_segment}")
else:
# 超过10条消息结束当前消息段并创建新的
# 结束当前消息段延伸到原消息段最后一条消息后5条消息的时间
after_messages = get_raw_msg_by_timestamp_with_chat(
self.subheartflow_id, last_segment["last_msg_time"], current_time, limit=5, limit_mode="earliest"
)
if after_messages and len(after_messages) >= 5:
# 如果有足够的后续消息使用第5条消息的时间作为结束时间
last_segment["end_time"] = after_messages[4]["time"]
else:
# 如果没有足够的后续消息,保持原有的结束时间
pass
# 重新计算当前消息段的消息数量
last_segment["message_count"] = self._count_messages_in_timerange(
last_segment["start_time"], last_segment["end_time"]
)
# 创建新的消息段
new_segment = {
"start_time": potential_start_time,
"end_time": message_time,
"last_msg_time": message_time,
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
person_info_manager = get_person_info_manager()
person_name = person_info_manager.get_value_sync(person_id, "person_name") or person_id
logger.info(f"{self.log_prefix} 重新眼熟用户 {person_name} 创建新消息段超过10条消息间隔: {new_segment}")
self._save_cache()
def _count_messages_in_timerange(self, start_time: float, end_time: float) -> int:
"""计算指定时间范围内的消息数量(包含边界)"""
messages = get_raw_msg_by_timestamp_with_chat_inclusive(self.subheartflow_id, start_time, end_time)
return len(messages)
def _count_messages_between(self, start_time: float, end_time: float) -> int:
"""计算两个时间点之间的消息数量(不包含边界),用于间隔检查"""
return num_new_messages_since(self.subheartflow_id, start_time, end_time)
def _get_total_message_count(self, person_id: str) -> int:
"""获取用户所有消息段的总消息数量"""
if person_id not in self.person_engaged_cache:
return 0
total_count = 0
for segment in self.person_engaged_cache[person_id]:
total_count += segment["message_count"]
return total_count
def _cleanup_old_segments(self) -> bool:
"""清理老旧的消息段
Returns:
bool: 是否执行了清理操作
"""
if not SEGMENT_CLEANUP_CONFIG["enable_cleanup"]:
return False
current_time = time.time()
# 检查是否需要执行清理(基于时间间隔)
cleanup_interval_seconds = SEGMENT_CLEANUP_CONFIG["cleanup_interval_hours"] * 3600
if current_time - self.last_cleanup_time < cleanup_interval_seconds:
return False
logger.info(f"{self.log_prefix} 开始执行老消息段清理...")
cleanup_stats = {
"users_cleaned": 0,
"segments_removed": 0,
"total_segments_before": 0,
"total_segments_after": 0,
}
max_age_seconds = SEGMENT_CLEANUP_CONFIG["max_segment_age_days"] * 24 * 3600
max_segments_per_user = SEGMENT_CLEANUP_CONFIG["max_segments_per_user"]
users_to_remove = []
for person_id, segments in self.person_engaged_cache.items():
cleanup_stats["total_segments_before"] += len(segments)
original_segment_count = len(segments)
# 1. 按时间清理:移除过期的消息段
segments_after_age_cleanup = []
for segment in segments:
segment_age = current_time - segment["end_time"]
if segment_age <= max_age_seconds:
segments_after_age_cleanup.append(segment)
else:
cleanup_stats["segments_removed"] += 1
logger.debug(
f"{self.log_prefix} 移除用户 {person_id} 的过期消息段: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(segment['start_time']))} - {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(segment['end_time']))}"
)
# 2. 按数量清理:如果消息段数量仍然过多,保留最新的
if len(segments_after_age_cleanup) > max_segments_per_user:
# 按end_time排序保留最新的
segments_after_age_cleanup.sort(key=lambda x: x["end_time"], reverse=True)
segments_removed_count = len(segments_after_age_cleanup) - max_segments_per_user
cleanup_stats["segments_removed"] += segments_removed_count
segments_after_age_cleanup = segments_after_age_cleanup[:max_segments_per_user]
logger.debug(
f"{self.log_prefix} 用户 {person_id} 消息段数量过多,移除 {segments_removed_count} 个最老的消息段"
)
# 使用清理后的消息段
# 更新缓存
if len(segments_after_age_cleanup) == 0:
# 如果没有剩余消息段,标记用户为待移除
users_to_remove.append(person_id)
else:
self.person_engaged_cache[person_id] = segments_after_age_cleanup
cleanup_stats["total_segments_after"] += len(segments_after_age_cleanup)
if original_segment_count != len(segments_after_age_cleanup):
cleanup_stats["users_cleaned"] += 1
# 移除没有消息段的用户
for person_id in users_to_remove:
del self.person_engaged_cache[person_id]
logger.debug(f"{self.log_prefix} 移除用户 {person_id}:没有剩余消息段")
# 更新最后清理时间
self.last_cleanup_time = current_time
# 保存缓存
if cleanup_stats["segments_removed"] > 0 or len(users_to_remove) > 0:
self._save_cache()
logger.info(
f"{self.log_prefix} 清理完成 - 影响用户: {cleanup_stats['users_cleaned']}, 移除消息段: {cleanup_stats['segments_removed']}, 移除用户: {len(users_to_remove)}"
)
logger.info(
f"{self.log_prefix} 消息段统计 - 清理前: {cleanup_stats['total_segments_before']}, 清理后: {cleanup_stats['total_segments_after']}"
)
else:
logger.debug(f"{self.log_prefix} 清理完成 - 无需清理任何内容")
return cleanup_stats["segments_removed"] > 0 or len(users_to_remove) > 0
def force_cleanup_user_segments(self, person_id: str) -> bool:
"""强制清理指定用户的所有消息段
Args:
person_id: 用户ID
Returns:
bool: 是否成功清理
"""
if person_id in self.person_engaged_cache:
segments_count = len(self.person_engaged_cache[person_id])
del self.person_engaged_cache[person_id]
self._save_cache()
logger.info(f"{self.log_prefix} 强制清理用户 {person_id}{segments_count} 个消息段")
return True
return False
def get_cache_status(self) -> str:
"""获取缓存状态信息,用于调试和监控"""
if not self.person_engaged_cache:
return f"{self.log_prefix} 关系缓存为空"
status_lines = [f"{self.log_prefix} 关系缓存状态:"]
status_lines.append(
f"最后处理消息时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_processed_message_time)) if self.last_processed_message_time > 0 else '未设置'}"
)
status_lines.append(
f"最后清理时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_cleanup_time)) if self.last_cleanup_time > 0 else '未执行'}"
)
status_lines.append(f"总用户数:{len(self.person_engaged_cache)}")
status_lines.append(
f"清理配置:{'启用' if SEGMENT_CLEANUP_CONFIG['enable_cleanup'] else '禁用'} (最大保存{SEGMENT_CLEANUP_CONFIG['max_segment_age_days']}天, 每用户最多{SEGMENT_CLEANUP_CONFIG['max_segments_per_user']}段)"
)
status_lines.append("")
for person_id, segments in self.person_engaged_cache.items():
total_count = self._get_total_message_count(person_id)
status_lines.append(f"用户 {person_id}:")
status_lines.append(f" 总消息数:{total_count} ({total_count}/45)")
status_lines.append(f" 消息段数:{len(segments)}")
for i, segment in enumerate(segments):
start_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(segment["start_time"]))
end_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(segment["end_time"]))
last_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(segment["last_msg_time"]))
status_lines.append(
f"{i + 1}: {start_str} -> {end_str} (最后消息: {last_str}, 消息数: {segment['message_count']})"
)
status_lines.append("")
return "\n".join(status_lines)
# ================================
# 主要处理流程
# 统筹各模块协作、对外提供服务接口
# ================================
async def process_info(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
**kwargs,
) -> List[InfoBase]:
"""处理信息对象
Args:
observations: 观察对象列表
action_type: 动作类型
action_data: 动作数据
Returns:
List[InfoBase]: 处理后的结构化信息列表
"""
await self.build_relation(observations)
relation_info_str = await self.relation_identify(observations, action_type, action_data)
if relation_info_str:
relation_info = RelationInfo()
relation_info.set_relation_info(relation_info_str)
else:
relation_info = None
return None
return [relation_info]
async def build_relation(self, observations: List[Observation] = None):
"""构建关系"""
self._cleanup_old_segments()
current_time = time.time()
if observations:
for observation in observations:
if isinstance(observation, ChattingObservation):
latest_messages = get_raw_msg_by_timestamp_with_chat(
self.subheartflow_id,
self.last_processed_message_time,
current_time,
limit=50, # 获取自上次处理后的消息
)
if latest_messages:
# 处理所有新的非bot消息
for latest_msg in latest_messages:
user_id = latest_msg.get("user_id")
platform = latest_msg.get("user_platform") or latest_msg.get("chat_info_platform")
msg_time = latest_msg.get("time", 0)
if (
user_id
and platform
and user_id != global_config.bot.qq_account
and msg_time > self.last_processed_message_time
):
from src.person_info.person_info import PersonInfoManager
person_id = PersonInfoManager.get_person_id(platform, user_id)
self._update_message_segments(person_id, msg_time)
logger.debug(
f"{self.log_prefix} 更新用户 {person_id} 的消息段,消息时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(msg_time))}"
)
self.last_processed_message_time = max(self.last_processed_message_time, msg_time)
break
# 1. 检查是否有用户达到关系构建条件总消息数达到45条
users_to_build_relationship = []
for person_id, segments in self.person_engaged_cache.items():
total_message_count = self._get_total_message_count(person_id)
if total_message_count >= 45:
users_to_build_relationship.append(person_id)
logger.info(
f"{self.log_prefix} 用户 {person_id} 满足关系构建条件,总消息数:{total_message_count},消息段数:{len(segments)}"
)
elif total_message_count > 0:
# 记录进度信息
logger.debug(
f"{self.log_prefix} 用户 {person_id} 进度:{total_message_count}/45 条消息,{len(segments)} 个消息段"
)
# 2. 为满足条件的用户构建关系
for person_id in users_to_build_relationship:
segments = self.person_engaged_cache[person_id]
# 异步执行关系构建
asyncio.create_task(self.update_impression_on_segments(person_id, self.subheartflow_id, segments))
# 移除已处理的用户缓存
del self.person_engaged_cache[person_id]
self._save_cache()
async def relation_identify(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
):
"""
从人物获取信息
"""
chat_observe_info = ""
current_time = time.time()
if observations:
for observation in observations:
if isinstance(observation, ChattingObservation):
chat_observe_info = observation.get_observe_info()
# latest_message_time = observation.last_observe_time
# 从聊天观察中提取用户信息并更新消息段
# 获取最新的非bot消息来更新消息段
latest_messages = get_raw_msg_by_timestamp_with_chat(
self.subheartflow_id,
self.last_processed_message_time,
current_time,
limit=50, # 获取自上次处理后的消息
)
if latest_messages:
# 处理所有新的非bot消息
for latest_msg in latest_messages:
user_id = latest_msg.get("user_id")
platform = latest_msg.get("user_platform") or latest_msg.get("chat_info_platform")
msg_time = latest_msg.get("time", 0)
if (
user_id
and platform
and user_id != global_config.bot.qq_account
and msg_time > self.last_processed_message_time
):
from src.person_info.person_info import PersonInfoManager
person_id = PersonInfoManager.get_person_id(platform, user_id)
self._update_message_segments(person_id, msg_time)
logger.debug(
f"{self.log_prefix} 更新用户 {person_id} 的消息段,消息时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(msg_time))}"
)
self.last_processed_message_time = max(self.last_processed_message_time, msg_time)
break
for person_id in list(self.info_fetched_cache.keys()):
for info_type in list(self.info_fetched_cache[person_id].keys()):
self.info_fetched_cache[person_id][info_type]["ttl"] -= 1
if self.info_fetched_cache[person_id][info_type]["ttl"] <= 0:
del self.info_fetched_cache[person_id][info_type]
if not self.info_fetched_cache[person_id]:
del self.info_fetched_cache[person_id]
if action_type != "reply":
return None
target_message = action_data.get("reply_to", "")
if ":" in target_message:
parts = target_message.split(":", 1)
elif "" in target_message:
parts = target_message.split("", 1)
else:
logger.warning(f"reply_to格式不正确: {target_message},跳过关系识别")
return None
if len(parts) != 2:
logger.warning(f"reply_to格式不正确: {target_message},跳过关系识别")
return None
sender = parts[0].strip()
text = parts[1].strip()
person_info_manager = get_person_info_manager()
person_id = person_info_manager.get_person_id_by_person_name(sender)
if not person_id:
logger.warning(f"未找到用户 {sender} 的ID跳过关系识别")
return None
nickname_str = ",".join(global_config.bot.alias_names)
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
info_cache_block = ""
if self.info_fetching_cache:
# 对于每个(person_id, info_type)组合,只保留最新的记录
latest_records = {}
for info_fetching in self.info_fetching_cache:
key = (info_fetching["person_id"], info_fetching["info_type"])
if key not in latest_records or info_fetching["start_time"] > latest_records[key]["start_time"]:
latest_records[key] = info_fetching
# 按时间排序并生成显示文本
sorted_records = sorted(latest_records.values(), key=lambda x: x["start_time"])
for info_fetching in sorted_records:
info_cache_block += (
f"你已经调取了[{info_fetching['person_name']}]的[{info_fetching['info_type']}]信息\n"
)
prompt = (await global_prompt_manager.get_prompt_async("relationship_prompt")).format(
chat_observe_info=chat_observe_info,
name_block=name_block,
info_cache_block=info_cache_block,
person_name=sender,
target_message=text,
)
try:
logger.info(f"{self.log_prefix} 人物信息prompt: \n{prompt}\n")
content, _ = await self.llm_model.generate_response_async(prompt=prompt)
if content:
# print(f"content: {content}")
content_json = json.loads(repair_json(content))
# 检查是否返回了不需要查询的标志
if "none" in content_json:
logger.info(f"{self.log_prefix} LLM判断当前不需要查询任何信息{content_json.get('none', '')}")
# 跳过新的信息提取,但仍会处理已有缓存
else:
info_type = content_json.get("info_type")
if info_type:
self.info_fetching_cache.append(
{
"person_id": person_id,
"person_name": sender,
"info_type": info_type,
"start_time": time.time(),
"forget": False,
}
)
if len(self.info_fetching_cache) > 20:
self.info_fetching_cache.pop(0)
logger.info(f"{self.log_prefix} 调取用户 {sender} 的[{info_type}]信息。")
# 执行信息提取
await self._fetch_single_info_instant(person_id, info_type, time.time())
else:
logger.warning(f"{self.log_prefix} LLM did not return a valid info_type. Response: {content}")
except Exception as e:
logger.error(f"{self.log_prefix} 执行LLM请求或处理响应时出错: {e}")
logger.error(traceback.format_exc())
# 7. 合并缓存和新处理的信息
persons_infos_str = ""
# 处理已获取到的信息
if self.info_fetched_cache:
persons_with_known_info = [] # 有已知信息的人员
persons_with_unknown_info = [] # 有未知信息的人员
for person_id in self.info_fetched_cache:
person_known_infos = []
person_unknown_infos = []
person_name = ""
for info_type in self.info_fetched_cache[person_id]:
person_name = self.info_fetched_cache[person_id][info_type]["person_name"]
if not self.info_fetched_cache[person_id][info_type]["unknow"]:
info_content = self.info_fetched_cache[person_id][info_type]["info"]
person_known_infos.append(f"[{info_type}]{info_content}")
else:
person_unknown_infos.append(info_type)
# 如果有已知信息,添加到已知信息列表
if person_known_infos:
known_info_str = "".join(person_known_infos) + ""
persons_with_known_info.append((person_name, known_info_str))
# 如果有未知信息,添加到未知信息列表
if person_unknown_infos:
persons_with_unknown_info.append((person_name, person_unknown_infos))
# 先输出有已知信息的人员
for person_name, known_info_str in persons_with_known_info:
persons_infos_str += f"你对 {person_name} 的了解:{known_info_str}\n"
# 统一处理未知信息,避免重复的警告文本
if persons_with_unknown_info:
unknown_persons_details = []
for person_name, unknown_types in persons_with_unknown_info:
unknown_types_str = "".join(unknown_types)
unknown_persons_details.append(f"{person_name}的[{unknown_types_str}]")
if len(unknown_persons_details) == 1:
persons_infos_str += (
f"你不了解{unknown_persons_details[0]}信息,不要胡乱回答,可以直接说不知道或忘记了;\n"
)
else:
unknown_all_str = "".join(unknown_persons_details)
persons_infos_str += f"你不了解{unknown_all_str}等信息,不要胡乱回答,可以直接说不知道或忘记了;\n"
return persons_infos_str
# ================================
# 关系构建模块
# 负责触发关系构建、整合消息段、更新用户印象
# ================================
async def update_impression_on_segments(self, person_id: str, chat_id: str, segments: List[Dict[str, any]]):
"""
基于消息段更新用户印象
Args:
person_id: 用户ID
chat_id: 聊天ID
segments: 消息段列表
"""
logger.debug(f"开始为 {person_id} 基于 {len(segments)} 个消息段更新印象")
try:
processed_messages = []
for i, segment in enumerate(segments):
start_time = segment["start_time"]
end_time = segment["end_time"]
segment["message_count"]
start_date = time.strftime("%Y-%m-%d %H:%M", time.localtime(start_time))
# 获取该段的消息(包含边界)
segment_messages = get_raw_msg_by_timestamp_with_chat_inclusive(
self.subheartflow_id, start_time, end_time
)
logger.info(
f"消息段 {i + 1}: {start_date} - {time.strftime('%Y-%m-%d %H:%M', time.localtime(end_time))}, 消息数: {len(segment_messages)}"
)
if segment_messages:
# 如果不是第一个消息段,在消息列表前添加间隔标识
if i > 0:
# 创建一个特殊的间隔消息
gap_message = {
"time": start_time - 0.1, # 稍微早于段开始时间
"user_id": "system",
"user_platform": "system",
"user_nickname": "系统",
"user_cardname": "",
"display_message": f"...(中间省略一些消息){start_date} 之后的消息如下...",
"is_action_record": True,
"chat_info_platform": segment_messages[0].get("chat_info_platform", ""),
"chat_id": chat_id,
}
processed_messages.append(gap_message)
# 添加该段的所有消息
processed_messages.extend(segment_messages)
if processed_messages:
# 按时间排序所有消息(包括间隔标识)
processed_messages.sort(key=lambda x: x["time"])
logger.info(f"{person_id} 获取到总共 {len(processed_messages)} 条消息(包含间隔标识)用于印象更新")
relationship_manager = get_relationship_manager()
# 调用原有的更新方法
await relationship_manager.update_person_impression(
person_id=person_id, timestamp=time.time(), bot_engaged_messages=processed_messages
)
else:
logger.info(f"没有找到 {person_id} 的消息段对应的消息,不更新印象")
except Exception as e:
logger.error(f"{person_id} 更新印象时发生错误: {e}")
logger.error(traceback.format_exc())
# ================================
# 信息调取模块
# 负责实时分析对话需求、提取用户信息、管理信息缓存
# ================================
async def _fetch_single_info_instant(self, person_id: str, info_type: str, start_time: float):
"""
使用小模型提取单个信息类型
"""
person_info_manager = get_person_info_manager()
# 首先检查 info_list 缓存
info_list = await person_info_manager.get_value(person_id, "info_list") or []
cached_info = None
person_name = await person_info_manager.get_value(person_id, "person_name")
# print(f"info_list: {info_list}")
# 查找对应的 info_type
for info_item in info_list:
if info_item.get("info_type") == info_type:
cached_info = info_item.get("info_content")
logger.debug(f"{self.log_prefix} 在info_list中找到 {person_name}{info_type} 信息: {cached_info}")
break
# 如果缓存中有信息,直接使用
if cached_info:
if person_id not in self.info_fetched_cache:
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": cached_info,
"ttl": 2,
"start_time": start_time,
"person_name": person_name,
"unknow": cached_info == "none",
}
logger.info(f"{self.log_prefix} 记得 {person_name}{info_type}: {cached_info}")
return
try:
person_name = await person_info_manager.get_value(person_id, "person_name")
person_impression = await person_info_manager.get_value(person_id, "impression")
if person_impression:
person_impression_block = (
f"<对{person_name}的总体了解>\n{person_impression}\n</对{person_name}的总体了解>"
)
else:
person_impression_block = ""
points = await person_info_manager.get_value(person_id, "points")
if points:
points_text = "\n".join([f"{point[2]}:{point[0]}" for point in points])
points_text_block = f"<对{person_name}的近期了解>\n{points_text}\n</对{person_name}的近期了解>"
else:
points_text_block = ""
if not points_text_block and not person_impression_block:
if person_id not in self.info_fetched_cache:
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": "none",
"ttl": 2,
"start_time": start_time,
"person_name": person_name,
"unknow": True,
}
logger.info(f"{self.log_prefix} 完全不认识 {person_name}")
await self._save_info_to_cache(person_id, info_type, "none")
return
nickname_str = ",".join(global_config.bot.alias_names)
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
prompt = (await global_prompt_manager.get_prompt_async("fetch_person_info_prompt")).format(
name_block=name_block,
info_type=info_type,
person_impression_block=person_impression_block,
person_name=person_name,
info_json_str=f'"{info_type}": "有关{info_type}的信息内容"',
points_text_block=points_text_block,
)
except Exception:
logger.error(traceback.format_exc())
return
try:
# 使用小模型进行即时提取
content, _ = await self.instant_llm_model.generate_response_async(prompt=prompt)
if content:
content_json = json.loads(repair_json(content))
if info_type in content_json:
info_content = content_json[info_type]
is_unknown = info_content == "none" or not info_content
# 保存到运行时缓存
if person_id not in self.info_fetched_cache:
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": "unknow" if is_unknown else info_content,
"ttl": 3,
"start_time": start_time,
"person_name": person_name,
"unknow": is_unknown,
}
# 保存到持久化缓存 (info_list)
await self._save_info_to_cache(person_id, info_type, info_content if not is_unknown else "none")
if not is_unknown:
logger.info(f"{self.log_prefix} 思考得到,{person_name}{info_type}: {content}")
else:
logger.info(f"{self.log_prefix} 思考了也不知道{person_name}{info_type} 信息")
else:
logger.warning(f"{self.log_prefix} 小模型返回空结果,获取 {person_name}{info_type} 信息失败。")
except Exception as e:
logger.error(f"{self.log_prefix} 执行小模型请求获取用户信息时出错: {e}")
logger.error(traceback.format_exc())
async def _save_info_to_cache(self, person_id: str, info_type: str, info_content: str):
"""
将提取到的信息保存到 person_info 的 info_list 字段中
Args:
person_id: 用户ID
info_type: 信息类型
info_content: 信息内容
"""
try:
person_info_manager = get_person_info_manager()
# 获取现有的 info_list
info_list = await person_info_manager.get_value(person_id, "info_list") or []
# 查找是否已存在相同 info_type 的记录
found_index = -1
for i, info_item in enumerate(info_list):
if isinstance(info_item, dict) and info_item.get("info_type") == info_type:
found_index = i
break
# 创建新的信息记录
new_info_item = {
"info_type": info_type,
"info_content": info_content,
}
if found_index >= 0:
# 更新现有记录
info_list[found_index] = new_info_item
logger.info(f"{self.log_prefix} [缓存更新] 更新 {person_id}{info_type} 信息缓存")
else:
# 添加新记录
info_list.append(new_info_item)
logger.info(f"{self.log_prefix} [缓存保存] 新增 {person_id}{info_type} 信息缓存")
# 保存更新后的 info_list
await person_info_manager.update_one_field(person_id, "info_list", info_list)
except Exception as e:
logger.error(f"{self.log_prefix} [缓存保存] 保存信息到缓存失败: {e}")
logger.error(traceback.format_exc())
init_prompt()

View File

@@ -1,186 +0,0 @@
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
import time
from src.common.logger import get_logger
from src.individuality.individuality import get_individuality
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.tools.tool_use import ToolUser
from src.chat.utils.json_utils import process_llm_tool_calls
from .base_processor import BaseProcessor
from typing import List
from src.chat.heart_flow.observation.observation import Observation
from src.chat.focus_chat.info.structured_info import StructuredInfo
from src.chat.heart_flow.observation.structure_observation import StructureObservation
logger = get_logger("processor")
def init_prompt():
# ... 原有代码 ...
# 添加工具执行器提示词
tool_executor_prompt = """
你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}
群里正在进行的聊天内容:
{chat_observe_info}
请仔细分析聊天内容,考虑以下几点:
1. 内容中是否包含需要查询信息的问题
2. 是否有明确的工具使用指令
If you need to use a tool, please directly call the corresponding tool function. If you do not need to use any tool, simply output "No tool needed".
"""
Prompt(tool_executor_prompt, "tool_executor_prompt")
class ToolProcessor(BaseProcessor):
log_prefix = "工具执行器"
def __init__(self, subheartflow_id: str):
super().__init__()
self.subheartflow_id = subheartflow_id
self.log_prefix = f"[{subheartflow_id}:ToolExecutor] "
self.llm_model = LLMRequest(
model=global_config.model.focus_tool_use,
request_type="focus.processor.tool",
)
self.structured_info = []
async def process_info(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
**kwargs,
) -> List[StructuredInfo]:
"""处理信息对象
Args:
observations: 可选的观察列表包含ChattingObservation和StructureObservation类型
action_type: 动作类型
action_data: 动作数据
**kwargs: 其他可选参数
Returns:
list: 处理后的结构化信息列表
"""
working_infos = []
result = []
if observations:
for observation in observations:
if isinstance(observation, ChattingObservation):
result, used_tools, prompt = await self.execute_tools(observation)
logger.info(f"工具调用结果: {result}")
# 更新WorkingObservation中的结构化信息
for observation in observations:
if isinstance(observation, StructureObservation):
for structured_info in result:
# logger.debug(f"{self.log_prefix} 更新WorkingObservation中的结构化信息: {structured_info}")
observation.add_structured_info(structured_info)
working_infos = observation.get_observe_info()
logger.debug(f"{self.log_prefix} 获取更新后WorkingObservation中的结构化信息: {working_infos}")
structured_info = StructuredInfo()
if working_infos:
for working_info in working_infos:
structured_info.set_info(key=working_info.get("type"), value=working_info.get("content"))
return [structured_info]
async def execute_tools(self, observation: ChattingObservation, action_type: str = None, action_data: dict = None):
"""
并行执行工具,返回结构化信息
参数:
sub_mind: 子思维对象
chat_target_name: 聊天目标名称,默认为"对方"
is_group_chat: 是否为群聊默认为False
return_details: 是否返回详细信息默认为False
cycle_info: 循环信息对象,可用于记录详细执行信息
action_type: 动作类型
action_data: 动作数据
返回:
如果return_details为False:
List[Dict]: 工具执行结果的结构化信息列表
如果return_details为True:
Tuple[List[Dict], List[str], str]: (工具执行结果列表, 使用的工具列表, 工具执行提示词)
"""
tool_instance = ToolUser()
tools = tool_instance._define_tools()
# logger.debug(f"observation: {observation}")
# logger.debug(f"observation.chat_target_info: {observation.chat_target_info}")
# logger.debug(f"observation.is_group_chat: {observation.is_group_chat}")
# logger.debug(f"observation.person_list: {observation.person_list}")
is_group_chat = observation.is_group_chat
# chat_observe_info = observation.get_observe_info()
chat_observe_info = observation.talking_message_str_truncate_short
# person_list = observation.person_list
# 获取时间信息
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# 构建专用于工具调用的提示词
prompt = await global_prompt_manager.format_prompt(
"tool_executor_prompt",
chat_observe_info=chat_observe_info,
is_group_chat=is_group_chat,
bot_name=get_individuality().name,
time_now=time_now,
)
# 调用LLM专注于工具使用
# logger.info(f"开始执行工具调用{prompt}")
response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools)
if len(other_info) == 3:
reasoning_content, model_name, tool_calls = other_info
else:
reasoning_content, model_name = other_info
tool_calls = None
# print("tooltooltooltooltooltooltooltooltooltooltooltooltooltooltooltooltool")
if tool_calls:
logger.info(f"获取到工具原始输出:\n{tool_calls}")
# 处理工具调用和结果收集类似于SubMind中的逻辑
new_structured_items = []
used_tools = [] # 记录使用了哪些工具
if tool_calls:
success, valid_tool_calls, error_msg = process_llm_tool_calls(tool_calls)
if success and valid_tool_calls:
for tool_call in valid_tool_calls:
try:
# 记录使用的工具名称
tool_name = tool_call.get("name", "unknown_tool")
used_tools.append(tool_name)
result = await tool_instance._execute_tool_call(tool_call)
name = result.get("type", "unknown_type")
content = result.get("content", "")
logger.info(f"工具{name},获得信息:{content}")
if result:
new_item = {
"type": result.get("type", "unknown_type"),
"id": result.get("id", f"tool_exec_{time.time()}"),
"content": result.get("content", ""),
"ttl": 3,
}
new_structured_items.append(new_item)
except Exception as e:
logger.error(f"{self.log_prefix}工具执行失败: {e}")
return new_structured_items, used_tools, prompt
init_prompt()

View File

@@ -1,5 +1,3 @@
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.heart_flow.observation.structure_observation import StructureObservation
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.common.logger import get_logger
@@ -76,7 +74,7 @@ class MemoryActivator:
self.running_memory = []
self.cached_keywords = set() # 用于缓存历史关键词
async def activate_memory(self, observations) -> List[Dict]:
async def activate_memory_with_chat_history(self, chat_id, target_message, chat_history_prompt) -> List[Dict]:
"""
激活记忆
@@ -90,14 +88,14 @@ class MemoryActivator:
if not global_config.memory.enable_memory:
return []
obs_info_text = ""
for observation in observations:
if isinstance(observation, ChattingObservation):
obs_info_text += observation.talking_message_str_truncate_short
elif isinstance(observation, StructureObservation):
working_info = observation.get_observe_info()
for working_info_item in working_info:
obs_info_text += f"{working_info_item['type']}: {working_info_item['content']}\n"
# obs_info_text = ""
# for observation in observations:
# if isinstance(observation, ChattingObservation):
# obs_info_text += observation.talking_message_str_truncate_short
# elif isinstance(observation, StructureObservation):
# working_info = observation.get_observe_info()
# for working_info_item in working_info:
# obs_info_text += f"{working_info_item['type']}: {working_info_item['content']}\n"
# logger.info(f"回忆待检索内容obs_info_text: {obs_info_text}")
@@ -106,7 +104,7 @@ class MemoryActivator:
prompt = await global_prompt_manager.format_prompt(
"memory_activator_prompt",
obs_info_text=obs_info_text,
obs_info_text=chat_history_prompt,
cached_keywords=cached_keywords_str,
)

View File

@@ -236,14 +236,6 @@ class ActionPlanner(BasePlanner):
action_data["loop_start_time"] = loop_start_time
memory_str = ""
if running_memorys:
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
for running_memory in running_memorys:
memory_str += f"{running_memory['content']}\n"
if memory_str:
action_data["memory_block"] = memory_str
# 对于reply动作不需要额外处理因为相关字段已经在上面的循环中添加到action_data
if extracted_action not in current_available_actions:

View File

@@ -67,7 +67,7 @@ class ChattingObservation(Observation):
self.talking_message_str_truncate_short = ""
self.name = global_config.bot.nickname
self.nick_name = global_config.bot.alias_names
self.max_now_obs_len = global_config.focus_chat.observation_context_size
self.max_now_obs_len = global_config.chat.max_context_size
self.overlap_len = global_config.focus_chat.compressed_length
self.person_list = []
self.compressor_prompt = ""

View File

@@ -1,42 +0,0 @@
from datetime import datetime
from src.common.logger import get_logger
# Import the new utility function
logger = get_logger("observation")
# 所有观察的基类
class StructureObservation:
def __init__(self, observe_id):
self.observe_info = ""
self.observe_id = observe_id
self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间
self.history_loop = []
self.structured_info = []
def to_dict(self) -> dict:
"""将观察对象转换为可序列化的字典"""
return {
"observe_info": self.observe_info,
"observe_id": self.observe_id,
"last_observe_time": self.last_observe_time,
"history_loop": self.history_loop,
"structured_info": self.structured_info,
}
def get_observe_info(self):
return self.structured_info
def add_structured_info(self, structured_info: dict):
self.structured_info.append(structured_info)
async def observe(self):
observed_structured_infos = []
for structured_info in self.structured_info:
if structured_info.get("ttl") > 0:
structured_info["ttl"] -= 1
observed_structured_infos.append(structured_info)
logger.debug(f"观察到结构化信息仍旧在: {structured_info}")
self.structured_info = observed_structured_infos

View File

@@ -133,10 +133,10 @@ class ChatBot:
user_info = message.message_info.user_info
if message.message_info.additional_config:
sent_message = message.message_info.additional_config.get("echo", False)
if sent_message: # 这一段只是为了在一切处理前劫持上报的自身消息用于更新message_id需要ada支持上报事件实际测试中不会对正常使用造成任何问题
if sent_message: # 这一段只是为了在一切处理前劫持上报的自身消息用于更新message_id需要ada支持上报事件实际测试中不会对正常使用造成任何问题
await MessageStorage.update_message(message)
return
get_chat_manager().register_message(message)
# 创建聊天流
@@ -203,4 +203,4 @@ class ChatBot:
# 创建全局ChatBot实例
chat_bot = ChatBot()
chat_bot = ChatBot()

View File

@@ -47,6 +47,16 @@ class ChatMessageContext:
return False
return True
def get_priority_mode(self) -> str:
"""获取优先级模式"""
return self.message.priority_mode
def get_priority_info(self) -> Optional[dict]:
"""获取优先级信息"""
if hasattr(self.message, "priority_info") and self.message.priority_info:
return self.message.priority_info
return None
class ChatStream:
"""聊天流对象,存储一个完整的聊天上下文"""

View File

@@ -108,6 +108,9 @@ class MessageRecv(Message):
self.detailed_plain_text = message_dict.get("detailed_plain_text", "")
self.is_emoji = False
self.is_picid = False
self.is_mentioned = 0.0
self.priority_mode = "interest"
self.priority_info = None
def update_chat_stream(self, chat_stream: "ChatStream"):
self.chat_stream = chat_stream
@@ -146,8 +149,27 @@ class MessageRecv(Message):
if isinstance(segment.data, str):
return await get_image_manager().get_emoji_description(segment.data)
return "[发了一个表情包,网卡了加载不出来]"
elif segment.type == "mention_bot":
self.is_mentioned = float(segment.data)
return ""
elif segment.type == "set_priority_mode":
# 处理设置优先级模式的消息段
if isinstance(segment.data, str):
self.priority_mode = segment.data
return ""
elif segment.type == "priority_info":
if isinstance(segment.data, dict):
# 处理优先级信息
self.priority_info = segment.data
"""
{
'message_type': 'vip', # vip or normal
'message_priority': 1.0, # 优先级大为优先float
}
"""
return ""
else:
return f"[{segment.type}:{str(segment.data)}]"
return ""
except Exception as e:
logger.error(f"处理消息段失败: {str(e)}, 类型: {segment.type}, 数据: {segment.data}")
return f"[处理失败的{segment.type}消息]"

View File

@@ -106,10 +106,11 @@ class MessageStorage:
except Exception:
logger.exception("删除撤回消息失败")
# 如果需要其他存储相关的函数,可以在这里添加
# 如果需要其他存储相关的函数,可以在这里添加
@staticmethod
async def update_message(message: MessageRecv) -> None: # 用于实时更新数据库的自身发送消息ID目前能处理text,reply,image和emoji
async def update_message(
message: MessageRecv,
) -> None: # 用于实时更新数据库的自身发送消息ID目前能处理text,reply,image和emoji
"""更新最新一条匹配消息的message_id"""
try:
if message.message_segment.type == "notify":
@@ -122,18 +123,16 @@ class MessageStorage:
logger.info("消息不存在message_id无法更新")
return
# 查询最新一条匹配消息
matched_message = Messages.select().where(
(Messages.message_id == mmc_message_id)
).order_by(Messages.time.desc()).first()
matched_message = (
Messages.select().where((Messages.message_id == mmc_message_id)).order_by(Messages.time.desc()).first()
)
if matched_message:
# 更新找到的消息记录
Messages.update(message_id=qq_message_id).where(
Messages.id == matched_message.id
).execute()
Messages.update(message_id=qq_message_id).where(Messages.id == matched_message.id).execute()
logger.info(f"更新消息ID成功: {matched_message.message_id} -> {qq_message_id}")
else:
logger.debug("未找到匹配的消息")
except Exception as e:
logger.error(f"更新消息ID失败: {e}")
logger.error(f"更新消息ID失败: {e}")

View File

@@ -1,28 +1,21 @@
import asyncio
import time
import traceback
from random import random
from typing import List, Optional, Dict # 导入类型提示
from typing import List, Dict, Optional
import os
import pickle
from maim_message import UserInfo, Seg
from src.common.logger import get_logger
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.chat.utils.timer_calculator import Timer
from src.chat.utils.prompt_builder import global_prompt_manager
from .normal_chat_generator import NormalChatGenerator
from ..message_receive.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from src.chat.message_receive.message_sender import message_manager
from src.chat.normal_chat.willing.willing_manager import get_willing_manager
from src.chat.normal_chat.normal_chat_utils import get_recent_message_stats
from src.config.config import global_config
from src.chat.focus_chat.planners.action_manager import ActionManager
from src.chat.normal_chat.normal_chat_planner import NormalChatPlanner
from src.chat.normal_chat.normal_chat_action_modifier import NormalChatActionModifier
from src.chat.normal_chat.normal_chat_expressor import NormalChatExpressor
from src.chat.replyer.default_generator import DefaultReplyer
from src.person_info.person_info import PersonInfoManager
from src.person_info.relationship_manager import get_relationship_manager
from src.chat.utils.chat_message_builder import (
@@ -31,6 +24,16 @@ from src.chat.utils.chat_message_builder import (
get_raw_msg_before_timestamp_with_chat,
num_new_messages_since,
)
from .priority_manager import PriorityManager
import traceback
from .normal_chat_generator import NormalChatGenerator
from src.chat.normal_chat.normal_chat_expressor import NormalChatExpressor
from src.chat.normal_chat.normal_chat_planner import NormalChatPlanner
from src.chat.normal_chat.normal_chat_action_modifier import NormalChatActionModifier
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
willing_manager = get_willing_manager()
@@ -46,16 +49,25 @@ SEGMENT_CLEANUP_CONFIG = {
class NormalChat:
def __init__(self, chat_stream: ChatStream, interest_dict: dict = None, on_switch_to_focus_callback=None):
"""初始化 NormalChat 实例。只进行同步操作。"""
"""
普通聊天处理类,负责处理非核心对话的聊天逻辑。
每个聊天私聊或群聊都会有一个独立的NormalChat实例。
"""
def __init__(self, chat_stream: ChatStream, interest_dict: dict = None, on_switch_to_focus_callback=None):
"""
初始化NormalChat实例。
Args:
chat_stream (ChatStream): 聊天流对象,包含与特定聊天相关的所有信息。
"""
self.chat_stream = chat_stream
self.stream_id = chat_stream.stream_id
self.stream_name = get_chat_manager().get_stream_name(self.stream_id) or self.stream_id
# 初始化Normal Chat专用表达器
self.expressor = NormalChatExpressor(self.chat_stream)
self.replyer = DefaultReplyer(self.chat_stream)
# Interest dict
self.interest_dict = interest_dict
@@ -69,7 +81,7 @@ class NormalChat:
self.gpt = NormalChatGenerator()
self.mood_manager = mood_manager
self.start_time = time.time()
self._chat_task: Optional[asyncio.Task] = None
self._initialized = False # Track initialization status
# Planner相关初始化
@@ -105,6 +117,35 @@ class NormalChat:
logger.debug(f"[{self.stream_name}] NormalChat 初始化完成 (异步部分)。")
self.action_type: Optional[str] = None # 当前动作类型
self.is_parallel_action: bool = False # 是否是可并行动作
# 任务管理
self._chat_task: Optional[asyncio.Task] = None
self._disabled = False # 停用标志
self.on_switch_to_focus_callback = on_switch_to_focus_callback
# 新增:回复模式和优先级管理器
self.reply_mode = self.chat_stream.context.get_priority_mode()
if self.reply_mode == "priority":
interest_dict = interest_dict or {}
self.priority_manager = PriorityManager(
interest_dict=interest_dict,
normal_queue_max_size=5,
)
else:
self.priority_manager = None
async def disable(self):
"""停用 NormalChat 实例,停止所有后台任务"""
self._disabled = True
if self._chat_task and not self._chat_task.done():
self._chat_task.cancel()
if self.reply_mode == "priority" and self._priority_chat_task and not self._priority_chat_task.done():
self._priority_chat_task.cancel()
logger.info(f"[{self.stream_name}] NormalChat 已停用。")
# ================================
# 缓存管理模块
# 负责持久化存储、状态管理、缓存读写
@@ -405,6 +446,65 @@ class NormalChat:
f"[{self.stream_name}] 更新用户 {person_id} 的消息段,消息时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(msg_time))}"
)
async def _priority_chat_loop_add_message(self):
while not self._disabled:
try:
ids = list(self.interest_dict.keys())
for msg_id in ids:
message, interest_value, _ = self.interest_dict[msg_id]
if not self._disabled:
# 更新消息段信息
self._update_user_message_segments(message)
# 添加消息到优先级管理器
if self.priority_manager:
self.priority_manager.add_message(message, interest_value)
self.interest_dict.pop(msg_id, None)
except Exception:
logger.error(
f"[{self.stream_name}] 优先级聊天循环添加消息时出现错误: {traceback.format_exc()}", exc_info=True
)
print(traceback.format_exc())
# 出现错误时,等待一段时间再重试
raise
await asyncio.sleep(0.1)
async def _priority_chat_loop(self):
"""
使用优先级队列的消息处理循环。
"""
while not self._disabled:
try:
if not self.priority_manager.is_empty():
# 获取最高优先级的消息
message = self.priority_manager.get_highest_priority_message()
if message:
logger.info(
f"[{self.stream_name}] 从队列中取出消息进行处理: User {message.message_info.user_info.user_id}, Time: {time.strftime('%H:%M:%S', time.localtime(message.message_info.time))}"
)
# 执行定期清理
self._cleanup_old_segments()
# 更新消息段信息
self._update_user_message_segments(message)
# 检查是否有用户满足关系构建条件
asyncio.create_task(self._check_relation_building_conditions())
await self.reply_one_message(message)
# 等待一段时间再检查队列
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 优先级聊天循环被取消。")
break
except Exception:
logger.error(f"[{self.stream_name}] 优先级聊天循环出现错误: {traceback.format_exc()}", exc_info=True)
# 出现错误时,等待更长时间避免频繁报错
await asyncio.sleep(10)
# 改为实例方法
async def _create_thinking_message(self, message: MessageRecv, timestamp: Optional[float] = None) -> str:
"""创建思考消息"""
@@ -602,15 +702,33 @@ class NormalChat:
# 改为实例方法, 移除 chat 参数
async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float) -> None:
# 新增:如果已停用,直接返回
"""
处理接收到的消息。
根据回复模式,决定是立即处理还是放入优先级队列。
"""
if self._disabled:
return
# 根据回复模式决定行为
if self.reply_mode == "priority":
# 优先模式下,所有消息都进入管理器
if self.priority_manager:
self.priority_manager.add_message(message)
return
# --- 以下为原有的 "兴趣" 模式逻辑 ---
await self._process_message(message, is_mentioned, interested_rate)
async def _process_message(self, message: MessageRecv, is_mentioned: bool, interested_rate: float) -> None:
"""
实际处理单条消息的逻辑,包括意愿判断、回复生成、动作执行等。
"""
if self._disabled:
logger.info(f"[{self.stream_name}] 已停用,忽略 normal_response。")
return
# 新增在auto模式下检查是否需要直接切换到focus模式
if global_config.chat.chat_mode == "auto":
should_switch = await self._check_should_switch_to_focus()
if should_switch:
if await self._check_should_switch_to_focus():
logger.info(f"[{self.stream_name}] 检测到切换到focus聊天模式的条件直接执行切换")
if self.on_switch_to_focus_callback:
await self.on_switch_to_focus_callback()
@@ -660,176 +778,10 @@ class NormalChat:
do_reply = False
response_set = None # 初始化 response_set
if random() < reply_probability:
do_reply = True
# 回复前处理
await willing_manager.before_generate_reply_handle(message.message_info.message_id)
thinking_id = await self._create_thinking_message(message)
# 如果启用planner预先修改可用actions避免在并行任务中重复调用
available_actions = None
if self.enable_planner:
try:
await self.action_modifier.modify_actions_for_normal_chat(
self.chat_stream, self.recent_replies, message.processed_plain_text
)
available_actions = self.action_manager.get_using_actions_for_mode("normal")
except Exception as e:
logger.warning(f"[{self.stream_name}] 获取available_actions失败: {e}")
available_actions = None
# 定义并行执行的任务
async def generate_normal_response():
"""生成普通回复"""
try:
return await self.gpt.generate_response(
message=message,
thinking_id=thinking_id,
enable_planner=self.enable_planner,
available_actions=available_actions,
)
except Exception as e:
logger.error(f"[{self.stream_name}] 回复生成出现错误:{str(e)} {traceback.format_exc()}")
return None
async def plan_and_execute_actions():
"""规划和执行额外动作"""
if not self.enable_planner:
logger.debug(f"[{self.stream_name}] Planner未启用跳过动作规划")
return None
try:
# 获取发送者名称(动作修改已在并行执行前完成)
sender_name = self._get_sender_name(message)
no_action = {
"action_result": {
"action_type": "no_action",
"action_data": {},
"reasoning": "规划器初始化默认",
"is_parallel": True,
},
"chat_context": "",
"action_prompt": "",
}
# 检查是否应该跳过规划
if self.action_modifier.should_skip_planning():
logger.debug(f"[{self.stream_name}] 没有可用动作,跳过规划")
self.action_type = "no_action"
return no_action
# 执行规划
plan_result = await self.planner.plan(message, sender_name)
action_type = plan_result["action_result"]["action_type"]
action_data = plan_result["action_result"]["action_data"]
reasoning = plan_result["action_result"]["reasoning"]
is_parallel = plan_result["action_result"].get("is_parallel", False)
logger.info(
f"[{self.stream_name}] Planner决策: {action_type}, 理由: {reasoning}, 并行执行: {is_parallel}"
)
self.action_type = action_type # 更新实例属性
self.is_parallel_action = is_parallel # 新增:保存并行执行标志
# 如果规划器决定不执行任何动作
if action_type == "no_action":
logger.debug(f"[{self.stream_name}] Planner决定不执行任何额外动作")
return no_action
# 执行额外的动作(不影响回复生成)
action_result = await self._execute_action(action_type, action_data, message, thinking_id)
if action_result is not None:
logger.info(f"[{self.stream_name}] 额外动作 {action_type} 执行完成")
else:
logger.warning(f"[{self.stream_name}] 额外动作 {action_type} 执行失败")
return {
"action_type": action_type,
"action_data": action_data,
"reasoning": reasoning,
"is_parallel": is_parallel,
}
except Exception as e:
logger.error(f"[{self.stream_name}] Planner执行失败: {e}")
return no_action
# 并行执行回复生成和动作规划
self.action_type = None # 初始化动作类型
self.is_parallel_action = False # 初始化并行动作标志
with Timer("并行生成回复和规划", timing_results):
response_set, plan_result = await asyncio.gather(
generate_normal_response(), plan_and_execute_actions(), return_exceptions=True
)
# 处理生成回复的结果
if isinstance(response_set, Exception):
logger.error(f"[{self.stream_name}] 回复生成异常: {response_set}")
response_set = None
# 处理规划结果(可选,不影响回复)
if isinstance(plan_result, Exception):
logger.error(f"[{self.stream_name}] 动作规划异常: {plan_result}")
elif plan_result:
logger.debug(f"[{self.stream_name}] 额外动作处理完成: {self.action_type}")
if not response_set or (
self.enable_planner and self.action_type not in ["no_action"] and not self.is_parallel_action
):
if not response_set:
logger.info(f"[{self.stream_name}] 模型未生成回复内容")
elif self.enable_planner and self.action_type not in ["no_action"] and not self.is_parallel_action:
logger.info(f"[{self.stream_name}] 模型选择其他动作(非并行动作)")
# 如果模型未生成回复,移除思考消息
container = await message_manager.get_container(self.stream_id) # 使用 self.stream_id
for msg in container.messages[:]:
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
container.messages.remove(msg)
logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}")
break
# 需要在此处也调用 not_reply_handle 和 delete 吗?
# 如果是因为模型没回复,也算是一种 "未回复"
await willing_manager.not_reply_handle(message.message_info.message_id)
willing_manager.delete(message.message_info.message_id)
return # 不执行后续步骤
# logger.info(f"[{self.stream_name}] 回复内容: {response_set}")
if self._disabled:
logger.info(f"[{self.stream_name}] 已停用,忽略 normal_response。")
return
# 发送回复 (不再需要传入 chat)
with Timer("消息发送", timing_results):
first_bot_msg = await self._add_messages_to_manager(message, response_set, thinking_id)
# 检查 first_bot_msg 是否为 None (例如思考消息已被移除的情况)
if first_bot_msg:
# 消息段已在接收消息时更新,这里不需要额外处理
# 记录回复信息到最近回复列表中
reply_info = {
"time": time.time(),
"user_message": message.processed_plain_text,
"user_info": {
"user_id": message.message_info.user_info.user_id,
"user_nickname": message.message_info.user_info.user_nickname,
},
"response": response_set,
"is_mentioned": is_mentioned,
"is_reference_reply": message.reply is not None, # 判断是否为引用回复
"timing": {k: round(v, 2) for k, v in timing_results.items()},
}
self.recent_replies.append(reply_info)
# 保持最近回复历史在限定数量内
if len(self.recent_replies) > self.max_replies_history:
self.recent_replies = self.recent_replies[-self.max_replies_history :]
# 回复后处理
await willing_manager.after_generate_reply_handle(message.message_info.message_id)
with Timer("获取回复", timing_results):
await willing_manager.before_generate_reply_handle(message.message_info.message_id)
do_reply = await self.reply_one_message(message)
response_set = do_reply if do_reply else None
# 输出性能计时结果
if do_reply and response_set: # 确保 response_set 不是 None
timing_str = " | ".join([f"{step}: {duration:.2f}" for step, duration in timing_results.items()])
@@ -838,6 +790,7 @@ class NormalChat:
logger.info(
f"[{self.stream_name}]回复消息: {trigger_msg[:30]}... | 回复内容: {response_msg[:30]}... | 计时: {timing_str}"
)
await willing_manager.after_generate_reply_handle(message.message_info.message_id)
elif not do_reply:
# 不回复处理
await willing_manager.not_reply_handle(message.message_info.message_id)
@@ -845,6 +798,165 @@ class NormalChat:
# 意愿管理器注销当前message信息 (无论是否回复,只要处理过就删除)
willing_manager.delete(message.message_info.message_id)
async def reply_one_message(self, message: MessageRecv) -> None:
# 回复前处理
thinking_id = await self._create_thinking_message(message)
# 如果启用planner预先修改可用actions避免在并行任务中重复调用
available_actions = None
if self.enable_planner:
try:
await self.action_modifier.modify_actions_for_normal_chat(
self.chat_stream, self.recent_replies, message.processed_plain_text
)
available_actions = self.action_manager.get_using_actions_for_mode("normal")
except Exception as e:
logger.warning(f"[{self.stream_name}] 获取available_actions失败: {e}")
available_actions = None
# 定义并行执行的任务
async def generate_normal_response():
"""生成普通回复"""
try:
return await self.gpt.generate_response(
message=message,
available_actions=available_actions,
)
except Exception as e:
logger.error(f"[{self.stream_name}] 回复生成出现错误:{str(e)} {traceback.format_exc()}")
return None
async def plan_and_execute_actions():
"""规划和执行额外动作"""
if not self.enable_planner:
logger.debug(f"[{self.stream_name}] Planner未启用跳过动作规划")
return None
try:
# 获取发送者名称(动作修改已在并行执行前完成)
sender_name = self._get_sender_name(message)
no_action = {
"action_result": {
"action_type": "no_action",
"action_data": {},
"reasoning": "规划器初始化默认",
"is_parallel": True,
},
"chat_context": "",
"action_prompt": "",
}
# 检查是否应该跳过规划
if self.action_modifier.should_skip_planning():
logger.debug(f"[{self.stream_name}] 没有可用动作,跳过规划")
self.action_type = "no_action"
return no_action
# 执行规划
plan_result = await self.planner.plan(message, sender_name)
action_type = plan_result["action_result"]["action_type"]
action_data = plan_result["action_result"]["action_data"]
reasoning = plan_result["action_result"]["reasoning"]
is_parallel = plan_result["action_result"].get("is_parallel", False)
logger.info(
f"[{self.stream_name}] Planner决策: {action_type}, 理由: {reasoning}, 并行执行: {is_parallel}"
)
self.action_type = action_type # 更新实例属性
self.is_parallel_action = is_parallel # 新增:保存并行执行标志
# 如果规划器决定不执行任何动作
if action_type == "no_action":
logger.debug(f"[{self.stream_name}] Planner决定不执行任何额外动作")
return no_action
# 执行额外的动作(不影响回复生成)
action_result = await self._execute_action(action_type, action_data, message, thinking_id)
if action_result is not None:
logger.info(f"[{self.stream_name}] 额外动作 {action_type} 执行完成")
else:
logger.warning(f"[{self.stream_name}] 额外动作 {action_type} 执行失败")
return {
"action_type": action_type,
"action_data": action_data,
"reasoning": reasoning,
"is_parallel": is_parallel,
}
except Exception as e:
logger.error(f"[{self.stream_name}] Planner执行失败: {e}")
return no_action
# 并行执行回复生成和动作规划
self.action_type = None # 初始化动作类型
self.is_parallel_action = False # 初始化并行动作标志
response_set, plan_result = await asyncio.gather(
generate_normal_response(), plan_and_execute_actions(), return_exceptions=True
)
# 处理生成回复的结果
if isinstance(response_set, Exception):
logger.error(f"[{self.stream_name}] 回复生成异常: {response_set}")
response_set = None
# 处理规划结果(可选,不影响回复)
if isinstance(plan_result, Exception):
logger.error(f"[{self.stream_name}] 动作规划异常: {plan_result}")
elif plan_result:
logger.debug(f"[{self.stream_name}] 额外动作处理完成: {self.action_type}")
if not response_set or (
self.enable_planner and self.action_type not in ["no_action"] and not self.is_parallel_action
):
if not response_set:
logger.info(f"[{self.stream_name}] 模型未生成回复内容")
elif self.enable_planner and self.action_type not in ["no_action"] and not self.is_parallel_action:
logger.info(f"[{self.stream_name}] 模型选择其他动作(非并行动作)")
# 如果模型未生成回复,移除思考消息
container = await message_manager.get_container(self.stream_id) # 使用 self.stream_id
for msg in container.messages[:]:
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
container.messages.remove(msg)
logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}")
break
# 需要在此处也调用 not_reply_handle 和 delete 吗?
# 如果是因为模型没回复,也算是一种 "未回复"
return False
# logger.info(f"[{self.stream_name}] 回复内容: {response_set}")
if self._disabled:
logger.info(f"[{self.stream_name}] 已停用,忽略 normal_response。")
return False
# 发送回复 (不再需要传入 chat)
first_bot_msg = await self._add_messages_to_manager(message, response_set, thinking_id)
# 检查 first_bot_msg 是否为 None (例如思考消息已被移除的情况)
if first_bot_msg:
# 消息段已在接收消息时更新,这里不需要额外处理
# 记录回复信息到最近回复列表中
reply_info = {
"time": time.time(),
"user_message": message.processed_plain_text,
"user_info": {
"user_id": message.message_info.user_info.user_id,
"user_nickname": message.message_info.user_info.user_nickname,
},
"response": response_set,
# "is_mentioned": is_mentioned,
"is_reference_reply": message.reply is not None, # 判断是否为引用回复
# "timing": {k: round(v, 2) for k, v in timing_results.items()},
}
self.recent_replies.append(reply_info)
# 保持最近回复历史在限定数量内
if len(self.recent_replies) > self.max_replies_history:
self.recent_replies = self.recent_replies[-self.max_replies_history :]
return response_set if response_set else False
# 改为实例方法, 移除 chat 参数
async def start_chat(self):
@@ -864,8 +976,16 @@ class NormalChat:
self._chat_task = None
try:
logger.debug(f"[{self.stream_name}] 创建新的聊天轮询任务")
polling_task = asyncio.create_task(self._reply_interested_message())
logger.info(f"[{self.stream_name}] 创建新的聊天轮询任务,模式: {self.reply_mode}")
if self.reply_mode == "priority":
polling_task_send = asyncio.create_task(self._priority_chat_loop())
polling_task_recv = asyncio.create_task(self._priority_chat_loop_add_message())
print("555")
polling_task = asyncio.gather(polling_task_send, polling_task_recv)
print("666")
else: # 默认或 "interest" 模式
polling_task = asyncio.create_task(self._reply_interested_message())
# 设置回调
polling_task.add_done_callback(lambda t: self._handle_task_completion(t))
@@ -904,7 +1024,7 @@ class NormalChat:
# 尝试获取异常,但不抛出
exc = task.exception()
if exc:
logger.error(f"[{self.stream_name}] 任务异常: {type(exc).__name__}: {exc}")
logger.error(f"[{self.stream_name}] 任务异常: {type(exc).__name__}: {exc}", exc_info=exc)
else:
logger.debug(f"[{self.stream_name}] 任务正常完成")
except Exception as e:

View File

@@ -80,7 +80,7 @@ class NormalChatActionModifier:
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size, # 使用相同的配置
limit=global_config.chat.max_context_size, # 使用相同的配置
)
# 构建可读的聊天上下文

View File

@@ -1,13 +1,11 @@
from typing import List, Optional, Union
import random
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.chat.message_receive.message import MessageThinking
from src.chat.normal_chat.normal_prompt import prompt_builder
from src.chat.utils.timer_calculator import Timer
from src.common.logger import get_logger
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
from src.chat.utils.utils import process_llm_response
from src.plugin_system.apis import generator_api
from src.chat.focus_chat.memory_activator import MemoryActivator
logger = get_logger("normal_chat_response")
@@ -15,90 +13,59 @@ logger = get_logger("normal_chat_response")
class NormalChatGenerator:
def __init__(self):
# TODO: API-Adapter修改标记
self.model_reasoning = LLMRequest(
model=global_config.model.replyer_1,
request_type="normal.chat_1",
)
self.model_normal = LLMRequest(
model=global_config.model.replyer_2,
request_type="normal.chat_2",
)
model_config_1 = global_config.model.replyer_1.copy()
model_config_2 = global_config.model.replyer_2.copy()
prob_first = global_config.chat.replyer_random_probability
model_config_1["weight"] = prob_first
model_config_2["weight"] = 1.0 - prob_first
self.model_configs = [model_config_1, model_config_2]
self.model_sum = LLMRequest(model=global_config.model.memory_summary, temperature=0.7, request_type="relation")
self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model"
self.memory_activator = MemoryActivator()
async def generate_response(
self, message: MessageThinking, thinking_id: str, enable_planner: bool = False, available_actions=None
) -> Optional[Union[str, List[str]]]:
"""根据当前模型类型选择对应的生成函数"""
# 从global_config中获取模型概率值并选择模型
if random.random() < global_config.normal_chat.normal_chat_first_probability:
current_model = self.model_reasoning
self.current_model_name = current_model.model_name
else:
current_model = self.model_normal
self.current_model_name = current_model.model_name
logger.info(
f"{self.current_model_name}思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
) # noqa: E501
model_response = await self._generate_response_with_model(
message, current_model, thinking_id, enable_planner, available_actions
)
if model_response:
logger.debug(f"{global_config.bot.nickname}的备选回复是:{model_response}")
model_response = process_llm_response(model_response)
return model_response
else:
logger.info(f"{self.current_model_name}思考,失败")
return None
async def _generate_response_with_model(
self,
message: MessageThinking,
model: LLMRequest,
thinking_id: str,
enable_planner: bool = False,
available_actions=None,
):
logger.info(
f"NormalChat思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
)
person_id = PersonInfoManager.get_person_id(
message.chat_stream.user_info.platform, message.chat_stream.user_info.user_id
)
person_info_manager = get_person_info_manager()
person_name = await person_info_manager.get_value(person_id, "person_name")
if message.chat_stream.user_info.user_cardname and message.chat_stream.user_info.user_nickname:
sender_name = (
f"[{message.chat_stream.user_info.user_nickname}]"
f"[群昵称:{message.chat_stream.user_info.user_cardname}]你叫ta{person_name}"
)
elif message.chat_stream.user_info.user_nickname:
sender_name = f"[{message.chat_stream.user_info.user_nickname}]你叫ta{person_name}"
else:
sender_name = f"用户({message.chat_stream.user_info.user_id})"
# 构建prompt
with Timer() as t_build_prompt:
prompt = await prompt_builder.build_prompt_normal(
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,
enable_planner=enable_planner,
available_actions=available_actions,
)
logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}")
relation_info = await person_info_manager.get_value(person_id, "short_impression")
reply_to_str = f"{person_name}:{message.processed_plain_text}"
try:
content, (reasoning_content, model_name) = await model.generate_response_async(prompt)
success, reply_set, prompt = await generator_api.generate_reply(
chat_stream=message.chat_stream,
reply_to=reply_to_str,
relation_info=relation_info,
available_actions=available_actions,
enable_tool=global_config.tool.enable_in_normal_chat,
model_configs=self.model_configs,
request_type="normal.replyer",
return_prompt=True,
)
logger.info(f"prompt:{prompt}\n生成回复:{content}")
if not success or not reply_set:
logger.info(f"{message.processed_plain_text} 的回复生成失败")
return None
logger.info(f"{message.processed_plain_text} 的回复:{content}")
content = " ".join([item[1] for item in reply_set if item[0] == "text"])
logger.debug(f"{message.processed_plain_text} 的回复:{content}")
if content:
logger.info(f"{global_config.bot.nickname}的备选回复是:{content}")
content = process_llm_response(content)
return content
except Exception:
logger.exception("生成回复时出错")

View File

@@ -122,7 +122,7 @@ class NormalChatPlanner:
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=message.chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size,
limit=global_config.chat.max_context_size,
)
chat_context = build_readable_messages(

View File

@@ -1,373 +0,0 @@
from src.config.config import global_config
from src.common.logger import get_logger
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
import time
from src.chat.utils.utils import get_recent_group_speaker
from src.manager.mood_manager import mood_manager
from src.chat.memory_system.Hippocampus import hippocampus_manager
from src.chat.knowledge.knowledge_lib import qa_manager
import random
from src.person_info.person_info import get_person_info_manager
from src.chat.express.expression_selector import expression_selector
import re
import ast
from src.person_info.relationship_manager import get_relationship_manager
logger = get_logger("prompt")
def init_prompt():
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
Prompt("在群里聊天", "chat_target_group2")
Prompt("{sender_name}私聊", "chat_target_private2")
Prompt(
"""
你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
{style_habbits}
请你根据情景使用以下,不要盲目使用,不要生硬使用,而是结合到表达中:
{grammar_habbits}
{memory_prompt}
{relation_prompt}
{prompt_info}
{chat_target}
现在时间是:{now_time}
{chat_talking_prompt}
现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言或者回复这条消息。\n
你的网名叫{bot_name},有人也叫你{bot_other_names}{prompt_personality}
{action_descriptions}你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},请你给出回复
尽量简短一些。请注意把握聊天内容。
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景。
{keywords_reaction_prompt}
请注意不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容。
{moderation_prompt}
不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容""",
"reasoning_prompt_main",
)
Prompt(
"你回忆起:{related_memory_info}\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n",
"memory_prompt",
)
Prompt("\n你有以下这些**知识**\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
Prompt(
"""
你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
{style_habbits}
请你根据情景使用以下句法,不要盲目使用,不要生硬使用,而是结合到表达中:
{grammar_habbits}
{memory_prompt}
{prompt_info}
你正在和 {sender_name} 聊天。
{relation_prompt}
你们之前的聊天记录如下:
{chat_talking_prompt}
现在 {sender_name} 说的: {message_txt} 引起了你的注意,针对这条消息回复他。
你的网名叫{bot_name}{sender_name}也叫你{bot_other_names}{prompt_personality}
{action_descriptions}你正在和 {sender_name} 聊天, 现在请你读读你们之前的聊天记录,给出回复。量简短一些。请注意把握聊天内容。
{keywords_reaction_prompt}
{moderation_prompt}
请说中文。不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容""",
"reasoning_prompt_private_main", # New template for private CHAT chat
)
class PromptBuilder:
def __init__(self):
self.prompt_built = ""
self.activate_messages = ""
async def build_prompt_normal(
self,
chat_stream,
message_txt: str,
sender_name: str = "某人",
enable_planner: bool = False,
available_actions=None,
) -> str:
person_info_manager = get_person_info_manager()
bot_person_id = person_info_manager.get_person_id("system", "bot_id")
short_impression = await person_info_manager.get_value(bot_person_id, "short_impression")
# 解析字符串形式的Python列表
try:
if isinstance(short_impression, str) and short_impression.strip():
short_impression = ast.literal_eval(short_impression)
elif not short_impression:
logger.warning("short_impression为空使用默认值")
short_impression = ["友好活泼", "人类"]
except (ValueError, SyntaxError) as e:
logger.error(f"解析short_impression失败: {e}, 原始值: {short_impression}")
short_impression = ["友好活泼", "人类"]
# 确保short_impression是列表格式且有足够的元素
if not isinstance(short_impression, list) or len(short_impression) < 2:
logger.warning(f"short_impression格式不正确: {short_impression}, 使用默认值")
short_impression = ["友好活泼", "人类"]
personality = short_impression[0]
identity = short_impression[1]
prompt_personality = personality + "" + identity
is_group_chat = bool(chat_stream.group_info)
who_chat_in_group = []
if is_group_chat:
who_chat_in_group = get_recent_group_speaker(
chat_stream.stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None,
limit=global_config.normal_chat.max_context_size,
)
elif chat_stream.user_info:
who_chat_in_group.append(
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)
)
relation_prompt = ""
if global_config.relationship.enable_relationship:
for person in who_chat_in_group:
relationship_manager = get_relationship_manager()
relation_prompt += f"{await relationship_manager.build_relationship_info(person)}\n"
mood_prompt = mood_manager.get_mood_prompt()
memory_prompt = ""
if global_config.memory.enable_memory:
related_memory = await hippocampus_manager.get_memory_from_text(
text=message_txt, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False
)
related_memory_info = ""
if related_memory:
for memory in related_memory:
related_memory_info += memory[1]
memory_prompt = await global_prompt_manager.format_prompt(
"memory_prompt", related_memory_info=related_memory_info
)
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size,
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
show_actions=True,
)
message_list_before_now_half = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=int(global_config.focus_chat.observation_context_size * 0.5),
)
chat_talking_prompt_half = build_readable_messages(
message_list_before_now_half,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
show_actions=True,
)
expressions = await expression_selector.select_suitable_expressions_llm(
chat_stream.stream_id, chat_talking_prompt_half, max_num=8, min_num=3
)
style_habbits = []
grammar_habbits = []
if expressions:
for expr in expressions:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
expr_type = expr.get("type", "style")
if expr_type == "grammar":
grammar_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
else:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
else:
logger.debug("没有从处理器获得表达方式,将使用空的表达方式")
style_habbits_str = "\n".join(style_habbits)
grammar_habbits_str = "\n".join(grammar_habbits)
# 关键词检测与反应
keywords_reaction_prompt = ""
try:
# 处理关键词规则
for rule in global_config.keyword_reaction.keyword_rules:
if any(keyword in message_txt for keyword in rule.keywords):
logger.info(f"检测到关键词规则:{rule.keywords},触发反应:{rule.reaction}")
keywords_reaction_prompt += f"{rule.reaction}"
# 处理正则表达式规则
for rule in global_config.keyword_reaction.regex_rules:
for pattern_str in rule.regex:
try:
pattern = re.compile(pattern_str)
if result := pattern.search(message_txt):
reaction = rule.reaction
for name, content in result.groupdict().items():
reaction = reaction.replace(f"[{name}]", content)
logger.info(f"匹配到正则表达式:{pattern_str},触发反应:{reaction}")
keywords_reaction_prompt += reaction + ""
break
except re.error as e:
logger.error(f"正则表达式编译错误: {pattern_str}, 错误信息: {str(e)}")
continue
except Exception as e:
logger.error(f"关键词检测与反应时发生异常: {str(e)}", exc_info=True)
moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
)
# 构建action描述 (如果启用planner)
action_descriptions = ""
# logger.debug(f"Enable planner {enable_planner}, available actions: {available_actions}")
if enable_planner and available_actions:
action_descriptions = "你有以下的动作能力,但执行这些动作不由你决定,由另外一个模型同步决定,因此你只需要知道有如下能力即可:\n"
for action_name, action_info in available_actions.items():
action_description = action_info.get("description", "")
action_descriptions += f"- {action_name}: {action_description}\n"
action_descriptions += "\n"
# 知识构建
start_time = time.time()
prompt_info = await self.get_prompt_info(message_txt, threshold=0.38)
if prompt_info:
prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info)
end_time = time.time()
logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}")
logger.debug("开始构建 normal prompt")
now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# --- Choose template and format based on chat type ---
if is_group_chat:
template_name = "reasoning_prompt_main"
effective_sender_name = sender_name
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
prompt = await global_prompt_manager.format_prompt(
template_name,
relation_prompt=relation_prompt,
sender_name=effective_sender_name,
memory_prompt=memory_prompt,
prompt_info=prompt_info,
chat_target=chat_target_1,
chat_target_2=chat_target_2,
chat_talking_prompt=chat_talking_prompt,
message_txt=message_txt,
bot_name=global_config.bot.nickname,
bot_other_names="/".join(global_config.bot.alias_names),
prompt_personality=prompt_personality,
mood_prompt=mood_prompt,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
keywords_reaction_prompt=keywords_reaction_prompt,
moderation_prompt=moderation_prompt_block,
now_time=now_time,
action_descriptions=action_descriptions,
)
else:
template_name = "reasoning_prompt_private_main"
effective_sender_name = sender_name
prompt = await global_prompt_manager.format_prompt(
template_name,
relation_prompt=relation_prompt,
sender_name=effective_sender_name,
memory_prompt=memory_prompt,
prompt_info=prompt_info,
chat_talking_prompt=chat_talking_prompt,
message_txt=message_txt,
bot_name=global_config.bot.nickname,
bot_other_names="/".join(global_config.bot.alias_names),
prompt_personality=prompt_personality,
mood_prompt=mood_prompt,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
keywords_reaction_prompt=keywords_reaction_prompt,
moderation_prompt=moderation_prompt_block,
now_time=now_time,
action_descriptions=action_descriptions,
)
# --- End choosing template ---
return prompt
async def get_prompt_info(self, message: str, threshold: float):
related_info = ""
start_time = time.time()
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 从LPMM知识库获取知识
try:
found_knowledge_from_lpmm = qa_manager.get_knowledge(message)
end_time = time.time()
if found_knowledge_from_lpmm is not None:
logger.debug(
f"从LPMM知识库获取知识相关信息{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}"
)
related_info += found_knowledge_from_lpmm
logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}")
logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
return related_info
else:
logger.debug("从LPMM知识库获取知识失败可能是从未导入过知识返回空知识...")
return "未检索到知识"
except Exception as e:
logger.error(f"获取知识库内容时发生异常: {str(e)}")
return "未检索到知识"
def weighted_sample_no_replacement(items, weights, k) -> list:
"""
加权且不放回地随机抽取k个元素。
参数:
items: 待抽取的元素列表
weights: 每个元素对应的权重与items等长且为正数
k: 需要抽取的元素个数
返回:
selected: 按权重加权且不重复抽取的k个元素组成的列表
如果items中的元素不足k就只会返回所有可用的元素
实现思路:
每次从当前池中按权重加权随机选出一个元素选中后将其从池中移除重复k次。
这样保证了:
1. count越大被选中概率越高
2. 不会重复选中同一个元素
"""
selected = []
pool = list(zip(items, weights))
for _ in range(min(k, len(pool))):
total = sum(w for _, w in pool)
r = random.uniform(0, total)
upto = 0
for idx, (item, weight) in enumerate(pool):
upto += weight
if upto >= r:
selected.append(item)
pool.pop(idx)
break
return selected
init_prompt()
prompt_builder = PromptBuilder()

View File

@@ -0,0 +1,108 @@
import time
import heapq
import math
from typing import List, Dict, Optional
from ..message_receive.message import MessageRecv
from src.common.logger import get_logger
logger = get_logger("normal_chat")
class PrioritizedMessage:
"""带有优先级的消息对象"""
def __init__(self, message: MessageRecv, interest_scores: List[float], is_vip: bool = False):
self.message = message
self.arrival_time = time.time()
self.interest_scores = interest_scores
self.is_vip = is_vip
self.priority = self.calculate_priority()
def calculate_priority(self, decay_rate: float = 0.01) -> float:
"""
计算优先级分数。
优先级 = 兴趣分 * exp(-衰减率 * 消息年龄)
"""
age = time.time() - self.arrival_time
decay_factor = math.exp(-decay_rate * age)
priority = sum(self.interest_scores) + decay_factor
return priority
def __lt__(self, other: "PrioritizedMessage") -> bool:
"""用于堆排序的比较函数,我们想要一个最大堆,所以用 >"""
return self.priority > other.priority
class PriorityManager:
"""
管理消息队列,根据优先级选择消息进行处理。
"""
def __init__(self, interest_dict: Dict[str, float], normal_queue_max_size: int = 5):
self.vip_queue: List[PrioritizedMessage] = [] # VIP 消息队列 (最大堆)
self.normal_queue: List[PrioritizedMessage] = [] # 普通消息队列 (最大堆)
self.interest_dict = interest_dict if interest_dict is not None else {}
self.normal_queue_max_size = normal_queue_max_size
def _get_interest_score(self, user_id: str) -> float:
"""获取用户的兴趣分默认为1.0"""
return self.interest_dict.get("interests", {}).get(user_id, 1.0)
def add_message(self, message: MessageRecv, interest_score: Optional[float] = None):
"""
添加新消息到合适的队列中。
"""
user_id = message.message_info.user_info.user_id
is_vip = message.priority_info.get("message_type") == "vip" if message.priority_info else False
message_priority = message.priority_info.get("message_priority", 0.0) if message.priority_info else 0.0
p_message = PrioritizedMessage(message, [interest_score, message_priority], is_vip)
if is_vip:
heapq.heappush(self.vip_queue, p_message)
logger.debug(f"消息来自VIP用户 {user_id}, 已添加到VIP队列. 当前VIP队列长度: {len(self.vip_queue)}")
else:
if len(self.normal_queue) >= self.normal_queue_max_size:
# 如果队列已满,只在消息优先级高于最低优先级消息时才添加
if p_message.priority > self.normal_queue[0].priority:
heapq.heapreplace(self.normal_queue, p_message)
logger.debug(f"普通队列已满,但新消息优先级更高,已替换. 用户: {user_id}")
else:
logger.debug(f"普通队列已满且新消息优先级较低,已忽略. 用户: {user_id}")
else:
heapq.heappush(self.normal_queue, p_message)
logger.debug(
f"消息来自普通用户 {user_id}, 已添加到普通队列. 当前普通队列长度: {len(self.normal_queue)}"
)
def get_highest_priority_message(self) -> Optional[MessageRecv]:
"""
从VIP和普通队列中获取当前最高优先级的消息。
"""
# 更新所有消息的优先级
for p_msg in self.vip_queue:
p_msg.priority = p_msg.calculate_priority()
for p_msg in self.normal_queue:
p_msg.priority = p_msg.calculate_priority()
# 重建堆
heapq.heapify(self.vip_queue)
heapq.heapify(self.normal_queue)
vip_msg = self.vip_queue[0] if self.vip_queue else None
normal_msg = self.normal_queue[0] if self.normal_queue else None
if vip_msg:
return heapq.heappop(self.vip_queue).message
elif normal_msg:
return heapq.heappop(self.normal_queue).message
else:
return None
def is_empty(self) -> bool:
"""检查所有队列是否为空"""
return not self.vip_queue and not self.normal_queue
def get_queue_status(self) -> str:
"""获取队列状态信息"""
return f"VIP队列: {len(self.vip_queue)}, 普通队列: {len(self.normal_queue)}"

View File

@@ -10,7 +10,6 @@ from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.chat.utils.timer_calculator import Timer # <--- Import Timer
from src.chat.focus_chat.heartFC_sender import HeartFCSender
from src.chat.utils.utils import process_llm_response
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
@@ -18,43 +17,58 @@ from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
from src.chat.express.exprssion_learner import get_expression_learner
import time
import asyncio
from src.chat.express.expression_selector import expression_selector
from src.manager.mood_manager import mood_manager
from src.person_info.relationship_fetcher import relationship_fetcher_manager
import random
import ast
from src.person_info.person_info import get_person_info_manager
from datetime import datetime
import re
from src.chat.knowledge.knowledge_lib import qa_manager
from src.chat.focus_chat.memory_activator import MemoryActivator
from src.tools.tool_executor import ToolExecutor
logger = get_logger("replyer")
def init_prompt():
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
Prompt("在群里聊天", "chat_target_group2")
Prompt("{sender_name}私聊", "chat_target_private2")
Prompt("\n你有以下这些**知识**\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
Prompt(
"""
{expression_habits_block}
{structured_info_block}
{tool_info_block}
{memory_block}
{relation_info_block}
{extra_info_block}
{time_block}
{chat_target}
{time_block}
{chat_info}
{reply_target_block}
{identity}
你需要使用合适的语言习惯和句法,参考聊天内容,组织一条日常且口语化的回复。注意不要复读你说过的话。
{config_expression_style}。回复不要浮夸,不要用夸张修辞,平淡一些。
{action_descriptions}
你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},请你给出回复
{config_expression_style}
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,注意不要复读你说过的话。
{keywords_reaction_prompt}
请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避
不要浮夸,不要夸张修辞,请注意不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出一条回复就好。
现在,你说:
""",
注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容
{moderation_prompt}
不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容""",
"default_generator_prompt",
)
Prompt(
"""
{expression_habits_block}
{structured_info_block}
{tool_info_block}
{memory_block}
{relation_info_block}
{extra_info_block}
@@ -120,18 +134,56 @@ def init_prompt():
class DefaultReplyer:
def __init__(self, chat_stream: ChatStream):
def __init__(
self,
chat_stream: ChatStream,
enable_tool: bool = False,
model_configs: Optional[List[Dict[str, Any]]] = None,
request_type: str = "focus.replyer",
):
self.log_prefix = "replyer"
# TODO: API-Adapter修改标记
self.express_model = LLMRequest(
model=global_config.model.replyer_1,
request_type="focus.replyer",
)
self.heart_fc_sender = HeartFCSender()
self.request_type = request_type
self.enable_tool = enable_tool
if model_configs:
self.express_model_configs = model_configs
else:
# 当未提供配置时,使用默认配置并赋予默认权重
model_config_1 = global_config.model.replyer_1.copy()
model_config_2 = global_config.model.replyer_2.copy()
prob_first = global_config.chat.replyer_random_probability
model_config_1["weight"] = prob_first
model_config_2["weight"] = 1.0 - prob_first
self.express_model_configs = [model_config_1, model_config_2]
if not self.express_model_configs:
logger.warning("未找到有效的模型配置,回复生成可能会失败。")
# 提供一个最终的回退,以防止在空列表上调用 random.choice
fallback_config = global_config.model.replyer_1.copy()
fallback_config.setdefault("weight", 1.0)
self.express_model_configs = [fallback_config]
self.chat_stream = chat_stream
self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id)
self.heart_fc_sender = HeartFCSender()
self.memory_activator = MemoryActivator()
self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=True, cache_ttl=3)
def _select_weighted_model_config(self) -> Dict[str, Any]:
"""使用加权随机选择来挑选一个模型配置"""
configs = self.express_model_configs
# 提取权重,如果模型配置中没有'weight'键则默认为1.0
weights = [config.get("weight", 1.0) for config in configs]
# random.choices 返回一个列表,我们取第一个元素
selected_config = random.choices(population=configs, weights=weights, k=1)[0]
return selected_config
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str):
"""创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
@@ -161,19 +213,36 @@ class DefaultReplyer:
async def generate_reply_with_context(
self,
reply_data: Dict[str, Any],
enable_splitter: bool=True,
enable_chinese_typo: bool=True
) -> Tuple[bool, Optional[List[str]]]:
reply_data: Dict[str, Any] = None,
reply_to: str = "",
relation_info: str = "",
extra_info: str = "",
available_actions: List[str] = None,
) -> Tuple[bool, Optional[str]]:
"""
回复器 (Replier): 核心逻辑,负责生成回复文本。
(已整合原 HeartFCGenerator 的功能)
"""
if available_actions is None:
available_actions = []
if reply_data is None:
reply_data = {}
try:
if not reply_data:
reply_data = {
"reply_to": reply_to,
"relation_info": relation_info,
"extra_info": extra_info,
}
for key, value in reply_data.items():
if not value:
logger.info(f"{self.log_prefix} 回复数据跳过{key},生成回复时将忽略。")
# 3. 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_reply_context(
reply_data=reply_data, # 传递action_data
available_actions=available_actions,
)
# 4. 调用 LLM 生成回复
@@ -183,8 +252,19 @@ class DefaultReplyer:
try:
with Timer("LLM生成", {}): # 内部计时器,可选保留
# 加权随机选择一个模型配置
selected_model_config = self._select_weighted_model_config()
logger.info(
f"{self.log_prefix} 使用模型配置: {selected_model_config.get('model_name', 'N/A')} (权重: {selected_model_config.get('weight', 1.0)})"
)
express_model = LLMRequest(
model=selected_model_config,
request_type=self.request_type,
)
logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n")
content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt)
content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt)
logger.info(f"最终回复: {content}")
@@ -193,34 +273,14 @@ class DefaultReplyer:
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
return False, None # LLM 调用失败则无法生成回复
processed_response = process_llm_response(content,enable_splitter,enable_chinese_typo)
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}LLM 生成了空内容。")
return False, None
if not processed_response:
logger.warning(f"{self.log_prefix}处理后的回复为空。")
return False, None
reply_set = []
for str in processed_response:
reply_seg = ("text", str)
reply_set.append(reply_seg)
return True, reply_set
return True, content, prompt
except Exception as e:
logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
traceback.print_exc()
return False, None
async def rewrite_reply_with_context(
self,
reply_data: Dict[str, Any],
enable_splitter: bool=True,
enable_chinese_typo: bool=True
) -> Tuple[bool, Optional[List[str]]]:
async def rewrite_reply_with_context(self, reply_data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
"""
表达器 (Expressor): 核心逻辑,负责生成回复文本。
"""
@@ -245,8 +305,18 @@ class DefaultReplyer:
try:
with Timer("LLM生成", {}): # 内部计时器,可选保留
# TODO: API-Adapter修改标记
content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt)
# 加权随机选择一个模型配置
selected_model_config = self._select_weighted_model_config()
logger.info(
f"{self.log_prefix} 使用模型配置进行重写: {selected_model_config.get('model_name', 'N/A')} (权重: {selected_model_config.get('weight', 1.0)})"
)
express_model = LLMRequest(
model=selected_model_config,
request_type=self.request_type,
)
content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt)
logger.info(f"想要表达:{raw_reply}||理由:{reason}")
logger.info(f"最终回复: {content}\n")
@@ -256,78 +326,41 @@ class DefaultReplyer:
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
return False, None # LLM 调用失败则无法生成回复
processed_response = process_llm_response(content,enable_splitter,enable_chinese_typo)
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}LLM 生成了空内容。")
return False, None
if not processed_response:
logger.warning(f"{self.log_prefix}处理后的回复为空。")
return False, None
reply_set = []
for str in processed_response:
reply_seg = ("text", str)
reply_set.append(reply_seg)
return True, reply_set
return True, content
except Exception as e:
logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
traceback.print_exc()
return False, None
async def build_prompt_reply_context(
self,
reply_data=None,
) -> str:
chat_stream = self.chat_stream
async def build_relation_info(self, reply_data=None, chat_history=None):
relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_stream.stream_id)
if not reply_data:
return ""
reply_to = reply_data.get("reply_to", "")
sender, text = self._parse_reply_target(reply_to)
if not sender or not text:
return ""
# 获取用户ID
person_info_manager = get_person_info_manager()
bot_person_id = person_info_manager.get_person_id("system", "bot_id")
person_id = person_info_manager.get_person_id_by_person_name(sender)
if not person_id:
logger.warning(f"{self.log_prefix} 未找到用户 {sender} 的ID跳过信息提取")
return None
is_group_chat = bool(chat_stream.group_info)
self_info_block = reply_data.get("self_info_block", "")
structured_info = reply_data.get("structured_info", "")
relation_info_block = reply_data.get("relation_info_block", "")
reply_to = reply_data.get("reply_to", "none")
memory_block = reply_data.get("memory_block", "")
# 优先使用 extra_info_block没有则用 extra_info
extra_info_block = reply_data.get("extra_info_block", "") or reply_data.get("extra_info", "")
sender = ""
target = ""
if ":" in reply_to or "" in reply_to:
# 使用正则表达式匹配中文或英文冒号
parts = re.split(pattern=r"[:]", string=reply_to, maxsplit=1)
if len(parts) == 2:
sender = parts[0].strip()
target = parts[1].strip()
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size,
)
# print(f"message_list_before_now: {message_list_before_now}")
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="normal_no_YMD",
read_mark=0.0,
truncate=True,
show_actions=True,
)
# print(f"chat_talking_prompt: {chat_talking_prompt}")
relation_info = await relationship_fetcher.build_relation_info(person_id, text, chat_history)
return relation_info
async def build_expression_habits(self, chat_history, target):
style_habbits = []
grammar_habbits = []
# 使用从处理器传来的选中表达方式
selected_expressions = reply_data.get("selected_expressions", []) if reply_data else []
# LLM模式调用LLM选择5-10个然后随机选5个
selected_expressions = await expression_selector.select_suitable_expressions_llm(
self.chat_stream.stream_id, chat_history, max_num=8, min_num=2, target_message=target
)
if selected_expressions:
logger.info(f"{self.log_prefix} 使用处理器选中的{len(selected_expressions)}个表达方式")
@@ -352,16 +385,81 @@ class DefaultReplyer:
if grammar_habbits_str.strip():
expression_habits_block += f"请你根据情景使用以下句法:\n{grammar_habbits_str}\n"
if structured_info:
structured_info_block = f"以下是一些额外的信息,现在请你阅读以下内容,进行决策\n{structured_info}\n以上是一些额外的信息,现在请你阅读以下内容,进行决策"
else:
structured_info_block = ""
return expression_habits_block
if extra_info_block:
extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策"
else:
extra_info_block = ""
async def build_memory_block(self, chat_history, target):
running_memorys = await self.memory_activator.activate_memory_with_chat_history(
chat_id=self.chat_stream.stream_id, target_message=target, chat_history_prompt=chat_history
)
if running_memorys:
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
for running_memory in running_memorys:
memory_str += f"- {running_memory['content']}\n"
memory_block = memory_str
logger.info(f"{self.log_prefix} 添加了 {len(running_memorys)} 个激活的记忆到prompt")
else:
memory_block = ""
return memory_block
async def build_tool_info(self, reply_data=None, chat_history=None):
"""构建工具信息块
Args:
reply_data: 回复数据,包含要回复的消息内容
chat_history: 聊天历史
Returns:
str: 工具信息字符串
"""
if not reply_data:
return ""
reply_to = reply_data.get("reply_to", "")
sender, text = self._parse_reply_target(reply_to)
if not text:
return ""
try:
# 使用工具执行器获取信息
tool_results = await self.tool_executor.execute_from_chat_message(
sender=sender, target_message=text, chat_history=chat_history, return_details=False
)
if tool_results:
tool_info_str = "以下是你通过工具获取到的实时信息:\n"
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", "info")
tool_info_str += f"- 【{tool_name}{result_type}: {content}\n"
tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。"
logger.info(f"{self.log_prefix} 获取到 {len(tool_results)} 个工具结果")
return tool_info_str
else:
logger.debug(f"{self.log_prefix} 未获取到任何工具结果")
return ""
except Exception as e:
logger.error(f"{self.log_prefix} 工具信息获取失败: {e}")
return ""
def _parse_reply_target(self, target_message: str) -> tuple:
sender = ""
target = ""
if ":" in target_message or "" in target_message:
# 使用正则表达式匹配中文或英文冒号
parts = re.split(pattern=r"[:]", string=target_message, maxsplit=1)
if len(parts) == 2:
sender = parts[0].strip()
target = parts[1].strip()
return sender, target
async def build_keywords_reaction_prompt(self, target):
# 关键词检测与反应
keywords_reaction_prompt = ""
try:
@@ -389,6 +487,95 @@ class DefaultReplyer:
except Exception as e:
logger.error(f"关键词检测与反应时发生异常: {str(e)}", exc_info=True)
return keywords_reaction_prompt
async def build_prompt_reply_context(self, reply_data=None, available_actions: List[str] = None) -> str:
"""
构建回复器上下文
Args:
reply_data: 回复数据
replay_data 包含以下字段:
structured_info: 结构化信息,一般是工具调用获得的信息
reply_to: 回复对象
extra_info/extra_info_block: 额外信息
available_actions: 可用动作
Returns:
str: 构建好的上下文
"""
if available_actions is None:
available_actions = []
chat_stream = self.chat_stream
chat_id = chat_stream.stream_id
person_info_manager = get_person_info_manager()
bot_person_id = person_info_manager.get_person_id("system", "bot_id")
is_group_chat = bool(chat_stream.group_info)
reply_to = reply_data.get("reply_to", "none")
extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "")
sender, target = self._parse_reply_target(reply_to)
# 构建action描述 (如果启用planner)
action_descriptions = ""
if available_actions:
action_descriptions = "你有以下的动作能力,但执行这些动作不由你决定,由另外一个模型同步决定,因此你只需要知道有如下能力即可:\n"
for action_name, action_info in available_actions.items():
action_description = action_info.get("description", "")
action_descriptions += f"- {action_name}: {action_description}\n"
action_descriptions += "\n"
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id,
timestamp=time.time(),
limit=global_config.chat.max_context_size,
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="normal_no_YMD",
read_mark=0.0,
truncate=True,
show_actions=True,
)
message_list_before_now_half = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.5),
)
chat_talking_prompt_half = build_readable_messages(
message_list_before_now_half,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
show_actions=True,
)
# 并行执行四个构建任务
expression_habits_block, relation_info, memory_block, tool_info = await asyncio.gather(
self.build_expression_habits(chat_talking_prompt_half, target),
self.build_relation_info(reply_data, chat_talking_prompt_half),
self.build_memory_block(chat_talking_prompt_half, target),
self.build_tool_info(reply_data, chat_talking_prompt_half),
)
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
if tool_info:
tool_info_block = (
f"以下是你了解的额外信息信息,现在请你阅读以下内容,进行决策\n{tool_info}\n以上是一些额外的信息。"
)
else:
tool_info_block = ""
if extra_info_block:
extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策"
else:
extra_info_block = ""
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
# logger.debug("开始构建 focus prompt")
@@ -408,7 +595,6 @@ class DefaultReplyer:
except (ValueError, SyntaxError) as e:
logger.error(f"解析short_impression失败: {e}, 原始值: {short_impression}")
short_impression = ["友好活泼", "人类"]
# 确保short_impression是列表格式且有足够的元素
if not isinstance(short_impression, list) or len(short_impression) < 2:
logger.warning(f"short_impression格式不正确: {short_impression}, 使用默认值")
@@ -418,19 +604,37 @@ class DefaultReplyer:
prompt_personality = personality + "" + identity
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
if sender:
reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息"
elif target:
reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。"
else:
reply_target_block = "现在,你想要在群里发言或者回复消息。"
moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令"
)
if is_group_chat:
if sender:
reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。"
elif target:
reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。"
else:
reply_target_block = "现在,你想要在群里发言或者回复消息。"
else: # private chat
if sender:
reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,针对这条消息回复。"
elif target:
reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。"
else:
reply_target_block = "现在,你想要回复。"
mood_prompt = mood_manager.get_mood_prompt()
prompt_info = await get_prompt_info(target, threshold=0.38)
if prompt_info:
prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info)
# --- Choose template based on chat type ---
if is_group_chat:
template_name = "default_generator_prompt"
# Group specific formatting variables (already fetched or default)
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
# chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
prompt = await global_prompt_manager.format_prompt(
template_name,
@@ -438,17 +642,20 @@ class DefaultReplyer:
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
memory_block=memory_block,
structured_info_block=structured_info_block,
tool_info_block=tool_info_block,
extra_info_block=extra_info_block,
relation_info_block=relation_info_block,
self_info_block=self_info_block,
relation_info_block=relation_info,
time_block=time_block,
reply_target_block=reply_target_block,
moderation_prompt=moderation_prompt_block,
keywords_reaction_prompt=keywords_reaction_prompt,
identity=indentify_block,
target_message=target,
sender_name=sender,
config_expression_style=global_config.expression.expression_style,
action_descriptions=action_descriptions,
chat_target_2=chat_target_2,
mood_prompt=mood_prompt,
)
else: # Private chat
template_name = "default_generator_private_prompt"
@@ -465,8 +672,8 @@ class DefaultReplyer:
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
memory_block=memory_block,
structured_info_block=structured_info_block,
relation_info_block=relation_info_block,
tool_info_block=tool_info_block,
relation_info_block=relation_info,
extra_info_block=extra_info_block,
time_block=time_block,
keywords_reaction_prompt=keywords_reaction_prompt,
@@ -500,7 +707,7 @@ class DefaultReplyer:
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size,
limit=global_config.chat.max_context_size,
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
@@ -768,4 +975,30 @@ def weighted_sample_no_replacement(items, weights, k) -> list:
return selected
async def get_prompt_info(message: str, threshold: float):
related_info = ""
start_time = time.time()
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 从LPMM知识库获取知识
try:
found_knowledge_from_lpmm = qa_manager.get_knowledge(message)
end_time = time.time()
if found_knowledge_from_lpmm is not None:
logger.debug(
f"从LPMM知识库获取知识相关信息{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}"
)
related_info += found_knowledge_from_lpmm
logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}")
logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
return related_info
else:
logger.debug("从LPMM知识库获取知识失败可能是从未导入过知识返回空知识...")
return ""
except Exception as e:
logger.error(f"获取知识库内容时发生异常: {str(e)}")
return ""
init_prompt()

View File

@@ -0,0 +1,62 @@
from typing import Dict, Any, Optional, List
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.chat.replyer.default_generator import DefaultReplyer
from src.common.logger import get_logger
logger = get_logger("ReplyerManager")
class ReplyerManager:
def __init__(self):
self._replyers: Dict[str, DefaultReplyer] = {}
def get_replyer(
self,
chat_stream: Optional[ChatStream] = None,
chat_id: Optional[str] = None,
enable_tool: bool = False,
model_configs: Optional[List[Dict[str, Any]]] = None,
request_type: str = "replyer",
) -> Optional[DefaultReplyer]:
"""
获取或创建回复器实例。
model_configs 仅在首次为某个 chat_id/stream_id 创建实例时有效。
后续调用将返回已缓存的实例,忽略 model_configs 参数。
"""
stream_id = chat_stream.stream_id if chat_stream else chat_id
if not stream_id:
logger.warning("[ReplyerManager] 缺少 stream_id无法获取回复器。")
return None
# 如果已有缓存实例,直接返回
if stream_id in self._replyers:
logger.debug(f"[ReplyerManager] 为 stream_id '{stream_id}' 返回已存在的回复器实例。")
return self._replyers[stream_id]
# 如果没有缓存,则创建新实例(首次初始化)
logger.debug(f"[ReplyerManager] 为 stream_id '{stream_id}' 创建新的回复器实例并缓存。")
target_stream = chat_stream
if not target_stream:
chat_manager = get_chat_manager()
if chat_manager:
target_stream = chat_manager.get_stream(stream_id)
if not target_stream:
logger.warning(f"[ReplyerManager] 未找到 stream_id='{stream_id}' 的聊天流,无法创建回复器。")
return None
# model_configs 只在此时(初始化时)生效
replyer = DefaultReplyer(
chat_stream=target_stream,
enable_tool=enable_tool,
model_configs=model_configs, # 可以是None此时使用默认模型
request_type=request_type,
)
self._replyers[stream_id] = replyer
return replyer
# 创建一个全局实例
replyer_manager = ReplyerManager()

View File

@@ -321,7 +321,7 @@ def random_remove_punctuation(text: str) -> str:
return result
def process_llm_response(text: str, enable_splitter: bool=True, enable_chinese_typo: bool=True) -> list[str]:
def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese_typo: bool = True) -> list[str]:
if not global_config.response_post_process.enable_response_post_process:
return [text]

View File

@@ -30,11 +30,11 @@ from src.config.official_configs import (
TelemetryConfig,
ExperimentalConfig,
ModelConfig,
FocusChatProcessorConfig,
MessageReceiveConfig,
MaimMessageConfig,
LPMMKnowledgeConfig,
RelationshipConfig,
ToolConfig,
)
install(extra_lines=3)
@@ -50,7 +50,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
# 对该字段的更新请严格参照语义化版本规范https://semver.org/lang/zh-CN/
MMC_VERSION = "0.8.0"
MMC_VERSION = "0.8.1-snapshot.1"
def update_config():
@@ -151,7 +151,6 @@ class Config(ConfigBase):
message_receive: MessageReceiveConfig
normal_chat: NormalChatConfig
focus_chat: FocusChatConfig
focus_chat_processor: FocusChatProcessorConfig
emoji: EmojiConfig
expression: ExpressionConfig
memory: MemoryConfig
@@ -165,6 +164,7 @@ class Config(ConfigBase):
model: ModelConfig
maim_message: MaimMessageConfig
lpmm_knowledge: LPMMKnowledgeConfig
tool: ToolConfig
def load_config(config_path: str) -> Config:

View File

@@ -75,6 +75,15 @@ class ChatConfig(ConfigBase):
chat_mode: str = "normal"
"""聊天模式"""
max_context_size: int = 18
"""上下文长度"""
replyer_random_probability: float = 0.5
"""
发言时选择推理模型的概率0-1之间
选择普通模型的概率为 1 - reasoning_normal_model_probability
"""
talk_frequency: float = 1
"""回复频率阈值"""
@@ -261,15 +270,6 @@ class MessageReceiveConfig(ConfigBase):
class NormalChatConfig(ConfigBase):
"""普通聊天配置类"""
normal_chat_first_probability: float = 0.3
"""
发言时选择推理模型的概率0-1之间
选择普通模型的概率为 1 - reasoning_normal_model_probability
"""
max_context_size: int = 15
"""上下文长度"""
message_buffer: bool = False
"""消息缓冲器"""
@@ -302,9 +302,6 @@ class NormalChatConfig(ConfigBase):
class FocusChatConfig(ConfigBase):
"""专注聊天配置类"""
observation_context_size: int = 20
"""可观察到的最长上下文大小,超过这个值的上下文会被压缩"""
compressed_length: int = 5
"""心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5"""
@@ -317,29 +314,9 @@ class FocusChatConfig(ConfigBase):
consecutive_replies: float = 1
"""连续回复能力,值越高,麦麦连续回复的概率越高"""
parallel_processing: bool = False
"""是否允许处理器阶段和回忆阶段并行执行"""
processor_max_time: int = 25
"""处理器最大时间,单位秒,如果超过这个时间,处理器会自动停止"""
@dataclass
class FocusChatProcessorConfig(ConfigBase):
"""专注聊天处理器配置类"""
person_impression_processor: bool = True
"""是否启用关系识别处理器"""
tool_use_processor: bool = True
"""是否启用工具使用处理器"""
working_memory_processor: bool = True
working_memory_processor: bool = False
"""是否启用工作记忆处理器"""
expression_selector_processor: bool = True
"""是否启用表达方式选择处理器"""
@dataclass
class ExpressionConfig(ConfigBase):
@@ -361,6 +338,17 @@ class ExpressionConfig(ConfigBase):
"""
@dataclass
class ToolConfig(ConfigBase):
"""工具配置类"""
enable_in_normal_chat: bool = False
"""是否在普通聊天中启用工具"""
enable_in_focus_chat: bool = True
"""是否在专注聊天中启用工具"""
@dataclass
class EmojiConfig(ConfigBase):
"""表情包配置类"""
@@ -438,6 +426,9 @@ class MemoryConfig(ConfigBase):
class MoodConfig(ConfigBase):
"""情绪配置类"""
enable_mood: bool = False
"""是否启用情绪系统"""
mood_update_interval: int = 1
"""情绪更新间隔(秒)"""
@@ -656,7 +647,7 @@ class ModelConfig(ConfigBase):
focus_working_memory: dict[str, Any] = field(default_factory=lambda: {})
"""专注工作记忆模型配置"""
focus_tool_use: dict[str, Any] = field(default_factory=lambda: {})
tool_use: dict[str, Any] = field(default_factory=lambda: {})
"""专注工具使用模型配置"""
planner: dict[str, Any] = field(default_factory=lambda: {})

View File

@@ -0,0 +1,465 @@
import time
import traceback
import os
import pickle
from typing import List, Dict
from src.config.config import global_config
from src.common.logger import get_logger
from src.chat.message_receive.chat_stream import get_chat_manager
from src.person_info.relationship_manager import get_relationship_manager
from src.person_info.person_info import get_person_info_manager, PersonInfoManager
from src.chat.utils.chat_message_builder import (
get_raw_msg_by_timestamp_with_chat,
get_raw_msg_by_timestamp_with_chat_inclusive,
get_raw_msg_before_timestamp_with_chat,
num_new_messages_since,
)
logger = get_logger("relationship_builder")
# 消息段清理配置
SEGMENT_CLEANUP_CONFIG = {
"enable_cleanup": True, # 是否启用清理
"max_segment_age_days": 7, # 消息段最大保存天数
"max_segments_per_user": 10, # 每用户最大消息段数
"cleanup_interval_hours": 1, # 清理间隔(小时)
}
class RelationshipBuilder:
"""关系构建器
独立运行的关系构建类基于特定的chat_id进行工作
负责跟踪用户消息活动、管理消息段、触发关系构建和印象更新
"""
def __init__(self, chat_id: str):
"""初始化关系构建器
Args:
chat_id: 聊天ID
"""
self.chat_id = chat_id
# 新的消息段缓存结构:
# {person_id: [{"start_time": float, "end_time": float, "last_msg_time": float, "message_count": int}, ...]}
self.person_engaged_cache: Dict[str, List[Dict[str, any]]] = {}
# 持久化存储文件路径
self.cache_file_path = os.path.join("data", "relationship", f"relationship_cache_{self.chat_id}.pkl")
# 最后处理的消息时间,避免重复处理相同消息
current_time = time.time()
self.last_processed_message_time = current_time
# 最后清理时间,用于定期清理老消息段
self.last_cleanup_time = 0.0
# 获取聊天名称用于日志
try:
chat_name = get_chat_manager().get_stream_name(self.chat_id)
self.log_prefix = f"[{chat_name}] 关系构建"
except Exception:
self.log_prefix = f"[{self.chat_id}] 关系构建"
# 加载持久化的缓存
self._load_cache()
# ================================
# 缓存管理模块
# 负责持久化存储、状态管理、缓存读写
# ================================
def _load_cache(self):
"""从文件加载持久化的缓存"""
if os.path.exists(self.cache_file_path):
try:
with open(self.cache_file_path, "rb") as f:
cache_data = pickle.load(f)
# 新格式:包含额外信息的缓存
self.person_engaged_cache = cache_data.get("person_engaged_cache", {})
self.last_processed_message_time = cache_data.get("last_processed_message_time", 0.0)
self.last_cleanup_time = cache_data.get("last_cleanup_time", 0.0)
logger.info(
f"{self.log_prefix} 成功加载关系缓存,包含 {len(self.person_engaged_cache)} 个用户,最后处理时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_processed_message_time)) if self.last_processed_message_time > 0 else '未设置'}"
)
except Exception as e:
logger.error(f"{self.log_prefix} 加载关系缓存失败: {e}")
self.person_engaged_cache = {}
self.last_processed_message_time = 0.0
else:
logger.info(f"{self.log_prefix} 关系缓存文件不存在,使用空缓存")
def _save_cache(self):
"""保存缓存到文件"""
try:
os.makedirs(os.path.dirname(self.cache_file_path), exist_ok=True)
cache_data = {
"person_engaged_cache": self.person_engaged_cache,
"last_processed_message_time": self.last_processed_message_time,
"last_cleanup_time": self.last_cleanup_time,
}
with open(self.cache_file_path, "wb") as f:
pickle.dump(cache_data, f)
logger.debug(f"{self.log_prefix} 成功保存关系缓存")
except Exception as e:
logger.error(f"{self.log_prefix} 保存关系缓存失败: {e}")
# ================================
# 消息段管理模块
# 负责跟踪用户消息活动、管理消息段、清理过期数据
# ================================
def _update_message_segments(self, person_id: str, message_time: float):
"""更新用户的消息段
Args:
person_id: 用户ID
message_time: 消息时间戳
"""
if person_id not in self.person_engaged_cache:
self.person_engaged_cache[person_id] = []
segments = self.person_engaged_cache[person_id]
# 获取该消息前5条消息的时间作为潜在的开始时间
before_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, message_time, limit=5)
if before_messages:
potential_start_time = before_messages[0]["time"]
else:
potential_start_time = message_time
# 如果没有现有消息段,创建新的
if not segments:
new_segment = {
"start_time": potential_start_time,
"end_time": message_time,
"last_msg_time": message_time,
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
person_name = get_person_info_manager().get_value_sync(person_id, "person_name") or person_id
logger.info(
f"{self.log_prefix} 眼熟用户 {person_name}{time.strftime('%H:%M:%S', time.localtime(potential_start_time))} - {time.strftime('%H:%M:%S', time.localtime(message_time))} 之间有 {new_segment['message_count']} 条消息"
)
self._save_cache()
return
# 获取最后一个消息段
last_segment = segments[-1]
# 计算从最后一条消息到当前消息之间的消息数量(不包含边界)
messages_between = self._count_messages_between(last_segment["last_msg_time"], message_time)
if messages_between <= 10:
# 在10条消息内延伸当前消息段
last_segment["end_time"] = message_time
last_segment["last_msg_time"] = message_time
# 重新计算整个消息段的消息数量
last_segment["message_count"] = self._count_messages_in_timerange(
last_segment["start_time"], last_segment["end_time"]
)
logger.debug(f"{self.log_prefix} 延伸用户 {person_id} 的消息段: {last_segment}")
else:
# 超过10条消息结束当前消息段并创建新的
# 结束当前消息段延伸到原消息段最后一条消息后5条消息的时间
current_time = time.time()
after_messages = get_raw_msg_by_timestamp_with_chat(
self.chat_id, last_segment["last_msg_time"], current_time, limit=5, limit_mode="earliest"
)
if after_messages and len(after_messages) >= 5:
# 如果有足够的后续消息使用第5条消息的时间作为结束时间
last_segment["end_time"] = after_messages[4]["time"]
# 重新计算当前消息段的消息数量
last_segment["message_count"] = self._count_messages_in_timerange(
last_segment["start_time"], last_segment["end_time"]
)
# 创建新的消息段
new_segment = {
"start_time": potential_start_time,
"end_time": message_time,
"last_msg_time": message_time,
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
person_info_manager = get_person_info_manager()
person_name = person_info_manager.get_value_sync(person_id, "person_name") or person_id
logger.info(f"{self.log_prefix} 重新眼熟用户 {person_name} 创建新消息段超过10条消息间隔: {new_segment}")
self._save_cache()
def _count_messages_in_timerange(self, start_time: float, end_time: float) -> int:
"""计算指定时间范围内的消息数量(包含边界)"""
messages = get_raw_msg_by_timestamp_with_chat_inclusive(self.chat_id, start_time, end_time)
return len(messages)
def _count_messages_between(self, start_time: float, end_time: float) -> int:
"""计算两个时间点之间的消息数量(不包含边界),用于间隔检查"""
return num_new_messages_since(self.chat_id, start_time, end_time)
def _get_total_message_count(self, person_id: str) -> int:
"""获取用户所有消息段的总消息数量"""
if person_id not in self.person_engaged_cache:
return 0
total_count = 0
for segment in self.person_engaged_cache[person_id]:
total_count += segment["message_count"]
return total_count
def _cleanup_old_segments(self) -> bool:
"""清理老旧的消息段"""
if not SEGMENT_CLEANUP_CONFIG["enable_cleanup"]:
return False
current_time = time.time()
# 检查是否需要执行清理(基于时间间隔)
cleanup_interval_seconds = SEGMENT_CLEANUP_CONFIG["cleanup_interval_hours"] * 3600
if current_time - self.last_cleanup_time < cleanup_interval_seconds:
return False
logger.info(f"{self.log_prefix} 开始执行老消息段清理...")
cleanup_stats = {
"users_cleaned": 0,
"segments_removed": 0,
"total_segments_before": 0,
"total_segments_after": 0,
}
max_age_seconds = SEGMENT_CLEANUP_CONFIG["max_segment_age_days"] * 24 * 3600
max_segments_per_user = SEGMENT_CLEANUP_CONFIG["max_segments_per_user"]
users_to_remove = []
for person_id, segments in self.person_engaged_cache.items():
cleanup_stats["total_segments_before"] += len(segments)
original_segment_count = len(segments)
# 1. 按时间清理:移除过期的消息段
segments_after_age_cleanup = []
for segment in segments:
segment_age = current_time - segment["end_time"]
if segment_age <= max_age_seconds:
segments_after_age_cleanup.append(segment)
else:
cleanup_stats["segments_removed"] += 1
logger.debug(
f"{self.log_prefix} 移除用户 {person_id} 的过期消息段: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(segment['start_time']))} - {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(segment['end_time']))}"
)
# 2. 按数量清理:如果消息段数量仍然过多,保留最新的
if len(segments_after_age_cleanup) > max_segments_per_user:
# 按end_time排序保留最新的
segments_after_age_cleanup.sort(key=lambda x: x["end_time"], reverse=True)
segments_removed_count = len(segments_after_age_cleanup) - max_segments_per_user
cleanup_stats["segments_removed"] += segments_removed_count
segments_after_age_cleanup = segments_after_age_cleanup[:max_segments_per_user]
logger.debug(
f"{self.log_prefix} 用户 {person_id} 消息段数量过多,移除 {segments_removed_count} 个最老的消息段"
)
# 更新缓存
if len(segments_after_age_cleanup) == 0:
# 如果没有剩余消息段,标记用户为待移除
users_to_remove.append(person_id)
else:
self.person_engaged_cache[person_id] = segments_after_age_cleanup
cleanup_stats["total_segments_after"] += len(segments_after_age_cleanup)
if original_segment_count != len(segments_after_age_cleanup):
cleanup_stats["users_cleaned"] += 1
# 移除没有消息段的用户
for person_id in users_to_remove:
del self.person_engaged_cache[person_id]
logger.debug(f"{self.log_prefix} 移除用户 {person_id}:没有剩余消息段")
# 更新最后清理时间
self.last_cleanup_time = current_time
# 保存缓存
if cleanup_stats["segments_removed"] > 0 or len(users_to_remove) > 0:
self._save_cache()
logger.info(
f"{self.log_prefix} 清理完成 - 影响用户: {cleanup_stats['users_cleaned']}, 移除消息段: {cleanup_stats['segments_removed']}, 移除用户: {len(users_to_remove)}"
)
logger.info(
f"{self.log_prefix} 消息段统计 - 清理前: {cleanup_stats['total_segments_before']}, 清理后: {cleanup_stats['total_segments_after']}"
)
else:
logger.debug(f"{self.log_prefix} 清理完成 - 无需清理任何内容")
return cleanup_stats["segments_removed"] > 0 or len(users_to_remove) > 0
def force_cleanup_user_segments(self, person_id: str) -> bool:
"""强制清理指定用户的所有消息段"""
if person_id in self.person_engaged_cache:
segments_count = len(self.person_engaged_cache[person_id])
del self.person_engaged_cache[person_id]
self._save_cache()
logger.info(f"{self.log_prefix} 强制清理用户 {person_id}{segments_count} 个消息段")
return True
return False
def get_cache_status(self) -> str:
"""获取缓存状态信息,用于调试和监控"""
if not self.person_engaged_cache:
return f"{self.log_prefix} 关系缓存为空"
status_lines = [f"{self.log_prefix} 关系缓存状态:"]
status_lines.append(
f"最后处理消息时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_processed_message_time)) if self.last_processed_message_time > 0 else '未设置'}"
)
status_lines.append(
f"最后清理时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_cleanup_time)) if self.last_cleanup_time > 0 else '未执行'}"
)
status_lines.append(f"总用户数:{len(self.person_engaged_cache)}")
status_lines.append(
f"清理配置:{'启用' if SEGMENT_CLEANUP_CONFIG['enable_cleanup'] else '禁用'} (最大保存{SEGMENT_CLEANUP_CONFIG['max_segment_age_days']}天, 每用户最多{SEGMENT_CLEANUP_CONFIG['max_segments_per_user']}段)"
)
status_lines.append("")
for person_id, segments in self.person_engaged_cache.items():
total_count = self._get_total_message_count(person_id)
status_lines.append(f"用户 {person_id}:")
status_lines.append(f" 总消息数:{total_count} ({total_count}/45)")
status_lines.append(f" 消息段数:{len(segments)}")
for i, segment in enumerate(segments):
start_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(segment["start_time"]))
end_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(segment["end_time"]))
last_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(segment["last_msg_time"]))
status_lines.append(
f"{i + 1}: {start_str} -> {end_str} (最后消息: {last_str}, 消息数: {segment['message_count']})"
)
status_lines.append("")
return "\n".join(status_lines)
# ================================
# 主要处理流程
# 统筹各模块协作、对外提供服务接口
# ================================
async def build_relation(self):
"""构建关系"""
self._cleanup_old_segments()
current_time = time.time()
latest_messages = get_raw_msg_by_timestamp_with_chat(
self.chat_id,
self.last_processed_message_time,
current_time,
limit=50, # 获取自上次处理后的消息
)
if latest_messages:
# 处理所有新的非bot消息
for latest_msg in latest_messages:
user_id = latest_msg.get("user_id")
platform = latest_msg.get("user_platform") or latest_msg.get("chat_info_platform")
msg_time = latest_msg.get("time", 0)
if (
user_id
and platform
and user_id != global_config.bot.qq_account
and msg_time > self.last_processed_message_time
):
person_id = PersonInfoManager.get_person_id(platform, user_id)
self._update_message_segments(person_id, msg_time)
logger.debug(
f"{self.log_prefix} 更新用户 {person_id} 的消息段,消息时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(msg_time))}"
)
self.last_processed_message_time = max(self.last_processed_message_time, msg_time)
# 1. 检查是否有用户达到关系构建条件总消息数达到45条
users_to_build_relationship = []
for person_id, segments in self.person_engaged_cache.items():
total_message_count = self._get_total_message_count(person_id)
if total_message_count >= 45:
users_to_build_relationship.append(person_id)
logger.info(
f"{self.log_prefix} 用户 {person_id} 满足关系构建条件,总消息数:{total_message_count},消息段数:{len(segments)}"
)
elif total_message_count > 0:
# 记录进度信息
logger.debug(
f"{self.log_prefix} 用户 {person_id} 进度:{total_message_count}/45 条消息,{len(segments)} 个消息段"
)
# 2. 为满足条件的用户构建关系
for person_id in users_to_build_relationship:
segments = self.person_engaged_cache[person_id]
# 异步执行关系构建
import asyncio
asyncio.create_task(self.update_impression_on_segments(person_id, self.chat_id, segments))
# 移除已处理的用户缓存
del self.person_engaged_cache[person_id]
self._save_cache()
# ================================
# 关系构建模块
# 负责触发关系构建、整合消息段、更新用户印象
# ================================
async def update_impression_on_segments(self, person_id: str, chat_id: str, segments: List[Dict[str, any]]):
"""基于消息段更新用户印象"""
logger.debug(f"开始为 {person_id} 基于 {len(segments)} 个消息段更新印象")
try:
processed_messages = []
for i, segment in enumerate(segments):
start_time = segment["start_time"]
end_time = segment["end_time"]
start_date = time.strftime("%Y-%m-%d %H:%M", time.localtime(start_time))
# 获取该段的消息(包含边界)
segment_messages = get_raw_msg_by_timestamp_with_chat_inclusive(self.chat_id, start_time, end_time)
logger.info(
f"消息段 {i + 1}: {start_date} - {time.strftime('%Y-%m-%d %H:%M', time.localtime(end_time))}, 消息数: {len(segment_messages)}"
)
if segment_messages:
# 如果不是第一个消息段,在消息列表前添加间隔标识
if i > 0:
# 创建一个特殊的间隔消息
gap_message = {
"time": start_time - 0.1, # 稍微早于段开始时间
"user_id": "system",
"user_platform": "system",
"user_nickname": "系统",
"user_cardname": "",
"display_message": f"...(中间省略一些消息){start_date} 之后的消息如下...",
"is_action_record": True,
"chat_info_platform": segment_messages[0].get("chat_info_platform", ""),
"chat_id": chat_id,
}
processed_messages.append(gap_message)
# 添加该段的所有消息
processed_messages.extend(segment_messages)
if processed_messages:
# 按时间排序所有消息(包括间隔标识)
processed_messages.sort(key=lambda x: x["time"])
logger.info(f"{person_id} 获取到总共 {len(processed_messages)} 条消息(包含间隔标识)用于印象更新")
relationship_manager = get_relationship_manager()
# 调用原有的更新方法
await relationship_manager.update_person_impression(
person_id=person_id, timestamp=time.time(), bot_engaged_messages=processed_messages
)
else:
logger.info(f"没有找到 {person_id} 的消息段对应的消息,不更新印象")
except Exception as e:
logger.error(f"{person_id} 更新印象时发生错误: {e}")
logger.error(traceback.format_exc())

View File

@@ -0,0 +1,103 @@
from typing import Dict, Optional, List
from src.common.logger import get_logger
from .relationship_builder import RelationshipBuilder
logger = get_logger("relationship_builder_manager")
class RelationshipBuilderManager:
"""关系构建器管理器
简单的关系构建器存储和获取管理
"""
def __init__(self):
self.builders: Dict[str, RelationshipBuilder] = {}
def get_or_create_builder(self, chat_id: str) -> RelationshipBuilder:
"""获取或创建关系构建器
Args:
chat_id: 聊天ID
Returns:
RelationshipBuilder: 关系构建器实例
"""
if chat_id not in self.builders:
self.builders[chat_id] = RelationshipBuilder(chat_id)
logger.info(f"创建聊天 {chat_id} 的关系构建器")
return self.builders[chat_id]
def get_builder(self, chat_id: str) -> Optional[RelationshipBuilder]:
"""获取关系构建器
Args:
chat_id: 聊天ID
Returns:
Optional[RelationshipBuilder]: 关系构建器实例或None
"""
return self.builders.get(chat_id)
def remove_builder(self, chat_id: str) -> bool:
"""移除关系构建器
Args:
chat_id: 聊天ID
Returns:
bool: 是否成功移除
"""
if chat_id in self.builders:
del self.builders[chat_id]
logger.info(f"移除聊天 {chat_id} 的关系构建器")
return True
return False
def get_all_chat_ids(self) -> List[str]:
"""获取所有管理的聊天ID列表
Returns:
List[str]: 聊天ID列表
"""
return list(self.builders.keys())
def get_status(self) -> Dict[str, any]:
"""获取管理器状态
Returns:
Dict[str, any]: 状态信息
"""
return {
"total_builders": len(self.builders),
"chat_ids": list(self.builders.keys()),
}
async def process_chat_messages(self, chat_id: str):
"""处理指定聊天的消息
Args:
chat_id: 聊天ID
"""
builder = self.get_or_create_builder(chat_id)
await builder.build_relation()
async def force_cleanup_user(self, chat_id: str, person_id: str) -> bool:
"""强制清理指定用户的关系构建缓存
Args:
chat_id: 聊天ID
person_id: 用户ID
Returns:
bool: 是否成功清理
"""
builder = self.get_builder(chat_id)
if builder:
return builder.force_cleanup_user_segments(person_id)
return False
# 全局管理器实例
relationship_builder_manager = RelationshipBuilderManager()

View File

@@ -0,0 +1,449 @@
from src.config.config import global_config
from src.llm_models.utils_model import LLMRequest
import time
import traceback
from src.common.logger import get_logger
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.person_info.person_info import get_person_info_manager
from typing import List, Dict
from json_repair import repair_json
from src.chat.message_receive.chat_stream import get_chat_manager
import json
logger = get_logger("relationship_fetcher")
def init_real_time_info_prompts():
"""初始化实时信息提取相关的提示词"""
relationship_prompt = """
<聊天记录>
{chat_observe_info}
</聊天记录>
{name_block}
现在,你想要回复{person_name}的消息,消息内容是:{target_message}。请根据聊天记录和你要回复的消息,从你对{person_name}的了解中提取有关的信息:
1.你需要提供你想要提取的信息具体是哪方面的信息例如年龄性别对ta的印象最近发生的事等等。
2.请注意,请不要重复调取相同的信息,已经调取的信息如下:
{info_cache_block}
3.如果当前聊天记录中没有需要查询的信息,或者现有信息已经足够回复,请返回{{"none": "不需要查询"}}
请以json格式输出例如
{{
"info_type": "信息类型",
}}
请严格按照json输出格式不要输出多余内容
"""
Prompt(relationship_prompt, "real_time_info_identify_prompt")
fetch_info_prompt = """
{name_block}
以下是你在之前与{person_name}的交流中,产生的对{person_name}的了解:
{person_impression_block}
{points_text_block}
请从中提取用户"{person_name}"的有关"{info_type}"信息
请以json格式输出例如
{{
{info_json_str}
}}
请严格按照json输出格式不要输出多余内容
"""
Prompt(fetch_info_prompt, "real_time_fetch_person_info_prompt")
class RelationshipFetcher:
def __init__(self, chat_id):
self.chat_id = chat_id
# 信息获取缓存:记录正在获取的信息请求
self.info_fetching_cache: List[Dict[str, any]] = []
# 信息结果缓存存储已获取的信息结果带TTL
self.info_fetched_cache: Dict[str, Dict[str, any]] = {}
# 结构:{person_id: {info_type: {"info": str, "ttl": int, "start_time": float, "person_name": str, "unknow": bool}}}
# LLM模型配置
self.llm_model = LLMRequest(
model=global_config.model.relation,
request_type="focus.real_time_info",
)
# 小模型用于即时信息提取
self.instant_llm_model = LLMRequest(
model=global_config.model.utils_small,
request_type="focus.real_time_info.instant",
)
name = get_chat_manager().get_stream_name(self.chat_id)
self.log_prefix = f"[{name}] 实时信息"
def _cleanup_expired_cache(self):
"""清理过期的信息缓存"""
for person_id in list(self.info_fetched_cache.keys()):
for info_type in list(self.info_fetched_cache[person_id].keys()):
self.info_fetched_cache[person_id][info_type]["ttl"] -= 1
if self.info_fetched_cache[person_id][info_type]["ttl"] <= 0:
del self.info_fetched_cache[person_id][info_type]
if not self.info_fetched_cache[person_id]:
del self.info_fetched_cache[person_id]
async def build_relation_info(self, person_id, target_message, chat_history):
# 清理过期的信息缓存
self._cleanup_expired_cache()
person_info_manager = get_person_info_manager()
person_name = await person_info_manager.get_value(person_id, "person_name")
short_impression = await person_info_manager.get_value(person_id, "short_impression")
info_type = await self._build_fetch_query(person_id, target_message, chat_history)
if info_type:
await self._extract_single_info(person_id, info_type, person_name)
relation_info = self._organize_known_info()
relation_info = f"你对{person_name}的印象是:{short_impression}\n{relation_info}"
return relation_info
async def _build_fetch_query(self, person_id, target_message, chat_history):
nickname_str = ",".join(global_config.bot.alias_names)
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
person_info_manager = get_person_info_manager()
person_name = await person_info_manager.get_value(person_id, "person_name")
info_cache_block = self._build_info_cache_block()
prompt = (await global_prompt_manager.get_prompt_async("real_time_info_identify_prompt")).format(
chat_observe_info=chat_history,
name_block=name_block,
info_cache_block=info_cache_block,
person_name=person_name,
target_message=target_message,
)
try:
logger.debug(f"{self.log_prefix} 信息识别prompt: \n{prompt}\n")
content, _ = await self.llm_model.generate_response_async(prompt=prompt)
if content:
content_json = json.loads(repair_json(content))
# 检查是否返回了不需要查询的标志
if "none" in content_json:
logger.info(f"{self.log_prefix} LLM判断当前不需要查询任何信息{content_json.get('none', '')}")
return None
info_type = content_json.get("info_type")
if info_type:
# 记录信息获取请求
self.info_fetching_cache.append(
{
"person_id": get_person_info_manager().get_person_id_by_person_name(person_name),
"person_name": person_name,
"info_type": info_type,
"start_time": time.time(),
"forget": False,
}
)
# 限制缓存大小
if len(self.info_fetching_cache) > 10:
self.info_fetching_cache.pop(0)
logger.info(f"{self.log_prefix} 识别到需要调取用户 {person_name} 的[{info_type}]信息")
return info_type
else:
logger.warning(f"{self.log_prefix} LLM未返回有效的info_type。响应: {content}")
except Exception as e:
logger.error(f"{self.log_prefix} 执行信息识别LLM请求时出错: {e}")
logger.error(traceback.format_exc())
return None
def _build_info_cache_block(self) -> str:
"""构建已获取信息的缓存块"""
info_cache_block = ""
if self.info_fetching_cache:
# 对于每个(person_id, info_type)组合,只保留最新的记录
latest_records = {}
for info_fetching in self.info_fetching_cache:
key = (info_fetching["person_id"], info_fetching["info_type"])
if key not in latest_records or info_fetching["start_time"] > latest_records[key]["start_time"]:
latest_records[key] = info_fetching
# 按时间排序并生成显示文本
sorted_records = sorted(latest_records.values(), key=lambda x: x["start_time"])
for info_fetching in sorted_records:
info_cache_block += (
f"你已经调取了[{info_fetching['person_name']}]的[{info_fetching['info_type']}]信息\n"
)
return info_cache_block
async def _extract_single_info(self, person_id: str, info_type: str, person_name: str):
"""提取单个信息类型
Args:
person_id: 用户ID
info_type: 信息类型
person_name: 用户名
"""
start_time = time.time()
person_info_manager = get_person_info_manager()
# 首先检查 info_list 缓存
info_list = await person_info_manager.get_value(person_id, "info_list") or []
cached_info = None
# 查找对应的 info_type
for info_item in info_list:
if info_item.get("info_type") == info_type:
cached_info = info_item.get("info_content")
logger.debug(f"{self.log_prefix} 在info_list中找到 {person_name}{info_type} 信息: {cached_info}")
break
# 如果缓存中有信息,直接使用
if cached_info:
if person_id not in self.info_fetched_cache:
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": cached_info,
"ttl": 2,
"start_time": start_time,
"person_name": person_name,
"unknow": cached_info == "none",
}
logger.info(f"{self.log_prefix} 记得 {person_name}{info_type}: {cached_info}")
return
# 如果缓存中没有,尝试从用户档案中提取
try:
person_impression = await person_info_manager.get_value(person_id, "impression")
points = await person_info_manager.get_value(person_id, "points")
# 构建印象信息块
if person_impression:
person_impression_block = (
f"<对{person_name}的总体了解>\n{person_impression}\n</对{person_name}的总体了解>"
)
else:
person_impression_block = ""
# 构建要点信息块
if points:
points_text = "\n".join([f"{point[2]}:{point[0]}" for point in points])
points_text_block = f"<对{person_name}的近期了解>\n{points_text}\n</对{person_name}的近期了解>"
else:
points_text_block = ""
# 如果完全没有用户信息
if not points_text_block and not person_impression_block:
if person_id not in self.info_fetched_cache:
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": "none",
"ttl": 2,
"start_time": start_time,
"person_name": person_name,
"unknow": True,
}
logger.info(f"{self.log_prefix} 完全不认识 {person_name}")
await self._save_info_to_cache(person_id, info_type, "none")
return
# 使用LLM提取信息
nickname_str = ",".join(global_config.bot.alias_names)
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
prompt = (await global_prompt_manager.get_prompt_async("real_time_fetch_person_info_prompt")).format(
name_block=name_block,
info_type=info_type,
person_impression_block=person_impression_block,
person_name=person_name,
info_json_str=f'"{info_type}": "有关{info_type}的信息内容"',
points_text_block=points_text_block,
)
# 使用小模型进行即时提取
content, _ = await self.instant_llm_model.generate_response_async(prompt=prompt)
if content:
content_json = json.loads(repair_json(content))
if info_type in content_json:
info_content = content_json[info_type]
is_unknown = info_content == "none" or not info_content
# 保存到运行时缓存
if person_id not in self.info_fetched_cache:
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": "unknow" if is_unknown else info_content,
"ttl": 3,
"start_time": start_time,
"person_name": person_name,
"unknow": is_unknown,
}
# 保存到持久化缓存 (info_list)
await self._save_info_to_cache(person_id, info_type, info_content if not is_unknown else "none")
if not is_unknown:
logger.info(f"{self.log_prefix} 思考得到,{person_name}{info_type}: {info_content}")
else:
logger.info(f"{self.log_prefix} 思考了也不知道{person_name}{info_type} 信息")
else:
logger.warning(f"{self.log_prefix} 小模型返回空结果,获取 {person_name}{info_type} 信息失败。")
except Exception as e:
logger.error(f"{self.log_prefix} 执行信息提取时出错: {e}")
logger.error(traceback.format_exc())
def _organize_known_info(self) -> str:
"""组织已知的用户信息为字符串
Returns:
str: 格式化的用户信息字符串
"""
persons_infos_str = ""
if self.info_fetched_cache:
persons_with_known_info = [] # 有已知信息的人员
persons_with_unknown_info = [] # 有未知信息的人员
for person_id in self.info_fetched_cache:
person_known_infos = []
person_unknown_infos = []
person_name = ""
for info_type in self.info_fetched_cache[person_id]:
person_name = self.info_fetched_cache[person_id][info_type]["person_name"]
if not self.info_fetched_cache[person_id][info_type]["unknow"]:
info_content = self.info_fetched_cache[person_id][info_type]["info"]
person_known_infos.append(f"[{info_type}]{info_content}")
else:
person_unknown_infos.append(info_type)
# 如果有已知信息,添加到已知信息列表
if person_known_infos:
known_info_str = "".join(person_known_infos) + ""
persons_with_known_info.append((person_name, known_info_str))
# 如果有未知信息,添加到未知信息列表
if person_unknown_infos:
persons_with_unknown_info.append((person_name, person_unknown_infos))
# 先输出有已知信息的人员
for person_name, known_info_str in persons_with_known_info:
persons_infos_str += f"你对 {person_name} 的了解:{known_info_str}\n"
# 统一处理未知信息,避免重复的警告文本
if persons_with_unknown_info:
unknown_persons_details = []
for person_name, unknown_types in persons_with_unknown_info:
unknown_types_str = "".join(unknown_types)
unknown_persons_details.append(f"{person_name}的[{unknown_types_str}]")
if len(unknown_persons_details) == 1:
persons_infos_str += (
f"你不了解{unknown_persons_details[0]}信息,不要胡乱回答,可以直接说不知道或忘记了;\n"
)
else:
unknown_all_str = "".join(unknown_persons_details)
persons_infos_str += f"你不了解{unknown_all_str}等信息,不要胡乱回答,可以直接说不知道或忘记了;\n"
return persons_infos_str
async def _save_info_to_cache(self, person_id: str, info_type: str, info_content: str):
"""将提取到的信息保存到 person_info 的 info_list 字段中
Args:
person_id: 用户ID
info_type: 信息类型
info_content: 信息内容
"""
try:
person_info_manager = get_person_info_manager()
# 获取现有的 info_list
info_list = await person_info_manager.get_value(person_id, "info_list") or []
# 查找是否已存在相同 info_type 的记录
found_index = -1
for i, info_item in enumerate(info_list):
if isinstance(info_item, dict) and info_item.get("info_type") == info_type:
found_index = i
break
# 创建新的信息记录
new_info_item = {
"info_type": info_type,
"info_content": info_content,
}
if found_index >= 0:
# 更新现有记录
info_list[found_index] = new_info_item
logger.info(f"{self.log_prefix} [缓存更新] 更新 {person_id}{info_type} 信息缓存")
else:
# 添加新记录
info_list.append(new_info_item)
logger.info(f"{self.log_prefix} [缓存保存] 新增 {person_id}{info_type} 信息缓存")
# 保存更新后的 info_list
await person_info_manager.update_one_field(person_id, "info_list", info_list)
except Exception as e:
logger.error(f"{self.log_prefix} [缓存保存] 保存信息到缓存失败: {e}")
logger.error(traceback.format_exc())
class RelationshipFetcherManager:
"""关系提取器管理器
管理不同 chat_id 的 RelationshipFetcher 实例
"""
def __init__(self):
self._fetchers: Dict[str, RelationshipFetcher] = {}
def get_fetcher(self, chat_id: str) -> RelationshipFetcher:
"""获取或创建指定 chat_id 的 RelationshipFetcher
Args:
chat_id: 聊天ID
Returns:
RelationshipFetcher: 关系提取器实例
"""
if chat_id not in self._fetchers:
self._fetchers[chat_id] = RelationshipFetcher(chat_id)
return self._fetchers[chat_id]
def remove_fetcher(self, chat_id: str):
"""移除指定 chat_id 的 RelationshipFetcher
Args:
chat_id: 聊天ID
"""
if chat_id in self._fetchers:
del self._fetchers[chat_id]
def clear_all(self):
"""清空所有 RelationshipFetcher"""
self._fetchers.clear()
def get_active_chat_ids(self) -> List[str]:
"""获取所有活跃的 chat_id 列表"""
return list(self._fetchers.keys())
# 全局管理器实例
relationship_fetcher_manager = RelationshipFetcherManager()
init_real_time_info_prompts()

View File

@@ -8,10 +8,13 @@
success, reply_set = await generator_api.generate_reply(chat_stream, action_data, reasoning)
"""
from typing import Tuple, Any, Dict, List
import traceback
from typing import Tuple, Any, Dict, List, Optional
from src.common.logger import get_logger
from src.chat.replyer.default_generator import DefaultReplyer
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.utils.utils import process_llm_response
from src.chat.replyer.replyer_manager import replyer_manager
logger = get_logger("generator_api")
@@ -21,46 +24,39 @@ logger = get_logger("generator_api")
# =============================================================================
def get_replyer(chat_stream=None, chat_id: str = None) -> DefaultReplyer:
def get_replyer(
chat_stream: Optional[ChatStream] = None,
chat_id: Optional[str] = None,
enable_tool: bool = False,
model_configs: Optional[List[Dict[str, Any]]] = None,
request_type: str = "replyer",
) -> Optional[DefaultReplyer]:
"""获取回复器对象
优先使用chat_stream如果没有则使用chat_id直接查找
优先使用chat_stream如果没有则使用chat_id直接查找
使用 ReplyerManager 来管理实例,避免重复创建。
Args:
chat_stream: 聊天流对象(优先)
chat_id: 聊天ID实际上就是stream_id
model_configs: 模型配置列表
request_type: 请求类型
Returns:
Optional[Any]: 回复器对象如果获取失败则返回None
Optional[DefaultReplyer]: 回复器对象如果获取失败则返回None
"""
try:
# 优先使用聊天流
if chat_stream:
logger.debug("[GeneratorAPI] 使用聊天流获取回复器")
return DefaultReplyer(chat_stream=chat_stream)
# 使用chat_id直接查找chat_id即为stream_id
if chat_id:
logger.debug("[GeneratorAPI] 使用chat_id获取回复器")
chat_manager = get_chat_manager()
if not chat_manager:
logger.warning("[GeneratorAPI] 无法获取聊天管理器")
return None
# 直接使用chat_id作为stream_id查找
target_stream = chat_manager.get_stream(chat_id)
if target_stream is None:
logger.warning(f"[GeneratorAPI] 未找到匹配的聊天流 chat_id={chat_id}")
return None
return DefaultReplyer(chat_stream=target_stream)
logger.warning("[GeneratorAPI] 缺少必要参数,无法获取回复器")
return None
logger.debug(f"[GeneratorAPI] 正在获取回复器chat_id: {chat_id}, chat_stream: {'' if chat_stream else ''}")
return replyer_manager.get_replyer(
chat_stream=chat_stream,
chat_id=chat_id,
model_configs=model_configs,
request_type=request_type,
enable_tool=enable_tool,
)
except Exception as e:
logger.error(f"[GeneratorAPI] 获取回复器失败: {e}")
logger.error(f"[GeneratorAPI] 获取回复器时发生意外错误: {e}", exc_info=True)
traceback.print_exc()
return None
@@ -71,10 +67,18 @@ def get_replyer(chat_stream=None, chat_id: str = None) -> DefaultReplyer:
async def generate_reply(
chat_stream=None,
action_data: Dict[str, Any] = None,
chat_id: str = None,
enable_splitter: bool=True,
enable_chinese_typo: bool=True
action_data: Dict[str, Any] = None,
reply_to: str = "",
relation_info: str = "",
extra_info: str = "",
available_actions: List[str] = None,
enable_tool: bool = False,
enable_splitter: bool = True,
enable_chinese_typo: bool = True,
return_prompt: bool = False,
model_configs: Optional[List[Dict[str, Any]]] = None,
request_type: str = "",
) -> Tuple[bool, List[Tuple[str, Any]]]:
"""生成回复
@@ -84,13 +88,15 @@ async def generate_reply(
chat_id: 聊天ID备用
enable_splitter: 是否启用消息分割器
enable_chinese_typo: 是否启用错字生成器
return_prompt: 是否返回提示词
Returns:
Tuple[bool, List[Tuple[str, Any]]]: (是否成功, 回复集合)
"""
try:
# 获取回复器
replyer = get_replyer(chat_stream, chat_id)
replyer = get_replyer(
chat_stream, chat_id, model_configs=model_configs, request_type=request_type, enable_tool=enable_tool
)
if not replyer:
logger.error("[GeneratorAPI] 无法获取回复器")
return False, []
@@ -98,18 +104,25 @@ async def generate_reply(
logger.info("[GeneratorAPI] 开始生成回复")
# 调用回复器生成回复
success, reply_set = await replyer.generate_reply_with_context(
success, content, prompt = await replyer.generate_reply_with_context(
reply_data=action_data or {},
enable_splitter=enable_splitter,
enable_chinese_typo=enable_chinese_typo
reply_to=reply_to,
relation_info=relation_info,
extra_info=extra_info,
available_actions=available_actions,
)
reply_set = await process_human_text(content, enable_splitter, enable_chinese_typo)
if success:
logger.info(f"[GeneratorAPI] 回复生成成功,生成了 {len(reply_set)} 个回复项")
else:
logger.warning("[GeneratorAPI] 回复生成失败")
return success, reply_set or []
if return_prompt:
return success, reply_set or [], prompt
else:
return success, reply_set or []
except Exception as e:
logger.error(f"[GeneratorAPI] 生成回复时出错: {e}")
@@ -120,8 +133,9 @@ async def rewrite_reply(
chat_stream=None,
reply_data: Dict[str, Any] = None,
chat_id: str = None,
enable_splitter: bool=True,
enable_chinese_typo: bool=True
enable_splitter: bool = True,
enable_chinese_typo: bool = True,
model_configs: Optional[List[Dict[str, Any]]] = None,
) -> Tuple[bool, List[Tuple[str, Any]]]:
"""重写回复
@@ -137,7 +151,7 @@ async def rewrite_reply(
"""
try:
# 获取回复器
replyer = get_replyer(chat_stream, chat_id)
replyer = get_replyer(chat_stream, chat_id, model_configs=model_configs)
if not replyer:
logger.error("[GeneratorAPI] 无法获取回复器")
return False, []
@@ -145,11 +159,9 @@ async def rewrite_reply(
logger.info("[GeneratorAPI] 开始重写回复")
# 调用回复器重写回复
success, reply_set = await replyer.rewrite_reply_with_context(
reply_data=reply_data or {},
enable_splitter=enable_splitter,
enable_chinese_typo=enable_chinese_typo
)
success, content = await replyer.rewrite_reply_with_context(reply_data=reply_data or {})
reply_set = await process_human_text(content, enable_splitter, enable_chinese_typo)
if success:
logger.info(f"[GeneratorAPI] 重写回复成功,生成了 {len(reply_set)} 个回复项")
@@ -161,3 +173,26 @@ async def rewrite_reply(
except Exception as e:
logger.error(f"[GeneratorAPI] 重写回复时出错: {e}")
return False, []
async def process_human_text(content: str, enable_splitter: bool, enable_chinese_typo: bool) -> List[Tuple[str, Any]]:
"""将文本处理为更拟人化的文本
Args:
content: 文本内容
enable_splitter: 是否启用消息分割器
enable_chinese_typo: 是否启用错字生成器
"""
try:
processed_response = process_llm_response(content, enable_splitter, enable_chinese_typo)
reply_set = []
for str in processed_response:
reply_seg = ("text", str)
reply_set.append(reply_seg)
return reply_set
except Exception as e:
logger.error(f"[GeneratorAPI] 处理人形文本时出错: {e}")
return []

View File

@@ -108,8 +108,6 @@ class BaseAction(ABC):
# print(self.chat_stream.group_info)
if self.chat_stream.group_info:
self.is_group = True
self.user_id = str(self.chat_stream.user_info.user_id)
self.user_nickname = getattr(self.chat_stream.user_info, "user_nickname", None)
self.group_id = str(self.chat_stream.group_info.group_id)
self.group_name = getattr(self.chat_stream.group_info, "group_name", None)
else:

View File

@@ -17,9 +17,27 @@ logger = get_logger("manifest_utils")
class VersionComparator:
"""版本号比较器
支持语义化版本号比较自动处理snapshot版本
支持语义化版本号比较自动处理snapshot版本,并支持向前兼容性检查
"""
# 版本兼容性映射表(硬编码)
# 格式: {插件最大支持版本: [实际兼容的版本列表]}
COMPATIBILITY_MAP = {
# 0.8.x 系列向前兼容规则
"0.8.0": ["0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.1": ["0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.2": ["0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.3": ["0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.4": ["0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.5": ["0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.6": ["0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.7": ["0.8.8", "0.8.9", "0.8.10"],
"0.8.8": ["0.8.9", "0.8.10"],
"0.8.9": ["0.8.10"],
# 可以根据需要添加更多兼容映射
# "0.9.0": ["0.9.1", "0.9.2", "0.9.3"], # 示例0.9.x系列兼容
}
@staticmethod
def normalize_version(version: str) -> str:
"""标准化版本号移除snapshot标识
@@ -88,9 +106,31 @@ class VersionComparator:
else:
return 0
@staticmethod
def check_forward_compatibility(current_version: str, max_version: str) -> Tuple[bool, str]:
"""检查向前兼容性(仅使用兼容性映射表)
Args:
current_version: 当前版本
max_version: 插件声明的最大支持版本
Returns:
Tuple[bool, str]: (是否兼容, 兼容信息)
"""
current_normalized = VersionComparator.normalize_version(current_version)
max_normalized = VersionComparator.normalize_version(max_version)
# 检查兼容性映射表
if max_normalized in VersionComparator.COMPATIBILITY_MAP:
compatible_versions = VersionComparator.COMPATIBILITY_MAP[max_normalized]
if current_normalized in compatible_versions:
return True, f"根据兼容性映射表,版本 {current_normalized}{max_normalized} 兼容"
return False, ""
@staticmethod
def is_version_in_range(version: str, min_version: str = "", max_version: str = "") -> Tuple[bool, str]:
"""检查版本是否在指定范围内
"""检查版本是否在指定范围内,支持兼容性检查
Args:
version: 要检查的版本号
@@ -98,7 +138,7 @@ class VersionComparator:
max_version: 最大版本号(可选)
Returns:
Tuple[bool, str]: (是否兼容, 错误信息)
Tuple[bool, str]: (是否兼容, 错误信息或兼容信息)
"""
if not min_version and not max_version:
return True, ""
@@ -114,8 +154,19 @@ class VersionComparator:
# 检查最大版本
if max_version:
max_normalized = VersionComparator.normalize_version(max_version)
if VersionComparator.compare_versions(version_normalized, max_normalized) > 0:
return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized}"
comparison = VersionComparator.compare_versions(version_normalized, max_normalized)
if comparison > 0:
# 严格版本检查失败,尝试兼容性检查
is_compatible, compat_msg = VersionComparator.check_forward_compatibility(
version_normalized, max_normalized
)
if is_compatible:
logger.info(f"版本兼容性检查:{compat_msg}")
return True, compat_msg
else:
return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射"
return True, ""
@@ -128,6 +179,29 @@ class VersionComparator:
"""
return VersionComparator.normalize_version(MMC_VERSION)
@staticmethod
def add_compatibility_mapping(base_version: str, compatible_versions: list) -> None:
"""动态添加兼容性映射
Args:
base_version: 基础版本(插件声明的最大支持版本)
compatible_versions: 兼容的版本列表
"""
base_normalized = VersionComparator.normalize_version(base_version)
VersionComparator.COMPATIBILITY_MAP[base_normalized] = [
VersionComparator.normalize_version(v) for v in compatible_versions
]
logger.info(f"添加兼容性映射:{base_normalized} -> {compatible_versions}")
@staticmethod
def get_compatibility_info() -> Dict[str, list]:
"""获取当前的兼容性映射表
Returns:
Dict[str, list]: 兼容性映射表的副本
"""
return VersionComparator.COMPATIBILITY_MAP.copy()
class ManifestValidator:
"""Manifest文件验证器"""

View File

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

View File

@@ -12,6 +12,7 @@ from typing import List, Tuple, Type
# 导入新插件系统
from src.plugin_system import BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.config_types import ConfigField
from src.config.config import global_config
# 导入依赖的系统组件
from src.common.logger import get_logger
@@ -61,6 +62,8 @@ class ReplyAction(BaseAction):
success, reply_set = await generator_api.generate_reply(
action_data=self.action_data,
chat_id=self.chat_id,
request_type="focus.replyer",
enable_tool=global_config.tool.enable_in_focus_chat,
)
# 检查从start_time以来的新消息数量
@@ -197,7 +200,6 @@ class CoreActionsPlugin(BasePlugin):
"plugin": "插件启用配置",
"components": "核心组件启用配置",
"no_reply": "不回复动作配置(智能等待机制)",
"emoji": "表情动作配置",
}
# 配置Schema定义
@@ -231,18 +233,13 @@ class CoreActionsPlugin(BasePlugin):
type=int, default=600, description="回复频率检查窗口时间(秒)", example=600
),
},
"emoji": {
"random_probability": ConfigField(
type=float, default=0.1, description="Normal模式下随机发送表情的概率0.0到1.0", example=0.15
)
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# --- 从配置动态设置Action/Command ---
emoji_chance = self.get_config("emoji.random_probability", 0.1)
emoji_chance = global_config.normal_chat.emoji_chance
EmojiAction.random_activation_probability = emoji_chance
no_reply_probability = self.get_config("no_reply.random_probability", 0.8)

View File

@@ -11,7 +11,7 @@
"host_application": {
"min_version": "0.8.0",
"max_version": "0.8.0"
"max_version": "0.8.10"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",

View File

@@ -10,7 +10,7 @@
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.8.0",
"max_version": "0.8.0"
"max_version": "0.8.10"
},
"keywords": ["mute", "ban", "moderation", "admin", "management", "group"],
"categories": ["Moderation", "Group Management", "Admin Tools"],

View File

@@ -11,7 +11,7 @@
"host_application": {
"min_version": "0.8.0",
"max_version": "0.8.0"
"max_version": "0.8.10"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",

View File

@@ -10,7 +10,7 @@
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.8.0",
"max_version": "0.8.0"
"max_version": "0.8.10"
},
"keywords": ["vtb", "vtuber", "emotion", "expression", "virtual", "streamer"],
"categories": ["Entertainment", "Virtual Assistant", "Emotion"],

404
src/tools/tool_executor.py Normal file
View File

@@ -0,0 +1,404 @@
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
import time
from src.common.logger import get_logger
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.tools.tool_use import ToolUser
from src.chat.utils.json_utils import process_llm_tool_calls
from typing import List, Dict, Tuple, Optional
logger = get_logger("tool_executor")
def init_tool_executor_prompt():
"""初始化工具执行器的提示词"""
tool_executor_prompt = """
你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}
群里正在进行的聊天内容:
{chat_history}
现在,{sender}发送了内容:{target_message},你想要回复ta。
请仔细分析聊天内容,考虑以下几点:
1. 内容中是否包含需要查询信息的问题
2. 是否有明确的工具使用指令
If you need to use a tool, please directly call the corresponding tool function. If you do not need to use any tool, simply output "No tool needed".
"""
Prompt(tool_executor_prompt, "tool_executor_prompt")
class ToolExecutor:
"""独立的工具执行器组件
可以直接输入聊天消息内容,自动判断并执行相应的工具,返回结构化的工具执行结果。
"""
def __init__(self, chat_id: str = None, enable_cache: bool = True, cache_ttl: int = 3):
"""初始化工具执行器
Args:
executor_id: 执行器标识符,用于日志记录
enable_cache: 是否启用缓存机制
cache_ttl: 缓存生存时间(周期数)
"""
self.chat_id = chat_id
self.log_prefix = f"[ToolExecutor:{self.chat_id}] "
self.llm_model = LLMRequest(
model=global_config.model.tool_use,
request_type="tool_executor",
)
# 初始化工具实例
self.tool_instance = ToolUser()
# 缓存配置
self.enable_cache = enable_cache
self.cache_ttl = cache_ttl
self.tool_cache = {} # 格式: {cache_key: {"result": result, "ttl": ttl, "timestamp": timestamp}}
logger.info(f"{self.log_prefix}工具执行器初始化完成,缓存{'启用' if enable_cache else '禁用'}TTL={cache_ttl}")
async def execute_from_chat_message(
self, target_message: str, chat_history: list[str], sender: str, return_details: bool = False
) -> List[Dict] | Tuple[List[Dict], List[str], str]:
"""从聊天消息执行工具
Args:
target_message: 目标消息内容
chat_history: 聊天历史
sender: 发送者
return_details: 是否返回详细信息(使用的工具列表和提示词)
Returns:
如果return_details为False: List[Dict] - 工具执行结果列表
如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词)
"""
# 首先检查缓存
cache_key = self._generate_cache_key(target_message, chat_history, sender)
cached_result = self._get_from_cache(cache_key)
if cached_result:
logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行")
if return_details:
# 从缓存结果中提取工具名称
used_tools = [result.get("tool_name", "unknown") for result in cached_result]
return cached_result, used_tools, "使用缓存结果"
else:
return cached_result
# 缓存未命中,执行工具调用
# 获取可用工具
tools = self.tool_instance._define_tools()
# 获取当前时间
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
bot_name = global_config.bot.nickname
# 构建工具调用提示词
prompt = await global_prompt_manager.format_prompt(
"tool_executor_prompt",
target_message=target_message,
chat_history=chat_history,
sender=sender,
bot_name=bot_name,
time_now=time_now,
)
logger.debug(f"{self.log_prefix}开始LLM工具调用分析")
# 调用LLM进行工具决策
response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools)
# 解析LLM响应
if len(other_info) == 3:
reasoning_content, model_name, tool_calls = other_info
else:
reasoning_content, model_name = other_info
tool_calls = None
# 执行工具调用
tool_results, used_tools = await self._execute_tool_calls(tool_calls)
# 缓存结果
if tool_results:
self._set_cache(cache_key, tool_results)
logger.info(f"{self.log_prefix}工具执行完成,共执行{len(used_tools)}个工具: {used_tools}")
if return_details:
return tool_results, used_tools, prompt
else:
return tool_results
async def _execute_tool_calls(self, tool_calls) -> Tuple[List[Dict], List[str]]:
"""执行工具调用
Args:
tool_calls: LLM返回的工具调用列表
Returns:
Tuple[List[Dict], List[str]]: (工具执行结果列表, 使用的工具名称列表)
"""
tool_results = []
used_tools = []
if not tool_calls:
logger.debug(f"{self.log_prefix}无需执行工具")
return tool_results, used_tools
logger.info(f"{self.log_prefix}开始执行工具调用: {tool_calls}")
# 处理工具调用
success, valid_tool_calls, error_msg = process_llm_tool_calls(tool_calls)
if not success:
logger.error(f"{self.log_prefix}工具调用解析失败: {error_msg}")
return tool_results, used_tools
if not valid_tool_calls:
logger.debug(f"{self.log_prefix}无有效工具调用")
return tool_results, used_tools
# 执行每个工具调用
for tool_call in valid_tool_calls:
try:
tool_name = tool_call.get("name", "unknown_tool")
used_tools.append(tool_name)
logger.debug(f"{self.log_prefix}执行工具: {tool_name}")
# 执行工具
result = await self.tool_instance._execute_tool_call(tool_call)
if result:
tool_info = {
"type": result.get("type", "unknown_type"),
"id": result.get("id", f"tool_exec_{time.time()}"),
"content": result.get("content", ""),
"tool_name": tool_name,
"timestamp": time.time(),
}
tool_results.append(tool_info)
logger.info(f"{self.log_prefix}工具{tool_name}执行成功,类型: {tool_info['type']}")
logger.debug(f"{self.log_prefix}工具{tool_name}结果内容: {tool_info['content'][:200]}...")
except Exception as e:
logger.error(f"{self.log_prefix}工具{tool_name}执行失败: {e}")
# 添加错误信息到结果中
error_info = {
"type": "tool_error",
"id": f"tool_error_{time.time()}",
"content": f"工具{tool_name}执行失败: {str(e)}",
"tool_name": tool_name,
"timestamp": time.time(),
}
tool_results.append(error_info)
return tool_results, used_tools
def _generate_cache_key(self, target_message: str, chat_history: list[str], sender: str) -> str:
"""生成缓存键
Args:
target_message: 目标消息内容
chat_history: 聊天历史
sender: 发送者
Returns:
str: 缓存键
"""
import hashlib
# 使用消息内容和群聊状态生成唯一缓存键
content = f"{target_message}_{chat_history}_{sender}"
return hashlib.md5(content.encode()).hexdigest()
def _get_from_cache(self, cache_key: str) -> Optional[List[Dict]]:
"""从缓存获取结果
Args:
cache_key: 缓存键
Returns:
Optional[List[Dict]]: 缓存的结果如果不存在或过期则返回None
"""
if not self.enable_cache or cache_key not in self.tool_cache:
return None
cache_item = self.tool_cache[cache_key]
if cache_item["ttl"] <= 0:
# 缓存过期,删除
del self.tool_cache[cache_key]
logger.debug(f"{self.log_prefix}缓存过期,删除缓存键: {cache_key}")
return None
# 减少TTL
cache_item["ttl"] -= 1
logger.debug(f"{self.log_prefix}使用缓存结果剩余TTL: {cache_item['ttl']}")
return cache_item["result"]
def _set_cache(self, cache_key: str, result: List[Dict]):
"""设置缓存
Args:
cache_key: 缓存键
result: 要缓存的结果
"""
if not self.enable_cache:
return
self.tool_cache[cache_key] = {"result": result, "ttl": self.cache_ttl, "timestamp": time.time()}
logger.debug(f"{self.log_prefix}设置缓存TTL: {self.cache_ttl}")
def _cleanup_expired_cache(self):
"""清理过期的缓存"""
if not self.enable_cache:
return
expired_keys = []
for cache_key, cache_item in self.tool_cache.items():
if cache_item["ttl"] <= 0:
expired_keys.append(cache_key)
for key in expired_keys:
del self.tool_cache[key]
if expired_keys:
logger.debug(f"{self.log_prefix}清理了{len(expired_keys)}个过期缓存")
def get_available_tools(self) -> List[str]:
"""获取可用工具列表
Returns:
List[str]: 可用工具名称列表
"""
tools = self.tool_instance._define_tools()
return [tool.get("function", {}).get("name", "unknown") for tool in tools]
async def execute_specific_tool(
self, tool_name: str, tool_args: Dict, validate_args: bool = True
) -> Optional[Dict]:
"""直接执行指定工具
Args:
tool_name: 工具名称
tool_args: 工具参数
validate_args: 是否验证参数
Returns:
Optional[Dict]: 工具执行结果失败时返回None
"""
try:
tool_call = {"name": tool_name, "arguments": tool_args}
logger.info(f"{self.log_prefix}直接执行工具: {tool_name}")
result = await self.tool_instance._execute_tool_call(tool_call)
if result:
tool_info = {
"type": result.get("type", "unknown_type"),
"id": result.get("id", f"direct_tool_{time.time()}"),
"content": result.get("content", ""),
"tool_name": tool_name,
"timestamp": time.time(),
}
logger.info(f"{self.log_prefix}直接工具执行成功: {tool_name}")
return tool_info
except Exception as e:
logger.error(f"{self.log_prefix}直接工具执行失败 {tool_name}: {e}")
return None
def clear_cache(self):
"""清空所有缓存"""
if self.enable_cache:
cache_count = len(self.tool_cache)
self.tool_cache.clear()
logger.info(f"{self.log_prefix}清空了{cache_count}个缓存项")
def get_cache_status(self) -> Dict:
"""获取缓存状态信息
Returns:
Dict: 包含缓存统计信息的字典
"""
if not self.enable_cache:
return {"enabled": False, "cache_count": 0}
# 清理过期缓存
self._cleanup_expired_cache()
total_count = len(self.tool_cache)
ttl_distribution = {}
for cache_item in self.tool_cache.values():
ttl = cache_item["ttl"]
ttl_distribution[ttl] = ttl_distribution.get(ttl, 0) + 1
return {
"enabled": True,
"cache_count": total_count,
"cache_ttl": self.cache_ttl,
"ttl_distribution": ttl_distribution,
}
def set_cache_config(self, enable_cache: bool = None, cache_ttl: int = None):
"""动态修改缓存配置
Args:
enable_cache: 是否启用缓存
cache_ttl: 缓存TTL
"""
if enable_cache is not None:
self.enable_cache = enable_cache
logger.info(f"{self.log_prefix}缓存状态修改为: {'启用' if enable_cache else '禁用'}")
if cache_ttl is not None and cache_ttl > 0:
self.cache_ttl = cache_ttl
logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}")
# 初始化提示词
init_tool_executor_prompt()
"""
使用示例:
# 1. 基础使用 - 从聊天消息执行工具启用缓存默认TTL=3
executor = ToolExecutor(executor_id="my_executor")
results = await executor.execute_from_chat_message(
talking_message_str="今天天气怎么样?现在几点了?",
is_group_chat=False
)
# 2. 禁用缓存的执行器
no_cache_executor = ToolExecutor(executor_id="no_cache", enable_cache=False)
# 3. 自定义缓存TTL
long_cache_executor = ToolExecutor(executor_id="long_cache", cache_ttl=10)
# 4. 获取详细信息
results, used_tools, prompt = await executor.execute_from_chat_message(
talking_message_str="帮我查询Python相关知识",
is_group_chat=False,
return_details=True
)
# 5. 直接执行特定工具
result = await executor.execute_specific_tool(
tool_name="get_knowledge",
tool_args={"query": "机器学习"}
)
# 6. 缓存管理
available_tools = executor.get_available_tools()
cache_status = executor.get_cache_status() # 查看缓存状态
executor.clear_cache() # 清空缓存
executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置
"""

View File

@@ -1,5 +1,5 @@
[inner]
version = "2.28.0"
version = "3.0.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更
@@ -44,7 +44,7 @@ compress_indentity = true # 是否压缩身份,压缩后会精简身份信息
[expression]
# 表达方式
expression_style = "描述麦麦说话的表达风格,表达习惯,例如:(回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。不要有额外的符号,尽量简单简短)"
expression_style = "描述麦麦说话的表达风格,表达习惯,例如:(回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景。)"
enable_expression_learning = false # 是否启用表达学习,麦麦会学习不同群里人类说话风格(群之间不互通)
learning_interval = 600 # 学习间隔 单位秒
@@ -64,6 +64,10 @@ chat_mode = "normal" # 聊天模式 —— 普通模式normal专注模式
# chat_mode = "focus"
# chat_mode = "auto"
max_context_size = 18 # 上下文长度
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
talk_frequency = 1 # 麦麦回复频率,越高,麦麦回复越频繁
time_based_talk_frequency = ["8:00,1", "12:00,1.5", "18:00,2", "01:00,0.5"]
@@ -111,8 +115,7 @@ ban_msgs_regex = [
[normal_chat] #普通聊天
#一般回复参数
normal_chat_first_probability = 0.5 # 麦麦回答时选择首要模型的概率与之相对的次要模型的概率为1 - normal_chat_first_probability
max_context_size = 15 #上下文长度
replyer_random_probability = 0.5 # 麦麦回答时选择首要模型的概率与之相对的次要模型的概率为1 - replyer_random_probability
emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率设置为1让麦麦自己决定发不发
thinking_timeout = 120 # 麦麦最长思考时间超过这个时间的思考会放弃往往是api反应太慢
@@ -124,22 +127,19 @@ emoji_response_penalty = 0 # 对其他人发的表情包回复惩罚系数,设
mentioned_bot_inevitable_reply = true # 提及 bot 必然回复
at_bot_inevitable_reply = true # @bot 必然回复(包含提及)
enable_planner = false # 是否启用动作规划器(实验性功能,与focus_chat共享actions
enable_planner = false # 是否启用动作规划器与focus_chat共享actions
[focus_chat] #专注聊天
think_interval = 3 # 思考间隔 单位秒,可以有效减少消耗
consecutive_replies = 1 # 连续回复能力,值越高,麦麦连续回复的概率越高
processor_max_time = 20 # 处理器最大时间,单位秒,如果超过这个时间,处理器会自动停止
observation_context_size = 20 # 观察到的最长上下文大小
compressed_length = 8 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit = 4 #最多压缩份数,超过该数值的压缩上下文会被删除
[focus_chat_processor] # 专注聊天处理器打开可以实现更多功能但是会增加token消耗
person_impression_processor = true # 是否启用关系识别处理器
tool_use_processor = false # 是否启用工具使用处理器
working_memory_processor = false # 是否启用工作记忆处理器,消耗量大
expression_selector_processor = true # 是否启用表达方式选择处理器
[tool]
enable_in_normal_chat = false # 是否在普通聊天中启用工具
enable_in_focus_chat = true # 是否在专注聊天中启用工具
[emoji]
max_reg_num = 60 # 表情包最大注册数量
@@ -169,6 +169,7 @@ consolidation_check_percentage = 0.05 # 检查节点比例
memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ]
[mood] # 仅在 普通聊天 有效
enable_mood = false # 是否启用情绪系统
mood_update_interval = 1.0 # 情绪更新间隔 单位秒
mood_decay_rate = 0.95 # 情绪衰减率
mood_intensity_factor = 1.0 # 情绪强度因子
@@ -270,7 +271,7 @@ pri_out = 8 #模型的输出价格(非必填,可以记录消耗)
#默认temp 0.2 如果你使用的是老V3或者其他模型请自己修改temp参数
temp = 0.2 #模型的温度新V3建议0.1-0.3
[model.replyer_2] # 一般聊天模式的次要回复模型
[model.replyer_2] # 次要回复模型
name = "Pro/deepseek-ai/DeepSeek-R1"
provider = "SILICONFLOW"
pri_in = 4.0 #模型的输入价格(非必填,可以记录消耗)
@@ -307,6 +308,13 @@ pri_out = 2.8
temp = 0.7
enable_thinking = false # 是否启用思考
[model.tool_use] #工具调用模型,需要使用支持工具调用的模型
name = "Qwen/Qwen3-14B"
provider = "SILICONFLOW"
pri_in = 0.5
pri_out = 2
temp = 0.7
enable_thinking = false # 是否启用思考qwen3 only
#嵌入模型
[model.embedding]
@@ -326,15 +334,6 @@ pri_out = 2.8
temp = 0.7
[model.focus_tool_use] #工具调用模型,需要使用支持工具调用的模型
name = "Qwen/Qwen3-14B"
provider = "SILICONFLOW"
pri_in = 0.5
pri_out = 2
temp = 0.7
enable_thinking = false # 是否启用思考qwen3 only
#------------LPMM知识库模型------------
[model.lpmm_entity_extract] # 实体提取模型