feat:为复读增加硬限制

This commit is contained in:
SengokuCola
2025-04-30 01:56:48 +08:00
parent 298bcbcee4
commit 4b1c678456
3 changed files with 200 additions and 50 deletions

View File

@@ -8,9 +8,22 @@ from src.plugins.moods.moods import MoodManager
logger = get_logger("mai_state") logger = get_logger("mai_state")
# enable_unlimited_hfc_chat = True # -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
enable_unlimited_hfc_chat = False enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
# enable_unlimited_hfc_chat = False
prevent_offline_state = True # 调试用:防止进入离线状态
# 不同状态下普通聊天的最大消息数
MAX_NORMAL_CHAT_NUM_PEEKING = 30
MAX_NORMAL_CHAT_NUM_NORMAL = 40
MAX_NORMAL_CHAT_NUM_FOCUSED = 30
# 不同状态下专注聊天的最大消息数
MAX_FOCUSED_CHAT_NUM_PEEKING = 20
MAX_FOCUSED_CHAT_NUM_NORMAL = 30
MAX_FOCUSED_CHAT_NUM_FOCUSED = 40
# -- 状态定义 --
class MaiState(enum.Enum): class MaiState(enum.Enum):
""" """
@@ -34,11 +47,11 @@ class MaiState(enum.Enum):
if self == MaiState.OFFLINE: if self == MaiState.OFFLINE:
return 0 return 0
elif self == MaiState.PEEKING: elif self == MaiState.PEEKING:
return 30 return MAX_NORMAL_CHAT_NUM_PEEKING
elif self == MaiState.NORMAL_CHAT: elif self == MaiState.NORMAL_CHAT:
return 40 return MAX_NORMAL_CHAT_NUM_NORMAL
elif self == MaiState.FOCUSED_CHAT: elif self == MaiState.FOCUSED_CHAT:
return 30 return MAX_NORMAL_CHAT_NUM_FOCUSED
def get_focused_chat_max_num(self): def get_focused_chat_max_num(self):
# 调试用 # 调试用
@@ -48,11 +61,11 @@ class MaiState(enum.Enum):
if self == MaiState.OFFLINE: if self == MaiState.OFFLINE:
return 0 return 0
elif self == MaiState.PEEKING: elif self == MaiState.PEEKING:
return 20 return MAX_FOCUSED_CHAT_NUM_PEEKING
elif self == MaiState.NORMAL_CHAT: elif self == MaiState.NORMAL_CHAT:
return 30 return MAX_FOCUSED_CHAT_NUM_NORMAL
elif self == MaiState.FOCUSED_CHAT: elif self == MaiState.FOCUSED_CHAT:
return 40 return MAX_FOCUSED_CHAT_NUM_FOCUSED
class MaiStateInfo: class MaiStateInfo:
@@ -110,7 +123,6 @@ class MaiStateManager:
"""管理 Mai 的整体状态转换逻辑""" """管理 Mai 的整体状态转换逻辑"""
def __init__(self): def __init__(self):
# MaiStateManager doesn't hold the state itself, it operates on a MaiStateInfo instance.
pass pass
def check_and_decide_next_state(self, current_state_info: MaiStateInfo) -> Optional[MaiState]: def check_and_decide_next_state(self, current_state_info: MaiStateInfo) -> Optional[MaiState]:
@@ -129,6 +141,13 @@ class MaiStateManager:
time_since_last_min_check = current_time - current_state_info.last_min_check_time time_since_last_min_check = current_time - current_state_info.last_min_check_time
next_state: Optional[MaiState] = None next_state: Optional[MaiState] = None
# 辅助函数:根据 prevent_offline_state 标志调整目标状态
def _resolve_offline(candidate_state: MaiState) -> MaiState:
if prevent_offline_state and candidate_state == MaiState.OFFLINE:
logger.debug(f"阻止进入 OFFLINE改为 PEEKING")
return MaiState.PEEKING
return candidate_state
if current_status == MaiState.OFFLINE: if current_status == MaiState.OFFLINE:
logger.info("当前[离线],没看手机,思考要不要上线看看......") logger.info("当前[离线],没看手机,思考要不要上线看看......")
elif current_status == MaiState.PEEKING: elif current_status == MaiState.PEEKING:
@@ -141,61 +160,71 @@ class MaiStateManager:
# 1. 麦麦每分钟都有概率离线 # 1. 麦麦每分钟都有概率离线
if time_since_last_min_check >= 60: if time_since_last_min_check >= 60:
if current_status != MaiState.OFFLINE: if current_status != MaiState.OFFLINE:
if random.random() < 0.03: # 3% 概率切换到 OFFLINE20分钟有50%的概率还在线 if random.random() < 0.03: # 3% 概率切换到 OFFLINE
logger.debug(f"突然不想聊了,从 {current_status.value} 切换到 离线") potential_next = MaiState.OFFLINE
next_state = MaiState.OFFLINE resolved_next = _resolve_offline(potential_next)
logger.debug(f"规则1概率触发下线resolve 为 {resolved_next.value}")
# 只有当解析后的状态与当前状态不同时才设置 next_state
if resolved_next != current_status:
next_state = resolved_next
# 2. 状态持续时间规则 (如果没有自行下线) # 2. 状态持续时间规则 (只有在规则1没有触发状态改变时才检查)
if next_state is None: if next_state is None:
time_limit_exceeded = False
choices_list = []
weights = []
rule_id = ""
if current_status == MaiState.OFFLINE: if current_status == MaiState.OFFLINE:
# OFFLINE 最多保持一分钟 # 注意:即使 prevent_offline_state=True也可能从初始的 OFFLINE 状态启动
# 目前是一个调试值,可以修改
if time_in_current_status >= 60: if time_in_current_status >= 60:
time_limit_exceeded = True
rule_id = "2.1 (From OFFLINE)"
weights = [30, 30, 20, 20] weights = [30, 30, 20, 20]
choices_list = [MaiState.PEEKING, MaiState.NORMAL_CHAT, MaiState.FOCUSED_CHAT, MaiState.OFFLINE] choices_list = [MaiState.PEEKING, MaiState.NORMAL_CHAT, MaiState.FOCUSED_CHAT, MaiState.OFFLINE]
next_state_candidate = random.choices(choices_list, weights=weights, k=1)[0]
if next_state_candidate != MaiState.OFFLINE:
next_state = next_state_candidate
logger.debug(f"上线!开始 {next_state.value}")
else:
# 继续离线状态
next_state = MaiState.OFFLINE
elif current_status == MaiState.PEEKING: elif current_status == MaiState.PEEKING:
if time_in_current_status >= 600: # PEEKING 最多持续 600 秒 if time_in_current_status >= 600: # PEEKING 最多持续 600 秒
time_limit_exceeded = True
rule_id = "2.2 (From PEEKING)"
weights = [70, 20, 10] weights = [70, 20, 10]
choices_list = [MaiState.OFFLINE, MaiState.NORMAL_CHAT, MaiState.FOCUSED_CHAT] choices_list = [MaiState.OFFLINE, MaiState.NORMAL_CHAT, MaiState.FOCUSED_CHAT]
next_state = random.choices(choices_list, weights=weights, k=1)[0]
logger.debug(f"手机看完了,接下来 {next_state.value}")
elif current_status == MaiState.NORMAL_CHAT: elif current_status == MaiState.NORMAL_CHAT:
if time_in_current_status >= 300: # NORMAL_CHAT 最多持续 300 秒 if time_in_current_status >= 300: # NORMAL_CHAT 最多持续 300 秒
time_limit_exceeded = True
rule_id = "2.3 (From NORMAL_CHAT)"
weights = [50, 50] weights = [50, 50]
choices_list = [MaiState.OFFLINE, MaiState.FOCUSED_CHAT] choices_list = [MaiState.OFFLINE, MaiState.FOCUSED_CHAT]
next_state = random.choices(choices_list, weights=weights, k=1)[0]
if next_state == MaiState.FOCUSED_CHAT:
logger.debug(f"继续深入聊天, {next_state.value}")
else:
logger.debug(f"聊完了,接下来 {next_state.value}")
elif current_status == MaiState.FOCUSED_CHAT: elif current_status == MaiState.FOCUSED_CHAT:
if time_in_current_status >= 600: # FOCUSED_CHAT 最多持续 600 秒 if time_in_current_status >= 600: # FOCUSED_CHAT 最多持续 600 秒
time_limit_exceeded = True
rule_id = "2.4 (From FOCUSED_CHAT)"
weights = [80, 20] weights = [80, 20]
choices_list = [MaiState.OFFLINE, MaiState.NORMAL_CHAT] choices_list = [MaiState.OFFLINE, MaiState.NORMAL_CHAT]
next_state = random.choices(choices_list, weights=weights, k=1)[0]
logger.debug(f"深入聊天结束,接下来 {next_state.value}")
if time_limit_exceeded:
next_state_candidate = random.choices(choices_list, weights=weights, k=1)[0]
resolved_candidate = _resolve_offline(next_state_candidate)
logger.debug(f"规则{rule_id}:时间到,随机选择 {next_state_candidate.value}resolve 为 {resolved_candidate.value}")
next_state = resolved_candidate # 直接使用解析后的状态
# 注意enable_unlimited_hfc_chat 优先级高于 prevent_offline_state
# 如果触发了这个它会覆盖上面规则2设置的 next_state
if enable_unlimited_hfc_chat: if enable_unlimited_hfc_chat:
logger.debug("调试用:开挂了,强制切换到专注聊天") logger.debug("调试用:开挂了,强制切换到专注聊天")
next_state = MaiState.FOCUSED_CHAT next_state = MaiState.FOCUSED_CHAT
# --- 最终决策 --- #
# 如果决定了下一个状态,且这个状态与当前状态不同,则返回下一个状态 # 如果决定了下一个状态,且这个状态与当前状态不同,则返回下一个状态
if next_state is not None and next_state != current_status: if next_state is not None and next_state != current_status:
return next_state return next_state
# 如果决定保持 OFFLINE (next_state == MaiState.OFFLINE) 且当前也是 OFFLINE # 如果决定保持 OFFLINE (next_state == MaiState.OFFLINE) 且当前也是 OFFLINE
# 并且是由于持续时间规则触发的,返回 OFFLINE 以便调用者可以重置计时器 # 并且是由于持续时间规则触发的,返回 OFFLINE 以便调用者可以重置计时器
# 注意:这个分支只有在 prevent_offline_state = False 时才可能被触发。
elif next_state == MaiState.OFFLINE and current_status == MaiState.OFFLINE and time_in_current_status >= 60: elif next_state == MaiState.OFFLINE and current_status == MaiState.OFFLINE and time_in_current_status >= 60:
logger.debug("决定保持 OFFLINE (持续时间规则),返回 OFFLINE 以提示重置计时器。") logger.debug("决定保持 OFFLINE (持续时间规则),返回 OFFLINE 以提示重置计时器。")
return MaiState.OFFLINE # Return OFFLINE to signal caller that timer reset might be needed return MaiState.OFFLINE # Return OFFLINE to signal caller that timer reset might be needed
else: else:
# 1. next_state is None (没有触发任何转换规则)
# 2. next_state is not None 但等于 current_status (例如规则1想切OFFLINE但被resolve成PEEKING而当前已经是PEEKING)
# 3. next_state is OFFLINE, current is OFFLINE, 但不是因为时间规则触发 (例如初始状态还没到60秒)
return None # 没有状态转换发生或无需重置计时器 return None # 没有状态转换发生或无需重置计时器

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import time import time
import traceback import traceback
import random # <--- 添加导入
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
@@ -31,6 +32,8 @@ from src.individuality.individuality import Individuality
INITIAL_DURATION = 60.0 INITIAL_DURATION = 60.0
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
logger = get_logger("interest") # Logger Name Changed logger = get_logger("interest") # Logger Name Changed
@@ -45,10 +48,11 @@ class ActionManager:
def __init__(self): def __init__(self):
# 初始化为默认动作集 # 初始化为默认动作集
self._available_actions: Dict[str, str] = DEFAULT_ACTIONS.copy() self._available_actions: Dict[str, str] = DEFAULT_ACTIONS.copy()
self._original_actions_backup: Optional[Dict[str, str]] = None # 用于临时移除时的备份
def get_available_actions(self) -> Dict[str, str]: def get_available_actions(self) -> Dict[str, str]:
"""获取当前可用的动作集""" """获取当前可用的动作集"""
return self._available_actions return self._available_actions.copy() # 返回副本以防外部修改
def add_action(self, action_name: str, description: str) -> bool: def add_action(self, action_name: str, description: str) -> bool:
""" """
@@ -81,6 +85,30 @@ class ActionManager:
del self._available_actions[action_name] del self._available_actions[action_name]
return True return True
def temporarily_remove_actions(self, actions_to_remove: List[str]):
"""
临时移除指定的动作,备份原始动作集。
如果已经有备份,则不重复备份。
"""
if self._original_actions_backup is None:
self._original_actions_backup = self._available_actions.copy()
actions_actually_removed = []
for action_name in actions_to_remove:
if action_name in self._available_actions:
del self._available_actions[action_name]
actions_actually_removed.append(action_name)
# logger.debug(f"临时移除了动作: {actions_actually_removed}") # 可选日志
def restore_actions(self):
"""
恢复之前备份的原始动作集。
"""
if self._original_actions_backup is not None:
self._available_actions = self._original_actions_backup.copy()
self._original_actions_backup = None
# logger.debug("恢复了原始动作集") # 可选日志
def clear_actions(self): def clear_actions(self):
"""清空所有动作""" """清空所有动作"""
self._available_actions.clear() self._available_actions.clear()
@@ -151,7 +179,7 @@ class HeartFChatting:
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
""" """
CONSECUTIVE_NO_REPLY_THRESHOLD = 5 # 连续不回复的阈值 CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
def __init__( def __init__(
self, self,
@@ -214,6 +242,7 @@ class HeartFChatting:
self._current_cycle: Optional[CycleInfo] = None self._current_cycle: Optional[CycleInfo] = None
self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器 self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器
self._shutting_down: bool = False # <--- 新增:关闭标志位 self._shutting_down: bool = False # <--- 新增:关闭标志位
self._lian_xu_deng_dai_shi_jian: float = 0.0 # <--- 新增:累计等待时间
async def _initialize(self) -> bool: async def _initialize(self) -> bool:
""" """
@@ -489,6 +518,7 @@ class HeartFChatting:
logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") logger.error(f"{self.log_prefix} 处理{action}时出错: {e}")
# 出错时也重置计数器 # 出错时也重置计数器
self._lian_xu_bu_hui_fu_ci_shu = 0 self._lian_xu_bu_hui_fu_ci_shu = 0
self._lian_xu_deng_dai_shi_jian = 0.0 # 重置累计等待时间
return False, "" return False, ""
async def _handle_text_reply(self, reasoning: str, emoji_query: str, cycle_timers: dict) -> tuple[bool, str]: async def _handle_text_reply(self, reasoning: str, emoji_query: str, cycle_timers: dict) -> tuple[bool, str]:
@@ -511,6 +541,7 @@ class HeartFChatting:
""" """
# 重置连续不回复计数器 # 重置连续不回复计数器
self._lian_xu_bu_hui_fu_ci_shu = 0 self._lian_xu_bu_hui_fu_ci_shu = 0
self._lian_xu_deng_dai_shi_jian = 0.0 # 重置累计等待时间
# 获取锚点消息 # 获取锚点消息
anchor_message = await self._get_anchor_message() anchor_message = await self._get_anchor_message()
@@ -566,6 +597,7 @@ class HeartFChatting:
bool: 是否发送成功 bool: 是否发送成功
""" """
logger.info(f"{self.log_prefix} 决定回复表情({emoji_query}): {reasoning}") logger.info(f"{self.log_prefix} 决定回复表情({emoji_query}): {reasoning}")
self._lian_xu_deng_dai_shi_jian = 0.0 # 重置累计等待时间(即使不计数也保持一致性)
try: try:
anchor = await self._get_anchor_message() anchor = await self._get_anchor_message()
@@ -601,23 +633,41 @@ class HeartFChatting:
observation = self.observations[0] if self.observations else None observation = self.observations[0] if self.observations else None
try: try:
dang_qian_deng_dai = 0.0 # 初始化本次等待时间
with Timer("等待新消息", cycle_timers): with Timer("等待新消息", cycle_timers):
# 等待新消息、超时或关闭信号,并获取结果 # 等待新消息、超时或关闭信号,并获取结果
await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix)
# 从计时器获取实际等待时间
dang_qian_deng_dai = cycle_timers.get("等待新消息", 0.0)
if not self._shutting_down: if not self._shutting_down:
self._lian_xu_bu_hui_fu_ci_shu += 1 self._lian_xu_bu_hui_fu_ci_shu += 1
self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间
logger.debug( logger.debug(
f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}" f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}, "
f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}"
) )
# 检查是否达到阈值 # 检查是否同时达到次数和时间阈值
if self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD: time_threshold = 0.66 * WAITING_TIME_THRESHOLD * self.CONSECUTIVE_NO_REPLY_THRESHOLD
if (self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD and
self._lian_xu_deng_dai_shi_jian >= time_threshold):
logger.info( logger.info(
f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次),调用回调请求状态转换" f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) "
f"且累计等待时间达到 {self._lian_xu_deng_dai_shi_jian:.2f}秒 (阈值 {time_threshold}秒)"
f"调用回调请求状态转换"
) )
# 调用回调。注意:这里不重置计数器,依赖回调函数成功改变状态来隐式重置上下文。 # 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。
await self.on_consecutive_no_reply_callback() await self.on_consecutive_no_reply_callback()
elif self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD:
# 仅次数达到阈值,但时间未达到
logger.debug(
f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) "
f"但累计等待时间 {self._lian_xu_deng_dai_shi_jian:.2f}秒 未达到时间阈值 ({time_threshold}秒),暂不调用回调"
)
# else: 次数和时间都未达到阈值,不做处理
return True return True
@@ -658,8 +708,8 @@ class HeartFChatting:
return True return True
# 检查超时 (放在检查新消息和关闭之后) # 检查超时 (放在检查新消息和关闭之后)
if time.monotonic() - wait_start_time > 120: if time.monotonic() - wait_start_time > WAITING_TIME_THRESHOLD:
logger.warning(f"{log_prefix} 等待新消息超时(20秒)") logger.warning(f"{log_prefix} 等待新消息超时({WAITING_TIME_THRESHOLD}秒)")
return False return False
try: try:
@@ -737,9 +787,49 @@ class HeartFChatting:
参数: 参数:
current_mind: 子思维的当前思考结果 current_mind: 子思维的当前思考结果
cycle_timers: 计时器字典
is_re_planned: 是否为重新规划
""" """
logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器") logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器")
# --- 新增:检查历史动作并调整可用动作 ---
lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
actions_to_remove_temporarily = []
probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断
# 反向遍历最近的循环历史
for cycle in reversed(self._cycle_history):
# 只关心实际执行了动作的循环
if cycle.action_taken:
if cycle.action_type == "text_reply":
lian_xu_wen_ben_hui_fu += 1
else:
break # 遇到非文本回复,中断计数
# 检查最近的3个循环即可避免检查过多历史 (如果历史很长)
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (len(self._cycle_history) - 4):
break
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
# 根据连续次数决定临时移除哪些动作
if lian_xu_wen_ben_hui_fu >= 3:
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
elif lian_xu_wen_ben_hui_fu == 2:
if probability_roll < 0.8: # 80% 概率
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)")
elif lian_xu_wen_ben_hui_fu == 1:
if probability_roll < 0.4: # 40% 概率
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)")
actions_to_remove_temporarily.append("text_reply")
else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)")
# 如果 lian_xu_wen_ben_hui_fu == 0则不移除任何动作
# --- 结束:检查历史动作 ---
# 获取观察信息 # 获取观察信息
observation = self.observations[0] observation = self.observations[0]
if is_re_planned: if is_re_planned:
@@ -754,6 +844,11 @@ class HeartFChatting:
emoji_query = "" # <--- 在这里初始化 emoji_query emoji_query = "" # <--- 在这里初始化 emoji_query
try: try:
# --- 新增:应用临时动作移除 ---
if actions_to_remove_temporarily:
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
logger.debug(f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}")
# --- 构建提示词 --- # --- 构建提示词 ---
replan_prompt_str = "" replan_prompt_str = ""
if is_re_planned: if is_re_planned:
@@ -767,6 +862,7 @@ class HeartFChatting:
# --- 调用 LLM --- # --- 调用 LLM ---
try: try:
planner_tools = self.action_manager.get_planner_tool_definition() planner_tools = self.action_manager.get_planner_tool_definition()
logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具
_response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async( _response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async(
prompt=prompt, prompt=prompt,
tools=planner_tools, tools=planner_tools,
@@ -810,15 +906,23 @@ class HeartFChatting:
extracted_action = arguments.get("action", "no_reply") extracted_action = arguments.get("action", "no_reply")
# 验证动作 # 验证动作
if extracted_action not in self.action_manager.get_available_actions(): if extracted_action not in self.action_manager.get_available_actions():
# 如果LLM返回了一个此时不该用的动作因为被临时移除了
# 或者完全无效的动作
logger.warning( logger.warning(
f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {extracted_action}使用默认动作no_reply" f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action}将强制使用 'no_reply'"
) )
action = "no_reply" action = "no_reply"
reasoning = f"LLM返回了未授权的动作: {extracted_action}" reasoning = f"LLM返回了当前不可用的动作: {extracted_action}"
emoji_query = "" emoji_query = ""
llm_error = False # 视为LLM错误,只是逻辑修正 llm_error = False # 视为逻辑修正而非 LLM 错误
# --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) ---
if "no_reply" not in self.action_manager.get_available_actions():
logger.error(f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。")
action = "error" # 回退到错误状态
reasoning = "无法执行任何有效动作,包括 no_reply"
llm_error = True
else: else:
# 动作有效,使用提取的值 # 动作有效且可用,使用提取的值
action = extracted_action action = extracted_action
reasoning = arguments.get("reasoning", "未提供理由") reasoning = arguments.get("reasoning", "未提供理由")
emoji_query = arguments.get("emoji_query", "") emoji_query = arguments.get("emoji_query", "")
@@ -837,8 +941,18 @@ class HeartFChatting:
reasoning = f"验证工具调用失败: {error_msg}" reasoning = f"验证工具调用失败: {error_msg}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}") logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # not valid_tool_calls else: # not valid_tool_calls
reasoning = "LLM未返回有效的工具调用" # 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
logger.warning(f"{self.log_prefix}[Planner] {reasoning}") available_actions = list(self.action_manager.get_available_actions().keys())
if available_actions == ["no_reply"]:
logger.info(f"{self.log_prefix}[Planner] LLM未返回工具调用但当前唯一可用动作是 'no_reply',将执行 'no_reply'")
action = "no_reply"
reasoning = "LLM未返回工具调用且当前仅 'no_reply' 可用"
emoji_query = ""
llm_error = False # 视为逻辑选择而非错误
else:
reasoning = "LLM未返回有效的工具调用"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
# llm_error 保持为 True
# 如果 llm_error 仍然是 True说明在处理过程中有错误发生 # 如果 llm_error 仍然是 True说明在处理过程中有错误发生
except Exception as llm_e: except Exception as llm_e:
@@ -847,6 +961,12 @@ class HeartFChatting:
action = "error" action = "error"
reasoning = f"Planner内部处理错误: {llm_e}" reasoning = f"Planner内部处理错误: {llm_e}"
llm_error = True llm_error = True
# --- 新增:确保动作恢复 ---
finally:
if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复
self.action_manager.restore_actions()
logger.debug(f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}")
# --- 结束:确保动作恢复 ---
# --- 结束 LLM 决策 --- # # --- 结束 LLM 决策 --- #
return { return {

View File

@@ -85,6 +85,7 @@ def init_prompt():
- 遵守回复原则 - 遵守回复原则
- 必须调用工具并包含action和reasoning - 必须调用工具并包含action和reasoning
- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) - 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply)
- 并不是所有选择都可用
- 选择text_reply或emoji_reply时必须提供emoji_query - 选择text_reply或emoji_reply时必须提供emoji_query
- 保持回复自然,符合日常聊天习惯""", - 保持回复自然,符合日常聊天习惯""",
"planner_prompt", "planner_prompt",