diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 32664ec9..5cd38576 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -22,20 +22,77 @@ logger = get_module_logger("chat_utils") # 预编译正则表达式以提高性能 _L_REGEX = regex.compile(r"\p{L}") # 匹配任何Unicode字母 _HAN_CHAR_REGEX = regex.compile(r"\p{Han}") # 匹配汉字 (Unicode属性) -_Nd_REGEX = regex.compile(r'\p{Nd}') # 新增:匹配Unicode数字 (Nd = Number, decimal digit) +_Nd_REGEX = regex.compile(r"\p{Nd}") # 新增:匹配Unicode数字 (Nd = Number, decimal digit) SEPARATORS = {"。", ",", ",", " ", ";", "\xa0", "\n", ".", "—", "!", "?"} KNOWN_ABBREVIATIONS_ENDING_WITH_DOT = { - "Mr.", "Mrs.", "Ms.", "Dr.", "Prof.", "St.", "Messrs.", "Mmes.", "Capt.", "Gov.", - "Inc.", "Ltd.", "Corp.", "Co.", "PLC", # PLC通常不带点,但有些可能 - "vs.", "etc.", "i.e.", "e.g.", "viz.", "al.", "et al.", "ca.", "cf.", - "No.", "Vol.", "pp.", "fig.", "figs.", "ed.", "Ph.D.", "M.D.", "B.A.", "M.A.", - "Jan.", "Feb.", "Mar.", "Apr.", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.", # May. 通常不用点 - "Mon.", "Tue.", "Wed.", "Thu.", "Fri.", "Sat.", "Sun.", - "U.S.", "U.K.", "E.U.", "U.S.A.", "U.S.S.R.", - "Ave.", "Blvd.", "Rd.", "Ln.", # Street suffixes - "approx.", "dept.", "appt.", "श्री." # Hindi Shri. + "Mr.", + "Mrs.", + "Ms.", + "Dr.", + "Prof.", + "St.", + "Messrs.", + "Mmes.", + "Capt.", + "Gov.", + "Inc.", + "Ltd.", + "Corp.", + "Co.", + "PLC", # PLC通常不带点,但有些可能 + "vs.", + "etc.", + "i.e.", + "e.g.", + "viz.", + "al.", + "et al.", + "ca.", + "cf.", + "No.", + "Vol.", + "pp.", + "fig.", + "figs.", + "ed.", + "Ph.D.", + "M.D.", + "B.A.", + "M.A.", + "Jan.", + "Feb.", + "Mar.", + "Apr.", + "Jun.", + "Jul.", + "Aug.", + "Sep.", + "Oct.", + "Nov.", + "Dec.", # May. 通常不用点 + "Mon.", + "Tue.", + "Wed.", + "Thu.", + "Fri.", + "Sat.", + "Sun.", + "U.S.", + "U.K.", + "E.U.", + "U.S.A.", + "U.S.S.R.", + "Ave.", + "Blvd.", + "Rd.", + "Ln.", # Street suffixes + "approx.", + "dept.", + "appt.", + "श्री.", # Hindi Shri. } + def is_letter_not_han(char_str: str) -> bool: """ 检查字符是否为“字母”且“非汉字”。 @@ -68,7 +125,7 @@ def is_digit(char_str: str) -> bool: return _Nd_REGEX.fullmatch(char_str) is not None -def is_relevant_word_char(char_str: str) -> bool: # 新增辅助函数 +def is_relevant_word_char(char_str: str) -> bool: # 新增辅助函数 """ 检查字符是否为“相关词语字符”(非汉字字母 或 数字)。 用于判断在非中文语境下,空格两侧是否应被视为一个词内部的部分。 @@ -85,7 +142,7 @@ def is_relevant_word_char(char_str: str) -> bool: # 新增辅助函数 # 检查是否为Unicode数字 if _Nd_REGEX.fullmatch(char_str): - return True # 数字本身被视为相关词语字符 + return True # 数字本身被视为相关词语字符 return False @@ -249,15 +306,17 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: # print(f"DEBUG: 输入文本 (repr): {repr(text)}") # 预处理 - text = regex.sub(r"\n\s*\n+", "\n", text) # 合并多个换行符 - text = regex.sub(r"\n\s*([—。.,,;\s\xa0!?])", r"\1", text) - text = regex.sub(r"([—。.,,;\s\xa0!?])\s*\n", r"\1", text) + text = regex.sub(r"\n\s*\n+", "\n", text) # 合并多个换行符 + text = regex.sub(r"\n\s*([—。.,,;\s\xa0!?])", r"\1", text) + text = regex.sub(r"([—。.,,;\s\xa0!?])\s*\n", r"\1", text) + def replace_han_newline(match): char1 = match.group(1) char2 = match.group(2) if is_han_character(char1) and is_han_character(char2): - return char1 + "," + char2 # 汉字间的换行符替换为逗号 + return char1 + "," + char2 # 汉字间的换行符替换为逗号 return match.group(0) + text = regex.sub(r"(.)\n(.)", replace_han_newline, text) len_text = len(text) @@ -277,25 +336,26 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: if char in SEPARATORS: can_split_current_char = True - if char == '.': + if char == ".": can_split_this_dot = True # 规则1: 小数点 (数字.数字) - if 0 < i < len_text - 1 and is_digit(text[i-1]) and is_digit(text[i+1]): + if 0 < i < len_text - 1 and is_digit(text[i - 1]) and is_digit(text[i + 1]): can_split_this_dot = False # 规则2: 西文缩写/域名内部的点 (西文字母.西文字母) - elif 0 < i < len_text - 1 and is_letter_not_han(text[i-1]) and is_letter_not_han(text[i+1]): + elif 0 < i < len_text - 1 and is_letter_not_han(text[i - 1]) and is_letter_not_han(text[i + 1]): can_split_this_dot = False # 规则3: 已知缩写词的末尾点 (例如 "e.g. ", "U.S.A. ") else: potential_abbreviation_word = current_segment + char - is_followed_by_space = (i + 1 < len_text and text[i+1] == ' ') - is_at_end_of_text = (i + 1 == len_text) + is_followed_by_space = i + 1 < len_text and text[i + 1] == " " + is_at_end_of_text = i + 1 == len_text - if potential_abbreviation_word in KNOWN_ABBREVIATIONS_ENDING_WITH_DOT and \ - (is_followed_by_space or is_at_end_of_text): + if potential_abbreviation_word in KNOWN_ABBREVIATIONS_ENDING_WITH_DOT and ( + is_followed_by_space or is_at_end_of_text + ): can_split_this_dot = False can_split_current_char = can_split_this_dot - elif char == ' ' or char == '\xa0': # 处理空格/NBSP + elif char == " " or char == "\xa0": # 处理空格/NBSP if 0 < i < len_text - 1: prev_char = text[i - 1] next_char = text[i + 1] @@ -304,19 +364,19 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: can_split_current_char = False if can_split_current_char: - if current_segment: # 如果当前段落有内容,则添加 (内容, 分隔符) + if current_segment: # 如果当前段落有内容,则添加 (内容, 分隔符) segments.append((current_segment, char)) # 如果当前段落为空,但分隔符不是简单的排版空格 (除非是换行符这种有意义的空行分隔) - elif char not in [' ', '\xa0'] or char == '\n': - segments.append(("", char)) # 添加 ("", 分隔符) - current_segment = "" # 重置当前段落 + elif char not in [" ", "\xa0"] or char == "\n": + segments.append(("", char)) # 添加 ("", 分隔符) + current_segment = "" # 重置当前段落 else: - current_segment += char # 不分割,将当前分隔符加入到当前段落 + current_segment += char # 不分割,将当前分隔符加入到当前段落 else: - current_segment += char # 非分隔符,加入当前段落 + current_segment += char # 非分隔符,加入当前段落 i += 1 - if current_segment: # 处理末尾剩余的段落 + if current_segment: # 处理末尾剩余的段落 segments.append((current_segment, "")) # 过滤掉仅由空格组成的segment,但保留其后的有效分隔符 @@ -325,7 +385,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: stripped_content = content.strip() if stripped_content: filtered_segments.append((stripped_content, sep)) - elif sep and (sep not in [' ', '\xa0'] or sep == '\n'): + elif sep and (sep not in [" ", "\xa0"] or sep == "\n"): filtered_segments.append(("", sep)) segments = filtered_segments @@ -335,33 +395,32 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: preliminary_final_sentences = [] current_sentence_build = "" for k, (content, sep) in enumerate(segments): - current_sentence_build += content # 先添加内容部分 + current_sentence_build += content # 先添加内容部分 # 判断分隔符类型 is_strong_terminator = sep in {"。", ".", "!", "?", "\n", "—"} - is_space_separator = sep in [' ', '\xa0'] + is_space_separator = sep in [" ", "\xa0"] if is_strong_terminator: - current_sentence_build += sep # 将强终止符加入 + current_sentence_build += sep # 将强终止符加入 if current_sentence_build.strip(): preliminary_final_sentences.append(current_sentence_build.strip()) - current_sentence_build = "" # 开始新的句子构建 + current_sentence_build = "" # 开始新的句子构建 elif is_space_separator: # 如果是空格,并且当前构建的句子不以空格结尾,则添加空格并继续构建 if not current_sentence_build.endswith(sep): current_sentence_build += sep - elif sep: # 其他分隔符 (如 ',', ';') - current_sentence_build += sep # 加入并继续构建,这些通常不独立成句 + elif sep: # 其他分隔符 (如 ',', ';') + current_sentence_build += sep # 加入并继续构建,这些通常不独立成句 # 如果这些弱分隔符后紧跟的就是文本末尾,则它们可能结束一个句子 - if k == len(segments) -1 and current_sentence_build.strip(): + if k == len(segments) - 1 and current_sentence_build.strip(): preliminary_final_sentences.append(current_sentence_build.strip()) current_sentence_build = "" - - if current_sentence_build.strip(): # 处理最后一个构建中的句子 + if current_sentence_build.strip(): # 处理最后一个构建中的句子 preliminary_final_sentences.append(current_sentence_build.strip()) - preliminary_final_sentences = [s for s in preliminary_final_sentences if s.strip()] # 清理空字符串 + preliminary_final_sentences = [s for s in preliminary_final_sentences if s.strip()] # 清理空字符串 # print(f"DEBUG: 初步分割(优化组装后)的句子: {preliminary_final_sentences}") if not preliminary_final_sentences: @@ -377,12 +436,12 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: if merge_probability == 1.0 and len(preliminary_final_sentences) > 1: merged_text = " ".join(preliminary_final_sentences).strip() - if merged_text.endswith(',') or merged_text.endswith(','): + if merged_text.endswith(",") or merged_text.endswith(","): merged_text = merged_text[:-1].strip() return [merged_text] if merged_text else [] elif len(preliminary_final_sentences) == 1: s = preliminary_final_sentences[0].strip() - if s.endswith(',') or s.endswith(','): + if s.endswith(",") or s.endswith(","): s = s[:-1].strip() return [s] if s else [] @@ -407,7 +466,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: processed_sentences_after_merge = [] for sentence in final_sentences_merged: s = sentence.strip() - if s.endswith(',') or s.endswith(','): + if s.endswith(",") or s.endswith(","): s = s[:-1].strip() if s: s = random_remove_punctuation(s) diff --git a/src/experimental/Legacy_HFC/heartflow_prompt_builder.py b/src/experimental/Legacy_HFC/heartflow_prompt_builder.py index 1877c2e9..d1873d58 100644 --- a/src/experimental/Legacy_HFC/heartflow_prompt_builder.py +++ b/src/experimental/Legacy_HFC/heartflow_prompt_builder.py @@ -294,7 +294,11 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s if isinstance(expr, dict) and "situation" in expr and "style" in expr: style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}") - style_habbits_str = "\n你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:\n".join(style_habbits) + style_habbits_str = ( + "\n你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:\n".join( + style_habbits + ) + ) grammar_habbits_str = "\n请你根据情景使用以下句法:\n".join(grammar_habbits) else: reply_styles1 = [ @@ -947,6 +951,7 @@ class PromptBuilder: logger.error(traceback.format_exc()) return "[构建 Planner Prompt 时出错]" + def weighted_sample_no_replacement(items, weights, k) -> list: """ 加权且不放回地随机抽取k个元素。 diff --git a/src/experimental/PFC/action_planner.py b/src/experimental/PFC/action_planner.py index 154e319b..5feaea0d 100644 --- a/src/experimental/PFC/action_planner.py +++ b/src/experimental/PFC/action_planner.py @@ -292,14 +292,16 @@ class ActionPlanner: "reason", "emoji_query", default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待", "emoji_query": ""}, - allow_empty_string_fields=["emoji_query"] + allow_empty_string_fields=["emoji_query"], ) initial_action = initial_result.get("action", "wait") initial_reason = initial_result.get("reason", "LLM未提供原因,默认等待") - 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 存在 + 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}") diff --git a/src/experimental/PFC/actions.py b/src/experimental/PFC/actions.py index 9978e0a3..ad55bc9c 100644 --- a/src/experimental/PFC/actions.py +++ b/src/experimental/PFC/actions.py @@ -11,10 +11,10 @@ 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.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 的发送器依赖这个 +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 # 用于类型提示以避免循环导入 @@ -468,7 +468,7 @@ async def handle_action( final_status = "max_checker_attempts_failed" final_reason = f"达到最大回复尝试次数({max_reply_attempts}),ReplyChecker仍判定不合适: {check_reason}" action_successful = False - if conversation_info: # 确保 conversation_info 存在 + if conversation_info: # 确保 conversation_info 存在 conversation_info.last_successful_reply_action = None # my_message_count 保持不变 @@ -632,49 +632,61 @@ async def handle_action( elif action == "send_memes": conversation_instance.state = ConversationState.GENERATING final_reason_prefix = "发送表情包" - action_successful = False # 先假设不成功 + 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 为空。") + 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 + 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 的末尾 + pass # 让代码继续到 try...except...finally 的末尾 - else: # conversation_info 和 observation_info 都存在 + 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 为空,无法获取表情包。") + 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}' 获取表情包...") + 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}") + 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 未初始化,无法发送。") + 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。") + logger.error( + f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 无法将图片 {emoji_path} 转换为 base64。" + ) raise ValueError(f"无法将图片 {emoji_path} 转换为Base64") # --- 统一 Seg 构造方式 (与群聊类似) --- @@ -707,22 +719,24 @@ async def handle_action( 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 + sender_info=None, # 表情通常不是对特定消息的回复 + message_segment=message_segment_for_emoji, # 直接使用构造的 Seg reply=None, is_head=True, is_emoji=True, - thinking_start_time=action_start_time, # 使用动作开始时间作为近似 + thinking_start_time=action_start_time, # 使用动作开始时间作为近似 ) - await message_to_send.process() # 消息预处理 + 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) # 使用全局管理器发送 + 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 # 标记发送成功 + logger.info( + f"[私聊][{conversation_instance.private_name}] PFC 动作 'send_memes': 表情包已发送: {emoji_path} ({emoji_description})" + ) + action_successful = True # 标记发送成功 # final_status 和 final_reason 会在 finally 中设置 # --- 后续成功处理逻辑 (与之前相同,但要确保 conversation_info 存在) --- @@ -745,7 +759,7 @@ async def handle_action( "user_info": bot_user_info.to_dict(), "processed_plain_text": f"[表情包: {emoji_description}]", "detailed_plain_text": f"[表情包: {emoji_path} - {emoji_description}]", - "raw_message": "[CQ:image,file=base64://...]" # 示例 + "raw_message": "[CQ:image,file=base64://...]", # 示例 } observation_info.chat_history.append(bot_meme_message_dict) observation_info.chat_history_count = len(observation_info.chat_history) @@ -756,11 +770,16 @@ async def handle_action( 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 + 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}") + 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() @@ -768,7 +787,9 @@ async def handle_action( 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 + 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 @@ -793,20 +814,23 @@ async def handle_action( 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}' 获取到合适的表情包。") + 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 - + 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( + f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 处理过程中出错: {get_send_emoji_err}" + ) logger.error(traceback.format_exc()) - final_status = "recall" # 或 "error" + final_status = "recall" # 或 "error" final_reason = f"{final_reason_prefix}失败:处理表情包时出错 ({get_send_emoji_err})" action_successful = False if conversation_info: @@ -854,7 +878,7 @@ async def handle_action( 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'): + if conversation_info and hasattr(conversation_info, "current_emoji_query"): conversation_info.current_emoji_query = None except asyncio.CancelledError: @@ -887,8 +911,8 @@ async def handle_action( and action_index < len(conversation_info.done_action) ): # 确定最终状态和原因 - if action_successful: # 如果动作本身标记为成功 - if final_status not in ["done", "done_no_reply"]: # 如果没有被特定成功状态覆盖 + 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": @@ -896,8 +920,8 @@ async def handle_action( # ... (其他动作的默认成功原因) ... else: final_reason = f"动作 {action} 成功完成" - else: # action_successful is False - if final_status in ["recall", "start", "unknown"]: # 如果状态还是初始或未定 + 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 == "动作未成功执行"): @@ -932,37 +956,38 @@ async def handle_action( # cancelled 会让 loop 捕获异常并停止。 # 重置非回复动作的追问状态 (确保 send_memes 被视为回复动作) - if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: # <--- 把 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'): + 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"] # send_memes 的发送内容不同 + and action in ["direct_reply", "send_new_message"] # send_memes 的发送内容不同 and hasattr(conversation_instance, "generated_reply") and conversation_instance.generated_reply ): log_final_reason_msg += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" elif ( - final_status == "done" - and action_successful - and action == "send_memes" + 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_msg}" ) diff --git a/src/experimental/PFC/conversation_info.py b/src/experimental/PFC/conversation_info.py index 47d715d5..a03dabea 100644 --- a/src/experimental/PFC/conversation_info.py +++ b/src/experimental/PFC/conversation_info.py @@ -16,4 +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 # 表情包 \ No newline at end of file + self.current_emoji_query: Optional[str] = None # 表情包 diff --git a/src/experimental/PFC/conversation_loop.py b/src/experimental/PFC/conversation_loop.py index 3d29313d..a12026d7 100644 --- a/src/experimental/PFC/conversation_loop.py +++ b/src/experimental/PFC/conversation_loop.py @@ -339,20 +339,22 @@ async def run_conversation_loop(conversation_instance: "Conversation"): 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 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判定不合适") + 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" + + 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: @@ -360,21 +362,20 @@ async def run_conversation_loop(conversation_instance: "Conversation"): 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" + 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 # 进入主循环的下一次迭代 - + _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( @@ -382,8 +383,10 @@ async def run_conversation_loop(conversation_instance: "Conversation"): ) forced_wait_action_on_consecutive_failure = "wait" - reason_for_consecutive_failure_wait = f"LLM连续失败{conversation_instance.consecutive_llm_action_failures}次,强制等待" - + 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: @@ -392,7 +395,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"): logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 执行强制等待动作...") await actions.handle_action( conversation_instance, - forced_wait_action_on_consecutive_failure, # "wait" + forced_wait_action_on_consecutive_failure, # "wait" reason_for_consecutive_failure_wait, conversation_instance.observation_info, conversation_instance.conversation_info, diff --git a/src/experimental/PFC/pfc_utils.py b/src/experimental/PFC/pfc_utils.py index adcf03b2..f250e8bc 100644 --- a/src/experimental/PFC/pfc_utils.py +++ b/src/experimental/PFC/pfc_utils.py @@ -505,12 +505,14 @@ def get_items_from_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}" ) @@ -547,16 +549,16 @@ def get_items_from_json( except Exception as e: logger.error(f"[私聊][{private_name}] 尝试解析JSON数组时发生未知错误: {str(e)}") # result = default_result.copy() - + json_data = None - valid_single_object = True # <--- 将初始化提前到这里 + 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)}") # 如果不是字典,即使 allow_array 为 False,这里也应该认为单个对象解析失败 - valid_single_object = False # 标记为无效 + valid_single_object = False # 标记为无效 # return False, default_result.copy() # 不立即返回,让后续逻辑统一处理 valid_single_object except json.JSONDecodeError: json_pattern = r"\{[\s\S]*?\}" @@ -567,31 +569,30 @@ def get_items_from_json( json_data = json.loads(potential_json_str) if not isinstance(json_data, dict): logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}") - valid_single_object = False # 标记为无效 + 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。") - valid_single_object = False # 标记为无效 + valid_single_object = False # 标记为无效 # return False, default_result.copy() else: logger.error( f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}..." ) - valid_single_object = False # 标记为无效 + 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 类型的检查 + 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对象或数组。") + 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() @@ -604,9 +605,9 @@ def get_items_from_json( 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.copy() # 如果字段缺失,则校验失败 + return False, default_result.copy() # 如果字段缺失,则校验失败 if required_types: for field, expected_type in required_types.items(): @@ -616,15 +617,17 @@ def get_items_from_json( ) valid_single_object = False break - + if not valid_single_object: - return False, default_result.copy() # 如果类型错误,则校验失败 + return False, default_result.copy() # 如果类型错误,则校验失败 for field in items: - 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: + 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 @@ -634,7 +637,6 @@ def get_items_from_json( return True, current_single_result else: return False, default_result.copy() - async def get_person_id(private_name: str, chat_stream: ChatStream):