feat(reply): 引入 reply 和 respond 动作,优化消息回复机制
- 增加 reply 动作,针对单条消息进行深度回复,使用 s4u 模板。 - 增加 respond 动作,统一回应未读消息,使用 normal 模板。 - 更新核心动作插件以支持新动作,确保配置选项可用。 - 优化动作执行逻辑,提升对话流畅性和响应准确性。
This commit is contained in:
@@ -75,16 +75,29 @@ class ChatterPlanFilter:
|
||||
|
||||
if "reply" in plan.available_actions and reply_not_available:
|
||||
# 如果reply动作不可用,但llm返回的仍然有reply,则改为no_reply
|
||||
if (
|
||||
isinstance(parsed_json, dict)
|
||||
and parsed_json.get("actions", {}).get("action_type", "") == "reply"
|
||||
):
|
||||
parsed_json["actions"]["action_type"] = "no_reply"
|
||||
if isinstance(parsed_json, dict):
|
||||
actions_obj = parsed_json.get("actions", {})
|
||||
# actions 可能是字典或列表
|
||||
if isinstance(actions_obj, dict) and actions_obj.get("action_type", "") == "reply":
|
||||
parsed_json["actions"]["action_type"] = "no_reply"
|
||||
elif isinstance(actions_obj, list):
|
||||
for action_item in actions_obj:
|
||||
if isinstance(action_item, dict) and action_item.get("action_type", "") == "reply":
|
||||
action_item["action_type"] = "no_reply"
|
||||
if "reason" in action_item:
|
||||
action_item["reason"] += " (但由于兴趣度不足,reply动作不可用,已改为no_reply)"
|
||||
elif isinstance(parsed_json, list):
|
||||
for item in parsed_json:
|
||||
if isinstance(item, dict) and item.get("actions", {}).get("action_type", "") == "reply":
|
||||
item["actions"]["action_type"] = "no_reply"
|
||||
item["actions"]["reason"] += " (但由于兴趣度不足,reply动作不可用,已改为no_reply)"
|
||||
if isinstance(item, dict):
|
||||
actions_obj = item.get("actions", {})
|
||||
if isinstance(actions_obj, dict) and actions_obj.get("action_type", "") == "reply":
|
||||
item["actions"]["action_type"] = "no_reply"
|
||||
elif isinstance(actions_obj, list):
|
||||
for action_item in actions_obj:
|
||||
if isinstance(action_item, dict) and action_item.get("action_type", "") == "reply":
|
||||
action_item["action_type"] = "no_reply"
|
||||
if "reason" in action_item:
|
||||
action_item["reason"] += " (但由于兴趣度不足,reply动作不可用,已改为no_reply)"
|
||||
|
||||
if isinstance(parsed_json, dict):
|
||||
parsed_json = [parsed_json]
|
||||
|
||||
@@ -333,16 +333,17 @@ class ChatterActionPlanner:
|
||||
context.processing_message_id = target_message_id
|
||||
logger.debug(f"Normal模式 - 开始处理目标消息: {target_message_id}")
|
||||
|
||||
# 4. 构建回复动作(Normal模式的简化流程)
|
||||
# 4. 构建回复动作(Normal模式使用respond动作)
|
||||
from src.common.data_models.info_data_model import ActionPlannerInfo, Plan
|
||||
from src.plugin_system.base.component_types import ChatType
|
||||
|
||||
# 构建目标消息字典 - 使用 flatten() 方法获取扁平化的字典
|
||||
target_message_dict = target_message.flatten()
|
||||
|
||||
reply_action = ActionPlannerInfo(
|
||||
action_type="reply",
|
||||
reasoning="Normal模式 - 兴趣度达到阈值,直接回复(简化流程)",
|
||||
# Normal模式使用respond动作,表示统一回应未读消息
|
||||
respond_action = ActionPlannerInfo(
|
||||
action_type="respond",
|
||||
reasoning="Normal模式 - 兴趣度达到阈值,使用respond动作统一回应未读消息",
|
||||
action_data={"target_message_id": target_message.message_id},
|
||||
action_message=target_message,
|
||||
should_quote_reply=False, # Normal模式默认不引用回复,保持对话流畅
|
||||
@@ -354,14 +355,14 @@ class ChatterActionPlanner:
|
||||
chat_id=self.chat_id,
|
||||
chat_type=ChatType.PRIVATE if not context else context.chat_type,
|
||||
mode=ChatMode.NORMAL,
|
||||
decided_actions=[reply_action],
|
||||
decided_actions=[respond_action],
|
||||
)
|
||||
|
||||
# 5. 执行reply动作
|
||||
# 5. 执行respond动作
|
||||
execution_result = await self.executor.execute(minimal_plan)
|
||||
self._update_stats_from_execution_result(execution_result)
|
||||
|
||||
logger.info("Normal模式 - 执行reply动作完成")
|
||||
logger.info("Normal模式 - 执行respond动作完成")
|
||||
|
||||
# 6. 更新兴趣计算器状态(回复成功,重置不回复计数)
|
||||
await self._update_interest_calculator_state(replied=True)
|
||||
@@ -374,7 +375,7 @@ class ChatterActionPlanner:
|
||||
# 8. 检查是否需要退出Normal模式
|
||||
await self._check_exit_normal_mode(context)
|
||||
|
||||
return [asdict(reply_action)], target_message_dict
|
||||
return [asdict(respond_action)], target_message_dict
|
||||
else:
|
||||
# 未达到reply阈值
|
||||
logger.debug("Normal模式 - 未达到reply阈值,不执行回复")
|
||||
|
||||
@@ -43,29 +43,30 @@ def init_prompts():
|
||||
|
||||
# 目标
|
||||
你的任务是根据当前对话,给出一个或多个动作,构成一次完整的响应组合。
|
||||
- 主要动作:通常是 reply(如需回复)。
|
||||
- 主要动作:通常是 reply或respond(如需回复)。
|
||||
- 辅助动作(可选):如 emoji、poke_user 等,用于增强表达。
|
||||
|
||||
# 决策流程
|
||||
1. 已读仅供参考,不能对已读执行任何动作。
|
||||
2. 目标消息必须来自未读历史,并使用其前缀 <m...> 作为 target_message_id。
|
||||
3. **【重要】兴趣度优先原则**:每条未读消息后都标注了 [兴趣度: X.XXX],数值越高表示该消息越值得你关注和回复。在选择回复目标时,**应优先选择兴趣度高的消息**(通常 ≥0.5 表示较高兴趣),除非有特殊情况(如被直接@或提问)。
|
||||
3. 兴趣度优先原则:每条未读消息后都标注了 [兴趣度: X.XXX],数值越高表示该消息越值得你关注和回复。在选择回复目标时,**应优先选择兴趣度高的消息**(通常 ≥0.5 表示较高兴趣),除非有特殊情况(如被直接@或提问)。
|
||||
4. 优先级:
|
||||
- 直接针对你:@你、回复你、点名提问、引用你的消息。
|
||||
- **兴趣度高的消息**:兴趣度 ≥0.5 的消息应优先考虑回复。
|
||||
- 与你强相关的话题或你熟悉的问题。
|
||||
- 其他与上下文弱相关的内容最后考虑。
|
||||
{mentioned_bonus}
|
||||
4. 多目标:若多人同时需要回应,请在 actions 中并行生成多个 reply,每个都指向各自的 target_message_id。
|
||||
5. **【核心规则】处理无上下文的纯表情包**: 对不含任何实质文本、且无紧密上下文互动的纯**表情包**消息(如消息内容仅为“[表情包:xxxxx]”),应默认选择 `no_action`。
|
||||
6. **【!!!绝对禁止!!!】处理失败消息**: 绝不能回复任何指示媒体内容(图片、表情包等)处理失败的消息。如果消息中出现如“[表情包(描述生成失败)]”或“[图片(描述生成失败)]”等文字,必须将其视为系统错误提示,并立即选择`no_action`。
|
||||
6. 风格:保持人设一致;避免重复你说过的话;避免冗余和口头禅。
|
||||
5. 多目标:若多人同时需要回应,请在 actions 中并行生成多个 reply,每个都指向各自的 target_message_id。
|
||||
6. 处理无上下文的纯表情包: 对不含任何实质文本、且无紧密上下文互动的纯**表情包**消息(如消息内容仅为“[表情包:xxxxx]”),应默认选择 `no_action`。
|
||||
7. 处理失败消息: 绝不能回复任何指示媒体内容(图片、表情包等)处理失败的消息。如果消息中出现如“[表情包(描述生成失败)]”或“[图片(描述生成失败)]”等文字,必须将其视为系统错误提示,并立即选择`no_action`。
|
||||
8. 正确决定回复时机: 在决定reply或respond前,务必评估当前对话氛围和上下文连贯性。避免在不合适的时机(如对方情绪低落、话题不相关等,对方并没有和你对话,贸然插入会很令人讨厌等)进行回复,以免打断对话流或引起误解。如判断当前不适合回复,请选择`no_action`。
|
||||
9. 认清自己的身份和角色: 在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。
|
||||
|
||||
# 思绪流规范(thinking)
|
||||
- 真实、自然、非结论化,像给自己看的随笔。
|
||||
- 描述你看到/想到/感觉到的过程,不要出现"因此/我决定"等总结词。
|
||||
- 直接使用对方昵称,而不是 <m1>/<m2> 这样的标签。
|
||||
- **禁止出现"兴趣度、分数"等技术术语或内部实现细节**。兴趣度仅用于你内部的决策权重,不要在thinking中提及,而应该用自然语言描述你对消息的感受(如"这个话题挺有意思的"、"我对这个很感兴趣"等)。
|
||||
- 禁止出现"兴趣度、分数"等技术术语或内部实现细节。兴趣度仅用于你内部的决策权重,不要在thinking中提及,而应该用自然语言描述你对消息的感受(如"这个话题挺有意思的"、"我对这个很感兴趣"等)。
|
||||
|
||||
## 可用动作列表
|
||||
{action_options_text}
|
||||
@@ -77,12 +78,10 @@ def init_prompts():
|
||||
"thinking": "在这里写下你的思绪流...",
|
||||
"actions": [
|
||||
{{
|
||||
"action_type": "reply",
|
||||
"action_type": "respond",
|
||||
"reasoning": "选择该动作的理由",
|
||||
"action_data": {{
|
||||
"target_message_id": "m123",
|
||||
"content": "你的回复内容",
|
||||
"should_quote_reply": false
|
||||
}}
|
||||
}}
|
||||
]
|
||||
@@ -116,21 +115,6 @@ def init_prompts():
|
||||
}}
|
||||
```
|
||||
|
||||
# 引用回复控制(should_quote_reply)
|
||||
在群聊中回复消息时,你可以通过 `should_quote_reply` 参数控制是否引用原消息:
|
||||
- **true**: 明确引用原消息(适用于需要明确指向特定消息时,如回答问题、回应多人之一、回复较早的消息)
|
||||
- **false**: 不引用原消息(适用于自然对话流、接续最新话题、轻松闲聊等场景)
|
||||
- **不填写**: 系统将自动决定(默认不引用,让对话更流畅)
|
||||
|
||||
**【重要】默认策略:大多数情况下应该使用 `false` 以保持对话自然流畅**
|
||||
|
||||
**使用建议**:
|
||||
- 当对话自然流畅、你的回复是接续最新话题时,**建议明确设为 `false`** 以避免打断对话节奏
|
||||
- 当需要明确回复某个特定用户或特定问题时,设为 `true` 以帮助定位
|
||||
- 当群聊中多人同时发言,你要回复其中一个较早的消息(非最新消息)时,设为 `true`
|
||||
- 当有人直接@你或明确向你提问时,可以考虑设为 `true` 表明你在回复他
|
||||
- 私聊场景**必须**设为 `false` 或不填(因为只有两个人,引用是多余的)
|
||||
|
||||
# 强制规则
|
||||
- 需要目标消息的动作(reply/poke_user/set_emoji_like 等),必须提供准确的 target_message_id(来自未读历史里的 <m...> 标签)。
|
||||
- 当动作需要额外参数时,必须在 action_data 中补全。
|
||||
|
||||
108
src/plugins/built_in/core_actions/reply.py
Normal file
108
src/plugins/built_in/core_actions/reply.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
回复动作模块
|
||||
|
||||
定义了两种回复动作:
|
||||
- reply: 针对单条消息的深度回复(使用 s4u 模板)
|
||||
- respond: 对未读消息的统一回应(使用 normal 模板)
|
||||
"""
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system import ActionActivationType, BaseAction, ChatMode
|
||||
|
||||
logger = get_logger("reply_actions")
|
||||
|
||||
|
||||
class ReplyAction(BaseAction):
|
||||
"""Reply动作 - 针对单条消息的深度回复
|
||||
|
||||
特点:
|
||||
- 使用 s4u (Speak for You) 模板
|
||||
- 专注于理解和回应单条消息的具体内容
|
||||
- 适合 Focus 模式下的精准回复
|
||||
"""
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "reply"
|
||||
action_description = "针对特定消息进行精准回复。深度理解并回应单条消息的具体内容。需要指定目标消息ID。"
|
||||
|
||||
# 激活设置
|
||||
activation_type = ActionActivationType.ALWAYS # 回复动作总是可用
|
||||
mode_enable = ChatMode.ALL # 在所有模式下都可用
|
||||
parallel_action = False # 回复动作不能与其他动作并行
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters: ClassVar = {
|
||||
"target_message_id": "要回复的目标消息ID(必需,来自未读消息的 <m...> 标签)",
|
||||
"content": "回复的具体内容(可选,由LLM生成)",
|
||||
"should_quote_reply": "是否引用原消息(可选,true/false,默认false。群聊中回复较早消息或需要明确指向时使用true)",
|
||||
}
|
||||
|
||||
# 动作使用场景
|
||||
action_require: ClassVar = [
|
||||
"需要针对特定消息进行精准回复时使用",
|
||||
"适合单条消息的深度理解和回应",
|
||||
"必须提供准确的 target_message_id(来自未读历史的 <m...> 标签)",
|
||||
"私聊场景必须使用此动作(不支持 respond)",
|
||||
"群聊中需要明确回应某个特定用户或问题时使用",
|
||||
"关注单条消息的具体内容和上下文细节",
|
||||
]
|
||||
|
||||
# 关联类型
|
||||
associated_types: ClassVar[list[str]] = ["text"]
|
||||
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行reply动作
|
||||
|
||||
注意:实际的回复生成由 action_manager 统一处理
|
||||
这里只是标记使用 reply 动作(s4u 模板)
|
||||
"""
|
||||
logger.info(f"{self.log_prefix} 使用 reply 动作(s4u 模板)")
|
||||
return True, ""
|
||||
|
||||
|
||||
class RespondAction(BaseAction):
|
||||
"""Respond动作 - 对未读消息的统一回应
|
||||
|
||||
特点:
|
||||
- 关注整体对话动态和未读消息的统一回应
|
||||
- 适合对于群聊消息下的宏观回应
|
||||
- 避免与单一用户深度对话而忽略其他用户的消息
|
||||
"""
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "respond"
|
||||
action_description = "统一回应所有未读消息。理解整体对话动态和话题走向,生成连贯的回复。无需指定目标消息。"
|
||||
|
||||
# 激活设置
|
||||
activation_type = ActionActivationType.ALWAYS # 回应动作总是可用
|
||||
mode_enable = ChatMode.ALL # 在所有模式下都可用
|
||||
parallel_action = False # 回应动作不能与其他动作并行
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters: ClassVar = {
|
||||
"content": "回复的具体内容(可选,由LLM生成)",
|
||||
}
|
||||
|
||||
# 动作使用场景
|
||||
action_require: ClassVar = [
|
||||
"需要统一回应多条未读消息时使用(Normal 模式专用)",
|
||||
"适合理解整体对话动态而非单条消息",
|
||||
"不需要指定 target_message_id,会自动处理所有未读消息",
|
||||
"关注对话流程、话题走向和整体氛围",
|
||||
"适合群聊中的自然对话流,无需精确指向特定消息",
|
||||
"可以同时回应多个话题或参与者",
|
||||
]
|
||||
|
||||
# 关联类型
|
||||
associated_types: ClassVar[list[str]] = ["text"]
|
||||
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行respond动作
|
||||
|
||||
注意:实际的回复生成由 action_manager 统一处理
|
||||
这里只是标记使用 respond 动作(normal 模板)
|
||||
"""
|
||||
logger.info(f"{self.log_prefix} 使用 respond 动作(normal 模板)")
|
||||
return True, ""
|
||||
@@ -16,6 +16,7 @@ from src.plugin_system.base.config_types import ConfigField
|
||||
|
||||
# 导入API模块 - 标准Python包方式
|
||||
from src.plugins.built_in.core_actions.emoji import EmojiAction
|
||||
from src.plugins.built_in.core_actions.reply import ReplyAction, RespondAction
|
||||
|
||||
logger = get_logger("core_actions")
|
||||
|
||||
@@ -52,7 +53,8 @@ class CoreActionsPlugin(BasePlugin):
|
||||
"config_version": ConfigField(type=str, default="0.6.0", description="配置文件版本"),
|
||||
},
|
||||
"components": {
|
||||
"enable_reply": ConfigField(type=bool, default=True, description="是否启用基本回复动作"),
|
||||
"enable_reply": ConfigField(type=bool, default=True, description="是否启用 reply 动作(s4u模板)"),
|
||||
"enable_respond": ConfigField(type=bool, default=True, description="是否启用 respond 动作(normal模板)"),
|
||||
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用发送表情/图片动作"),
|
||||
},
|
||||
}
|
||||
@@ -62,6 +64,16 @@ class CoreActionsPlugin(BasePlugin):
|
||||
|
||||
# --- 根据配置注册组件 ---
|
||||
components: ClassVar = []
|
||||
|
||||
# 注册 reply 动作
|
||||
if self.get_config("components.enable_reply", True):
|
||||
components.append((ReplyAction.get_action_info(), ReplyAction))
|
||||
|
||||
# 注册 respond 动作
|
||||
if self.get_config("components.enable_respond", True):
|
||||
components.append((RespondAction.get_action_info(), RespondAction))
|
||||
|
||||
# 注册 emoji 动作
|
||||
if self.get_config("components.enable_emoji", True):
|
||||
components.append((EmojiAction.get_action_info(), EmojiAction))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user