mirror of https://github.com/Mai-with-u/MaiBot.git
私聊PFC表情包支持(基本逻辑没问题,prompt可能还需要微调)
parent
22bce5c9c5
commit
2ecbdab23c
|
|
@ -38,6 +38,7 @@ PROMPT_INITIAL_REPLY = """
|
|||
可选行动类型以及解释:
|
||||
listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择
|
||||
direct_reply: 直接回复对方
|
||||
send_memes: 发送一个符合当前聊天氛围或{persona_text}心情的表情包,当你觉得用表情包回应更合适,或者想要活跃气氛时选择。
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
end_conversation: 结束对话,对方长时间没回复,繁忙,或者当你觉得对话告一段落时可以选择
|
||||
block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当你觉得对话让[{persona_text}]感到十分不适,或[{persona_text}]遭到各类骚扰时选择
|
||||
|
|
@ -45,7 +46,8 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在
|
|||
请以JSON格式输出你的决策:
|
||||
{{
|
||||
"action": "选择的行动类型 (必须是上面列表中的一个)",
|
||||
"reason": "选择该行动的原因 "
|
||||
"reason": "选择该行动的原因 ",
|
||||
"emoji_query": "string" // 可选。如果行动是 'send_memes',必须提供表情主题(填写表情包的适用场合或情感描述);如果行动是 'direct_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""
|
||||
}}
|
||||
|
||||
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
|
||||
|
|
@ -74,6 +76,7 @@ PROMPT_FOLLOW_UP = """
|
|||
wait: 暂时不说话,留给对方交互空间,等待对方回复。
|
||||
listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个)
|
||||
send_new_message: 发送一条新消息,当你觉得[{persona_text}]还有话要说,或现在适合/需要发送消息时可以选择
|
||||
send_memes: 发送一个符合当前聊天氛围或{persona_text}心情的表情包,当你觉得用表情包回应更合适,或者想要活跃气氛时选择。
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、或你觉得对话告一段落时可以选择
|
||||
block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当你觉得对话让[{persona_text}]感到十分不适,或[{persona_text}]遭到各类骚扰时选择
|
||||
|
|
@ -81,7 +84,8 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在
|
|||
请以JSON格式输出你的决策:
|
||||
{{
|
||||
"action": "选择的行动类型 (必须是上面列表中的一个)",
|
||||
"reason": "选择该行动的原因"
|
||||
"reason": "选择该行动的原因",
|
||||
"emoji_query": "string" // 可选。如果行动是 'send_memes',必须提供表情主题(填写表情包的适用场合或情感描述);如果行动是 'send_new_message' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""
|
||||
}}
|
||||
|
||||
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
|
||||
|
|
@ -231,24 +235,10 @@ class ActionPlanner:
|
|||
if use_reflect_prompt: # 新增的判断
|
||||
prompt_template = PROMPT_REFLECT_AND_ACT
|
||||
log_msg = "使用 PROMPT_REFLECT_AND_ACT (反思决策)"
|
||||
# 对于 PROMPT_REFLECT_AND_ACT,它不包含 send_new_message 选项,所以 spam_warning_message 中的相关提示可以调整或省略
|
||||
# 但为了保持占位符填充的一致性,我们仍然计算它
|
||||
# spam_warning_message = ""
|
||||
# if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少
|
||||
# spam_warning_message = (
|
||||
# f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**"
|
||||
# )
|
||||
# elif conversation_info.my_message_count > 2:
|
||||
# spam_warning_message = f"💬【提示】**你之前已连续发送{str(conversation_info.my_message_count)}条消息。请注意保持对话平衡。**"
|
||||
|
||||
elif last_successful_reply_action in ["direct_reply", "send_new_message"]:
|
||||
elif last_successful_reply_action in ["direct_reply", "send_new_message", "send_memes"]:
|
||||
prompt_template = PROMPT_FOLLOW_UP
|
||||
log_msg = "使用 PROMPT_FOLLOW_UP (追问决策)"
|
||||
# spam_warning_message = ""
|
||||
# if conversation_info.my_message_count > 5:
|
||||
# spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**"
|
||||
# elif conversation_info.my_message_count > 2:
|
||||
# spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**"
|
||||
|
||||
else:
|
||||
prompt_template = PROMPT_INITIAL_REPLY
|
||||
|
|
@ -300,12 +290,17 @@ class ActionPlanner:
|
|||
self.private_name,
|
||||
"action",
|
||||
"reason",
|
||||
default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待"},
|
||||
"emoji_query",
|
||||
default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待", "emoji_query": ""},
|
||||
allow_empty_string_fields=["emoji_query"]
|
||||
)
|
||||
|
||||
initial_action = initial_result.get("action", "wait")
|
||||
initial_reason = initial_result.get("reason", "LLM未提供原因,默认等待")
|
||||
logger.info(f"[私聊][{self.private_name}] LLM 初步规划行动: {initial_action}, 原因: {initial_reason}")
|
||||
current_emoji_query = initial_result.get("emoji_query", "") # 获取 emoji_query
|
||||
logger.info(f"[私聊][{self.private_name}] LLM 初步规划行动: {initial_action}, 原因: {initial_reason}表情查询: '{current_emoji_query}'")
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.current_emoji_query = current_emoji_query
|
||||
except Exception as llm_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 调用 LLM 或解析初步规划结果时出错: {llm_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
|
@ -348,6 +343,7 @@ class ActionPlanner:
|
|||
valid_actions_default = [
|
||||
"direct_reply",
|
||||
"send_new_message",
|
||||
"send_memes",
|
||||
"wait",
|
||||
"listening",
|
||||
"rethink_goal",
|
||||
|
|
|
|||
|
|
@ -3,14 +3,20 @@ import asyncio
|
|||
import datetime
|
||||
import traceback
|
||||
import json
|
||||
import os
|
||||
from typing import Optional, Set, TYPE_CHECKING
|
||||
|
||||
from src.chat.emoji_system.emoji_manager import emoji_manager
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
from .pfc_types import ConversationState
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
from src.chat.emoji_system.emoji_manager import emoji_manager
|
||||
from src.chat.utils.utils_image import image_path_to_base64 # 假设路径正确
|
||||
from maim_message import Seg, UserInfo # 从 maim_message 导入 Seg 和 UserInfo
|
||||
from src.chat.message_receive.message import MessageSending, MessageSet # PFC 的发送器依赖这些
|
||||
from src.chat.message_receive.message_sender import message_manager # PFC 的发送器依赖这个
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .conversation import Conversation # 用于类型提示以避免循环导入
|
||||
|
|
@ -459,13 +465,13 @@ async def handle_action(
|
|||
|
||||
else: # 达到最大尝试次数仍未找到合适回复 (is_suitable is False and not need_replan_from_checker)
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 达到最大尝试次数 ({max_reply_attempts}),未能生成/检查通过合适的回复。最终原因: {check_reason}"
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 达到最大回复尝试次数 ({max_reply_attempts}),ReplyChecker 仍判定不合适。最终检查原因: {check_reason}"
|
||||
)
|
||||
final_status = "recall" # 标记为 recall
|
||||
final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}"
|
||||
action_successful = False # 确保 action_successful 为 False
|
||||
# 重置追问状态
|
||||
conversation_info.last_successful_reply_action = None
|
||||
final_status = "max_checker_attempts_failed"
|
||||
final_reason = f"达到最大回复尝试次数({max_reply_attempts}),ReplyChecker仍判定不合适: {check_reason}"
|
||||
action_successful = False
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# my_message_count 保持不变
|
||||
|
||||
# 2. 处理发送告别语动作 (保持简单,不加重试)
|
||||
|
|
@ -624,6 +630,191 @@ async def handle_action(
|
|||
event_description=event_for_emotion_update,
|
||||
)
|
||||
|
||||
# X. 处理发送表情包动作
|
||||
elif action == "send_memes":
|
||||
conversation_instance.state = ConversationState.GENERATING
|
||||
final_reason_prefix = "发送表情包"
|
||||
action_successful = False # 先假设不成功
|
||||
|
||||
# 确保 conversation_info 和 observation_info 存在
|
||||
if not conversation_info or not observation_info:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ConversationInfo 或 ObservationInfo 为空。")
|
||||
final_status = "error"
|
||||
final_reason = f"{final_reason_prefix}失败:内部信息缺失"
|
||||
# done_action 的更新会在 finally 中处理
|
||||
# 理论上这不应该发生,因为调用 handle_action 前应该有检查
|
||||
# 但作为防御性编程,可以加上
|
||||
if conversation_info: # 即使另一个为空,也尝试更新
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# 直接跳到 finally 块
|
||||
# 注意:此处不能直接 return,否则 finally 不会被完整执行。
|
||||
# 而是让后续的 final_status 和 action_successful 决定流程。
|
||||
# 这里我们通过设置 action_successful = False 和 final_status = "error" 来让 finally 处理
|
||||
# 更好的方式可能是直接在 finally 前面抛出异常,但为了简化,我们先这样。
|
||||
# 此处保持 action_successful = False,后续的 finally 会处理状态。
|
||||
pass # 让代码继续到 try...except...finally 的末尾
|
||||
|
||||
else: # conversation_info 和 observation_info 都存在
|
||||
emoji_query = conversation_info.current_emoji_query
|
||||
if not emoji_query:
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': emoji_query 为空,无法获取表情包。")
|
||||
final_status = "recall"
|
||||
final_reason = f"{final_reason_prefix}失败:缺少表情包查询语句"
|
||||
conversation_info.last_successful_reply_action = None
|
||||
else:
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 使用查询 '{emoji_query}' 获取表情包...")
|
||||
try:
|
||||
emoji_result = await emoji_manager.get_emoji_for_text(emoji_query)
|
||||
|
||||
if emoji_result:
|
||||
emoji_path, emoji_description = emoji_result
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 获取到表情包: {emoji_path}, 描述: {emoji_description}")
|
||||
|
||||
if not conversation_instance.chat_stream:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ChatStream 未初始化,无法发送。")
|
||||
raise RuntimeError("ChatStream 未初始化")
|
||||
|
||||
image_b64_content = image_path_to_base64(emoji_path)
|
||||
if not image_b64_content:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 无法将图片 {emoji_path} 转换为 base64。")
|
||||
raise ValueError(f"无法将图片 {emoji_path} 转换为Base64")
|
||||
|
||||
# --- 统一 Seg 构造方式 (与群聊类似) ---
|
||||
# 直接使用 type="emoji" 和 base64 数据
|
||||
message_segment_for_emoji = Seg(type="emoji", data=image_b64_content)
|
||||
# --------------------------------------
|
||||
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.BOT_QQ,
|
||||
user_nickname=global_config.BOT_NICKNAME,
|
||||
platform=conversation_instance.chat_stream.platform,
|
||||
)
|
||||
message_id_emoji = f"pfc_meme_{round(time.time(), 3)}"
|
||||
|
||||
# --- 直接使用 DirectMessageSender (如果其 send_message 适配单个 Seg) ---
|
||||
# 或者如果 DirectMessageSender.send_message 需要 content: str,
|
||||
# 我们就需要调整 DirectMessageSender 或这里的逻辑。
|
||||
# 假设 DirectMessageSender 能被改造或其依赖的 message_manager 能处理 Seg 对象。
|
||||
# 我们先按照 PFC/message_sender.py 的结构来尝试构造 MessageSending
|
||||
# PFC/message_sender.py 中的 DirectMessageSender.send_message(content: str)
|
||||
# 它内部是 segments = Seg(type="seglist", data=[Seg(type="text", data=content)])
|
||||
# 这意味着 DirectMessageSender 目前只设计来发送文本。
|
||||
|
||||
# *** 为了与群聊发送逻辑一致,并假设底层 message_manager 可以处理任意 Seg ***
|
||||
# 我们需要绕过 DirectMessageSender 的 send_message(content: str)
|
||||
# 或者修改 DirectMessageSender 以接受 Seg 对象。
|
||||
# 更简单的做法是直接调用与群聊相似的底层发送机制,即构造 MessageSending 并使用 message_manager
|
||||
|
||||
message_to_send = MessageSending(
|
||||
message_id=message_id_emoji,
|
||||
chat_stream=conversation_instance.chat_stream,
|
||||
bot_user_info=bot_user_info,
|
||||
sender_info=None, # 表情通常不是对特定消息的回复
|
||||
message_segment=message_segment_for_emoji, # 直接使用构造的 Seg
|
||||
reply=None,
|
||||
is_head=True,
|
||||
is_emoji=True,
|
||||
thinking_start_time=action_start_time, # 使用动作开始时间作为近似
|
||||
)
|
||||
|
||||
await message_to_send.process() # 消息预处理
|
||||
|
||||
message_set_emoji = MessageSet(conversation_instance.chat_stream, message_id_emoji)
|
||||
message_set_emoji.add_message(message_to_send)
|
||||
await message_manager.add_message(message_set_emoji) # 使用全局管理器发送
|
||||
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] PFC 动作 'send_memes': 表情包已发送: {emoji_path} ({emoji_description})")
|
||||
action_successful = True # 标记发送成功
|
||||
# final_status 和 final_reason 会在 finally 中设置
|
||||
|
||||
# --- 后续成功处理逻辑 (与之前相同,但要确保 conversation_info 存在) ---
|
||||
if conversation_info:
|
||||
conversation_info.my_message_count += 1
|
||||
conversation_info.last_successful_reply_action = action
|
||||
conversation_info.current_instance_message_count += 1
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 成功执行 '{action}', my_message_count: {conversation_info.my_message_count}, 下一轮将使用【追问】逻辑。"
|
||||
)
|
||||
|
||||
current_send_time = time.time()
|
||||
if conversation_instance.idle_chat:
|
||||
await conversation_instance.idle_chat.update_last_message_time(current_send_time)
|
||||
|
||||
if observation_info and conversation_instance.bot_qq_str:
|
||||
bot_meme_message_dict = {
|
||||
"message_id": message_id_emoji,
|
||||
"time": current_send_time,
|
||||
"user_info": bot_user_info.to_dict(),
|
||||
"processed_plain_text": f"[表情包: {emoji_description}]",
|
||||
"detailed_plain_text": f"[表情包: {emoji_path} - {emoji_description}]",
|
||||
"raw_message": f"[CQ:image,file=base64://...]" # 示例
|
||||
}
|
||||
observation_info.chat_history.append(bot_meme_message_dict)
|
||||
observation_info.chat_history_count = len(observation_info.chat_history)
|
||||
max_history_len = getattr(global_config, "pfc_max_chat_history_for_checker", 50)
|
||||
if len(observation_info.chat_history) > max_history_len:
|
||||
observation_info.chat_history = observation_info.chat_history[-max_history_len:]
|
||||
observation_info.chat_history_count = len(observation_info.chat_history)
|
||||
history_slice_for_str = observation_info.chat_history[-30:]
|
||||
try:
|
||||
observation_info.chat_history_str = await build_readable_messages(
|
||||
history_slice_for_str, replace_bot_name=True, merge_messages=False,
|
||||
timestamp_mode="relative", read_mark=0.0
|
||||
)
|
||||
except Exception as e_build_hist_meme:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str (表情包后) 时出错: {e_build_hist_meme}")
|
||||
|
||||
current_unprocessed_messages_emoji = observation_info.unprocessed_messages
|
||||
message_ids_to_clear_emoji: Set[str] = set()
|
||||
for msg_item in current_unprocessed_messages_emoji:
|
||||
msg_time_item = msg_item.get("time")
|
||||
msg_id_item = msg_item.get("message_id")
|
||||
sender_id_info_item = msg_item.get("user_info", {})
|
||||
sender_id_item = str(sender_id_info_item.get("user_id")) if sender_id_info_item else None
|
||||
if (
|
||||
msg_id_item
|
||||
and msg_time_item
|
||||
and sender_id_item != conversation_instance.bot_qq_str
|
||||
and msg_time_item < current_send_time
|
||||
):
|
||||
message_ids_to_clear_emoji.add(msg_id_item)
|
||||
if message_ids_to_clear_emoji:
|
||||
await observation_info.clear_processed_messages(message_ids_to_clear_emoji)
|
||||
|
||||
if conversation_instance.relationship_updater and conversation_info:
|
||||
await conversation_instance.relationship_updater.update_relationship_incremental(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer,
|
||||
)
|
||||
event_for_emotion_update_emoji = f"你发送了一个表情包 ({emoji_description})"
|
||||
if conversation_instance.emotion_updater and conversation_info:
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer,
|
||||
event_description=event_for_emotion_update_emoji,
|
||||
)
|
||||
else: # emoji_result is None
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 未能根据查询 '{emoji_query}' 获取到合适的表情包。")
|
||||
final_status = "recall"
|
||||
final_reason = f"{final_reason_prefix}失败:未找到合适表情包"
|
||||
action_successful = False
|
||||
if conversation_info:
|
||||
conversation_info.last_successful_reply_action = None
|
||||
conversation_info.current_emoji_query = None
|
||||
|
||||
|
||||
except Exception as get_send_emoji_err:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 处理过程中出错: {get_send_emoji_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
final_status = "recall" # 或 "error"
|
||||
final_reason = f"{final_reason_prefix}失败:处理表情包时出错 ({get_send_emoji_err})"
|
||||
action_successful = False
|
||||
if conversation_info:
|
||||
conversation_info.last_successful_reply_action = None
|
||||
conversation_info.current_emoji_query = None
|
||||
|
||||
# 7. 处理等待动作
|
||||
elif action == "wait":
|
||||
conversation_instance.state = ConversationState.WAITING
|
||||
|
|
@ -658,12 +849,16 @@ async def handle_action(
|
|||
|
||||
# --- 重置非回复动作的追问状态 ---
|
||||
# 确保执行完非回复动作后,下一次规划不会错误地进入追问逻辑
|
||||
if action not in ["direct_reply", "send_new_message", "say_goodbye"]:
|
||||
if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]:
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# 清理可能残留的拒绝信息
|
||||
conversation_info.last_reply_rejection_reason = None
|
||||
conversation_info.last_rejected_reply_content = None
|
||||
|
||||
if action != "send_memes" or not action_successful:
|
||||
if conversation_info and hasattr(conversation_info, 'current_emoji_query'):
|
||||
conversation_info.current_emoji_query = None
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# 处理任务被取消的异常
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时被取消。")
|
||||
|
|
@ -685,72 +880,34 @@ async def handle_action(
|
|||
conversation_info.last_successful_reply_action = None
|
||||
|
||||
finally:
|
||||
# --- 无论成功与否,都执行 ---
|
||||
|
||||
# 1. 重置临时存储的计数值
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.other_new_messages_during_planning_count = 0
|
||||
|
||||
# 2. 更新动作历史记录的最终状态和原因
|
||||
# 优化:如果动作成功但状态仍是默认的 recall,则更新为 done
|
||||
if action_successful:
|
||||
# 如果动作标记为成功,但 final_status 仍然是初始的 "recall" 或者 "start"
|
||||
# (因为可能在try块中成功执行了但没有显式更新 final_status 为 "done")
|
||||
# 或者是 "done_no_reply" 这种特殊的成功状态
|
||||
if (
|
||||
final_status in ["recall", "start"] and action != "send_new_message"
|
||||
): # send_new_message + no_reply 是特殊成功
|
||||
final_status = "done"
|
||||
if not final_reason or final_reason == "动作未成功执行": # 避免覆盖已有的具体成功原因
|
||||
# 为不同类型的成功动作提供更具体的默认成功原因
|
||||
if action == "wait":
|
||||
# 检查 conversation_info.goal_list 是否存在且不为空
|
||||
timeout_occurred = (
|
||||
any(
|
||||
"分钟," in g.get("goal", "")
|
||||
for g in conversation_info.goal_list
|
||||
if isinstance(g, dict)
|
||||
)
|
||||
if conversation_info and conversation_info.goal_list
|
||||
else False
|
||||
)
|
||||
final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)")
|
||||
elif action == "listening":
|
||||
final_reason = "进入倾听状态"
|
||||
elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]:
|
||||
final_reason = f"成功执行 {action}"
|
||||
elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case
|
||||
final_reason = "成功发送"
|
||||
else:
|
||||
final_reason = f"动作 {action} 成功完成"
|
||||
# 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason
|
||||
|
||||
else: # action_successful is False
|
||||
# 如果动作标记为失败,且 final_status 还是 "recall" (初始值) 或 "start"
|
||||
if final_status in ["recall", "start"]:
|
||||
# 尝试从 conversation_info 中获取更具体的失败原因(例如 checker 的原因)
|
||||
# 这个 specific_rejection_reason 是在 try 块中被设置的
|
||||
specific_rejection_reason = getattr(conversation_info, "last_reply_rejection_reason", None)
|
||||
rejected_content = getattr(conversation_info, "last_rejected_reply_content", None)
|
||||
|
||||
if specific_rejection_reason: # 如果有更具体的原因
|
||||
final_reason = f"执行失败: {specific_rejection_reason}"
|
||||
if (
|
||||
rejected_content and specific_rejection_reason == "机器人尝试发送重复消息"
|
||||
): # 对复读提供更清晰的日志
|
||||
final_reason += f" (内容: '{rejected_content[:30]}...')"
|
||||
elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因,且当前原因还是默认的
|
||||
final_reason = f"动作 {action} 执行失败或被意外中止"
|
||||
# 如果 final_status 已经是 "error" 或 "cancelled",则保留它们和它们对应的 final_reason
|
||||
|
||||
# 更新 done_action 中的记录
|
||||
# 防御性检查,确保 conversation_info, done_action 存在,并且索引有效
|
||||
# --- 统一更新动作历史记录的最终状态和原因 ---
|
||||
# (确保 conversation_info 和 done_action[action_index] 有效)
|
||||
if (
|
||||
conversation_info
|
||||
and hasattr(conversation_info, "done_action")
|
||||
and conversation_info.done_action
|
||||
and action_index < len(conversation_info.done_action)
|
||||
):
|
||||
# 确定最终状态和原因
|
||||
if action_successful: # 如果动作本身标记为成功
|
||||
if final_status not in ["done", "done_no_reply"]: # 如果没有被特定成功状态覆盖
|
||||
final_status = "done"
|
||||
if not final_reason or final_reason == "动作未成功执行":
|
||||
if action == "send_memes":
|
||||
final_reason = f"{final_reason_prefix}成功发送"
|
||||
# ... (其他动作的默认成功原因) ...
|
||||
else:
|
||||
final_reason = f"动作 {action} 成功完成"
|
||||
else: # action_successful is False
|
||||
if final_status in ["recall", "start", "unknown"]: # 如果状态还是初始或未定
|
||||
# 尝试从 conversation_info 获取更具体的失败原因
|
||||
specific_rejection_reason = getattr(conversation_info, "last_reply_rejection_reason", None)
|
||||
if specific_rejection_reason and (not final_reason or final_reason == "动作未成功执行"):
|
||||
final_reason = f"执行失败: {specific_rejection_reason}"
|
||||
elif not final_reason or final_reason == "动作未成功执行":
|
||||
final_reason = f"动作 {action} 执行失败或被中止"
|
||||
# 如果 final_status 已经是 "error", "cancelled", "max_checker_attempts_failed" 等,则保留
|
||||
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": final_status,
|
||||
|
|
@ -764,18 +921,50 @@ async def handle_action(
|
|||
f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。"
|
||||
)
|
||||
|
||||
# 最终日志输出
|
||||
log_final_reason = final_reason if final_reason else "无明确原因"
|
||||
# 为成功发送的动作添加发送内容摘要
|
||||
# --- 统一设置 ConversationState ---
|
||||
if final_status == "done" or final_status == "done_no_reply" or final_status == "recall":
|
||||
# "recall" 状态也应该回到 ANALYZING 准备重新规划
|
||||
conversation_instance.state = ConversationState.ANALYZING
|
||||
elif final_status == "error" or final_status == "max_checker_attempts_failed":
|
||||
conversation_instance.state = ConversationState.ERROR
|
||||
# 对于 "cancelled", "listening", "waiting", "ignored", "ended" 等状态,
|
||||
# 它们应该在各自的动作逻辑内部或者由外部 (如 loop) 来决定下一个 ConversationState。
|
||||
# 例如,end_conversation/say_goodbye 会设置 should_continue=False,loop 会退出。
|
||||
# listening/wait 会在动作完成后(可能因为新消息或超时)使 loop 自然进入下一轮 ANALYZING。
|
||||
# cancelled 会让 loop 捕获异常并停止。
|
||||
|
||||
# 重置非回复动作的追问状态 (确保 send_memes 被视为回复动作)
|
||||
if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: # <--- 把 send_memes 加到这里
|
||||
if conversation_info:
|
||||
conversation_info.last_successful_reply_action = None
|
||||
conversation_info.last_reply_rejection_reason = None
|
||||
conversation_info.last_rejected_reply_content = None
|
||||
|
||||
# 清理表情查询(如果动作不是send_memes但查询还存在,或者send_memes失败了)
|
||||
if action != "send_memes" or not action_successful:
|
||||
if conversation_info and hasattr(conversation_info, 'current_emoji_query'):
|
||||
conversation_info.current_emoji_query = None
|
||||
|
||||
|
||||
log_final_reason_msg = final_reason if final_reason else "无明确原因"
|
||||
if (
|
||||
final_status == "done"
|
||||
and action_successful
|
||||
and action in ["direct_reply", "send_new_message"]
|
||||
and action in ["direct_reply", "send_new_message"] # send_memes 的发送内容不同
|
||||
and hasattr(conversation_instance, "generated_reply")
|
||||
and conversation_instance.generated_reply
|
||||
):
|
||||
log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')"
|
||||
log_final_reason_msg += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')"
|
||||
elif (
|
||||
final_status == "done"
|
||||
and action_successful
|
||||
and action == "send_memes"
|
||||
# emoji_description 在 send_memes 内部获取,这里不再重复记录到 log_final_reason_msg,
|
||||
# 因为 logger.info 已经记录过发送的表情描述
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}"
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason_msg}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import time
|
||||
import asyncio
|
||||
import traceback
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from src.chat.emoji_system.emoji_manager import emoji_manager
|
||||
from maim_message import Seg
|
||||
from src.common.logger_manager import get_logger
|
||||
from maim_message import UserInfo
|
||||
from src.chat.message_receive.chat_stream import chat_manager, ChatStream
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ class ConversationInfo:
|
|||
self.current_emotion_text: Optional[str] = "心情平静。" # 机器人当前的情绪描述文本
|
||||
self.current_instance_message_count: int = 0 # 当前私聊实例中的消息计数
|
||||
self.other_new_messages_during_planning_count: int = 0 # 在计划阶段期间收到的其他新消息计数
|
||||
self.current_emoji_query: Optional[str] = None # 表情包
|
||||
|
|
@ -335,13 +335,55 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
|
|||
|
||||
# --- Post LLM Action Task Handling ---
|
||||
if not llm_action_completed_successfully:
|
||||
if conversation_instance.consecutive_llm_action_failures >= MAX_CONSECUTIVE_LLM_ACTION_FAILURES:
|
||||
last_action_record = {}
|
||||
last_action_final_status = "unknown"
|
||||
# 从 conversation_info.done_action 获取上一个动作的最终状态
|
||||
if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action:
|
||||
if conversation_instance.conversation_info.done_action: # 确保列表不为空
|
||||
last_action_record = conversation_instance.conversation_info.done_action[-1]
|
||||
last_action_final_status = last_action_record.get("status", "unknown")
|
||||
|
||||
if last_action_final_status == "max_checker_attempts_failed":
|
||||
original_planned_action = last_action_record.get("action", "unknown_original_action")
|
||||
original_plan_reason = last_action_record.get("plan_reason", "unknown_original_reason")
|
||||
checker_fail_reason_from_history = last_action_record.get("final_reason", "ReplyChecker判定不合适")
|
||||
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] (Loop) 原规划动作 '{original_planned_action}' 因达到ReplyChecker最大尝试次数而失败。将强制执行 'wait' 动作。"
|
||||
)
|
||||
|
||||
action_to_perform_now = "wait" # 强制动作为 "wait"
|
||||
reason_for_forced_wait = f"原动作 '{original_planned_action}' (规划原因: {original_plan_reason}) 因 ReplyChecker 多次判定不合适 ({checker_fail_reason_from_history}) 而失败,现强制等待。"
|
||||
|
||||
if conversation_instance.conversation_info:
|
||||
# 确保下次规划不是基于这个失败的回复动作的追问
|
||||
conversation_instance.conversation_info.last_successful_reply_action = None
|
||||
# 重置连续LLM失败计数器,因为我们已经用特定的“等待”动作处理了这种失败类型
|
||||
conversation_instance.consecutive_llm_action_failures = 0
|
||||
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 执行强制等待动作...")
|
||||
await actions.handle_action(
|
||||
conversation_instance,
|
||||
action_to_perform_now, # "wait"
|
||||
reason_for_forced_wait,
|
||||
conversation_instance.observation_info,
|
||||
conversation_instance.conversation_info,
|
||||
)
|
||||
# "wait" 动作执行后,其内部逻辑会将状态设置为 ANALYZING (通过 finally 块)
|
||||
# 所以循环的下一轮会自然地重新规划或根据等待结果行动
|
||||
_force_reflect_and_act_next_iter = False # 确保此路径不会强制反思
|
||||
await asyncio.sleep(0.1) # 短暂暂停,等待状态更新等
|
||||
continue # 进入主循环的下一次迭代
|
||||
|
||||
|
||||
elif conversation_instance.consecutive_llm_action_failures >= MAX_CONSECUTIVE_LLM_ACTION_FAILURES:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] (Loop) LLM相关动作连续失败或被取消 {conversation_instance.consecutive_llm_action_failures} 次。将强制等待并重置计数器。"
|
||||
)
|
||||
|
||||
action = "wait" # Force action to wait
|
||||
reason = f"LLM连续失败{conversation_instance.consecutive_llm_action_failures}次,强制等待"
|
||||
forced_wait_action_on_consecutive_failure = "wait"
|
||||
reason_for_consecutive_failure_wait = f"LLM连续失败{conversation_instance.consecutive_llm_action_failures}次,强制等待"
|
||||
|
||||
conversation_instance.consecutive_llm_action_failures = 0
|
||||
|
||||
if conversation_instance.conversation_info:
|
||||
|
|
@ -350,8 +392,8 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
|
|||
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 执行强制等待动作...")
|
||||
await actions.handle_action(
|
||||
conversation_instance,
|
||||
action,
|
||||
reason,
|
||||
forced_wait_action_on_consecutive_failure, # "wait"
|
||||
reason_for_consecutive_failure_wait,
|
||||
conversation_instance.observation_info,
|
||||
conversation_instance.conversation_info,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class PfcRepationshipTranslator:
|
|||
"初识", # level_num 2
|
||||
"友好", # level_num 3
|
||||
"喜欢", # level_num 4
|
||||
"暧昧", # level_num 5
|
||||
"依赖", # level_num 5
|
||||
]
|
||||
|
||||
if 0 <= level_num < len(relationship_descriptions):
|
||||
|
|
|
|||
|
|
@ -457,6 +457,7 @@ def get_items_from_json(
|
|||
default_values: Optional[Dict[str, Any]] = None,
|
||||
required_types: Optional[Dict[str, type]] = None,
|
||||
allow_array: bool = True,
|
||||
allow_empty_string_fields: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Union[Dict[str, Any], List[Dict[str, Any]]]]:
|
||||
"""从文本中提取JSON内容并获取指定字段
|
||||
|
||||
|
|
@ -478,9 +479,12 @@ def get_items_from_json(
|
|||
cleaned_content = markdown_match.group(1).strip()
|
||||
logger.debug(f"[私聊][{private_name}] 已去除 Markdown 标记,剩余内容: {cleaned_content[:100]}...")
|
||||
default_result: Dict[str, Any] = {}
|
||||
|
||||
if default_values:
|
||||
default_result.update(default_values)
|
||||
result = default_result.copy()
|
||||
# result = default_result.copy()
|
||||
_allow_empty_string_fields = allow_empty_string_fields if allow_empty_string_fields is not None else []
|
||||
|
||||
if allow_array:
|
||||
try:
|
||||
json_array = json.loads(cleaned_content)
|
||||
|
|
@ -490,6 +494,7 @@ def get_items_from_json(
|
|||
if not isinstance(item_json, dict):
|
||||
logger.warning(f"[私聊][{private_name}] JSON数组中的元素不是字典: {item_json}")
|
||||
continue
|
||||
|
||||
current_item_result = default_result.copy()
|
||||
valid_item = True
|
||||
for field in items:
|
||||
|
|
@ -499,13 +504,13 @@ def get_items_from_json(
|
|||
logger.warning(f"[私聊][{private_name}] JSON数组元素缺少必要字段 '{field}': {item_json}")
|
||||
valid_item = False
|
||||
break
|
||||
if not valid_item:
|
||||
|
||||
if not valid_item:
|
||||
continue
|
||||
|
||||
if required_types:
|
||||
for field, expected_type in required_types.items():
|
||||
if field in current_item_result and not isinstance(
|
||||
current_item_result[field], expected_type
|
||||
):
|
||||
if field in current_item_result and not isinstance(current_item_result[field], expected_type):
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] JSON数组元素字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_item_result[field]).__name__}): {item_json}"
|
||||
)
|
||||
|
|
@ -513,87 +518,121 @@ def get_items_from_json(
|
|||
break
|
||||
if not valid_item:
|
||||
continue
|
||||
|
||||
for field in items:
|
||||
if (
|
||||
field in current_item_result
|
||||
and isinstance(current_item_result[field], str)
|
||||
and not current_item_result[field].strip()
|
||||
and field not in _allow_empty_string_fields
|
||||
):
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] JSON数组元素字段 '{field}' 不能为空字符串: {item_json}"
|
||||
)
|
||||
valid_item = False
|
||||
break
|
||||
|
||||
if valid_item:
|
||||
valid_items_list.append(current_item_result)
|
||||
|
||||
if valid_items_list:
|
||||
logger.debug(f"[私聊][{private_name}] 成功解析JSON数组,包含 {len(valid_items_list)} 个有效项目。")
|
||||
return True, valid_items_list
|
||||
else:
|
||||
logger.debug(f"[私聊][{private_name}] 解析为JSON数组,但未找到有效项目,尝试解析单个JSON对象。")
|
||||
result = default_result.copy()
|
||||
# result = default_result.copy()
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(f"[私聊][{private_name}] JSON数组直接解析失败,尝试解析单个JSON对象")
|
||||
result = default_result.copy()
|
||||
# result = default_result.copy()
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{private_name}] 尝试解析JSON数组时发生未知错误: {str(e)}")
|
||||
result = default_result.copy()
|
||||
# result = default_result.copy()
|
||||
|
||||
json_data = None
|
||||
valid_single_object = True # <--- 将初始化提前到这里
|
||||
|
||||
try:
|
||||
json_data = json.loads(cleaned_content)
|
||||
if not isinstance(json_data, dict):
|
||||
logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}")
|
||||
return False, default_result
|
||||
# 如果不是字典,即使 allow_array 为 False,这里也应该认为单个对象解析失败
|
||||
valid_single_object = False # 标记为无效
|
||||
# return False, default_result.copy() # 不立即返回,让后续逻辑统一处理 valid_single_object
|
||||
except json.JSONDecodeError:
|
||||
json_pattern = r"\{[\s\S]*?\}"
|
||||
json_match = re.search(json_pattern, cleaned_content)
|
||||
if json_match:
|
||||
try:
|
||||
potential_json_str = json_match.group()
|
||||
potential_json_str = json_match.group(0)
|
||||
json_data = json.loads(potential_json_str)
|
||||
if not isinstance(json_data, dict):
|
||||
logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}")
|
||||
return False, default_result
|
||||
logger.debug(f"[私聊][{private_name}] 通过正则提取并成功解析JSON对象。")
|
||||
valid_single_object = False # 标记为无效
|
||||
# return False, default_result.copy()
|
||||
else:
|
||||
logger.debug(f"[私聊][{private_name}] 通过正则提取并成功解析JSON对象。")
|
||||
# valid_single_object 保持 True
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"[私聊][{private_name}] 正则提取的部分 '{potential_json_str[:100]}...' 无法解析为JSON。")
|
||||
return False, default_result
|
||||
valid_single_object = False # 标记为无效
|
||||
# return False, default_result.copy()
|
||||
else:
|
||||
logger.error(
|
||||
f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}..."
|
||||
)
|
||||
return False, default_result
|
||||
if not isinstance(result, dict):
|
||||
result = default_result.copy()
|
||||
valid_single_object = True
|
||||
for item_field in items: # Renamed item to item_field
|
||||
valid_single_object = False # 标记为无效
|
||||
# return False, default_result.copy()
|
||||
|
||||
# 如果前面的步骤未能成功解析出一个 dict 类型的 json_data,则 valid_single_object 会是 False
|
||||
if not isinstance(json_data, dict) or not valid_single_object: # 增加对 json_data 类型的检查
|
||||
# 如果 allow_array 为 True 且数组解析成功过,这里不应该执行 (因为之前会 return True, valid_items_list)
|
||||
# 如果 allow_array 为 False,或者数组解析也失败了,那么到这里就意味着整体解析失败
|
||||
if not (allow_array and isinstance(json_array, list) and valid_items_list): # 修正:检查之前数组解析是否成功
|
||||
logger.debug(f"[私聊][{private_name}] 未能成功解析为有效的JSON对象或数组。")
|
||||
return False, default_result.copy()
|
||||
|
||||
|
||||
# 如果成功解析了单个 JSON 对象 (json_data 是 dict 且 valid_single_object 仍为 True)
|
||||
# current_single_result 的初始化和填充逻辑可以保持
|
||||
current_single_result = default_result.copy()
|
||||
# valid_single_object = True # 这一行现在是多余的,因为在上面已经初始化并可能被修改
|
||||
|
||||
for item_field in items:
|
||||
if item_field in json_data:
|
||||
result[item_field] = json_data[item_field]
|
||||
current_single_result[item_field] = json_data[item_field]
|
||||
elif item_field not in default_result:
|
||||
logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item_field}'。JSON内容: {json_data}")
|
||||
valid_single_object = False
|
||||
break
|
||||
if not valid_single_object:
|
||||
return False, default_result
|
||||
|
||||
if not valid_single_object: return False, default_result.copy() # 如果字段缺失,则校验失败
|
||||
|
||||
if required_types:
|
||||
for field, expected_type in required_types.items():
|
||||
if field in result and not isinstance(result[field], expected_type):
|
||||
if field in current_single_result and not isinstance(current_single_result[field], expected_type):
|
||||
logger.error(
|
||||
f"[私聊][{private_name}] JSON对象字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(result[field]).__name__})"
|
||||
f"[私聊][{private_name}] JSON对象字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_single_result[field]).__name__})"
|
||||
)
|
||||
valid_single_object = False
|
||||
break
|
||||
if not valid_single_object:
|
||||
return False, default_result
|
||||
|
||||
if not valid_single_object: return False, default_result.copy() # 如果类型错误,则校验失败
|
||||
|
||||
for field in items:
|
||||
if field in result and isinstance(result[field], str) and not result[field].strip():
|
||||
logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 不能为空字符串")
|
||||
if field in current_single_result and \
|
||||
isinstance(current_single_result[field], str) and \
|
||||
not current_single_result[field].strip() and \
|
||||
field not in _allow_empty_string_fields:
|
||||
logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 不能为空字符串 (除非特别允许)")
|
||||
valid_single_object = False
|
||||
break
|
||||
|
||||
if valid_single_object:
|
||||
logger.debug(f"[私聊][{private_name}] 成功解析并验证了单个JSON对象。")
|
||||
return True, result
|
||||
return True, current_single_result
|
||||
else:
|
||||
return False, default_result
|
||||
return False, default_result.copy()
|
||||
|
||||
|
||||
|
||||
async def get_person_id(private_name: str, chat_stream: ChatStream):
|
||||
|
|
|
|||
Loading…
Reference in New Issue