diff --git a/CORRECTED_ARCHITECTURE.md b/CORRECTED_ARCHITECTURE.md index 5a4cbf89b..ca522383b 100644 --- a/CORRECTED_ARCHITECTURE.md +++ b/CORRECTED_ARCHITECTURE.md @@ -39,7 +39,7 @@ async def modify_actions_task(): **处理内容:** - 传统观察处理(循环历史分析、类型匹配等) -- 激活类型判定(ALWAYS, RANDOM, LLM_JUDGE, KEYWORD) +- 双激活类型判定(Focus模式和Normal模式分别处理) - 并行LLM判定 - 智能缓存 - 动态关键词收集 @@ -94,41 +94,123 @@ for action_name, action_info in llm_judge_actions.items(): # 检查消息中的关键词匹配 ``` +## 双激活类型系统 🆕 + +### 系统设计理念 +**Focus模式** 和 **Normal模式** 采用不同的激活策略: +- **Focus模式**: 智能化优先,支持复杂的LLM判定 +- **Normal模式**: 性能优先,使用快速的关键词和随机触发 + +### 双激活类型配置 +```python +class MyAction(BaseAction): + action_name = "my_action" + action_description = "我的动作" + + # Focus模式激活类型(支持LLM_JUDGE) + focus_activation_type = ActionActivationType.LLM_JUDGE + + # Normal模式激活类型(建议使用KEYWORD/RANDOM/ALWAYS) + normal_activation_type = ActionActivationType.KEYWORD + activation_keywords = ["关键词1", "keyword"] + + # 模式启用控制 + mode_enable = ChatMode.ALL # 在所有模式下启用 + + # 并行执行控制 + parallel_action = False # 是否与回复并行执行 +``` + +### 模式启用类型 (ChatMode) +```python +from src.chat.chat_mode import ChatMode + +# 可选值: +mode_enable = ChatMode.FOCUS # 仅在Focus模式启用 +mode_enable = ChatMode.NORMAL # 仅在Normal模式启用 +mode_enable = ChatMode.ALL # 在所有模式启用(默认) +``` + +### 并行动作系统 🆕 +```python +# 并行动作:可以与回复生成同时进行 +parallel_action = True # 不会阻止回复生成 + +# 串行动作:会替代回复生成 +parallel_action = False # 默认值,传统行为 +``` + +**并行动作的优势:** +- 提升用户体验(同时获得回复和动作执行) +- 减少响应延迟 +- 适用于情感表达、状态变更等辅助性动作 + ## 四种激活类型 ### 1. ALWAYS - 始终激活 ```python -activation_type = ActionActivationType.ALWAYS +focus_activation_type = ActionActivationType.ALWAYS +normal_activation_type = ActionActivationType.ALWAYS # 基础动作,如 reply, no_reply ``` ### 2. RANDOM - 随机激活 ```python -activation_type = ActionActivationType.RANDOM +focus_activation_type = ActionActivationType.RANDOM +normal_activation_type = ActionActivationType.RANDOM random_probability = 0.3 # 激活概率 # 用于增加惊喜元素,如随机表情 ``` ### 3. LLM_JUDGE - 智能判定 ```python -activation_type = ActionActivationType.LLM_JUDGE -llm_judge_prompt = "自定义判定提示词" +focus_activation_type = ActionActivationType.LLM_JUDGE +# 注意:Normal模式不建议使用LLM_JUDGE,会发出警告 +normal_activation_type = ActionActivationType.KEYWORD # 需要理解上下文的复杂动作,如情感表达 ``` ### 4. KEYWORD - 关键词触发 ```python -activation_type = ActionActivationType.KEYWORD +focus_activation_type = ActionActivationType.KEYWORD +normal_activation_type = ActionActivationType.KEYWORD activation_keywords = ["画", "图片", "生成"] # 明确指令触发的动作,如图片生成 ``` +## 推荐配置模式 + +### 模式1:智能自适应 +```python +# Focus模式使用智能判定,Normal模式使用关键词 +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.KEYWORD +activation_keywords = ["相关", "关键词"] +``` + +### 模式2:统一关键词 +```python +# 两个模式都使用关键词,确保一致性 +focus_activation_type = ActionActivationType.KEYWORD +normal_activation_type = ActionActivationType.KEYWORD +activation_keywords = ["画", "图片", "生成"] +``` + +### 模式3:Focus专享 +```python +# 仅在Focus模式启用的智能功能 +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.ALWAYS # 不会生效 +mode_enable = ChatMode.FOCUS +``` + ## 性能提升 ### 理论性能改进 - **并行LLM判定**: 1.5-2x 提升 - **智能缓存**: 20-30% 额外提升 -- **整体预期**: 2-3x 性能提升 +- **双模式优化**: Normal模式额外1.5x提升 +- **整体预期**: 3-5x 性能提升 ### 缓存策略 - **缓存键**: `{action_name}_{context_hash}` @@ -137,19 +219,43 @@ activation_keywords = ["画", "图片", "生成"] ## 向后兼容性 -### 废弃方法处理 +### ⚠️ 重大变更说明 +**旧的 `action_activation_type` 属性已被移除**,必须更新为新的双激活类型系统: + +#### 迁移指南 ```python -async def process_actions_for_planner(...): - """[已废弃] 此方法现在已被整合到 modify_actions() 中""" - logger.warning("process_actions_for_planner() 已废弃") - # 仍然返回结果以保持兼容性 - return current_using_actions +# 旧的配置(已废弃) +class OldAction(BaseAction): + action_activation_type = ActionActivationType.LLM_JUDGE # ❌ 已移除 + +# 新的配置(必须使用) +class NewAction(BaseAction): + focus_activation_type = ActionActivationType.LLM_JUDGE # ✅ Focus模式 + normal_activation_type = ActionActivationType.KEYWORD # ✅ Normal模式 + activation_keywords = ["相关", "关键词"] + mode_enable = ChatMode.ALL + parallel_action = False ``` -### 迁移指南 -1. **主循环**: 使用 `modify_actions(observations, messages, context, extra)` -2. **规划器**: 直接使用 `ActionManager.get_using_actions()` -3. **移除**: 规划器中对 `process_actions_for_planner()` 的调用 +#### 快速迁移脚本 +对于简单的迁移,可以使用以下模式: +```python +# 如果原来是 ALWAYS +focus_activation_type = ActionActivationType.ALWAYS +normal_activation_type = ActionActivationType.ALWAYS + +# 如果原来是 LLM_JUDGE +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.KEYWORD # 需要添加关键词 + +# 如果原来是 KEYWORD +focus_activation_type = ActionActivationType.KEYWORD +normal_activation_type = ActionActivationType.KEYWORD + +# 如果原来是 RANDOM +focus_activation_type = ActionActivationType.RANDOM +normal_activation_type = ActionActivationType.RANDOM +``` ## 测试验证 @@ -159,11 +265,12 @@ python test_corrected_architecture.py ``` ### 测试内容 -- 架构正确性验证 +- 双激活类型系统验证 - 数据一致性检查 - 职责分离确认 - 性能测试 - 向后兼容性验证 +- 并行动作功能验证 ## 优势总结 @@ -175,15 +282,18 @@ python test_corrected_architecture.py ### 2. 高性能 - **并行处理**: 多个LLM判定同时进行 - **智能缓存**: 避免重复计算 +- **双模式优化**: Focus智能化,Normal快速化 ### 3. 智能化 - **动态配置**: 从动作配置中收集关键词 - **上下文感知**: 基于聊天内容智能激活 - **冲突避免**: 防止重复激活 +- **模式自适应**: 根据聊天模式选择最优策略 ### 4. 可扩展性 - **插件式**: 新的激活类型易于添加 - **配置驱动**: 通过配置控制行为 - **模块化**: 各组件独立可测试 +- **双模式支持**: 灵活适应不同使用场景 -这个修正后的架构实现了正确的职责分工,确保了主循环负责动作管理,规划器专注于决策,同时集成了并行判定和智能缓存等优化功能。 \ No newline at end of file +这个修正后的架构实现了正确的职责分工,确保了主循环负责动作管理,规划器专注于决策,同时集成了双激活类型、并行判定和智能缓存等优化功能。 \ No newline at end of file diff --git a/action_activation_system_usage.md b/action_activation_system_usage.md index a3f1c8ad9..cbc6e60b7 100644 --- a/action_activation_system_usage.md +++ b/action_activation_system_usage.md @@ -2,44 +2,80 @@ ## 概述 -MaiBot 的动作激活系统支持四种不同的激活类型,让机器人能够智能地根据上下文选择合适的动作。 +MaiBot 的动作激活系统采用**双激活类型架构**,为Focus模式和Normal模式分别提供最优的激活策略。 -**系统已集成三大优化策略:** +**系统已集成四大核心特性:** +- 🎯 **双激活类型**:Focus模式智能化,Normal模式高性能 - 🚀 **并行判定**:多个LLM判定任务并行执行 - 💾 **智能缓存**:相同上下文的判定结果缓存复用 -- 🔍 **分层判定**:快速过滤 + 精确判定的两层架构 +- ⚡ **并行动作**:支持与回复同时执行的动作 + +## 双激活类型系统 🆕 + +### 系统设计理念 + +**Focus模式**:智能优先 +- 支持复杂的LLM判定 +- 提供精确的上下文理解 +- 适合需要深度分析的场景 + +**Normal模式**:性能优先 +- 使用快速的关键词匹配 +- 采用简单的随机触发 +- 确保快速响应用户 + +### 核心属性配置 + +```python +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType +from src.chat.chat_mode import ChatMode + +@register_action +class MyAction(BaseAction): + action_name = "my_action" + action_description = "我的动作描述" + + # 双激活类型配置 + focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用智能判定 + normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词 + activation_keywords = ["关键词1", "关键词2", "keyword"] + keyword_case_sensitive = False + + # 模式启用控制 + mode_enable = ChatMode.ALL # 支持的聊天模式 + + # 并行执行控制 + parallel_action = False # 是否与回复并行执行 + + # 插件系统控制 + enable_plugin = True # 是否启用此插件 +``` ## 激活类型详解 ### 1. ALWAYS - 总是激活 **用途**:基础必需动作,始终可用 ```python -action_activation_type = ActionActivationType.ALWAYS +focus_activation_type = ActionActivationType.ALWAYS +normal_activation_type = ActionActivationType.ALWAYS ``` **示例**:`reply_action`, `no_reply_action` ### 2. RANDOM - 随机激活 **用途**:增加不可预测性和趣味性 ```python -action_activation_type = ActionActivationType.RANDOM +focus_activation_type = ActionActivationType.RANDOM +normal_activation_type = ActionActivationType.RANDOM random_activation_probability = 0.2 # 20%概率激活 ``` -**示例**:`pic_action` (20%概率) +**示例**:`vtb_action` (表情动作) ### 3. LLM_JUDGE - LLM智能判定 **用途**:需要上下文理解的复杂判定 ```python -action_activation_type = ActionActivationType.LLM_JUDGE -llm_judge_prompt = """ -判定条件: -1. 当前聊天涉及情感表达 -2. 需要生动的情感回应 -3. 场景适合虚拟主播动作 - -不适用场景: -1. 纯信息查询 -2. 技术讨论 -""" +focus_activation_type = ActionActivationType.LLM_JUDGE +# 注意:Normal模式使用LLM_JUDGE会产生性能警告 +normal_activation_type = ActionActivationType.KEYWORD # 推荐在Normal模式使用KEYWORD ``` **优化特性**: - ⚡ **直接判定**:直接进行LLM判定,减少复杂度 @@ -49,11 +85,115 @@ llm_judge_prompt = """ ### 4. KEYWORD - 关键词触发 **用途**:精确命令式触发 ```python -action_activation_type = ActionActivationType.KEYWORD +focus_activation_type = ActionActivationType.KEYWORD +normal_activation_type = ActionActivationType.KEYWORD activation_keywords = ["画", "画图", "生成图片", "draw"] keyword_case_sensitive = False # 不区分大小写 ``` -**示例**:`help_action`, `edge_search_action`, `pic_action` +**示例**:`pic_action`, `mute_action` + +## 模式启用控制 (ChatMode) + +### 模式类型 +```python +from src.chat.chat_mode import ChatMode + +# 在所有模式下启用 +mode_enable = ChatMode.ALL # 默认值 + +# 仅在Focus模式启用 +mode_enable = ChatMode.FOCUS + +# 仅在Normal模式启用 +mode_enable = ChatMode.NORMAL +``` + +### 使用场景建议 +- **ChatMode.ALL**: 通用功能(如回复、图片生成) +- **ChatMode.FOCUS**: 需要深度理解的智能功能 +- **ChatMode.NORMAL**: 快速响应的基础功能 + +## 并行动作系统 🆕 + +### 概念说明 +```python +# 并行动作:与回复生成同时执行 +parallel_action = True # 不会阻止回复,提升用户体验 + +# 串行动作:替代回复生成(传统行为) +parallel_action = False # 默认值,动作执行时不生成回复 +``` + +### 适用场景 +**并行动作 (parallel_action = True)**: +- 情感表达(表情、动作) +- 状态变更(禁言、设置) +- 辅助功能(TTS播报) + +**串行动作 (parallel_action = False)**: +- 内容生成(图片、文档) +- 搜索查询 +- 需要完整注意力的操作 + +### 实际案例 +```python +@register_action +class MuteAction(PluginAction): + action_name = "mute_action" + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.KEYWORD + activation_keywords = ["禁言", "mute", "ban", "silence"] + parallel_action = True # 禁言的同时还可以回复确认信息 + +@register_action +class PicAction(PluginAction): + action_name = "pic_action" + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.KEYWORD + activation_keywords = ["画", "绘制", "生成图片", "画图", "draw", "paint"] + parallel_action = False # 专注于图片生成,不同时回复 +``` + +## 推荐配置模式 + +### 模式1:智能自适应(推荐) +```python +# Focus模式智能判定,Normal模式快速触发 +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.KEYWORD +activation_keywords = ["相关", "关键词", "英文keyword"] +mode_enable = ChatMode.ALL +parallel_action = False # 根据具体需求调整 +``` + +### 模式2:统一关键词 +```python +# 两个模式都使用关键词,确保行为一致 +focus_activation_type = ActionActivationType.KEYWORD +normal_activation_type = ActionActivationType.KEYWORD +activation_keywords = ["画", "图片", "生成"] +mode_enable = ChatMode.ALL +parallel_action = False +``` + +### 模式3:Focus专享功能 +```python +# 仅在Focus模式启用的高级功能 +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.ALWAYS # 不会生效 +mode_enable = ChatMode.FOCUS +parallel_action = False +``` + +### 模式4:随机娱乐功能 +```python +# 增加趣味性的随机功能 +focus_activation_type = ActionActivationType.RANDOM +normal_activation_type = ActionActivationType.RANDOM +random_activation_probability = 0.08 # 8%概率 +mode_enable = ChatMode.ALL +parallel_action = True # 通常与回复并行 +``` ## 性能优化详解 @@ -194,26 +334,22 @@ focus_chat: ```python from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action, ActionActivationType +from src.chat.chat_mode import ChatMode @register_action class MyAction(PluginAction): action_name = "my_action" action_description = "我的自定义动作" - # 选择合适的激活类型 - action_activation_type = ActionActivationType.LLM_JUDGE + # 双激活类型配置 + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.KEYWORD + activation_keywords = ["自定义", "触发", "custom"] - # LLM判定的自定义提示词 - llm_judge_prompt = """ - 判定是否激活my_action的条件: - 1. 用户明确要求执行特定操作 - 2. 当前场景适合此动作 - 3. 没有其他更合适的动作 - - 不应激活的情况: - 1. 普通聊天对话 - 2. 用户只是随便说说 - """ + # 模式和并行控制 + mode_enable = ChatMode.ALL + parallel_action = False + enable_plugin = True async def process(self): # 动作执行逻辑 @@ -225,9 +361,12 @@ class MyAction(PluginAction): @register_action class SearchAction(PluginAction): action_name = "search_action" - action_activation_type = ActionActivationType.KEYWORD + focus_activation_type = ActionActivationType.KEYWORD + normal_activation_type = ActionActivationType.KEYWORD activation_keywords = ["搜索", "查找", "什么是", "search", "find"] keyword_case_sensitive = False + mode_enable = ChatMode.ALL + parallel_action = False ``` ### 随机触发动作 @@ -235,8 +374,51 @@ class SearchAction(PluginAction): @register_action class SurpriseAction(PluginAction): action_name = "surprise_action" - action_activation_type = ActionActivationType.RANDOM + focus_activation_type = ActionActivationType.RANDOM + normal_activation_type = ActionActivationType.RANDOM random_activation_probability = 0.1 # 10%概率 + mode_enable = ChatMode.ALL + parallel_action = True # 惊喜动作与回复并行 +``` + +### Focus专享智能动作 +```python +@register_action +class AdvancedAnalysisAction(PluginAction): + action_name = "advanced_analysis" + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.ALWAYS # 不会生效 + mode_enable = ChatMode.FOCUS # 仅Focus模式 + parallel_action = False +``` + +## 现有插件的配置示例 + +### MuteAction (禁言动作) +```python +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.KEYWORD +activation_keywords = ["禁言", "mute", "ban", "silence"] +mode_enable = ChatMode.ALL +parallel_action = True # 可以与回复同时进行 +``` + +### PicAction (图片生成) +```python +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.KEYWORD +activation_keywords = ["画", "绘制", "生成图片", "画图", "draw", "paint", "图片生成"] +mode_enable = ChatMode.ALL +parallel_action = False # 专注生成,不同时回复 +``` + +### VTBAction (虚拟主播表情) +```python +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.RANDOM +random_activation_probability = 0.08 +mode_enable = ChatMode.ALL +parallel_action = False # 替代文字回复 ``` ## 性能监控 @@ -257,6 +439,101 @@ logger.debug(f"并行调整动作、回忆和处理完成,耗时: {duration:.2 3. **监控并行效果**:关注 `asyncio.gather` 的执行时间 4. **缓存命中率**:监控缓存使用情况,优化策略 5. **启用流程并行化**:确保 `parallel_processing` 配置为 `true` +6. **激活类型选择**:Normal模式优先使用KEYWORD,避免LLM_JUDGE + +## 迁移指南 ⚠️ + +### 重大变更说明 +**旧的 `action_activation_type` 属性已被移除**,必须更新为新的双激活类型系统。 + +### 快速迁移步骤 + +#### 第一步:更新基本属性 +```python +# 旧的配置(已废弃)❌ +class OldAction(BaseAction): + action_activation_type = ActionActivationType.LLM_JUDGE + +# 新的配置(必须使用)✅ +class NewAction(BaseAction): + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.KEYWORD + activation_keywords = ["相关", "关键词"] + mode_enable = ChatMode.ALL + parallel_action = False + enable_plugin = True +``` + +#### 第二步:根据原类型选择对应策略 +```python +# 原来是 ALWAYS +focus_activation_type = ActionActivationType.ALWAYS +normal_activation_type = ActionActivationType.ALWAYS + +# 原来是 LLM_JUDGE +focus_activation_type = ActionActivationType.LLM_JUDGE +normal_activation_type = ActionActivationType.KEYWORD # 添加关键词 +activation_keywords = ["需要", "添加", "关键词"] + +# 原来是 KEYWORD +focus_activation_type = ActionActivationType.KEYWORD +normal_activation_type = ActionActivationType.KEYWORD +# 保持原有的 activation_keywords + +# 原来是 RANDOM +focus_activation_type = ActionActivationType.RANDOM +normal_activation_type = ActionActivationType.RANDOM +# 保持原有的 random_activation_probability +``` + +#### 第三步:配置新功能 +```python +# 添加模式控制 +mode_enable = ChatMode.ALL # 或 ChatMode.FOCUS / ChatMode.NORMAL + +# 添加并行控制 +parallel_action = False # 根据动作特性选择True/False + +# 添加插件控制 +enable_plugin = True # 是否启用此插件 +``` + +### 批量迁移脚本 +可以创建以下脚本来帮助批量迁移: + +```python +# migrate_actions.py +import os +import re + +def migrate_action_file(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # 替换 action_activation_type + if 'action_activation_type = ActionActivationType.ALWAYS' in content: + content = content.replace( + 'action_activation_type = ActionActivationType.ALWAYS', + 'focus_activation_type = ActionActivationType.ALWAYS\n normal_activation_type = ActionActivationType.ALWAYS' + ) + elif 'action_activation_type = ActionActivationType.LLM_JUDGE' in content: + content = content.replace( + 'action_activation_type = ActionActivationType.LLM_JUDGE', + 'focus_activation_type = ActionActivationType.LLM_JUDGE\n normal_activation_type = ActionActivationType.KEYWORD\n activation_keywords = ["需要", "添加", "关键词"] # TODO: 配置合适的关键词' + ) + # ... 其他替换逻辑 + + # 添加新属性 + if 'mode_enable' not in content: + # 在class定义后添加新属性 + # ... + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + +# 使用示例 +migrate_action_file('src/plugins/your_plugin/actions/your_action.py') +``` ## 测试验证 @@ -271,41 +548,54 @@ python test_parallel_optimization.py ``` 测试内容包括: +- ✅ 双激活类型功能验证 - ✅ 并行处理功能验证 - ✅ 缓存机制效果测试 - ✅ 分层判定规则验证 - ✅ 性能对比分析 - ✅ HFC流程并行化效果 - ✅ 多循环平均性能测试 +- ✅ 并行动作系统验证 +- ✅ 迁移兼容性测试 ## 最佳实践 ### 1. 激活类型选择 - **ALWAYS**:reply, no_reply 等基础动作 -- **LLM_JUDGE**:需要智能判断的复杂动作 -- **KEYWORD**:明确的命令式动作 +- **LLM_JUDGE**:需要智能判断的复杂动作(建议仅用于Focus模式) +- **KEYWORD**:明确的命令式动作(推荐在Normal模式使用) - **RANDOM**:增趣动作,低概率触发 -### 2. LLM判定提示词编写 +### 2. 双模式配置策略 +- **智能自适应**:Focus用LLM_JUDGE,Normal用KEYWORD +- **性能优先**:两个模式都用KEYWORD或RANDOM +- **功能分离**:某些功能仅在特定模式启用 + +### 3. 并行动作使用建议 +- **parallel_action = True**:辅助性、非内容生成类动作 +- **parallel_action = False**:主要内容生成、需要完整注意力的动作 + +### 4. LLM判定提示词编写 - 明确描述激活条件和排除条件 - 避免模糊的描述 - 考虑边界情况 - 保持简洁明了 -### 3. 关键词设置 +### 5. 关键词设置 - 包含同义词和英文对应词 - 考虑用户的不同表达习惯 - 避免过于宽泛的关键词 - 根据实际使用调整 -### 4. 性能优化 +### 6. 性能优化 - 定期监控处理时间 - 根据使用模式调整缓存策略 - 优化激活判定逻辑 - 平衡准确性和性能 - **启用并行处理配置** +- **Normal模式避免使用LLM_JUDGE** -### 5. 并行化最佳实践 +### 7. 并行化最佳实践 - 在生产环境启用 `parallel_processing` - 监控并行阶段的执行时间 - 确保各阶段的独立性 @@ -313,30 +603,48 @@ python test_parallel_optimization.py ## 总结 -优化后的动作激活系统通过**四层优化策略**,实现了全方位的性能提升: +优化后的动作激活系统通过**五层优化策略**,实现了全方位的性能提升: -### 第一层:动作激活内部优化 +### 第一层:双激活类型系统 +- **Focus模式**:智能化优先,支持复杂LLM判定 +- **Normal模式**:性能优先,使用快速关键词匹配 +- **模式自适应**:根据聊天模式选择最优策略 + +### 第二层:动作激活内部优化 - **并行判定**:多个LLM判定任务并行执行 - **智能缓存**:相同上下文的判定结果缓存复用 - **分层判定**:快速过滤 + 精确判定的两层架构 -### 第二层:HFC流程级并行化 +### 第三层:并行动作系统 +- **并行执行**:支持动作与回复同时进行 +- **用户体验**:减少等待时间,提升交互流畅性 +- **灵活控制**:每个动作可独立配置并行行为 + +### 第四层:HFC流程级并行化 - **三阶段并行**:调整动作、回忆、处理器同时执行 - **性能提升**:2.3x 理论加速比 - **配置控制**:可根据环境灵活开启/关闭 +### 第五层:插件系统增强 +- **enable_plugin**:精确控制插件启用状态 +- **mode_enable**:支持模式级别的功能控制 +- **向后兼容**:平滑迁移旧系统配置 + ### 综合效果 - **响应速度**:显著提升机器人反应速度 - **成本优化**:减少不必要的LLM调用 -- **智能决策**:四种激活类型覆盖所有场景 +- **智能决策**:双激活类型覆盖所有场景 - **用户体验**:更快速、更智能的交互 +- **灵活配置**:精细化的功能控制 -**总性能提升预估:3-5x** -- 动作激活系统内部优化:1.5-2x +**总性能提升预估:4-6x** +- 双激活类型系统:1.5x (Normal模式优化) +- 动作激活内部优化:1.5-2x - HFC流程并行化:2.3x +- 并行动作系统:额外30-50%提升 - 缓存和过滤优化:额外20-30%提升 -这使得MaiBot能够更快速、更智能地响应用户需求,提供卓越的交互体验。 +这使得MaiBot能够更快速、更智能地响应用户需求,同时提供灵活的配置选项以适应不同的使用场景,实现了卓越的交互体验。 ## 如何为Action添加激活类型 @@ -344,17 +652,24 @@ python test_parallel_optimization.py ```python from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType +from src.chat.chat_mode import ChatMode @register_action class YourAction(BaseAction): action_name = "your_action" action_description = "你的动作描述" - # 设置激活类型 - 关键词触发示例 - action_activation_type = ActionActivationType.KEYWORD + # 双激活类型配置 + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.KEYWORD activation_keywords = ["关键词1", "关键词2", "keyword"] keyword_case_sensitive = False + # 新增属性 + mode_enable = ChatMode.ALL + parallel_action = False + enable_plugin = True + # ... 其他代码 ``` @@ -362,48 +677,47 @@ class YourAction(BaseAction): ```python from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action, ActionActivationType +from src.chat.chat_mode import ChatMode @register_action class YourPluginAction(PluginAction): action_name = "your_plugin_action" action_description = "你的插件动作描述" - # 设置激活类型 - 关键词触发示例 - action_activation_type = ActionActivationType.KEYWORD + # 双激活类型配置 + focus_activation_type = ActionActivationType.KEYWORD + normal_activation_type = ActionActivationType.KEYWORD activation_keywords = ["触发词1", "trigger", "启动"] keyword_case_sensitive = False + # 新增属性 + mode_enable = ChatMode.ALL + parallel_action = True # 与回复并行执行 + enable_plugin = True + # ... 其他代码 ``` -## 现有Action的激活类型设置 - -### 基础动作 (ALWAYS) -- `reply` - 回复动作 -- `no_reply` - 不回复动作 - -### LLM判定动作 (LLM_JUDGE) -- `vtb_action` - 虚拟主播表情 -- `mute_action` - 禁言动作 - -### 关键词触发动作 (KEYWORD) 🆕 -- `edge_search_action` - 网络搜索 (搜索、查找、什么是等) -- `pic_action` - 图片生成 (画、画图、生成图片等) -- `help_action` - 帮助功能 (帮助、help、求助等) - ## 工作流程 1. **ActionModifier处理**: 在planner运行前,ActionModifier会遍历所有注册的动作 -2. **类型判断**: 根据每个动作的激活类型决定是否激活 -3. **激活决策**: +2. **模式检查**: 根据当前聊天模式(Focus/Normal)和action的mode_enable进行过滤 +3. **激活类型判断**: 根据当前模式选择对应的激活类型(focus_activation_type或normal_activation_type) +4. **激活决策**: - ALWAYS: 直接激活 - RANDOM: 根据概率随机决定 - - LLM_JUDGE: 调用小模型判定 + - LLM_JUDGE: 调用小模型判定(Normal模式会警告) - KEYWORD: 检测关键词匹配 -4. **结果收集**: 收集所有激活的动作供planner使用 +5. **并行性检查**: 根据parallel_action决定是否与回复并行 +6. **结果收集**: 收集所有激活的动作供planner使用 ## 配置建议 +### 双激活类型策略选择 +- **智能自适应(推荐)**: Focus用LLM_JUDGE,Normal用KEYWORD +- **性能优先**: 两个模式都用KEYWORD或RANDOM +- **功能专享**: 某些高级功能仅在Focus模式启用 + ### LLM判定提示词编写 - 明确指出激活条件和不激活条件 - 使用简单清晰的语言 @@ -423,6 +737,7 @@ class YourPluginAction(PluginAction): ### 性能考虑 - LLM判定会增加响应时间,适度使用 - 关键词检测性能最好,推荐优先使用 +- Normal模式避免使用LLM_JUDGE - 建议优先级:KEYWORD > ALWAYS > RANDOM > LLM_JUDGE ## 调试和测试 @@ -434,20 +749,25 @@ python test_action_activation.py ``` 该脚本会显示: -- 所有注册动作的激活类型 -- 模拟不同消息下的激活结果 +- 所有注册动作的双激活类型配置 +- 模拟不同模式下的激活结果 +- 并行动作系统的工作状态 - 帮助验证配置是否正确 ## 注意事项 -1. **向后兼容**: 未设置激活类型的动作默认为ALWAYS -2. **错误处理**: LLM判定失败时默认不激活该动作 -3. **日志记录**: 系统会记录激活决策过程,便于调试 -4. **性能影响**: LLM判定会略微增加响应时间 +1. **重大变更**: `action_activation_type` 已被移除,必须使用双激活类型 +2. **向后兼容**: 系统不再兼容旧的单一激活类型配置 +3. **错误处理**: LLM判定失败时默认不激活该动作 +4. **性能警告**: Normal模式使用LLM_JUDGE会产生警告 +5. **日志记录**: 系统会记录激活决策过程,便于调试 +6. **性能影响**: LLM判定会略微增加响应时间 ## 未来扩展 -系统设计支持未来添加更多激活类型,如: +系统设计支持未来添加更多激活类型和功能,如: - 基于时间的激活 - 基于用户权限的激活 -- 基于群组设置的激活 \ No newline at end of file +- 基于群组设置的激活 +- 基于对话历史的激活 +- 基于情感状态的激活 \ No newline at end of file diff --git a/src/chat/focus_chat/expressors/exprssion_learner.py b/src/chat/focus_chat/expressors/exprssion_learner.py index 57f441a42..b7de6ce6d 100644 --- a/src/chat/focus_chat/expressors/exprssion_learner.py +++ b/src/chat/focus_chat/expressors/exprssion_learner.py @@ -304,7 +304,7 @@ class ExpressionLearner: # 如果没选够,随机补充 if len(remove_set) < remove_count: remaining = set(indices) - remove_set - remove_set.update(random.sample(remaining, remove_count - len(remove_set))) + remove_set.update(random.sample(list(remaining), remove_count - len(remove_set))) remove_indices = list(remove_set) diff --git a/src/chat/focus_chat/info_processors/relationship_processor.py b/src/chat/focus_chat/info_processors/relationship_processor.py index 257594711..b9ca263ff 100644 --- a/src/chat/focus_chat/info_processors/relationship_processor.py +++ b/src/chat/focus_chat/info_processors/relationship_processor.py @@ -33,8 +33,8 @@ def init_prompt(): {name_block} -请你阅读聊天记录,查看是否需要调取某个人的信息。 -你不同程度上认识群聊里的人,你可以根据聊天记录,回忆起有关他们的信息,帮助你参与聊天 +请你阅读聊天记录,查看是否需要调取某个人的信息,这个人可以是出现在聊天记录中的,也可以是记录中提到的人。 +你不同程度上认识群聊里的人,以及他们谈论到的人,你可以根据聊天记录,回忆起有关他们的信息,帮助你参与聊天 1.你需要提供用户名,以及你想要提取的信息名称类型来进行调取 2.你也可以完全不输出任何信息 3.阅读调取记录,如果已经回忆过某个人的信息,请不要重复调取,除非你忘记了 @@ -205,10 +205,10 @@ class RelationshipProcessor(BaseProcessor): ) try: - # logger.info(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}") + print(f"content: {content}") content_json = json.loads(repair_json(content)) for person_name, info_type in content_json.items(): diff --git a/src/chat/focus_chat/planners/action_manager.py b/src/chat/focus_chat/planners/action_manager.py index fa9225055..b4910d1a1 100644 --- a/src/chat/focus_chat/planners/action_manager.py +++ b/src/chat/focus_chat/planners/action_manager.py @@ -41,6 +41,9 @@ class ActionManager: # 初始化时将默认动作加载到使用中的动作 self._using_actions = self._default_actions.copy() + + # 添加系统核心动作 + self._add_system_core_actions() def _load_registered_actions(self) -> None: """ @@ -59,14 +62,22 @@ class ActionManager: action_parameters: dict[str:str] = getattr(action_class, "action_parameters", {}) action_require: list[str] = getattr(action_class, "action_require", []) associated_types: list[str] = getattr(action_class, "associated_types", []) - is_default: bool = getattr(action_class, "default", False) + is_enabled: bool = getattr(action_class, "enable_plugin", True) # 获取激活类型相关属性 - activation_type: str = getattr(action_class, "action_activation_type", "always") + focus_activation_type: str = getattr(action_class, "focus_activation_type", "always") + normal_activation_type: str = getattr(action_class, "normal_activation_type", "always") + random_probability: float = getattr(action_class, "random_activation_probability", 0.3) llm_judge_prompt: str = getattr(action_class, "llm_judge_prompt", "") activation_keywords: list[str] = getattr(action_class, "activation_keywords", []) keyword_case_sensitive: bool = getattr(action_class, "keyword_case_sensitive", False) + + # 获取模式启用属性 + mode_enable: str = getattr(action_class, "mode_enable", "all") + + # 获取并行执行属性 + parallel_action: bool = getattr(action_class, "parallel_action", False) if action_name and action_description: # 创建动作信息字典 @@ -75,18 +86,21 @@ class ActionManager: "parameters": action_parameters, "require": action_require, "associated_types": associated_types, - "activation_type": activation_type, + "focus_activation_type": focus_activation_type, + "normal_activation_type": normal_activation_type, "random_probability": random_probability, "llm_judge_prompt": llm_judge_prompt, "activation_keywords": activation_keywords, "keyword_case_sensitive": keyword_case_sensitive, + "mode_enable": mode_enable, + "parallel_action": parallel_action, } # 添加到所有已注册的动作 self._registered_actions[action_name] = action_info - # 添加到默认动作(如果是默认动作) - if is_default: + # 添加到默认动作(如果启用插件) + if is_enabled: self._default_actions[action_name] = action_info # logger.info(f"所有注册动作: {list(self._registered_actions.keys())}") @@ -212,9 +226,34 @@ class ActionManager: return self._default_actions.copy() def get_using_actions(self) -> Dict[str, ActionInfo]: - """获取当前正在使用的动作集""" + """获取当前正在使用的动作集合""" return self._using_actions.copy() + def get_using_actions_for_mode(self, mode: str) -> Dict[str, ActionInfo]: + """ + 根据聊天模式获取可用的动作集合 + + Args: + mode: 聊天模式 ("focus", "normal", "all") + + Returns: + Dict[str, ActionInfo]: 在指定模式下可用的动作集合 + """ + filtered_actions = {} + + for action_name, action_info in self._using_actions.items(): + action_mode = action_info.get("mode_enable", "all") + + # 检查动作是否在当前模式下启用 + if action_mode == "all" or action_mode == mode: + filtered_actions[action_name] = action_info + logger.debug(f"动作 {action_name} 在模式 {mode} 下可用 (mode_enable: {action_mode})") + else: + logger.debug(f"动作 {action_name} 在模式 {mode} 下不可用 (mode_enable: {action_mode})") + + logger.info(f"模式 {mode} 下可用动作: {list(filtered_actions.keys())}") + return filtered_actions + def add_action_to_using(self, action_name: str) -> bool: """ 添加已注册的动作到当前使用的动作集 @@ -306,6 +345,36 @@ class ActionManager: def restore_default_actions(self) -> None: """恢复默认动作集到使用集""" self._using_actions = self._default_actions.copy() + # 添加系统核心动作(即使enable_plugin为False的系统动作) + self._add_system_core_actions() + + def _add_system_core_actions(self) -> None: + """ + 添加系统核心动作到使用集 + 系统核心动作是那些enable_plugin为False但是系统必需的动作 + """ + system_core_actions = ["exit_focus_chat"] # 可以根据需要扩展 + + for action_name in system_core_actions: + if action_name in self._registered_actions and action_name not in self._using_actions: + self._using_actions[action_name] = self._registered_actions[action_name] + logger.info(f"添加系统核心动作到使用集: {action_name}") + + def add_system_action_if_needed(self, action_name: str) -> bool: + """ + 根据需要添加系统动作到使用集 + + Args: + action_name: 动作名称 + + Returns: + bool: 是否成功添加 + """ + if action_name in self._registered_actions and action_name not in self._using_actions: + self._using_actions[action_name] = self._registered_actions[action_name] + logger.info(f"临时添加系统动作到使用集: {action_name}") + return True + return False def get_action(self, action_name: str) -> Optional[Type[BaseAction]]: """ diff --git a/src/chat/focus_chat/planners/actions/__init__.py b/src/chat/focus_chat/planners/actions/__init__.py index 6fc139d74..537090dc1 100644 --- a/src/chat/focus_chat/planners/actions/__init__.py +++ b/src/chat/focus_chat/planners/actions/__init__.py @@ -2,5 +2,6 @@ from . import reply_action # noqa from . import no_reply_action # noqa from . import exit_focus_chat_action # noqa +from . import emoji_action # noqa # 在此处添加更多动作模块导入 diff --git a/src/chat/focus_chat/planners/actions/base_action.py b/src/chat/focus_chat/planners/actions/base_action.py index d854a84d0..3b56a5a3d 100644 --- a/src/chat/focus_chat/planners/actions/base_action.py +++ b/src/chat/focus_chat/planners/actions/base_action.py @@ -15,6 +15,12 @@ class ActionActivationType: RANDOM = "random" # 随机启用action到planner KEYWORD = "keyword" # 关键词触发启用action到planner +# 聊天模式枚举 +class ChatMode: + FOCUS = "focus" # Focus聊天模式 + NORMAL = "normal" # Normal聊天模式 + ALL = "all" # 所有聊天模式 + def register_action(cls): """ 动作注册装饰器 @@ -24,7 +30,10 @@ def register_action(cls): class MyAction(BaseAction): action_name = "my_action" action_description = "我的动作" - action_activation_type = ActionActivationType.ALWAYS + focus_activation_type = ActionActivationType.ALWAYS + normal_activation_type = ActionActivationType.ALWAYS + mode_enable = ChatMode.ALL + parallel_action = False ... """ # 检查类是否有必要的属性 @@ -34,7 +43,7 @@ def register_action(cls): action_name = cls.action_name action_description = cls.action_description - is_default = getattr(cls, "default", False) + is_enabled = getattr(cls, "enable_plugin", True) # 默认启用插件 if not action_name or not action_description: logger.error(f"动作类 {cls.__name__} 的 action_name 或 action_description 为空") @@ -43,11 +52,11 @@ def register_action(cls): # 将动作类注册到全局注册表 _ACTION_REGISTRY[action_name] = cls - # 如果是默认动作,添加到默认动作集 - if is_default: + # 如果启用插件,添加到默认动作集 + if is_enabled: _DEFAULT_ACTIONS[action_name] = action_description - logger.info(f"已注册动作: {action_name} -> {cls.__name__},默认: {is_default}") + logger.info(f"已注册动作: {action_name} -> {cls.__name__},插件启用: {is_enabled}") return cls @@ -73,20 +82,32 @@ class BaseAction(ABC): self.action_parameters: dict = {} self.action_require: list[str] = [] - # 动作激活类型,默认为always - self.action_activation_type: str = ActionActivationType.ALWAYS - # 随机激活的概率(0.0-1.0),仅当activation_type为random时有效 + # 动作激活类型设置 + # Focus模式下的激活类型,默认为always + self.focus_activation_type: str = ActionActivationType.ALWAYS + # Normal模式下的激活类型,默认为always + self.normal_activation_type: str = ActionActivationType.ALWAYS + + # 随机激活的概率(0.0-1.0),用于RANDOM激活类型 self.random_activation_probability: float = 0.3 - # LLM判定的提示词,仅当activation_type为llm_judge时有效 + # LLM判定的提示词,用于LLM_JUDGE激活类型 self.llm_judge_prompt: str = "" - # 关键词触发列表,仅当activation_type为keyword时有效 + # 关键词触发列表,用于KEYWORD激活类型 self.activation_keywords: list[str] = [] # 关键词匹配是否区分大小写 self.keyword_case_sensitive: bool = False + # 模式启用设置:指定在哪些聊天模式下启用此动作 + # 可选值: "focus"(仅Focus模式), "normal"(仅Normal模式), "all"(所有模式) + self.mode_enable: str = ChatMode.ALL + + # 并行执行设置:仅在Normal模式下生效,设置为True的动作可以与回复动作并行执行 + # 而不是替代回复动作,适用于图片生成、TTS、禁言等不需要覆盖回复的动作 + self.parallel_action: bool = False + self.associated_types: list[str] = [] - self.default: bool = False + self.enable_plugin: bool = True # 是否启用插件,默认启用 self.action_data = action_data self.reasoning = reasoning diff --git a/src/chat/focus_chat/planners/actions/emoji_action.py b/src/chat/focus_chat/planners/actions/emoji_action.py new file mode 100644 index 000000000..298f33ed4 --- /dev/null +++ b/src/chat/focus_chat/planners/actions/emoji_action.py @@ -0,0 +1,150 @@ +from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType, ChatMode +from typing import Tuple, List +from src.chat.heart_flow.observation.observation import Observation +from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer +from src.chat.message_receive.chat_stream import ChatStream +from src.chat.focus_chat.hfc_utils import create_empty_anchor_message +from src.config.config import global_config + +logger = get_logger("action_taken") + + +@register_action +class EmojiAction(BaseAction): + """表情动作处理类 + + 处理构建和发送消息表情的动作。 + """ + + action_name: str = "emoji" + action_description: str = "当你想单独发送一个表情包辅助你的回复表达" + action_parameters: dict[str:str] = { + "description": "文字描述你想要发送的表情包内容", + } + action_require: list[str] = [ + "表达情绪时可以选择使用", + "重点:不要连续发,如果你已经发过[表情包],就不要选择此动作"] + + associated_types: list[str] = ["emoji"] + + enable_plugin = True + + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.RANDOM + + random_activation_probability = global_config.normal_chat.emoji_chance + + parallel_action = True + + + llm_judge_prompt = """ + 判定是否需要使用表情动作的条件: + 1. 用户明确要求使用表情包 + 2. 这是一个适合表达强烈情绪的场合 + 3. 不要发送太多表情包,如果你已经发送过多个表情包 + """ + + # 模式启用设置 - 表情动作只在Focus模式下使用 + mode_enable = ChatMode.ALL + + def __init__( + self, + action_data: dict, + reasoning: str, + cycle_timers: dict, + thinking_id: str, + observations: List[Observation], + chat_stream: ChatStream, + log_prefix: str, + replyer: DefaultReplyer, + **kwargs, + ): + """初始化回复动作处理器 + + Args: + action_name: 动作名称 + action_data: 动作数据,包含 message, emojis, target 等 + reasoning: 执行该动作的理由 + cycle_timers: 计时器字典 + thinking_id: 思考ID + observations: 观察列表 + replyer: 回复器 + chat_stream: 聊天流 + log_prefix: 日志前缀 + """ + super().__init__(action_data, reasoning, cycle_timers, thinking_id) + self.observations = observations + self.replyer = replyer + self.chat_stream = chat_stream + self.log_prefix = log_prefix + + async def handle_action(self) -> Tuple[bool, str]: + """ + 处理回复动作 + + Returns: + Tuple[bool, str]: (是否执行成功, 回复文本) + """ + # 注意: 此处可能会使用不同的expressor实现根据任务类型切换不同的回复策略 + return await self._handle_reply( + reasoning=self.reasoning, + reply_data=self.action_data, + cycle_timers=self.cycle_timers, + thinking_id=self.thinking_id, + ) + + async def _handle_reply( + self, reasoning: str, reply_data: dict, cycle_timers: dict, thinking_id: str + ) -> tuple[bool, str]: + """ + 处理统一的回复动作 - 可包含文本和表情,顺序任意 + + reply_data格式: + { + "description": "描述你想要发送的表情" + } + """ + logger.info(f"{self.log_prefix} 决定发送表情") + # 从聊天观察获取锚定消息 + # chatting_observation: ChattingObservation = next( + # obs for obs in self.observations if isinstance(obs, ChattingObservation) + # ) + # if reply_data.get("target"): + # anchor_message = chatting_observation.search_message_by_text(reply_data["target"]) + # else: + # anchor_message = None + + # 如果没有找到锚点消息,创建一个占位符 + # if not anchor_message: + # logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符") + # anchor_message = await create_empty_anchor_message( + # self.chat_stream.platform, self.chat_stream.group_info, self.chat_stream + # ) + # else: + # anchor_message.update_chat_stream(self.chat_stream) + + logger.info(f"{self.log_prefix} 为了表情包创建占位符") + anchor_message = await create_empty_anchor_message( + self.chat_stream.platform, self.chat_stream.group_info, self.chat_stream + ) + + success, reply_set = await self.replyer.deal_emoji( + cycle_timers=cycle_timers, + action_data=reply_data, + anchor_message=anchor_message, + # reasoning=reasoning, + thinking_id=thinking_id, + ) + + reply_text = "" + if reply_set: + for reply in reply_set: + type = reply[0] + data = reply[1] + if type == "text": + reply_text += data + elif type == "emoji": + reply_text += data + + return success, reply_text \ No newline at end of file diff --git a/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py b/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py index 8ab43f96d..1d80f1ebf 100644 --- a/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py +++ b/src/chat/focus_chat/planners/actions/exit_focus_chat_action.py @@ -1,7 +1,7 @@ import asyncio import traceback from src.common.logger_manager import get_logger -from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ChatMode from typing import Tuple, List from src.chat.heart_flow.observation.observation import Observation from src.chat.message_receive.chat_stream import ChatStream @@ -25,7 +25,11 @@ class ExitFocusChatAction(BaseAction): "当前内容不需要持续专注关注,你决定退出专注聊天", "聊天内容已经完成,你决定退出专注聊天", ] - default = False + # 退出专注聊天是系统核心功能,不是插件,但默认不启用(需要特定条件触发) + enable_plugin = False + + # 模式启用设置 - 退出专注聊天动作只在Focus模式下使用 + mode_enable = ChatMode.FOCUS def __init__( self, diff --git a/src/chat/focus_chat/planners/actions/no_reply_action.py b/src/chat/focus_chat/planners/actions/no_reply_action.py index 4e93e8486..8cb45e8f3 100644 --- a/src/chat/focus_chat/planners/actions/no_reply_action.py +++ b/src/chat/focus_chat/planners/actions/no_reply_action.py @@ -2,7 +2,7 @@ import asyncio import traceback from src.common.logger_manager import get_logger from src.chat.utils.timer_calculator import Timer -from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType, ChatMode from typing import Tuple, List from src.chat.heart_flow.observation.observation import Observation from src.chat.heart_flow.observation.chatting_observation import ChattingObservation @@ -28,10 +28,13 @@ class NoReplyAction(BaseAction): "你连续发送了太多消息,且无人回复", "想要休息一下", ] - default = True + enable_plugin = True # 激活类型设置 - action_activation_type = ActionActivationType.ALWAYS + focus_activation_type = ActionActivationType.ALWAYS + + # 模式启用设置 - no_reply动作只在Focus模式下使用 + mode_enable = ChatMode.FOCUS def __init__( self, diff --git a/src/chat/focus_chat/planners/actions/plugin_action.py b/src/chat/focus_chat/planners/actions/plugin_action.py index e8bdf12d6..3a5313830 100644 --- a/src/chat/focus_chat/planners/actions/plugin_action.py +++ b/src/chat/focus_chat/planners/actions/plugin_action.py @@ -1,6 +1,6 @@ import traceback from typing import Tuple, Dict, List, Any, Optional, Union, Type -from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType # noqa F401 +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType, ChatMode # noqa F401 from src.chat.heart_flow.observation.chatting_observation import ChattingObservation from src.chat.focus_chat.hfc_utils import create_empty_anchor_message from src.common.logger_manager import get_logger @@ -35,11 +35,15 @@ class PluginAction(BaseAction): action_config_file_name: Optional[str] = None # 插件可以覆盖此属性来指定配置文件名 # 默认激活类型设置,插件可以覆盖 - action_activation_type = ActionActivationType.ALWAYS + focus_activation_type = ActionActivationType.ALWAYS + normal_activation_type = ActionActivationType.ALWAYS random_activation_probability: float = 0.3 llm_judge_prompt: str = "" activation_keywords: list[str] = [] keyword_case_sensitive: bool = False + + # 默认模式启用设置 - 插件动作默认在所有模式下可用,插件可以覆盖 + mode_enable = ChatMode.ALL def __init__( self, diff --git a/src/chat/focus_chat/planners/actions/reply_action.py b/src/chat/focus_chat/planners/actions/reply_action.py index caa31cb21..4d9bcadc5 100644 --- a/src/chat/focus_chat/planners/actions/reply_action.py +++ b/src/chat/focus_chat/planners/actions/reply_action.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from src.common.logger_manager import get_logger -from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType +from src.chat.focus_chat.planners.actions.base_action import BaseAction, register_action, ActionActivationType, ChatMode from typing import Tuple, List from src.chat.heart_flow.observation.observation import Observation from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer @@ -26,21 +26,23 @@ class ReplyAction(BaseAction): action_name: str = "reply" action_description: str = "当你想要参与回复或者聊天" action_parameters: dict[str:str] = { - "reply_to": "如果是明确回复某个人的发言,请在reply_to参数中指定,格式:(用户名:发言内容),如果不是,reply_to的值设为none", - "emoji": "如果你想用表情包辅助你的回答,请在emoji参数中用文字描述你想要发送的表情包内容,如果没有,值设为空", + "reply_to": "如果是明确回复某个人的发言,请在reply_to参数中指定,格式:(用户名:发言内容),如果不是,reply_to的值设为none" } action_require: list[str] = [ "你想要闲聊或者随便附和", "有人提到你", - "如果你刚刚回复,不要对同一个话题重复回应" + "如果你刚刚进行了回复,不要对同一个话题重复回应" ] - associated_types: list[str] = ["text", "emoji"] + associated_types: list[str] = ["text"] - default = True + enable_plugin = True # 激活类型设置 - action_activation_type = ActionActivationType.ALWAYS + focus_activation_type = ActionActivationType.ALWAYS + + # 模式启用设置 - 回复动作只在Focus模式下使用 + mode_enable = ChatMode.FOCUS def __init__( self, @@ -105,7 +107,6 @@ class ReplyAction(BaseAction): { "text": "你好啊" # 文本内容列表(可选) "target": "锚定消息", # 锚定消息的文本内容 - "emojis": "微笑" # 表情关键词列表(可选) } """ logger.info(f"{self.log_prefix} 决定回复: {self.reasoning}") diff --git a/src/chat/focus_chat/planners/modify_actions.py b/src/chat/focus_chat/planners/modify_actions.py index cb04947d2..998f83213 100644 --- a/src/chat/focus_chat/planners/modify_actions.py +++ b/src/chat/focus_chat/planners/modify_actions.py @@ -6,7 +6,7 @@ from src.chat.heart_flow.observation.chatting_observation import ChattingObserva from src.chat.message_receive.chat_stream import chat_manager from src.config.config import global_config from src.llm_models.utils_model import LLMRequest -from src.chat.focus_chat.planners.actions.base_action import ActionActivationType +from src.chat.focus_chat.planners.actions.base_action import ActionActivationType, ChatMode import random import asyncio import hashlib @@ -29,7 +29,7 @@ class ActionModifier: def __init__(self, action_manager: ActionManager): """初始化动作处理器""" self.action_manager = action_manager - self.all_actions = self.action_manager.get_registered_actions() + self.all_actions = self.action_manager.get_using_actions_for_mode(ChatMode.FOCUS) # 用于LLM判定的小模型 self.llm_judge = LLMRequest( @@ -78,7 +78,8 @@ class ActionModifier: # 处理HFCloopObservation - 传统的循环历史分析 if hfc_obs: obs = hfc_obs - all_actions = self.all_actions + # 获取适用于FOCUS模式的动作 + all_actions = self.action_manager.get_using_actions_for_mode(ChatMode.FOCUS) action_changes = await self.analyze_loop_actions(obs) if action_changes["add"] or action_changes["remove"]: # 合并动作变更 @@ -129,9 +130,9 @@ class ActionModifier: if chat_content is not None: logger.debug(f"{self.log_prefix}开始激活类型判定阶段") - # 获取当前使用的动作集(经过第一阶段处理) + # 获取当前使用的动作集(经过第一阶段处理,且适用于FOCUS模式) current_using_actions = self.action_manager.get_using_actions() - all_registered_actions = self.action_manager.get_registered_actions() + all_registered_actions = self.action_manager.get_using_actions_for_mode(ChatMode.FOCUS) # 构建完整的动作信息 current_actions_with_info = {} @@ -157,7 +158,7 @@ class ActionModifier: # 确定移除原因 if action_name in all_registered_actions: action_info = all_registered_actions[action_name] - activation_type = action_info.get("activation_type", ActionActivationType.ALWAYS) + activation_type = action_info.get("focus_activation_type", ActionActivationType.ALWAYS) if activation_type == ActionActivationType.RANDOM: probability = action_info.get("random_probability", 0.3) @@ -207,7 +208,7 @@ class ActionModifier: keyword_actions = {} for action_name, action_info in actions_with_info.items(): - activation_type = action_info.get("activation_type", ActionActivationType.ALWAYS) + activation_type = action_info.get("focus_activation_type", ActionActivationType.ALWAYS) if activation_type == ActionActivationType.ALWAYS: always_actions[action_name] = action_info @@ -433,6 +434,7 @@ class ActionModifier: action_require = action_info.get("require", []) custom_prompt = action_info.get("llm_judge_prompt", "") + # 构建基础判定提示词 base_prompt = f""" 你需要判断在当前聊天情况下,是否应该激活名为"{action_name}"的动作。 @@ -462,7 +464,7 @@ class ActionModifier: # 解析响应 response = response.strip().lower() - print(base_prompt) + # print(base_prompt) print(f"LLM判定动作 {action_name}:响应='{response}'") diff --git a/src/chat/focus_chat/planners/planner_simple.py b/src/chat/focus_chat/planners/planner_simple.py index b6b55c6af..1889c3952 100644 --- a/src/chat/focus_chat/planners/planner_simple.py +++ b/src/chat/focus_chat/planners/planner_simple.py @@ -16,6 +16,7 @@ from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.individuality.individuality import individuality from src.chat.focus_chat.planners.action_manager import ActionManager from src.chat.focus_chat.planners.modify_actions import ActionModifier +from src.chat.focus_chat.planners.actions.base_action import ChatMode from json_repair import repair_json from src.chat.focus_chat.planners.base_planner import BasePlanner from datetime import datetime @@ -144,7 +145,8 @@ class ActionPlanner(BasePlanner): # 获取经过modify_actions处理后的最终可用动作集 # 注意:动作的激活判定现在在主循环的modify_actions中完成 - current_available_actions_dict = self.action_manager.get_using_actions() + # 使用Focus模式过滤动作 + current_available_actions_dict = self.action_manager.get_using_actions_for_mode(ChatMode.FOCUS) # 获取完整的动作信息 all_registered_actions = self.action_manager.get_registered_actions() diff --git a/src/chat/focus_chat/replyer/default_replyer.py b/src/chat/focus_chat/replyer/default_replyer.py index 234c2f5fe..4195d4f73 100644 --- a/src/chat/focus_chat/replyer/default_replyer.py +++ b/src/chat/focus_chat/replyer/default_replyer.py @@ -150,17 +150,6 @@ class DefaultReplyer: action_data=action_data, ) - with Timer("选择表情", cycle_timers): - emoji_keyword = action_data.get("emoji", "") - print(f"emoji_keyword: {emoji_keyword}") - if emoji_keyword: - emoji_base64, _description, _emotion = await self._choose_emoji(emoji_keyword) - # print(f"emoji_base64: {emoji_base64}") - # print(f"emoji_description: {_description}") - # print(f"emoji_emotion: {emotion}") - if emoji_base64: - reply.append(("emoji", emoji_base64)) - if reply: with Timer("发送消息", cycle_timers): sent_msg_list = await self.send_response_messages( diff --git a/src/chat/normal_chat/normal_chat.py b/src/chat/normal_chat/normal_chat.py index 8c6119b93..9b013d093 100644 --- a/src/chat/normal_chat/normal_chat.py +++ b/src/chat/normal_chat/normal_chat.py @@ -280,28 +280,26 @@ class NormalChat: info_catcher = info_catcher_manager.get_info_catcher(thinking_id) info_catcher.catch_decide_to_response(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() + except Exception as e: + logger.warning(f"[{self.stream_name}] 获取available_actions失败: {e}") + available_actions = None + # 定义并行执行的任务 async def generate_normal_response(): """生成普通回复""" try: - # 如果启用planner,获取可用actions - enable_planner = self.enable_planner - available_actions = None - - if enable_planner: - try: - await self.action_modifier.modify_actions_for_normal_chat( - self.chat_stream, self.recent_replies - ) - available_actions = self.action_manager.get_using_actions() - except Exception as e: - logger.warning(f"[{self.stream_name}] 获取available_actions失败: {e}") - available_actions = None - return await self.gpt.generate_response( message=message, thinking_id=thinking_id, - enable_planner=enable_planner, + enable_planner=self.enable_planner, available_actions=available_actions, ) except Exception as e: @@ -315,38 +313,37 @@ class NormalChat: return None try: - # 并行执行动作修改和规划准备 - async def modify_actions(): - """修改可用动作集合""" - return await self.action_modifier.modify_actions_for_normal_chat( - self.chat_stream, self.recent_replies - ) - - async def prepare_planning(): - """准备规划所需的信息""" - return self._get_sender_name(message) - - # 并行执行动作修改和准备工作 - _, sender_name = await asyncio.gather(modify_actions(), prepare_planning()) + # 获取发送者名称(动作修改已在并行执行前完成) + 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}] 没有可用动作,跳过规划") - return None + 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}") + 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 None + return no_action elif action_type == "change_to_focus_chat": logger.info(f"[{self.stream_name}] Planner决定切换到focus聊天模式") return None @@ -358,14 +355,15 @@ class NormalChat: else: logger.warning(f"[{self.stream_name}] 额外动作 {action_type} 执行失败") - return {"action_type": action_type, "action_data": action_data, "reasoning": reasoning} + 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 None + 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 @@ -382,15 +380,15 @@ class NormalChat: if isinstance(plan_result, Exception): logger.error(f"[{self.stream_name}] 动作规划异常: {plan_result}") elif plan_result: - logger.debug(f"[{self.stream_name}] 额外动作处理完成: {plan_result['action_type']}") + 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", "change_to_focus_chat"] + self.enable_planner and self.action_type not in ["no_action", "change_to_focus_chat"] 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", "change_to_focus_chat"]: - logger.info(f"[{self.stream_name}] 模型选择其他动作") + elif self.enable_planner and self.action_type not in ["no_action", "change_to_focus_chat"] 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[:]: @@ -446,7 +444,7 @@ class NormalChat: logger.warning(f"[{self.stream_name}] 没有设置切换到focus聊天模式的回调函数,无法执行切换") return else: - await self._check_switch_to_focus() + # await self._check_switch_to_focus() pass info_catcher.done_catch() diff --git a/src/chat/normal_chat/normal_chat_action_modifier.py b/src/chat/normal_chat/normal_chat_action_modifier.py index f4d0285c5..afc2f1c5b 100644 --- a/src/chat/normal_chat/normal_chat_action_modifier.py +++ b/src/chat/normal_chat/normal_chat_action_modifier.py @@ -1,6 +1,11 @@ -from typing import List, Any +from typing import List, Any, Dict from src.common.logger_manager import get_logger from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.focus_chat.planners.actions.base_action import ActionActivationType, ChatMode +from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat +from src.config.config import global_config +import random +import time logger = get_logger("normal_chat_action_modifier") @@ -9,6 +14,7 @@ class NormalChatActionModifier: """Normal Chat动作修改器 负责根据Normal Chat的上下文和状态动态调整可用的动作集合 + 实现与Focus Chat类似的动作激活策略,但将LLM_JUDGE转换为概率激活以提升性能 """ def __init__(self, action_manager: ActionManager, stream_id: str, stream_name: str): @@ -25,9 +31,14 @@ class NormalChatActionModifier: self, chat_stream, recent_replies: List[dict], + message_content: str, **kwargs: Any, ): """为Normal Chat修改可用动作集合 + + 实现动作激活策略: + 1. 基于关联类型的动态过滤 + 2. 基于激活类型的智能判定(LLM_JUDGE转为概率激活) Args: chat_stream: 聊天流对象 @@ -35,24 +46,19 @@ class NormalChatActionModifier: **kwargs: 其他参数 """ - # 合并所有动作变更 - merged_action_changes = {"add": [], "remove": []} reasons = [] + merged_action_changes = {"add": [], "remove": []} + type_mismatched_actions = [] # 在外层定义避免作用域问题 + + self.action_manager.restore_default_actions() - # 1. 移除Normal Chat不适用的动作 - excluded_actions = ["exit_focus_chat_action", "no_reply", "reply"] - for action_name in excluded_actions: - if action_name in self.action_manager.get_using_actions(): - merged_action_changes["remove"].append(action_name) - reasons.append(f"移除{action_name}(Normal Chat不适用)") - - # 2. 检查动作的关联类型 + # 第一阶段:基于关联类型的动态过滤 if chat_stream: chat_context = chat_stream.context if hasattr(chat_stream, "context") else None if chat_context: - type_mismatched_actions = [] - - current_using_actions = self.action_manager.get_using_actions() + # 获取Normal模式下的可用动作(已经过滤了mode_enable) + current_using_actions = self.action_manager.get_using_actions_for_mode(ChatMode.NORMAL) + # print(f"current_using_actions: {current_using_actions}") for action_name in current_using_actions.keys(): if action_name in self.all_actions: data = self.all_actions[action_name] @@ -65,26 +71,218 @@ class NormalChatActionModifier: merged_action_changes["remove"].extend(type_mismatched_actions) reasons.append(f"移除{type_mismatched_actions}(关联类型不匹配)") - # 应用动作变更 + # 第二阶段:应用激活类型判定 + # 构建聊天内容 - 使用与planner一致的方式 + chat_content = "" + if chat_stream and hasattr(chat_stream, 'stream_id'): + try: + # 获取消息历史,使用与normal_chat_planner相同的方法 + 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_content = build_readable_messages( + message_list_before_now, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + show_actions=True, + ) + + logger.debug(f"{self.log_prefix} 成功构建聊天内容,长度: {len(chat_content)}") + + except Exception as e: + logger.warning(f"{self.log_prefix} 构建聊天内容失败: {e}") + chat_content = "" + + # 获取当前Normal模式下的动作集进行激活判定 + current_actions = self.action_manager.get_using_actions_for_mode(ChatMode.NORMAL) + + # print(f"current_actions: {current_actions}") + # print(f"chat_content: {chat_content}") + final_activated_actions = await self._apply_normal_activation_filtering( + current_actions, + chat_content, + message_content + ) + # print(f"final_activated_actions: {final_activated_actions}") + + # 统一处理所有需要移除的动作,避免重复移除 + all_actions_to_remove = set() # 使用set避免重复 + + # 添加关联类型不匹配的动作 + if type_mismatched_actions: + all_actions_to_remove.update(type_mismatched_actions) + + # 添加激活类型判定未通过的动作 + for action_name in current_actions.keys(): + if action_name not in final_activated_actions: + all_actions_to_remove.add(action_name) + + # 统计移除原因(避免重复) + activation_failed_actions = [name for name in current_actions.keys() if name not in final_activated_actions and name not in type_mismatched_actions] + if activation_failed_actions: + reasons.append(f"移除{activation_failed_actions}(激活类型判定未通过)") + + # 统一执行移除操作 + for action_name in all_actions_to_remove: + success = self.action_manager.remove_action_from_using(action_name) + if success: + logger.debug(f"{self.log_prefix} 移除动作: {action_name}") + else: + logger.debug(f"{self.log_prefix} 动作 {action_name} 已经不在使用集中,跳过移除") + + # 应用动作添加(如果有的话) for action_name in merged_action_changes["add"]: - if action_name in self.all_actions and action_name not in excluded_actions: + if action_name in self.all_actions: success = self.action_manager.add_action_to_using(action_name) if success: logger.debug(f"{self.log_prefix} 添加动作: {action_name}") - for action_name in merged_action_changes["remove"]: - success = self.action_manager.remove_action_from_using(action_name) - if success: - logger.debug(f"{self.log_prefix} 移除动作: {action_name}") - # 记录变更原因 - if merged_action_changes["add"] or merged_action_changes["remove"]: + if reasons: logger.info(f"{self.log_prefix} 动作调整完成: {' | '.join(reasons)}") - logger.debug(f"{self.log_prefix} 当前可用动作: {list(self.action_manager.get_using_actions().keys())}") + + # 获取最终的Normal模式可用动作并记录 + final_actions = self.action_manager.get_using_actions_for_mode(ChatMode.NORMAL) + logger.debug(f"{self.log_prefix} 当前Normal模式可用动作: {list(final_actions.keys())}") + + async def _apply_normal_activation_filtering( + self, + actions_with_info: Dict[str, Any], + chat_content: str = "", + message_content: str = "", + ) -> Dict[str, Any]: + """ + 应用Normal模式的激活类型过滤逻辑 + + 与Focus模式的区别: + 1. LLM_JUDGE类型转换为概率激活(避免LLM调用) + 2. RANDOM类型保持概率激活 + 3. KEYWORD类型保持关键词匹配 + 4. ALWAYS类型直接激活 + + Args: + actions_with_info: 带完整信息的动作字典 + chat_content: 聊天内容 + + Returns: + Dict[str, Any]: 过滤后激活的actions字典 + """ + activated_actions = {} + + # 分类处理不同激活类型的actions + always_actions = {} + random_actions = {} + keyword_actions = {} + + for action_name, action_info in actions_with_info.items(): + # 使用normal_activation_type + activation_type = action_info.get("normal_activation_type", ActionActivationType.ALWAYS) + + if activation_type == ActionActivationType.ALWAYS: + always_actions[action_name] = action_info + elif activation_type == ActionActivationType.RANDOM or activation_type == ActionActivationType.LLM_JUDGE: + random_actions[action_name] = action_info + elif activation_type == ActionActivationType.KEYWORD: + keyword_actions[action_name] = action_info + else: + logger.warning(f"{self.log_prefix}未知的激活类型: {activation_type},跳过处理") + + # 1. 处理ALWAYS类型(直接激活) + for action_name, action_info in always_actions.items(): + activated_actions[action_name] = action_info + logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: ALWAYS类型直接激活") + + # 2. 处理RANDOM类型(概率激活) + for action_name, action_info in random_actions.items(): + probability = action_info.get("random_probability", 0.3) + should_activate = random.random() < probability + if should_activate: + activated_actions[action_name] = action_info + logger.info(f"{self.log_prefix}激活动作: {action_name},原因: RANDOM类型触发(概率{probability})") + else: + logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: RANDOM类型未触发(概率{probability})") + + # 3. 处理KEYWORD类型(关键词匹配) + for action_name, action_info in keyword_actions.items(): + should_activate = self._check_keyword_activation( + action_name, + action_info, + chat_content, + message_content + ) + if should_activate: + activated_actions[action_name] = action_info + keywords = action_info.get("activation_keywords", []) + logger.info(f"{self.log_prefix}激活动作: {action_name},原因: KEYWORD类型匹配关键词({keywords})") + else: + keywords = action_info.get("activation_keywords", []) + logger.info(f"{self.log_prefix}未激活动作: {action_name},原因: KEYWORD类型未匹配关键词({keywords})") + # print(f"keywords: {keywords}") + # print(f"chat_content: {chat_content}") + + logger.debug(f"{self.log_prefix}Normal模式激活类型过滤完成: {list(activated_actions.keys())}") + return activated_actions + + def _check_keyword_activation( + self, + action_name: str, + action_info: Dict[str, Any], + chat_content: str = "", + message_content: str = "", + ) -> bool: + """ + 检查是否匹配关键词触发条件 + + Args: + action_name: 动作名称 + action_info: 动作信息 + chat_content: 聊天内容(已经是格式化后的可读消息) + + Returns: + bool: 是否应该激活此action + """ + + activation_keywords = action_info.get("activation_keywords", []) + case_sensitive = action_info.get("keyword_case_sensitive", False) + + if not activation_keywords: + logger.warning(f"{self.log_prefix}动作 {action_name} 设置为关键词触发但未配置关键词") + return False + + # 使用构建好的聊天内容作为检索文本 + search_text = chat_content +message_content + + # 如果不区分大小写,转换为小写 + if not case_sensitive: + search_text = search_text.lower() + + # 检查每个关键词 + matched_keywords = [] + for keyword in activation_keywords: + check_keyword = keyword if case_sensitive else keyword.lower() + if check_keyword in search_text: + matched_keywords.append(keyword) + + + # print(f"search_text: {search_text}") + # print(f"activation_keywords: {activation_keywords}") + + if matched_keywords: + logger.info(f"{self.log_prefix}动作 {action_name} 匹配到关键词: {matched_keywords}") + return True + else: + logger.info(f"{self.log_prefix}动作 {action_name} 未匹配到任何关键词: {activation_keywords}") + return False def get_available_actions_count(self) -> int: """获取当前可用动作数量(排除默认的no_action)""" - current_actions = self.action_manager.get_using_actions() + current_actions = self.action_manager.get_using_actions_for_mode(ChatMode.NORMAL) # 排除no_action(如果存在) filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"} return len(filtered_actions) diff --git a/src/chat/normal_chat/normal_chat_planner.py b/src/chat/normal_chat/normal_chat_planner.py index bbe649f41..41661906d 100644 --- a/src/chat/normal_chat/normal_chat_planner.py +++ b/src/chat/normal_chat/normal_chat_planner.py @@ -7,6 +7,7 @@ from src.common.logger_manager import get_logger from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.individuality.individuality import individuality from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.focus_chat.planners.actions.base_action import ChatMode from src.chat.message_receive.message import MessageThinking from json_repair import repair_json from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat @@ -98,16 +99,18 @@ class NormalChatPlanner: self_info = name_block + personality_block + identity_block - # 获取当前可用的动作 - current_available_actions = self.action_manager.get_using_actions() + # 获取当前可用的动作,使用Normal模式过滤 + current_available_actions = self.action_manager.get_using_actions_for_mode(ChatMode.NORMAL) + + # 注意:动作的激活判定现在在 normal_chat_action_modifier 中完成 + # 这里直接使用经过 action_modifier 处理后的最终动作集 + # 符合职责分离原则:ActionModifier负责动作管理,Planner专注于决策 - # 如果没有可用动作或只有no_action动作,直接返回no_action - if not current_available_actions or ( - len(current_available_actions) == 1 and "no_action" in current_available_actions - ): - logger.debug(f"{self.log_prefix}规划器: 没有可用动作或只有no_action动作,返回no_action") + # 如果没有可用动作,直接返回no_action + if not current_available_actions: + logger.debug(f"{self.log_prefix}规划器: 没有可用动作,返回no_action") return { - "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning}, + "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning, "is_parallel": True}, "chat_context": "", "action_prompt": "", } @@ -138,7 +141,7 @@ class NormalChatPlanner: if not prompt: logger.warning(f"{self.log_prefix}规划器: 构建提示词失败") return { - "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning}, + "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning, "is_parallel": False}, "chat_context": chat_context, "action_prompt": "", } @@ -185,13 +188,21 @@ class NormalChatPlanner: except Exception as outer_e: logger.error(f"{self.log_prefix}规划器异常: {outer_e}") - chat_context = "无法获取聊天上下文" # 设置默认值 - prompt = "" # 设置默认值 + # 设置异常时的默认值 + current_available_actions = {} + chat_context = "无法获取聊天上下文" + prompt = "" action = "no_action" reasoning = "规划器出现异常,使用默认动作" action_data = {} - logger.debug(f"{self.log_prefix}规划器决策动作:{action}, 动作信息: '{action_data}', 理由: {reasoning}") + # 检查动作是否支持并行执行 + is_parallel = False + if action in current_available_actions: + action_info = current_available_actions[action] + is_parallel = action_info.get("parallel_action", False) + + logger.debug(f"{self.log_prefix}规划器决策动作:{action}, 动作信息: '{action_data}', 理由: {reasoning}, 并行执行: {is_parallel}") # 恢复到默认动作集 self.action_manager.restore_actions() @@ -212,6 +223,7 @@ class NormalChatPlanner: "action_type": action, "action_data": action_data, "reasoning": reasoning, + "is_parallel": is_parallel, "action_record": json.dumps(action_record, ensure_ascii=False) } @@ -304,4 +316,6 @@ class NormalChatPlanner: return "" + + init_prompt() diff --git a/src/person_info/person_info.py b/src/person_info/person_info.py index 6a7e60bc1..e5efe2f43 100644 --- a/src/person_info/person_info.py +++ b/src/person_info/person_info.py @@ -531,7 +531,6 @@ class PersonInfoManager: "know_since": int(datetime.datetime.now().timestamp()), "last_know": int(datetime.datetime.now().timestamp()), "impression": None, - "interaction": None, "points": [], "forgotten_points": [] } diff --git a/src/person_info/relationship_manager.py b/src/person_info/relationship_manager.py index a3958b95e..4b63e2162 100644 --- a/src/person_info/relationship_manager.py +++ b/src/person_info/relationship_manager.py @@ -125,7 +125,6 @@ class RelationshipManager: if not person_name or person_name == "none": return "" impression = await person_info_manager.get_value(person_id, "impression") - interaction = await person_info_manager.get_value(person_id, "interaction") points = await person_info_manager.get_value(person_id, "points") or [] if isinstance(points, str): @@ -141,11 +140,9 @@ class RelationshipManager: relation_prompt = f"'{person_name}' ,ta在{platform}上的昵称是{nickname_str}。" - if impression: - relation_prompt += f"你对ta的印象是:{impression}。" - - if interaction: - relation_prompt += f"你与ta的关系是:{interaction}。" + # if impression: + # relation_prompt += f"你对ta的印象是:{impression}。" + if random_points: for point in random_points: diff --git a/src/plugins/doubao_pic/actions/pic_action.py b/src/plugins/doubao_pic/actions/pic_action.py index f414c3490..360838db9 100644 --- a/src/plugins/doubao_pic/actions/pic_action.py +++ b/src/plugins/doubao_pic/actions/pic_action.py @@ -6,7 +6,7 @@ import base64 # 新增:用于Base64编码 import traceback # 新增:用于打印堆栈跟踪 from typing import Tuple from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action -from src.chat.focus_chat.planners.actions.base_action import ActionActivationType +from src.chat.focus_chat.planners.actions.base_action import ActionActivationType, ChatMode from src.common.logger_manager import get_logger from .generate_pic_config import generate_config @@ -35,11 +35,18 @@ class PicAction(PluginAction): "当有人要求你生成并发送一张图片时使用", "当有人让你画一张图时使用", ] - default = True + enable_plugin = True action_config_file_name = "pic_action_config.toml" - # 激活类型设置 - 使用LLM判定,能更好理解用户意图 - action_activation_type = ActionActivationType.LLM_JUDGE + # 激活类型设置 + focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定,精确理解需求 + normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活,快速响应 + + # 关键词设置(用于Normal模式) + activation_keywords = ["画", "绘制", "生成图片", "画图", "draw", "paint", "图片生成"] + keyword_case_sensitive = False + + # LLM判定提示词(用于Focus模式) llm_judge_prompt = """ 判定是否需要使用图片生成动作的条件: 1. 用户明确要求画图、生成图片或创作图像 @@ -60,11 +67,20 @@ class PicAction(PluginAction): 4. 技术讨论中提到绘图概念但无生成需求 5. 用户明确表示不需要图片时 """ - + + # Random激活概率(备用) + random_activation_probability = 0.15 # 适中概率,图片生成比较有趣 + # 简单的请求缓存,避免短时间内重复请求 _request_cache = {} _cache_max_size = 10 + # 模式启用设置 - 图片生成在所有模式下可用 + mode_enable = ChatMode.ALL + + # 并行执行设置 - 图片生成可以与回复并行执行,不覆盖回复内容 + parallel_action = False + @classmethod def _get_cache_key(cls, description: str, model: str, size: str) -> str: """生成缓存键""" diff --git a/src/plugins/mute_plugin/actions/mute_action.py b/src/plugins/mute_plugin/actions/mute_action.py index 35de6bcd4..4f0149efd 100644 --- a/src/plugins/mute_plugin/actions/mute_action.py +++ b/src/plugins/mute_plugin/actions/mute_action.py @@ -1,5 +1,6 @@ from src.common.logger_manager import get_logger from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action, ActionActivationType +from src.chat.focus_chat.planners.actions.base_action import ChatMode from typing import Tuple logger = get_logger("mute_action") @@ -22,12 +23,20 @@ class MuteAction(PluginAction): "当有人发了擦边,或者色情内容时使用", "当有人要求禁言自己时使用", ] - default = True # 默认动作,是否手动添加到使用集 + enable_plugin = True # 启用插件 associated_types = ["command", "text"] action_config_file_name = "mute_action_config.toml" - # 激活类型设置 - 使用LLM判定,因为禁言是严肃的管理动作,需要谨慎判断 - action_activation_type = ActionActivationType.LLM_JUDGE + # 激活类型设置 + focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定,确保谨慎 + normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活,快速响应 + + + # 关键词设置(用于Normal模式) + activation_keywords = ["禁言", "mute", "ban", "silence"] + keyword_case_sensitive = False + + # LLM判定提示词(用于Focus模式) llm_judge_prompt = """ 判定是否需要使用禁言动作的严格条件: @@ -49,6 +58,15 @@ class MuteAction(PluginAction): 注意:禁言是严厉措施,只在明确违规或用户主动要求时使用。 宁可保守也不要误判,保护用户的发言权利。 """ + + # Random激活概率(备用) + random_activation_probability = 0.05 # 设置很低的概率作为兜底 + + # 模式启用设置 - 禁言功能在所有模式下都可用 + mode_enable = ChatMode.ALL + + # 并行执行设置 - 禁言动作可以与回复并行执行,不覆盖回复内容 + parallel_action = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/plugins/tts_plgin/actions/tts_action.py b/src/plugins/tts_plgin/actions/tts_action.py index a029d035e..d309a27ec 100644 --- a/src/plugins/tts_plgin/actions/tts_action.py +++ b/src/plugins/tts_plgin/actions/tts_action.py @@ -1,4 +1,5 @@ from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.base_action import ActionActivationType from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action from typing import Tuple @@ -20,8 +21,18 @@ class TTSAction(PluginAction): "当表达内容更适合用语音而不是文字传达时使用", "当用户想听到语音回答而非阅读文本时使用", ] - default = True # 设为默认动作 + enable_plugin = True # 启用插件 associated_types = ["tts_text"] + + focus_activation_type = ActionActivationType.LLM_JUDGE + normal_activation_type = ActionActivationType.KEYWORD + + # 关键词配置 - Normal模式下使用关键词触发 + activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "听", "朗读"] + keyword_case_sensitive = False + + # 并行执行设置 - TTS可以与回复并行执行,不覆盖回复内容 + parallel_action = False async def process(self) -> Tuple[bool, str]: """处理TTS文本转语音动作""" diff --git a/src/plugins/vtb_action/actions/vtb_action.py b/src/plugins/vtb_action/actions/vtb_action.py index 8d20cdb79..70d99b951 100644 --- a/src/plugins/vtb_action/actions/vtb_action.py +++ b/src/plugins/vtb_action/actions/vtb_action.py @@ -20,11 +20,14 @@ class VTBAction(PluginAction): "当回应内容需要更生动的情感表达时使用", "当想要通过预设动作增强互动体验时使用", ] - default = True # 设为默认动作 + enable_plugin = True # 启用插件 associated_types = ["vtb_text"] - # 激活类型设置 - 使用LLM判定,因为需要根据情感表达需求判断 - action_activation_type = ActionActivationType.LLM_JUDGE + # 激活类型设置 + focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定,精确识别情感表达需求 + normal_activation_type = ActionActivationType.RANDOM # Normal模式使用随机激活,增加趣味性 + + # LLM判定提示词(用于Focus模式) llm_judge_prompt = """ 判定是否需要使用VTB虚拟主播动作的条件: 1. 当前聊天内容涉及明显的情感表达需求 @@ -38,6 +41,9 @@ class VTBAction(PluginAction): 3. 不涉及情感的日常对话 4. 已经有足够的情感表达 """ + + # Random激活概率(用于Normal模式) + random_activation_probability = 0.08 # 较低概率,避免过度使用 async def process(self) -> Tuple[bool, str]: """处理VTB虚拟主播动作""" diff --git a/tests/test_relationship_processor.py b/tests/test_relationship_processor.py deleted file mode 100644 index f190ab947..000000000 --- a/tests/test_relationship_processor.py +++ /dev/null @@ -1,608 +0,0 @@ -import os -import sys -import asyncio -import random -import time -import traceback -from typing import List, Dict, Any, Tuple, Optional -from datetime import datetime - -# 添加项目根目录到Python路径 -current_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(current_dir) -sys.path.append(project_root) - -from src.common.message_repository import find_messages -from src.common.database.database_model import ActionRecords, ChatStreams -from src.config.config import global_config -from src.person_info.person_info import person_info_manager -from src.chat.utils.utils import translate_timestamp_to_human_readable -from src.chat.heart_flow.observation.observation import Observation -from src.llm_models.utils_model import LLMRequest -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.person_info.relationship_manager import relationship_manager -from src.common.logger_manager import get_logger -from src.chat.focus_chat.info.info_base import InfoBase -from src.chat.focus_chat.info.relation_info import RelationInfo - -logger = get_logger("processor") - -async def get_person_id_list(messages: List[Dict[str, Any]]) -> List[str]: - """ - 从消息列表中提取不重复的 person_id 列表 (忽略机器人自身)。 - - Args: - messages: 消息字典列表。 - - Returns: - 一个包含唯一 person_id 的列表。 - """ - person_ids_set = set() # 使用集合来自动去重 - - for msg in messages: - platform = msg.get("user_platform") - user_id = msg.get("user_id") - - # 检查必要信息是否存在 且 不是机器人自己 - if not all([platform, user_id]) or user_id == global_config.bot.qq_account: - continue - - person_id = person_info_manager.get_person_id(platform, user_id) - - # 只有当获取到有效 person_id 时才添加 - if person_id: - person_ids_set.add(person_id) - - return list(person_ids_set) # 将集合转换为列表返回 - -class ChattingObservation(Observation): - def __init__(self, chat_id): - super().__init__(chat_id) - self.chat_id = chat_id - self.platform = "qq" - - # 从数据库获取聊天类型和目标信息 - chat_info = ChatStreams.select().where(ChatStreams.stream_id == chat_id).first() - self.is_group_chat = True - self.chat_target_info = { - "person_name": chat_info.group_name if chat_info else None, - "user_nickname": chat_info.group_name if chat_info else None - } - - # 初始化其他属性 - self.talking_message = [] - self.talking_message_str = "" - self.talking_message_str_truncate = "" - 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.overlap_len = global_config.focus_chat.compressed_length - self.mid_memories = [] - self.max_mid_memory_len = global_config.focus_chat.compress_length_limit - self.mid_memory_info = "" - self.person_list = [] - self.oldest_messages = [] - self.oldest_messages_str = "" - self.compressor_prompt = "" - self.last_observe_time = 0 - - def get_observe_info(self, ids=None): - """获取观察信息""" - return self.talking_message_str - -def init_prompt(): - relationship_prompt = """ -<聊天记录> -{chat_observe_info} - - -<人物信息> -{relation_prompt} - - -请区分聊天记录的内容和你之前对人的了解,聊天记录是现在发生的事情,人物信息是之前对某个人的持久的了解。 - -{name_block} -现在请你总结提取某人的信息,提取成一串文本 -1. 根据聊天记录的需求,如果需要你和某个人的信息,请输出你和这个人之间精简的信息 -2. 如果没有特别需要提及的信息,就不用输出这个人的信息 -3. 如果有人问你对他的看法或者关系,请输出你和这个人之间的信息 - -请从这些信息中提取出你对某人的了解信息,信息提取成一串文本: - -请严格按照以下输出格式,不要输出多余内容,person_name可以有多个: -{{ - "person_name": "信息", - "person_name2": "信息", - "person_name3": "信息", -}} - -""" - Prompt(relationship_prompt, "relationship_prompt") - -class RelationshipProcessor: - log_prefix = "关系" - - def __init__(self, subheartflow_id: str): - self.subheartflow_id = subheartflow_id - - self.llm_model = LLMRequest( - model=global_config.model.relation, - request_type="relation", - ) - - # 直接从数据库获取名称 - chat_info = ChatStreams.select().where(ChatStreams.stream_id == subheartflow_id).first() - name = chat_info.group_name if chat_info else "未知" - self.log_prefix = f"[{name}] " - - async def process_info( - self, observations: Optional[List[Observation]] = None, running_memorys: Optional[List[Dict]] = None, *infos - ) -> List[InfoBase]: - """处理信息对象 - - Args: - *infos: 可变数量的InfoBase类型的信息对象 - - Returns: - List[InfoBase]: 处理后的结构化信息列表 - """ - relation_info_str = await self.relation_identify(observations) - - 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 relation_identify( - self, observations: Optional[List[Observation]] = None, - ): - """ - 在回复前进行思考,生成内心想法并收集工具调用结果 - - 参数: - observations: 观察信息 - - 返回: - 如果return_prompt为False: - tuple: (current_mind, past_mind) 当前想法和过去的想法列表 - 如果return_prompt为True: - tuple: (current_mind, past_mind, prompt) 当前想法、过去的想法列表和使用的prompt - """ - - if observations is None: - observations = [] - for observation in observations: - if isinstance(observation, ChattingObservation): - # 获取聊天元信息 - is_group_chat = observation.is_group_chat - chat_target_info = observation.chat_target_info - chat_target_name = "对方" # 私聊默认名称 - if not is_group_chat and chat_target_info: - # 优先使用person_name,其次user_nickname,最后回退到默认值 - chat_target_name = ( - chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name - ) - # 获取聊天内容 - chat_observe_info = observation.get_observe_info() - person_list = observation.person_list - - nickname_str = "" - for nicknames in global_config.bot.alias_names: - nickname_str += f"{nicknames}," - name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。" - - if is_group_chat: - relation_prompt_init = "你对群聊里的人的印象是:\n" - else: - relation_prompt_init = "你对对方的印象是:\n" - - relation_prompt = "" - for person in person_list: - relation_prompt += f"{await relationship_manager.build_relationship_info(person, is_id=True)}\n" - - if relation_prompt: - relation_prompt = relation_prompt_init + relation_prompt - else: - relation_prompt = relation_prompt_init + "没有特别在意的人\n" - - prompt = (await global_prompt_manager.get_prompt_async("relationship_prompt")).format( - name_block=name_block, - relation_prompt=relation_prompt, - time_now=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), - chat_observe_info=chat_observe_info, - ) - # The above code is a Python script that is attempting to print the variable `prompt`. - # However, the code is not complete as the content of the `prompt` variable is missing. - # print(prompt) - - content = "" - try: - content, _ = await self.llm_model.generate_response_async(prompt=prompt) - if not content: - logger.warning(f"{self.log_prefix} LLM返回空结果,关系识别失败。") - except Exception as e: - # 处理总体异常 - logger.error(f"{self.log_prefix} 执行LLM请求或处理响应时出错: {e}") - logger.error(traceback.format_exc()) - content = "关系识别过程中出现错误" - - if content == "None": - content = "" - # 记录初步思考结果 - logger.info(f"{self.log_prefix} 关系识别prompt: \n{prompt}\n") - logger.info(f"{self.log_prefix} 关系识别: {content}") - - return content - -init_prompt() - -# ==== 只复制最小依赖的relationship_manager ==== -class SimpleRelationshipManager: - async def build_relationship_info(self, person, is_id: bool = False) -> str: - if is_id: - person_id = person - else: - person_id = person_info_manager.get_person_id(person[0], person[1]) - - person_name = await person_info_manager.get_value(person_id, "person_name") - if not person_name or person_name == "none": - return "" - impression = await person_info_manager.get_value(person_id, "impression") - interaction = await person_info_manager.get_value(person_id, "interaction") - points = await person_info_manager.get_value(person_id, "points") or [] - - if isinstance(points, str): - try: - import ast - points = ast.literal_eval(points) - except (SyntaxError, ValueError): - points = [] - - import random - random_points = random.sample(points, min(3, len(points))) if points else [] - - nickname_str = await person_info_manager.get_value(person_id, "nickname") - platform = await person_info_manager.get_value(person_id, "platform") - relation_prompt = f"'{person_name}' ,ta在{platform}上的昵称是{nickname_str}。" - - if impression: - relation_prompt += f"你对ta的印象是:{impression}。" - if interaction: - relation_prompt += f"你与ta的关系是:{interaction}。" - if random_points: - for point in random_points: - point_str = f"时间:{point[2]}。内容:{point[0]}" - relation_prompt += f"你记得{person_name}最近的点是:{point_str}。" - return relation_prompt - -# 用于替换原有的relationship_manager -relationship_manager = SimpleRelationshipManager() - -def get_raw_msg_by_timestamp_random( - timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest" -) -> List[Dict[str, Any]]: - """先在范围时间戳内随机选择一条消息,取得消息的chat_id,然后根据chat_id获取该聊天在指定时间戳范围内的消息""" - # 获取所有消息,只取chat_id字段 - filter_query = {"time": {"$gt": timestamp_start, "$lt": timestamp_end}} - all_msgs = find_messages(message_filter=filter_query) - if not all_msgs: - return [] - # 随机选一条 - msg = random.choice(all_msgs) - chat_id = msg["chat_id"] - timestamp_start = msg["time"] - # 用 chat_id 获取该聊天在指定时间戳范围内的消息 - filter_query = {"chat_id": chat_id, "time": {"$gt": timestamp_start, "$lt": timestamp_end}} - sort_order = [("time", 1)] if limit == 0 else None - return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode="earliest") - -def _build_readable_messages_internal( - messages: List[Dict[str, Any]], - replace_bot_name: bool = True, - merge_messages: bool = False, - timestamp_mode: str = "relative", - truncate: bool = False, -) -> Tuple[str, List[Tuple[float, str, str]]]: - """内部辅助函数,构建可读消息字符串和原始消息详情列表""" - if not messages: - return "", [] - - message_details_raw: List[Tuple[float, str, str]] = [] - - # 1 & 2: 获取发送者信息并提取消息组件 - for msg in messages: - # 检查是否是动作记录 - if msg.get("is_action_record", False): - is_action = True - timestamp = msg.get("time") - content = msg.get("display_message", "") - message_details_raw.append((timestamp, global_config.bot.nickname, content, is_action)) - continue - - # 检查并修复缺少的user_info字段 - if "user_info" not in msg: - msg["user_info"] = { - "platform": msg.get("user_platform", ""), - "user_id": msg.get("user_id", ""), - "user_nickname": msg.get("user_nickname", ""), - "user_cardname": msg.get("user_cardname", ""), - } - - user_info = msg.get("user_info", {}) - platform = user_info.get("platform") - user_id = user_info.get("user_id") - user_nickname = user_info.get("user_nickname") - user_cardname = user_info.get("user_cardname") - timestamp = msg.get("time") - - if msg.get("display_message"): - content = msg.get("display_message") - else: - content = msg.get("processed_plain_text", "") - - if "ᶠ" in content: - content = content.replace("ᶠ", "") - if "ⁿ" in content: - content = content.replace("ⁿ", "") - - if not all([platform, user_id, timestamp is not None]): - continue - - person_id = person_info_manager.get_person_id(platform, user_id) - if replace_bot_name and user_id == global_config.bot.qq_account: - person_name = f"{global_config.bot.nickname}(你)" - else: - person_name = person_info_manager.get_value_sync(person_id, "person_name") - - if not person_name: - if user_cardname: - person_name = f"昵称:{user_cardname}" - elif user_nickname: - person_name = f"{user_nickname}" - else: - person_name = "某人" - - if content != "": - message_details_raw.append((timestamp, person_name, content, False)) - - if not message_details_raw: - return "", [] - - message_details_raw.sort(key=lambda x: x[0]) - - # 为每条消息添加一个标记,指示它是否是动作记录 - message_details_with_flags = [] - for timestamp, name, content, is_action in message_details_raw: - message_details_with_flags.append((timestamp, name, content, is_action)) - - # 应用截断逻辑 - message_details: List[Tuple[float, str, str, bool]] = [] - n_messages = len(message_details_with_flags) - if truncate and n_messages > 0: - for i, (timestamp, name, content, is_action) in enumerate(message_details_with_flags): - if is_action: - message_details.append((timestamp, name, content, is_action)) - continue - - percentile = i / n_messages - original_len = len(content) - limit = -1 - - if percentile < 0.2: - limit = 50 - replace_content = "......(记不清了)" - elif percentile < 0.5: - limit = 100 - replace_content = "......(有点记不清了)" - elif percentile < 0.7: - limit = 200 - replace_content = "......(内容太长了)" - elif percentile < 1.0: - limit = 300 - replace_content = "......(太长了)" - - truncated_content = content - if 0 < limit < original_len: - truncated_content = f"{content[:limit]}{replace_content}" - - message_details.append((timestamp, name, truncated_content, is_action)) - else: - message_details = message_details_with_flags - - # 合并连续消息 - merged_messages = [] - if merge_messages and message_details: - current_merge = { - "name": message_details[0][1], - "start_time": message_details[0][0], - "end_time": message_details[0][0], - "content": [message_details[0][2]], - "is_action": message_details[0][3] - } - - for i in range(1, len(message_details)): - timestamp, name, content, is_action = message_details[i] - - if is_action or current_merge["is_action"]: - merged_messages.append(current_merge) - current_merge = { - "name": name, - "start_time": timestamp, - "end_time": timestamp, - "content": [content], - "is_action": is_action - } - continue - - if name == current_merge["name"] and (timestamp - current_merge["end_time"] <= 60): - current_merge["content"].append(content) - current_merge["end_time"] = timestamp - else: - merged_messages.append(current_merge) - current_merge = { - "name": name, - "start_time": timestamp, - "end_time": timestamp, - "content": [content], - "is_action": is_action - } - merged_messages.append(current_merge) - elif message_details: - for timestamp, name, content, is_action in message_details: - merged_messages.append( - { - "name": name, - "start_time": timestamp, - "end_time": timestamp, - "content": [content], - "is_action": is_action - } - ) - - # 格式化为字符串 - output_lines = [] - for merged in merged_messages: - readable_time = translate_timestamp_to_human_readable(merged["start_time"], mode=timestamp_mode) - - if merged["is_action"]: - output_lines.append(f"{readable_time}, {merged['content'][0]}") - else: - header = f"{readable_time}, {merged['name']} :" - output_lines.append(header) - for line in merged["content"]: - stripped_line = line.strip() - if stripped_line: - if stripped_line.endswith("。"): - stripped_line = stripped_line[:-1] - if not stripped_line.endswith("(内容太长)"): - output_lines.append(f"{stripped_line}") - else: - output_lines.append(stripped_line) - output_lines.append("\n") - - formatted_string = "".join(output_lines).strip() - return formatted_string, [(t, n, c) for t, n, c, is_action in message_details if not is_action] - -def build_readable_messages( - messages: List[Dict[str, Any]], - replace_bot_name: bool = True, - merge_messages: bool = False, - timestamp_mode: str = "relative", - read_mark: float = 0.0, - truncate: bool = False, - show_actions: bool = False, -) -> str: - """将消息列表转换为可读的文本格式""" - copy_messages = [msg.copy() for msg in messages] - - if show_actions and copy_messages: - min_time = min(msg.get("time", 0) for msg in copy_messages) - max_time = max(msg.get("time", 0) for msg in copy_messages) - chat_id = copy_messages[0].get("chat_id") if copy_messages else None - - actions = ActionRecords.select().where( - (ActionRecords.time >= min_time) & - (ActionRecords.time <= max_time) & - (ActionRecords.chat_id == chat_id) - ).order_by(ActionRecords.time) - - for action in actions: - if action.action_build_into_prompt: - action_msg = { - "time": action.time, - "user_id": global_config.bot.qq_account, - "user_nickname": global_config.bot.nickname, - "user_cardname": "", - "processed_plain_text": f"{action.action_prompt_display}", - "display_message": f"{action.action_prompt_display}", - "chat_info_platform": action.chat_info_platform, - "is_action_record": True, - "action_name": action.action_name, - } - copy_messages.append(action_msg) - - copy_messages.sort(key=lambda x: x.get("time", 0)) - - if read_mark <= 0: - formatted_string, _ = _build_readable_messages_internal( - copy_messages, replace_bot_name, merge_messages, timestamp_mode, truncate - ) - return formatted_string - else: - messages_before_mark = [msg for msg in copy_messages if msg.get("time", 0) <= read_mark] - messages_after_mark = [msg for msg in copy_messages if msg.get("time", 0) > read_mark] - - formatted_before, _ = _build_readable_messages_internal( - messages_before_mark, replace_bot_name, merge_messages, timestamp_mode, truncate - ) - formatted_after, _ = _build_readable_messages_internal( - messages_after_mark, - replace_bot_name, - merge_messages, - timestamp_mode, - ) - - read_mark_line = "\n--- 以上消息是你已经看过---\n--- 请关注以下未读的新消息---\n" - - if formatted_before and formatted_after: - return f"{formatted_before}{read_mark_line}{formatted_after}" - elif formatted_before: - return f"{formatted_before}{read_mark_line}" - elif formatted_after: - return f"{read_mark_line}{formatted_after}" - else: - return read_mark_line.strip() - -async def test_relationship_processor(): - """测试关系处理器的功能""" - - # 测试10次 - for i in range(10): - print(f"\n=== 测试 {i+1} ===") - - # 获取随机消息 - current_time = time.time() - start_time = current_time - 864000 # 10天前 - messages = get_raw_msg_by_timestamp_random(start_time, current_time, limit=25) - - if not messages: - print("没有找到消息,跳过此次测试") - continue - - chat_id = messages[0]["chat_id"] - - # 构建可读消息 - chat_observe_info = build_readable_messages( - messages, - replace_bot_name=True, - timestamp_mode="normal_no_YMD", - truncate=True, - show_actions=True, - ) - # print(chat_observe_info) - # 创建观察对象 - processor = RelationshipProcessor(chat_id) - observation = ChattingObservation(chat_id) - observation.talking_message_str = chat_observe_info - observation.talking_message = messages # 设置消息列表 - observation.person_list = await get_person_id_list(messages) # 使用get_person_id_list获取person_list - - # 处理关系 - result = await processor.process_info([observation]) - - if result: - print("\n关系识别结果:") - print(result[0].get_processed_info()) - else: - print("关系识别失败") - - # 等待一下,避免请求过快 - await asyncio.sleep(1) - -if __name__ == "__main__": - asyncio.run(test_relationship_processor()) \ No newline at end of file