Merge branch 'MaiM-with-u:dev' into dev

This commit is contained in:
A0000Xz
2025-06-28 20:41:34 +08:00
committed by GitHub
11 changed files with 234 additions and 465 deletions

View File

@@ -1,126 +1,75 @@
# Changelog
## [0.8.0] - 2025-1-6
重大升级插件系统全面重构表达方式系统大幅优化focus大幅降低token花费和反应速度加入了人物印象系统麦麦可以记住群友的特点。支持更精细的分群聊天频率控制normal模式可以使用planner和action
## [0.8.0] - 2025-6-27
MaiBot 0.8.0 重磅升级插件系统全面重构支持更强大的扩展能力表达方式系统大幅优化支持智能学习和衰减机制聊天频率控制更加精细支持时段化管理HFC系统性能大幅提升处理器后置化减少消耗关系系统升级支持即时构建和人物侧写日志系统重构使用structlog大量稳定性修复和性能优化。
MaiBot 0.8.0 现已推出!
## 🚀 主要更新点
### **主要升级点:**
1.插件系统正式加入现已上线插件商店同时支持normal和focus
2.大幅降低了token消耗更省钱
3.加入人物印象系统,麦麦可以对群友有不同的印象
4.可以精细化控制不同时段和不同群聊的发言频率
#### 其他升级
日志系统重构使用structlog
大量稳定性修复和性能优化。
MMC启动速度加快
### 🔌 插系统正式推出
**全面重构的插件生态系统,支持强大 的扩展能力**
### 🔌 插件系统正式推出
**全面重构的插件生态系统,支持强大的扩展能力**
- **插件API重构**: 全面重构插件系统,统一加载机制,区分内部插件和外部插件
- **插件仓库**:现可以分享和下载插件
- **依赖管理**: 新增插件依赖管理系统,支持自动注册和依赖检查
- **命令支持**: 插件现已支持命令(command)功能,提供更丰富的交互方式
- **示例插件升级**: 更新禁言插件、豆包绘图插件、TTS插件等示例插件
- **配置文件管理**: 插件支持自动生成和管理配置文件,支持版本自动更新
- **文档完善**: 补全插件API文档提供详细的开发指南
- **Action选择优化**: 大大优化action的选择能力提升动作执行的智能性
### ⚡ Focus模式大幅优化 - 降低Token消耗与提升速度
**HFC系统性能革命性提升大幅降低成本和提升响应速度**
- **Planner架构更新**: 更新planner架构大大加快速度和表现效果
- **处理器重构**:
- 移除旧回复意愿控制系统
- 精简处理器上下文,减少不必要的处理
- 后置工具处理器大大减少token消耗
- 合并自我处理器和关系处理器,提高效率
- 可关闭思考处理器(建议默认关闭)
- **统计系统**: 提供focus统计功能可查看详细的no_reply统计信息
- **异步优化**: 将统计和person_info改为异步提升整体性能
### 👥 人物印象系统
**麦麦现在能认得群友,记住每个人的特点**
- **人物侧写功能**: 加入了人物侧写!麦麦现在能认得群友,新增用户侧写功能,将印象拆分为多方面特点
- **即时构建**: 重构关系构建逻辑,改为即时构建,提高实时性
- **关系处理器**: 新增专门的关系处理器,支持更精准的关系管理
- **熟悉度系统**: 添加熟悉度和关系值功能,提供更丰富的关系信息
### ⚡ Focus模式大幅优化 - 降低Token消耗与提升速度
- **Planner架构更新**: 更新planner架构大大加快速度和表现效果
- **处理器重构**:
- 移除冗余处理器
- 精简处理器上下文,减少不必要的处理
- 后置工具处理器大大减少token消耗
- **统计系统**: 提供focus统计功能可查看详细的no_reply统计信息
### ⏰ 聊天频率精细控制
**支持时段化的精细频率管理,让麦麦在合适的时间说合适的话**
- **时段化控制**: 添加时段talk_frequency控制支持不同时间段的精细频率管理
- **时段化控制**: 添加时段talk_frequency控制支持不同时间段不同群聊的精细频率管理
- **严格频率控制**: 实现更加严格和可靠的频率控制机制
- **Normal模式优化**: 大幅优化normal模式的频率控制逻辑提升回复的智能性
### 🎭 表达方式系统大幅优化
**智能学习群友聊天风格,让麦麦的表达更加多样化**
- **智能学习机制**: 优化表达方式学习算法,支持衰减机制,太久没学的会被自动抛弃
- **表达方式选择**: 新增表达方式选择处理器,大幅提升表达的多样性
- **表达方式选择**: 新增表达方式选择器,让表达使用更合理
- **跨群互通配置**: 表达方式现在可以选择在不同群互通或独立
- **可视化工具**: 提供表达方式可视化脚本和检查脚本
- **人格表达优化**: 修复人格表达生成太固定的问题,增加表达方式的多样性
### 💾 记忆系统改进
**更快的记忆处理和更好的短期记忆管理**
- **海马体优化**: 大大优化海马体同步速度,提升记忆处理效率
- **记忆同步优化**: 优化记忆同步算法修复记忆构建缺少chat_id的问题
- **工作记忆升级**: 精简升级工作记忆模块,提供更好的短期记忆管理
- **聊天记录构建**: 优化聊天记录构建方式,提升记忆提取效率
### 📊 日志系统重构
**使用structlog提供更好的结构化日志**
- **structlog替换**: 使用structlog替代loguru提供更好的结构化日志
- **日志查看器**: 新增更强大的日志查看,支持更好的日志浏览
- **日志查看器**: 新增日志查看脚本,支持更好的日志浏览
- **可配置日志**: 提供可配置的日志级别和格式,支持不同环境的需求
## 🔧 优化修复点
### 🛠️ 开发体验提升
- **API系统扩展**:
- **Chat Stream API**: 提供获取Chat_stream的API接口
- **统计API**: 支持统计数据输出API
- **插件API扩展**: 新增多个插件相关的API接口
- **API重构**: 重构插件API架构提供更完善的接口体系
- **Docker部署优化**:
- **配置更新**: 更新docker-compose配置优化端口映射和服务配置
- **部署文档**: 优化Docker构建流程和部署文档
### 🐛 重要问题修复
#### 稳定性修复
- **模式切换**: 修复无法退出专注模式的问题优化auto模式切换逻辑
- **循环异常**: 修复循环异常思索问题,提高系统稳定性
- **插件重载**: 解决插件重复加载问题,增加重复加载警告机制
- **SQL问题**: 修复数据库相关的SQL问题和索引创建失败问题
- **性能优化**: 修复日志占用和文件层级问题解决too many open files问题
#### 功能修复
- **display_name问题**: 修复base_command和base_action中未使用display_name的错误
- **消息截断**: 修复被指令截断的消息无法保存到prompt的问题
- **统计数据**: 修复统计相关的多个问题
- **图片处理**: 优化图片展示形式,修复图片发送相关问题
- **群名称**: 修复群名称导致log保存失败的问题
- **Focus首条消息**: 修复focus吞掉首条消息的问题
- **关键词功能**: 修复关键词功能并且在focus中可用
- **表情包Action**: 修复了表情包action相关问题
- **Focus时间信息**: 修复focus没有时间信息的问题
### ⚙️ 配置与性能优化
- **配置系统增强**:
- **新配置项**: 添加大量新的配置项,支持更细粒度的控制
- **版本管理**: 支持插件配置文件版本自动更新
- **模板更新**: 更新bot_config_template.toml配置文件模板
- **关闭选项**: 可关闭记忆和关系系统、消息后处理等功能
- **性能优化**:
- **Token优化**: 移除部分不必要的token限制优化token消耗
- **异步改进**: 人格表达和remote请求改为异步不阻塞主线程
- **内存优化**: 优化工作记忆处理器性能和内存使用
- **Normal动作执行**: 为normal_chat加入动作执行能力
### 📚 文档与工具
- **文档完善**:
- **插件开发指南**: 提供详细的插件系统开发文档
- **API文档**: 补全插件API文档和使用说明
- **部署指南**: 优化部署相关文档的完整性
- **辅助工具**:
- **表达方式工具**: 提供表达方式检查脚本和可视化工具
- **插件管理工具**: 新增插件和清单管理工具
- **关系构建脚本**: 提供关系构建和回溯脚本
### 🎯 其他改进
- **emoji系统**: 移除emoji默认发送模式优化表情包审查功能
- **控制台发送**: 添加不完善的控制台发送功能
- **行为准则**: 添加贡献者契约行为准则
- **代码质量**: 使用ruff进行代码格式化和质量检查
- **图像清理**: 自动清理images文件夹优化存储空间使用

