feat(actions): 支持同时进行回复与其他动作
重构了动作执行流程,以支持更丰富的多动作组合,例如在发送文本回复的同时发送一个表情。 主要变更: - **执行流程**: 在 `CycleProcessor` 中,将动作分为“回复”和“其他”两类。系统会先串行执行回复动作,再并行执行所有其他动作,确保了核心回复的优先性。 - **规划逻辑**: 在 `Planner` 中优化了提示词,并增加后处理步骤,以鼓励并确保在回复时触发补充性动作(如100%概率的emoji)。 - **emoji动作**: 重构了表情选择逻辑,现在会评估所有可用的表情,而不仅仅是随机抽样,提高了选择的准确性。 - **修复**: 修复了 `ActionModifier` 中随机激活概率为100%的动作可能不触发的bug。
This commit is contained in:
@@ -46,7 +46,7 @@ class SetEmojiLikeAction(BaseAction):
|
||||
|
||||
# === 基本信息(必须填写)===
|
||||
action_name = "set_emoji_like"
|
||||
action_description = "为消息设置表情回应/贴表情"
|
||||
action_description = "为一个已存在的消息添加点赞或表情回应(也叫‘贴表情’)"
|
||||
activation_type = ActionActivationType.ALWAYS # 消息接收时激活(?)
|
||||
chat_type_allow = ChatType.GROUP
|
||||
parallel_action = True
|
||||
@@ -64,10 +64,9 @@ class SetEmojiLikeAction(BaseAction):
|
||||
"set": "是否设置回应 (True/False)",
|
||||
}
|
||||
action_require = [
|
||||
"当需要对消息贴表情时使用",
|
||||
"当你想回应某条消息但又不想发文字时使用",
|
||||
"不要连续发送,如果你已经贴表情包,就不要选择此动作",
|
||||
"当你想用贴表情回应某条消息时使用",
|
||||
"当需要对一个已存在消息进行‘贴表情’回应时使用",
|
||||
"这是一个对旧消息的操作,而不是发送新消息",
|
||||
"如果你想发送一个新的表情包消息,请使用 'emoji' 动作",
|
||||
]
|
||||
llm_judge_prompt = """
|
||||
判定是否需要使用贴表情动作的条件:
|
||||
|
||||
@@ -299,63 +299,67 @@ class CycleProcessor:
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
# 创建所有动作的后台任务
|
||||
action_tasks = [asyncio.create_task(execute_action(action)) for action in actions]
|
||||
# 分离 reply 动作和其他动作
|
||||
reply_actions = [a for a in actions if a.get("action_type") == "reply"]
|
||||
other_actions = [a for a in actions if a.get("action_type") != "reply"]
|
||||
|
||||
# 并行执行所有任务
|
||||
results = await asyncio.gather(*action_tasks, return_exceptions=True)
|
||||
|
||||
# 处理执行结果
|
||||
reply_loop_info = None
|
||||
reply_text_from_reply = ""
|
||||
action_success = False
|
||||
action_reply_text = ""
|
||||
action_command = ""
|
||||
other_actions_results = []
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, BaseException):
|
||||
logger.error(f"{self.log_prefix} 动作执行异常: {result}")
|
||||
continue
|
||||
|
||||
action_info = actions[i]
|
||||
if result["action_type"] != "reply":
|
||||
action_success = result["success"]
|
||||
action_reply_text = result["reply_text"]
|
||||
action_command = result.get("command", "")
|
||||
elif result["action_type"] == "reply":
|
||||
if result["success"]:
|
||||
reply_loop_info = result["loop_info"]
|
||||
reply_text_from_reply = result["reply_text"]
|
||||
# 1. 首先串行执行所有 reply 动作(通常只有一个)
|
||||
if reply_actions:
|
||||
logger.info(f"{self.log_prefix} 正在执行文本回复...")
|
||||
for action in reply_actions:
|
||||
result = await execute_action(action)
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"{self.log_prefix} 回复动作执行异常: {result}")
|
||||
continue
|
||||
if result.get("success"):
|
||||
reply_loop_info = result.get("loop_info")
|
||||
reply_text_from_reply = result.get("reply_text", "")
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 回复动作执行失败")
|
||||
|
||||
# 2. 然后并行执行所有其他动作
|
||||
if other_actions:
|
||||
logger.info(f"{self.log_prefix} 正在执行附加动作: {[a.get('action_type') for a in other_actions]}")
|
||||
other_action_tasks = [asyncio.create_task(execute_action(action)) for action in other_actions]
|
||||
results = await asyncio.gather(*other_action_tasks, return_exceptions=True)
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, BaseException):
|
||||
logger.error(f"{self.log_prefix} 附加动作执行异常: {result}")
|
||||
continue
|
||||
other_actions_results.append(result)
|
||||
|
||||
# 构建最终的循环信息
|
||||
if reply_loop_info:
|
||||
# 如果有回复信息,使用回复的loop_info作为基础
|
||||
loop_info = reply_loop_info
|
||||
# 更新动作执行信息
|
||||
loop_info["loop_action_info"].update(
|
||||
{
|
||||
"action_taken": action_success,
|
||||
"command": action_command,
|
||||
"taken_time": time.time(),
|
||||
}
|
||||
)
|
||||
# 将其他动作的结果合并到loop_info中
|
||||
if "other_actions" not in loop_info["loop_action_info"]:
|
||||
loop_info["loop_action_info"]["other_actions"] = []
|
||||
loop_info["loop_action_info"]["other_actions"].extend(other_actions_results)
|
||||
reply_text = reply_text_from_reply
|
||||
else:
|
||||
# 没有回复信息,构建纯动作的loop_info
|
||||
# 即使没有回复,也要正确处理其他动作
|
||||
final_action_taken = any(res.get("success", False) for res in other_actions_results)
|
||||
final_reply_text = " ".join(res.get("reply_text", "") for res in other_actions_results if res.get("reply_text"))
|
||||
final_command = " ".join(res.get("command", "") for res in other_actions_results if res.get("command"))
|
||||
|
||||
loop_info = {
|
||||
"loop_plan_info": {
|
||||
"action_result": actions,
|
||||
},
|
||||
"loop_action_info": {
|
||||
"action_taken": action_success,
|
||||
"reply_text": action_reply_text,
|
||||
"command": action_command,
|
||||
"action_taken": final_action_taken,
|
||||
"reply_text": final_reply_text,
|
||||
"command": final_command,
|
||||
"taken_time": time.time(),
|
||||
"other_actions": other_actions_results,
|
||||
},
|
||||
}
|
||||
reply_text = action_reply_text
|
||||
reply_text = final_reply_text
|
||||
|
||||
# 停止正在输入状态
|
||||
if ENABLE_S4U:
|
||||
@@ -421,7 +425,7 @@ class CycleProcessor:
|
||||
if fallback_action and fallback_action != action:
|
||||
logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}")
|
||||
action_handler = self.context.action_manager.create_action(
|
||||
action_name=fallback_action if isinstance(fallback_action, list) else fallback_action,
|
||||
action_name=fallback_action,
|
||||
action_data=action_data,
|
||||
reasoning=f"原动作'{action}'不可用,自动回退。{reasoning}",
|
||||
cycle_timers=cycle_timers,
|
||||
|
||||
@@ -205,7 +205,9 @@ class ActionModifier:
|
||||
|
||||
elif activation_type == ActionActivationType.RANDOM:
|
||||
probability = action_info.random_activation_probability
|
||||
if random.random() >= probability:
|
||||
if probability >= 1.0:
|
||||
continue # 概率为100%或更高,直接激活
|
||||
if random.random() > probability:
|
||||
reason = f"RANDOM类型未触发(概率{probability})"
|
||||
deactivated_actions.append((action_name, reason))
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}")
|
||||
|
||||
@@ -76,6 +76,8 @@ def init_prompt():
|
||||
|
||||
{action_options_text}
|
||||
|
||||
- 如果用户明确要求使用某个动作,请优先选择该动作。
|
||||
- 当一个动作可以作为另一个动作的补充时,你应该同时选择它们。例如,在回复的同时可以发送表情包(emoji)。
|
||||
你必须从上面列出的可用action中选择一个或多个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字
|
||||
|
||||
请根据动作示例,以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果只选择一个动作,也请将其包含在列表中。如果没有任何合适的动作,返回一个空列表[]。不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容:
|
||||
@@ -407,6 +409,23 @@ class ActionPlanner:
|
||||
# --- 3. 后处理 ---
|
||||
final_actions = self._filter_no_actions(final_actions)
|
||||
|
||||
# === 强制后处理:确保100%概率的动作在回复时被附带 ===
|
||||
has_reply_action = any(a.get("action_type") == "reply" for a in final_actions)
|
||||
if has_reply_action:
|
||||
for action_name, action_info in available_actions.items():
|
||||
if action_info.activation_type == ActionActivationType.RANDOM and action_info.random_activation_probability >= 1.0:
|
||||
# 检查此动作是否已被选择
|
||||
is_already_chosen = any(a.get("action_type") == action_name for a in final_actions)
|
||||
if not is_already_chosen:
|
||||
logger.info(f"{self.log_prefix}强制添加100%概率动作: {action_name}")
|
||||
final_actions.append({
|
||||
"action_type": action_name,
|
||||
"reasoning": "根据100%概率设置强制添加",
|
||||
"action_data": {},
|
||||
"action_message": self.get_latest_message(used_message_id_list),
|
||||
"available_actions": available_actions,
|
||||
})
|
||||
|
||||
if not final_actions:
|
||||
final_actions = [
|
||||
{
|
||||
@@ -531,9 +550,13 @@ class ActionPlanner:
|
||||
|
||||
if mode == ChatMode.FOCUS:
|
||||
no_action_block = """
|
||||
- 'no_reply' 表示不进行回复,等待合适的回复时机
|
||||
- 当你刚刚发送了消息,没有人回复时,选择no_reply
|
||||
- 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply
|
||||
动作:no_action
|
||||
动作描述:不选择任何动作
|
||||
{{
|
||||
"action": "no_action",
|
||||
"reason":"不动作的原因"
|
||||
}}
|
||||
|
||||
动作:no_reply
|
||||
动作描述:不进行回复,等待合适的回复时机
|
||||
- 当你刚刚发送了消息,没有人回复时,选择no_reply
|
||||
|
||||
@@ -8,10 +8,9 @@ from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||
from src.common.logger import get_logger
|
||||
|
||||
# 导入API模块 - 标准Python包方式
|
||||
from src.plugin_system.apis import emoji_api, llm_api, message_api
|
||||
|
||||
# 注释:不再需要导入NoReplyAction,因为计数器管理已移至heartFC_chat.py
|
||||
# from src.plugins.built_in.core_actions.no_reply import NoReplyAction
|
||||
from src.plugin_system.apis import llm_api, message_api
|
||||
from src.chat.emoji_system.emoji_manager import get_emoji_manager
|
||||
from src.chat.utils.utils_image import image_path_to_base64
|
||||
from src.config.config import global_config
|
||||
|
||||
|
||||
@@ -60,7 +59,6 @@ class EmojiAction(BaseAction):
|
||||
associated_types = ["emoji"]
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# sourcery skip: assign-if-exp, introduce-default-else, swap-if-else-branches, use-named-expression
|
||||
"""执行表情动作"""
|
||||
logger.info(f"{self.log_prefix} 决定发送表情")
|
||||
|
||||
@@ -69,30 +67,46 @@ class EmojiAction(BaseAction):
|
||||
reason = self.action_data.get("reason", "表达当前情绪")
|
||||
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
|
||||
|
||||
# 2. 随机获取20个表情包
|
||||
sampled_emojis = await emoji_api.get_random(30)
|
||||
if not sampled_emojis:
|
||||
logger.warning(f"{self.log_prefix} 无法获取随机表情包")
|
||||
return False, "无法获取随机表情包"
|
||||
# 2. 获取所有表情包
|
||||
emoji_manager = get_emoji_manager()
|
||||
all_emojis_obj = [e for e in emoji_manager.emoji_objects if not e.is_deleted]
|
||||
if not all_emojis_obj:
|
||||
logger.warning(f"{self.log_prefix} 无法获取任何表情包")
|
||||
return False, "无法获取任何表情包"
|
||||
|
||||
# 3. 准备情感数据
|
||||
# 3. 准备情感数据和后备列表
|
||||
emotion_map = {}
|
||||
for b64, desc, emo in sampled_emojis:
|
||||
if emo not in emotion_map:
|
||||
emotion_map[emo] = []
|
||||
emotion_map[emo].append((b64, desc))
|
||||
all_emojis_data = []
|
||||
|
||||
for emoji in all_emojis_obj:
|
||||
b64 = image_path_to_base64(emoji.full_path)
|
||||
if not b64:
|
||||
continue
|
||||
|
||||
desc = emoji.description
|
||||
emotions = emoji.emotion
|
||||
all_emojis_data.append((b64, desc))
|
||||
|
||||
for emo in emotions:
|
||||
if emo not in emotion_map:
|
||||
emotion_map[emo] = []
|
||||
emotion_map[emo].append((b64, desc))
|
||||
|
||||
if not all_emojis_data:
|
||||
logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据")
|
||||
return False, "无法加载任何有效的表情包数据"
|
||||
|
||||
available_emotions = list(emotion_map.keys())
|
||||
emoji_base64, emoji_description = "", ""
|
||||
|
||||
if not available_emotions:
|
||||
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
|
||||
emoji_base64, emoji_description, _ = random.choice(sampled_emojis)
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
else:
|
||||
# 获取最近的5条消息内容用于判断
|
||||
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
|
||||
messages_text = ""
|
||||
if recent_messages:
|
||||
# 使用message_api构建可读的消息字符串
|
||||
messages_text = message_api.build_readable_messages(
|
||||
messages=recent_messages,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
@@ -118,7 +132,7 @@ class EmojiAction(BaseAction):
|
||||
|
||||
# 5. 调用LLM
|
||||
models = llm_api.get_available_models()
|
||||
chat_model_config = models.get("utils_small") # 使用字典访问方式
|
||||
chat_model_config = models.get("utils_small")
|
||||
if not chat_model_config:
|
||||
logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM")
|
||||
return False, "未找到'utils_small'模型配置"
|
||||
@@ -128,21 +142,20 @@ class EmojiAction(BaseAction):
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"{self.log_prefix} LLM调用失败: {chosen_emotion}")
|
||||
return False, f"LLM调用失败: {chosen_emotion}"
|
||||
|
||||
chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "")
|
||||
logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}")
|
||||
|
||||
# 6. 根据选择的情感匹配表情包
|
||||
if chosen_emotion in emotion_map:
|
||||
emoji_base64, emoji_description = random.choice(emotion_map[chosen_emotion])
|
||||
logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' 的表情包: {emoji_description}")
|
||||
logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包")
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
else:
|
||||
logger.warning(
|
||||
f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包"
|
||||
)
|
||||
emoji_base64, emoji_description, _ = random.choice(sampled_emojis)
|
||||
chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "")
|
||||
logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}")
|
||||
|
||||
if chosen_emotion in emotion_map:
|
||||
emoji_base64, emoji_description = random.choice(emotion_map[chosen_emotion])
|
||||
logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' 的表情包: {emoji_description}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包"
|
||||
)
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
|
||||
# 7. 发送表情包
|
||||
success = await self.send_emoji(emoji_base64)
|
||||
@@ -151,9 +164,6 @@ class EmojiAction(BaseAction):
|
||||
logger.error(f"{self.log_prefix} 表情包发送失败")
|
||||
return False, "表情包发送失败"
|
||||
|
||||
# 注释:重置NoReplyAction的连续计数器现在由heartFC_chat.py统一管理
|
||||
# NoReplyAction.reset_consecutive_count()
|
||||
|
||||
return True, f"发送表情包: {emoji_description}"
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user