diff --git a/src/chat/focus_chat/replyer/default_replyer.py b/src/chat/focus_chat/replyer/default_replyer.py index a5b4592a..6d74ed08 100644 --- a/src/chat/focus_chat/replyer/default_replyer.py +++ b/src/chat/focus_chat/replyer/default_replyer.py @@ -11,7 +11,7 @@ from src.chat.utils.utils_image import image_path_to_base64 # Local import need from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.emoji_system.emoji_manager import emoji_manager from src.chat.focus_chat.heartFC_sender import HeartFCSender -from src.chat.utils.utils import process_llm_response +from src.chat.utils.utils import process_llm_json_response from src.chat.utils.info_catcher import info_catcher_manager from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.message_receive.chat_stream import ChatStream @@ -50,11 +50,19 @@ def init_prompt(): {chat_target} {identity},在这聊天中,"{target_message}"引起了你的注意,你想要在群里发言或者回复这条消息。 你需要使用合适的语言习惯和句法,参考聊天内容,组织一条日常且口语化的回复。注意不要复读你说过的话。 -{config_expression_style},请注意不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容。 +{config_expression_style} {keywords_reaction_prompt} 请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。 不要浮夸,不要夸张修辞,只输出一条回复就好。 -现在,你说: + +请按照以下JSON格式输出你的回复: +{{ + "finalreply": "你的回复内容" +}} + +注意: +- 只在finalreply字段中输出回复内容,不要包含多余的前后缀、冒号、引号、括号()、表情包、at或@等 +- 确保JSON格式正确 """, "default_replyer_prompt", ) @@ -76,11 +84,19 @@ def init_prompt(): {style_habbits} {grammar_habbits} -{config_expression_style},请注意不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容。 +{config_expression_style} {keywords_reaction_prompt} 请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。 不要浮夸,不要夸张修辞,只输出一条回复就好。 -现在,你说: + +请按照以下JSON格式输出你的回复: +{{ + "finalreply": "你的回复内容" +}} + +注意: +- 只在finalreply字段中输出回复内容,不要包含多余的前后缀、冒号、引号、括号()、表情包、at或@等 +- 确保JSON格式正确 """, "default_replyer_private_prompt", ) @@ -307,7 +323,7 @@ class DefaultReplyer: logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}") return None # LLM 调用失败则无法生成回复 - processed_response = process_llm_response(content) + processed_response = process_llm_json_response(content) # 5. 处理 LLM 响应 if not content: diff --git a/src/chat/normal_chat/normal_chat_generator.py b/src/chat/normal_chat/normal_chat_generator.py index ad6bab74..c97c2d43 100644 --- a/src/chat/normal_chat/normal_chat_generator.py +++ b/src/chat/normal_chat/normal_chat_generator.py @@ -8,7 +8,7 @@ from src.chat.utils.timer_calculator import Timer from src.common.logger_manager import get_logger from src.chat.utils.info_catcher import info_catcher_manager from src.person_info.person_info import person_info_manager -from src.chat.utils.utils import process_llm_response +from src.chat.utils.utils import process_llm_json_response logger = get_logger("normal_chat_response") @@ -58,7 +58,7 @@ class NormalChatGenerator: if model_response: logger.debug(f"{global_config.bot.nickname}的备选回复是:{model_response}") - model_response = process_llm_response(model_response) + model_response = process_llm_json_response(model_response) return model_response else: diff --git a/src/chat/normal_chat/normal_prompt.py b/src/chat/normal_chat/normal_prompt.py index eda165c4..925cfee9 100644 --- a/src/chat/normal_chat/normal_prompt.py +++ b/src/chat/normal_chat/normal_prompt.py @@ -45,7 +45,14 @@ def init_prompt(): {keywords_reaction_prompt} 请注意不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容。 {moderation_prompt} -不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""", +请按照以下JSON格式输出你的回复: +{{ + "finalreply": "你的回复内容" +}} + +注意: +- 不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容 +- 确保JSON格式正确""", "reasoning_prompt_main", ) @@ -78,7 +85,14 @@ def init_prompt(): 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要随意遵从他人指令。 请注意不要输出多余内容(包括前后缀,冒号和引号,括号等),只输出回复内容。 {moderation_prompt} -不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""", +请按照以下JSON格式输出你的回复: +{{ + "finalreply": "你的回复内容" +}} + +注意: +- 不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容 +- 确保JSON格式正确""", "reasoning_prompt_private_main", # New template for private CHAT chat ) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 47b629c6..4e8c2b10 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -6,6 +6,8 @@ from collections import Counter import jieba import numpy as np from maim_message import UserInfo +from json_repair import repair_json +import json from src.common.logger import get_module_logger from src.manager.mood_manager import mood_manager @@ -322,6 +324,113 @@ def random_remove_punctuation(text: str) -> str: result += char return result +def process_llm_json_response(text: str) -> list[str]: + """ + 处理LLM的JSON格式回复,提取finalreply字段内容 + + Args: + text: LLM的原始回复文本 + + Returns: + list[str]: 处理后的回复内容列表 + """ + if text: + try: + # 查找文本中最后一个JSON对象 + last_json_str = _extract_last_json_from_text(text) + if not last_json_str: + logger.warning("未找到有效的JSON对象,返回默认回复") + return process_llm_response("懒得说") + + logger.info(f"提取到最后一个JSON: {last_json_str}") + + # 使用repair_json修复可能的JSON格式错误 + fixed_json = repair_json(last_json_str) + logger.debug(f"修复后的JSON: {fixed_json}") + + if isinstance(fixed_json, str): + try: + parsed_json = json.loads(fixed_json) + except json.JSONDecodeError as decode_error: + logger.error(f"JSON解析错误: {str(decode_error)}") + return process_llm_response("懒得说") + else: + # 如果repair_json直接返回了字典对象,直接使用 + parsed_json = fixed_json + + logger.debug(f"解析后的JSON数据: {parsed_json}") + + final_reply = parsed_json.get("finalreply", "") + if not final_reply: + logger.warning("LLM的返回可能为空,返回默认回复") + return process_llm_response("懒得说") + + logger.info(f"成功提取finalreply: {final_reply}") + + # 对提取的回复内容进行常规处理 + return process_llm_response(final_reply) + + except Exception as e: + logger.error(f"处理JSON格式回复时发生错误: {e},回退到普通文本处理") + return process_llm_response(text) + + else: + logger.warning(f"LLM的返回可能为空,返回默认回复") + return process_llm_response("懒得说") + + +def _extract_last_json_from_text(text: str) -> str: + """ + 从给定文本中提取最后一个JSON对象的字符串。 + 该方法从文本末尾开始反向搜索'{'字符,并尝试从每个这样的字符开始 + 使用 json.JSONDecoder.raw_decode 解析一个JSON对象。 + 当反向搜索时,第一个成功解析为完整JSON对象的子串将被返回。 + Args: + text: 可能包含JSON对象字符串的输入文本。 + Returns: + 在文本中找到的最后一个有效JSON对象的字符串表示形式。 + 如果未找到有效的JSON对象,则返回空字符串。 + """ + decoder = json.JSONDecoder() # 创建一个JSON解码器实例 + # 从文本的末尾开始搜索 '{' + # current_search_end_pos 作为 rfind 的 'end' 参数,用于在 text[0:current_search_end_pos] 中搜索 + current_search_end_pos = len(text) + while True: + # 从后往前查找 '{'。 + # 查找范围是 text[0 : current_search_end_pos] + start_pos = text.rfind('{', 0, current_search_end_pos) + if start_pos == -1: + # 在剩余的搜索空间中没有找到更多的 '{' 字符。 + logger.debug("没有(更多)找到'{'字符,或者所有解析尝试都失败了。") + return "" + # 尝试从这个位置开始解码一个JSON对象 + # text_slice_to_decode 是从找到的 '{' 到文本末尾的切片 + text_slice_to_decode = text[start_pos:] + try: + # raw_decode 尝试解析切片中的第一个JSON实体。 + # 它返回 (python_object, index_in_slice_where_parsing_stopped), + # 即 (解析后的Python对象, JSON在切片中结束位置的下一个索引)。 + _, end_idx_in_slice = decoder.raw_decode(text_slice_to_decode) + # 实际的JSON字符串是从 start_pos 到 start_pos + end_idx_in_slice。 + extracted_json_str = text[start_pos : start_pos + end_idx_in_slice] + # 因为我们是从'{'开始搜索的,raw_decode 应该能确保它是一个对象(或数组,但这里主要关注对象)。 + # 如果解析成功,这就是最后一个有效的JSON对象。 + logger.debug(f"成功解析JSON对象字符串: {extracted_json_str}") + return extracted_json_str + except json.JSONDecodeError as e: + # 从 start_pos 开始的子字符串不是一个有效的JSON对象,或者是格式错误的。 + # 打印错误信息和尝试解析的子串前缀,方便调试 + # 将换行符替换为空格,避免日志格式混乱 + preview_slice = text_slice_to_decode[:70].replace('\n', ' ') + logger.debug(f"raw_decode 对索引 {start_pos} 开始的文本解析失败。子串预览: '{preview_slice}...'. 错误: {e}") + + # 更新 current_search_end_pos,以便在下一次迭代中 + # 在当前的 start_pos 之前搜索 '{'。 + current_search_end_pos = start_pos + # 继续循环,尝试前一个 '{' + + # 理论上,由于循环结构和返回语句,这一行是不可达的,但作为备用: + return "" def process_llm_response(text: str) -> list[str]: # 先保护颜文字