View File

@@ -15,11 +15,11 @@ logger = get_logger("expression_selector")
def init_prompt():
expression_evaluation_prompt = """
你的名字是{bot_name}
以下是正在进行的聊天内容:
{chat_observe_info}
你的名字是{bot_name}{target_message}
以下是可选的表达情境:
{all_situations}
@@ -28,6 +28,7 @@ def init_prompt():
1. 聊天的情绪氛围(轻松、严肃、幽默等)
2. 话题类型(日常、技术、游戏、情感等)
3. 情境与当前语境的匹配度
{target_message_extra_block}
请以JSON格式输出只需要输出选中的情境编号
例如:
@@ -156,7 +157,7 @@ class ExpressionSelector:
new_count = min(current_count + increment, 5.0)
expr_in_map["count"] = new_count
expr_in_map["last_active_time"] = time.time()
logger.info(
logger.debug(
f"表达方式激活: 原count={current_count:.3f}, 增量={increment}, 新count={new_count:.3f} in {file_path}"
)
@@ -168,7 +169,7 @@ class ExpressionSelector:
logger.error(f"批量更新表达方式count失败 for {file_path}: {e}")
async def select_suitable_expressions_llm(
self, chat_id: str, chat_info: str, max_num: int = 10, min_num: int = 5
self, chat_id: str, chat_info: str, max_num: int = 10, min_num: int = 5, target_message: str = None
) -> List[Dict[str, str]]:
"""使用LLM选择适合的表达方式"""
@@ -209,6 +210,13 @@ class ExpressionSelector:
all_situations_str = "\n".join(all_situations)
if target_message:
target_message_str = f",现在你想要回复消息:{target_message}"
target_message_extra_block = "4.考虑你要回复的目标消息"
else:
target_message_str = ""
target_message_extra_block = ""
# 3. 构建prompt只包含情境不包含完整的表达方式
prompt = (await global_prompt_manager.get_prompt_async("expression_evaluation_prompt")).format(
bot_name=global_config.bot.nickname,
@@ -216,8 +224,12 @@ class ExpressionSelector:
all_situations=all_situations_str,
min_num=min_num,
max_num=max_num,
target_message=target_message_str,
target_message_extra_block=target_message_extra_block,
)
# print(prompt)
# 4. 调用LLM
try:
content, (_, _) = await self.llm_model.generate_response_async(prompt=prompt)

View File

@@ -413,7 +413,6 @@ class HeartFChatting:
"action_result": {
"action_type": "error",
"action_data": {},
"reasoning": f"上下文处理失败: {e}",
},
"observed_messages": "",
},
@@ -650,143 +649,8 @@ class HeartFChatting:
return all_plan_info, processor_time_costs
async def _process_post_planning_processors(self, observations: List[Observation], action_data: dict) -> dict:
"""
处理后期处理器(规划后执行的处理器)
包括:关系处理器、表达选择器、记忆激活器
参数:
observations: 观察器列表
action_data: 原始动作数据
返回:
dict: 更新后的动作数据
"""
logger.info(f"{self.log_prefix} 开始执行后期处理器")
# 创建所有后期任务
task_list = []
task_to_name_map = {}
# 添加后期处理器任务
for processor in self.post_planning_processors:
processor_name = processor.__class__.__name__
async def run_processor_with_timeout(proc=processor):
return await asyncio.wait_for(
proc.process_info(observations=observations),
timeout=global_config.focus_chat.processor_max_time,
)
task = asyncio.create_task(run_processor_with_timeout())
task_list.append(task)
task_to_name_map[task] = ("processor", processor_name)
logger.info(f"{self.log_prefix} 启动后期处理器任务: {processor_name}")
# 添加记忆激活器任务
async def run_memory_with_timeout():
return await asyncio.wait_for(
self.memory_activator.activate_memory(observations),
timeout=MEMORY_ACTIVATION_TIMEOUT,
)
memory_task = asyncio.create_task(run_memory_with_timeout())
task_list.append(memory_task)
task_to_name_map[memory_task] = ("memory", "MemoryActivator")
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:
if task_type == "processor":
logger.warning(
f"{self.log_prefix} 后期处理器 {task_name} 超时(>{global_config.focus_chat.processor_max_time}s已跳过"
)
elif task_type == "memory":
logger.warning(f"{self.log_prefix} 记忆激活器超时(>{MEMORY_ACTIVATION_TIMEOUT}s已跳过")
running_memorys = []
except Exception as e:
if task_type == "processor":
logger.error(
f"{self.log_prefix} 后期处理器 {task_name} 执行失败. 错误: {e}",
exc_info=True,
)
elif task_type == "memory":
logger.error(f"{self.log_prefix} 记忆激活器执行失败. 错误: {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)} 个记忆"
)
return updated_action_data
async def _process_post_planning_processors_with_timing(
self, observations: List[Observation], action_data: dict
self, observations: List[Observation], action_type: str, action_data: dict
) -> tuple[dict, dict]:
"""
处理后期处理器(规划后执行的处理器)并收集详细时间统计
@@ -794,6 +658,7 @@ class HeartFChatting:
参数:
observations: 观察器列表
action_type: 动作类型
action_data: 原始动作数据
返回:
@@ -815,7 +680,7 @@ class HeartFChatting:
start_time = time.time()
try:
result = await asyncio.wait_for(
proc.process_info(observations=observations),
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()
@@ -1074,7 +939,7 @@ class HeartFChatting:
# 记录详细的后处理器时间
post_start_time = time.time()
action_data, post_processor_time_costs = await self._process_post_planning_processors_with_timing(
self.observations, action_data
self.observations, action_type, action_data
)
post_end_time = time.time()
logger.info(f"{self.log_prefix} 后期处理器总耗时: {post_end_time - post_start_time:.3f}")

View File

@@ -181,6 +181,14 @@ class HeartFCMessageReceiver:
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
# current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
current_talk_frequency = global_config.chat.get_current_talk_frequency(chat.stream_id)
# 如果消息中包含图片标识,则日志展示为图片
import re
picid_match = re.search(r"\[picid:([^\]]+)\]", message.processed_plain_text)
if picid_match:
logger.info(f"[{mes_name}]{userinfo.user_nickname}: [图片] [当前回复频率: {current_talk_frequency}]")
else:
logger.info(
f"[{mes_name}]{userinfo.user_nickname}:{message.processed_plain_text}[当前回复频率: {current_talk_frequency}]"
)

View File

@@ -27,7 +27,13 @@ class ExpressionSelectorProcessor(BaseProcessor):
name = get_chat_manager().get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] 表达选择器"
async def process_info(self, observations: List[Observation] = None, *infos) -> List[InfoBase]:
async def process_info(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
**kwargs,
) -> List[InfoBase]:
"""处理信息对象
Args:
@@ -70,9 +76,14 @@ class ExpressionSelectorProcessor(BaseProcessor):
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
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}个)"

View File

@@ -13,7 +13,6 @@ 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 src.person_info.person_info import PersonInfoManager
from json_repair import repair_json
from src.person_info.person_info import get_person_info_manager
import json
@@ -26,7 +25,6 @@ from src.chat.utils.chat_message_builder import (
)
import os
import pickle
import random
# 消息段清理配置
@@ -37,139 +35,51 @@ SEGMENT_CLEANUP_CONFIG = {
"cleanup_interval_hours": 1, # 清理间隔(小时)
}
# 用于随机生成prompt示例的资源池
USER_EXAMPLE_KEYS = ["用户A", "小明", "Alice", "陈皮", "老王", "Bob", "张三", "李四"]
USER_EXAMPLE_VALUES = [
"ta的昵称",
"ta对你的态度",
"你对ta的印象",
"ta最近心情如何",
"你们的关系",
"ta的身份",
"ta的兴趣爱好",
"ta和你的共同点",
"ta的习惯",
"你们最近做的事",
"你对ta的语气",
"你们的互动方式",
"给你的第一印象",
"你们最近聊过什么",
]
BOT_EXAMPLE_VALUES = [
"身份",
"性格",
"你的原则",
"你的知识",
"你的目标",
"你的爱好",
"你最近在做什么",
"头像",
"年龄",
"性别",
"职业",
"兴趣爱好",
"习惯",
"目标",
"原则",
"知识",
"爱好",
]
logger = get_logger("processor")
def _generate_random_prompt_example() -> str:
"""动态生成一个随机的、符合规则的JSON示例字符串"""
bot_nickname = global_config.bot.nickname
bot_aliases = list(global_config.bot.alias_names)
# 确定示例数量
num_user_examples = random.randint(1, 2)
num_bot_examples = random.randint(1, 2)
example_dict = {}
# 1. 生成用户提取示例
user_keys = random.sample(USER_EXAMPLE_KEYS, min(num_user_examples, len(USER_EXAMPLE_KEYS)))
user_values = random.sample(USER_EXAMPLE_VALUES, min(num_user_examples, len(USER_EXAMPLE_VALUES)))
for i in range(len(user_keys)):
example_dict[user_keys[i]] = user_values[i]
# 2. 生成bot自身示例 (使用昵称和别名避免key重复)
bot_name_pool = [bot_nickname] + bot_aliases
random.shuffle(bot_name_pool)
bot_values = random.sample(BOT_EXAMPLE_VALUES, min(num_bot_examples, len(BOT_EXAMPLE_VALUES)))
for i in range(min(num_bot_examples, len(bot_name_pool), len(bot_values))):
example_dict[bot_name_pool[i]] = bot_values[i]
# 3. 添加固定示例
example_dict["person_name"] = "其他信息"
# 随机化顺序并格式化为JSON字符串
items = list(example_dict.items())
random.shuffle(items)
shuffled_dict = dict(items)
return json.dumps(shuffled_dict, ensure_ascii=False, indent=4)
def init_prompt():
relationship_prompt = """
<聊天记录>
{chat_observe_info}
</聊天记录>
{info_cache_block}
请不要重复调取相同的信息
{name_block}
请你阅读聊天记录,查看是否需要调取某个人的信息,这个人可以是出现在聊天记录中的,也可以是记录中提到的人,也可以是你自己({bot_name})。
你不同程度上认识群聊里的人,以及他们谈论到的人,你可以根据聊天记录,回忆起有关他们的信息,帮助你参与聊天
1.你需要提供用户名和你想要提取的信息名称类型来进行调取
2.请注意,提取的信息类型一定要和用户有关,不要提取无关的信息
3.你也可以调取有关自己({bot_name})的信息
4.如果当前聊天记录中没有需要查询的信息,或者现有信息已经足够回复,请返回{{"none": "不需要查询"}}
现在,你想要回复{person_name}的消息,消息内容是:{target_message}。请根据聊天记录和你要回复的消息,从你对{person_name}的了解中提取有关的信息:
1.你需要提供你想要提取的信息具体是哪方面的信息例如年龄性别对ta的印象最近发生的事等等。
2.请注意,请不要重复调取相同的信息,已经调取的信息如下:
{info_cache_block}
3.如果当前聊天记录中没有需要查询的信息,或者现有信息已经足够回复,请返回{{"none": "不需要查询"}}
请以json格式输出例如
{example_json}
如果不需要查询任何信息,请输出:
{{"none": "不需要查询"}}
请严格按照json输出格式不要输出多余内容可以同时查询多个人的信息
{{
"info_type": "信息类型",
}}
请严格按照json输出格式不要输出多余内容
"""
Prompt(relationship_prompt, "relationship_prompt")
fetch_info_prompt = """
{name_block}
以下是你在之前与{person_name}的交流中,产生的对{person_name}的了解,请你从中提取用户的有关"{info_type}"的信息如果用户没有相关信息请输出none
以下是你在之前与{person_name}的交流中,产生的对{person_name}的了解:
{person_impression_block}
{points_text_block}
请严格按照以下json输出格式不要输出多余内容
请从中提取用户"{person_name}"的有关"{info_type}"信息
请以json格式输出例如
{{
{info_json_str}
}}
请严格按照json输出格式不要输出多余内容
"""
Prompt(fetch_info_prompt, "fetch_person_info_prompt")
fetch_bot_info_prompt = """
你是{nickname},你的昵称有{alias_names}
以下是你对自己的了解,请你从中提取和"{info_type}"有关的信息如果无法提取请输出none
{person_impression_block}
{points_text_block}
请严格按照以下json输出格式不要输出多余内容
{{
"{info_type}": "有关你自己的{info_type}的信息内容"
}}
"""
Prompt(fetch_bot_info_prompt, "fetch_bot_info_prompt")
class PersonImpressionpProcessor(BaseProcessor):
log_prefix = "关系"
@@ -293,7 +203,7 @@ class PersonImpressionpProcessor(BaseProcessor):
}
segments.append(new_segment)
person_name = get_person_info_manager().get_value(person_id, "person_name") or person_id
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']} 条消息"
)
@@ -341,7 +251,8 @@ class PersonImpressionpProcessor(BaseProcessor):
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
person_name = get_person_info_manager().get_value(person_id, "person_name") or person_id
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()
@@ -515,16 +426,26 @@ class PersonImpressionpProcessor(BaseProcessor):
# 统筹各模块协作、对外提供服务接口
# ================================
async def process_info(self, observations: List[Observation] = None, *infos) -> List[InfoBase]:
async def process_info(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
**kwargs,
) -> List[InfoBase]:
"""处理信息对象
Args:
*infos: 可变数量的InfoBase类型的信息对象
observations: 观察对象列表
action_type: 动作类型
action_data: 动作数据
Returns:
List[InfoBase]: 处理后的结构化信息列表
"""
relation_info_str = await self.relation_identify(observations)
await self.build_relation(observations)
relation_info_str = await self.relation_identify(observations, action_type, action_data)
if relation_info_str:
relation_info = RelationInfo()
@@ -535,28 +456,14 @@ class PersonImpressionpProcessor(BaseProcessor):
return [relation_info]
async def relation_identify(
self,
observations: List[Observation] = None,
):
"""
在回复前进行思考,生成内心想法并收集工具调用结果
"""
# 0. 执行定期清理
async def build_relation(self, observations: List[Observation] = None):
"""构建关系"""
self._cleanup_old_segments()
# 1. 从观察信息中提取所需数据
# 需要兼容私聊
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,
@@ -610,6 +517,54 @@ class PersonImpressionpProcessor(BaseProcessor):
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
@@ -618,7 +573,33 @@ class PersonImpressionpProcessor(BaseProcessor):
if not self.info_fetched_cache[person_id]:
del self.info_fetched_cache[person_id]
# 5. 为需要处理的人员准备LLM prompt
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},有人也会用这些昵称称呼你。"
@@ -638,19 +619,16 @@ class PersonImpressionpProcessor(BaseProcessor):
f"你已经调取了[{info_fetching['person_name']}]的[{info_fetching['info_type']}]信息\n"
)
example_json = _generate_random_prompt_example()
prompt = (await global_prompt_manager.get_prompt_async("relationship_prompt")).format(
name_block=name_block,
bot_name=global_config.bot.nickname,
time_now=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
chat_observe_info=chat_observe_info,
name_block=name_block,
info_cache_block=info_cache_block,
example_json=example_json,
person_name=sender,
target_message=text,
)
try:
logger.debug(f"{self.log_prefix} 人物信息prompt: \n{prompt}\n")
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}")
@@ -661,29 +639,12 @@ class PersonImpressionpProcessor(BaseProcessor):
logger.info(f"{self.log_prefix} LLM判断当前不需要查询任何信息{content_json.get('none', '')}")
# 跳过新的信息提取,但仍会处理已有缓存
else:
# 收集即时提取任务
instant_tasks = []
async_tasks = []
person_info_manager = get_person_info_manager()
for person_name, info_type in content_json.items():
is_bot = (
person_name == global_config.bot.nickname or person_name in global_config.bot.alias_names
)
if is_bot:
person_id = person_info_manager.get_person_id("system", "bot_id")
logger.info(f"{self.log_prefix} 检测到对bot自身({person_name})的信息查询使用特殊ID。")
else:
person_id = person_info_manager.get_person_id_by_person_name(person_name)
if not person_id:
logger.warning(f"{self.log_prefix} 未找到用户 {person_name} 的ID跳过调取信息。")
continue
info_type = content_json.get("info_type")
if info_type:
self.info_fetching_cache.append(
{
"person_id": person_id,
"person_name": person_name,
"person_name": sender,
"info_type": info_type,
"start_time": time.time(),
"forget": False,
@@ -692,22 +653,12 @@ class PersonImpressionpProcessor(BaseProcessor):
if len(self.info_fetching_cache) > 20:
self.info_fetching_cache.pop(0)
logger.info(f"{self.log_prefix} 调取用户 {person_name} {info_type} 信息。")
# 收集即时提取任务
instant_tasks.append((person_id, info_type, time.time()))
# 执行即时提取任务
if instant_tasks:
await self._execute_instant_extraction_batch(instant_tasks)
# 启动异步任务(如果不是即时模式)
if async_tasks:
# 异步任务不需要等待完成
pass
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返回空结果,关系识别失败。")
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}")
@@ -838,31 +789,6 @@ class PersonImpressionpProcessor(BaseProcessor):
# 负责实时分析对话需求、提取用户信息、管理信息缓存
# ================================
async def _execute_instant_extraction_batch(self, instant_tasks: list):
"""
批量执行即时提取任务
"""
if not instant_tasks:
return
logger.info(f"{self.log_prefix} [即时提取] 开始批量提取 {len(instant_tasks)} 个信息")
# 创建所有提取任务
extraction_tasks = []
for person_id, info_type, start_time in instant_tasks:
# 检查缓存中是否已存在且未过期的信息
if person_id in self.info_fetched_cache and info_type in self.info_fetched_cache[person_id]:
logger.debug(f"{self.log_prefix} 用户 {person_id}{info_type} 信息已存在且未过期,跳过调取。")
continue
task = asyncio.create_task(self._fetch_single_info_instant(person_id, info_type, start_time))
extraction_tasks.append(task)
# 并行执行所有提取任务并等待完成
if extraction_tasks:
await asyncio.gather(*extraction_tasks, return_exceptions=True)
logger.info(f"{self.log_prefix} [即时提取] 批量提取完成")
async def _fetch_single_info_instant(self, person_id: str, info_type: str, start_time: float):
"""
使用小模型提取单个信息类型
@@ -890,7 +816,7 @@ class PersonImpressionpProcessor(BaseProcessor):
self.info_fetched_cache[person_id][info_type] = {
"info": cached_info,
"ttl": 4,
"ttl": 2,
"start_time": start_time,
"person_name": person_name,
"unknow": cached_info == "none",
@@ -898,9 +824,6 @@ class PersonImpressionpProcessor(BaseProcessor):
logger.info(f"{self.log_prefix} 记得 {person_name}{info_type}: {cached_info}")
return
bot_person_id = PersonInfoManager.get_person_id("system", "bot_id")
is_bot = person_id == bot_person_id
try:
person_name = await person_info_manager.get_value(person_id, "person_name")
person_impression = await person_info_manager.get_value(person_id, "impression")
@@ -923,7 +846,7 @@ class PersonImpressionpProcessor(BaseProcessor):
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": "none",
"ttl": 4,
"ttl": 2,
"start_time": start_time,
"person_name": person_name,
"unknow": True,
@@ -932,19 +855,8 @@ class PersonImpressionpProcessor(BaseProcessor):
await self._save_info_to_cache(person_id, info_type, "none")
return
if is_bot:
prompt = (await global_prompt_manager.get_prompt_async("fetch_bot_info_prompt")).format(
nickname=global_config.bot.nickname,
alias_names=",".join(global_config.bot.alias_names),
info_type=info_type,
person_impression_block=person_impression_block,
points_text_block=points_text_block,
)
else:
nickname_str = ",".join(global_config.bot.alias_names)
name_block = (
f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
)
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,
@@ -972,7 +884,7 @@ class PersonImpressionpProcessor(BaseProcessor):
self.info_fetched_cache[person_id] = {}
self.info_fetched_cache[person_id][info_type] = {
"info": "unknow" if is_unknown else info_content,
"ttl": 8,
"ttl": 3,
"start_time": start_time,
"person_name": person_name,
"unknow": is_unknown,

View File

@@ -8,7 +8,7 @@ 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, Optional
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
@@ -47,13 +47,20 @@ class ToolProcessor(BaseProcessor):
)
self.structured_info = []
async def process_info(self, observations: Optional[List[Observation]] = None) -> List[StructuredInfo]:
async def process_info(
self,
observations: List[Observation] = None,
action_type: str = None,
action_data: dict = None,
**kwargs,
) -> List[StructuredInfo]:
"""处理信息对象
Args:
observations: 可选的观察列表包含ChattingObservation和StructureObservation类型
running_memories: 可选的运行时记忆列表,包含字典类型的记忆信息
*infos: 可变数量的InfoBase类型的信息对象
action_type: 动作类型
action_data: 动作数据
**kwargs: 其他可选参数
Returns:
list: 处理后的结构化信息列表
@@ -85,7 +92,7 @@ class ToolProcessor(BaseProcessor):
return [structured_info]
async def execute_tools(self, observation: ChattingObservation):
async def execute_tools(self, observation: ChattingObservation, action_type: str = None, action_data: dict = None):
"""
并行执行工具,返回结构化信息
@@ -95,6 +102,8 @@ class ToolProcessor(BaseProcessor):
is_group_chat: 是否为群聊默认为False
return_details: 是否返回详细信息默认为False
cycle_info: 循环信息对象,可用于记录详细执行信息
action_type: 动作类型
action_data: 动作数据
返回:
如果return_details为False:

View File

@@ -470,7 +470,7 @@ class ActionModifier:
response = response.strip().lower()
# print(base_prompt)
print(f"LLM判定动作 {action_name}:响应='{response}'")
# print(f"LLM判定动作 {action_name}:响应='{response}'")
should_activate = "" in response or "yes" in response or "true" in response

View File

@@ -183,7 +183,7 @@ class NormalChat:
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
logger.info(
logger.debug(
f"[{self.stream_name}] 为用户 {person_id} 创建新消息段: 时间范围 {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()
@@ -230,7 +230,7 @@ class NormalChat:
"message_count": self._count_messages_in_timerange(potential_start_time, message_time),
}
segments.append(new_segment)
logger.info(f"[{self.stream_name}] 为用户 {person_id} 创建新消息段超过10条消息间隔: {new_segment}")
logger.debug(f"[{self.stream_name}] 为用户 {person_id} 创建新消息段超过10条消息间隔: {new_segment}")
self._save_cache()

View File

@@ -53,7 +53,9 @@ async def generate_with_model(
Tuple[bool, str, str, str]: (是否成功, 生成的内容, 推理过程, 模型名称)
"""
try:
logger.info(f"[LLMAPI] 使用模型生成内容,提示词: {prompt[:200]}...")
model_name = model_config.get("name")
logger.info(f"[LLMAPI] 使用模型 {model_name} 生成内容")
logger.debug(f"[LLMAPI] 完整提示词: {prompt}")
llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs)

View File

@@ -343,14 +343,15 @@ class NoReplyAction(BaseAction):
if success and response:
response = response.strip()
logger.info(f"{self.log_prefix} 模型({model_name})原始JSON响应: {response}")
logger.debug(f"{self.log_prefix} 模型({model_name})原始JSON响应: {response}")
# 解析LLM的JSON响应提取判断结果和理由
judge_result, reason = self._parse_llm_judge_response(response)
logger.info(
f"{self.log_prefix} JSON解析结果 - 判断: {judge_result}, 理由: {reason}"
)
if judge_result:
logger.info(f"{self.log_prefix} 决定继续参与讨论,结束等待,原因: {reason}")
else:
logger.info(f"{self.log_prefix} 决定不参与讨论,继续等待,原因: {reason}")
# 将判断结果保存到历史中
judge_history.append((current_time, judge_result, reason))