mirror of https://github.com/Mai-with-u/MaiBot.git
🤖 自动格式化代码 [skip ci]
parent
ffc247eaa8
commit
a90f735af9
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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个元素。
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 # 表情包
|
||||
self.current_emoji_query: Optional[str] = None # 表情包
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue