Merge branch 'dev' of https://github.com/MaiM-with-u/MaiBot into dev
This commit is contained in:
@@ -21,6 +21,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str}
|
||||
{knowledge_info_str}
|
||||
|
||||
【最近行动历史概要】
|
||||
{action_history_summary}
|
||||
@@ -33,7 +34,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,
|
||||
|
||||
------
|
||||
可选行动类型以及解释:
|
||||
fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择
|
||||
fetch_knowledge: 需要调取知识或记忆,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择
|
||||
listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择
|
||||
direct_reply: 直接回复对方
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
@@ -53,6 +54,7 @@ PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str}
|
||||
{knowledge_info_str}
|
||||
|
||||
【最近行动历史概要】
|
||||
{action_history_summary}
|
||||
@@ -224,6 +226,41 @@ class ActionPlanner:
|
||||
logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}")
|
||||
goals_str = "- 构建对话目标时出错。\n"
|
||||
|
||||
# --- 知识信息字符串构建开始 ---
|
||||
knowledge_info_str = "【已获取的相关知识和记忆】\n"
|
||||
try:
|
||||
# 检查 conversation_info 是否有 knowledge_list 并且不为空
|
||||
if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list:
|
||||
# 最多只显示最近的 5 条知识,防止 Prompt 过长
|
||||
recent_knowledge = conversation_info.knowledge_list[-5:]
|
||||
for i, knowledge_item in enumerate(recent_knowledge):
|
||||
if isinstance(knowledge_item, dict):
|
||||
query = knowledge_item.get("query", "未知查询")
|
||||
knowledge = knowledge_item.get("knowledge", "无知识内容")
|
||||
source = knowledge_item.get("source", "未知来源")
|
||||
# 只取知识内容的前 2000 个字,避免太长
|
||||
knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge
|
||||
knowledge_info_str += (
|
||||
f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n"
|
||||
)
|
||||
else:
|
||||
# 处理列表里不是字典的异常情况
|
||||
knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n"
|
||||
|
||||
if not recent_knowledge: # 如果 knowledge_list 存在但为空
|
||||
knowledge_info_str += "- 暂无相关知识和记忆。\n"
|
||||
|
||||
else:
|
||||
# 如果 conversation_info 没有 knowledge_list 属性,或者列表为空
|
||||
knowledge_info_str += "- 暂无相关知识记忆。\n"
|
||||
except AttributeError:
|
||||
logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。")
|
||||
knowledge_info_str += "- 获取知识列表时出错。\n"
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}")
|
||||
knowledge_info_str += "- 处理知识列表时出错。\n"
|
||||
# --- 知识信息字符串构建结束 ---
|
||||
|
||||
# 获取聊天历史记录 (chat_history_text)
|
||||
chat_history_text = ""
|
||||
try:
|
||||
@@ -349,6 +386,7 @@ class ActionPlanner:
|
||||
time_since_last_bot_message_info=time_since_last_bot_message_info,
|
||||
timeout_context=timeout_context,
|
||||
chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。",
|
||||
knowledge_info_str=knowledge_info_str,
|
||||
)
|
||||
|
||||
logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------")
|
||||
|
||||
@@ -525,9 +525,9 @@ class Conversation:
|
||||
)
|
||||
action_successful = True
|
||||
except Exception as fetch_err:
|
||||
logger.error(f"[私聊][{self.private_name}]获取知识时出错: {fetch_err}")
|
||||
logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}")
|
||||
conversation_info.done_action[action_index].update(
|
||||
{"status": "recall", "final_reason": f"获取知识失败: {fetch_err}"}
|
||||
{"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"}
|
||||
)
|
||||
self.conversation_info.last_successful_reply_action = None # 重置状态
|
||||
|
||||
|
||||
@@ -50,21 +50,18 @@ class MessageStorage(ABC):
|
||||
class MongoDBMessageStorage(MessageStorage):
|
||||
"""MongoDB消息存储实现"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = db
|
||||
|
||||
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
|
||||
query = {"chat_id": chat_id}
|
||||
# print(f"storage_check_message: {message_time}")
|
||||
|
||||
query["time"] = {"$gt": message_time}
|
||||
|
||||
return list(self.db.messages.find(query).sort("time", 1))
|
||||
return list(db.messages.find(query).sort("time", 1))
|
||||
|
||||
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
query = {"chat_id": chat_id, "time": {"$lt": time_point}}
|
||||
|
||||
messages = list(self.db.messages.find(query).sort("time", -1).limit(limit))
|
||||
messages = list(db.messages.find(query).sort("time", -1).limit(limit))
|
||||
|
||||
# 将消息按时间正序排列
|
||||
messages.reverse()
|
||||
@@ -73,7 +70,7 @@ class MongoDBMessageStorage(MessageStorage):
|
||||
async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
|
||||
query = {"chat_id": chat_id, "time": {"$gt": after_time}}
|
||||
|
||||
return self.db.messages.find_one(query) is not None
|
||||
return db.messages.find_one(query) is not None
|
||||
|
||||
|
||||
# # 创建一个内存消息存储实现,用于测试
|
||||
|
||||
@@ -68,16 +68,18 @@ class KnowledgeFetcher:
|
||||
max_depth=3,
|
||||
fast_retrieval=False,
|
||||
)
|
||||
knowledge = ""
|
||||
knowledge_text = ""
|
||||
sources_text = "无记忆匹配" # 默认值
|
||||
if related_memory:
|
||||
sources = []
|
||||
for memory in related_memory:
|
||||
knowledge += memory[1] + "\n"
|
||||
knowledge_text += memory[1] + "\n"
|
||||
sources.append(f"记忆片段{memory[0]}")
|
||||
knowledge = knowledge.strip(), ",".join(sources)
|
||||
knowledge_text = knowledge_text.strip()
|
||||
sources_text = ",".join(sources)
|
||||
|
||||
knowledge += "现在有以下**知识**可供参考:\n "
|
||||
knowledge += self._lpmm_get_knowledge(query)
|
||||
knowledge += "请记住这些**知识**,并根据**知识**回答问题。\n"
|
||||
knowledge_text += "\n现在有以下**知识**可供参考:\n "
|
||||
knowledge_text += self._lpmm_get_knowledge(query)
|
||||
knowledge_text += "\n请记住这些**知识**,并根据**知识**回答问题。\n"
|
||||
|
||||
return "未找到相关知识", "无记忆匹配"
|
||||
return knowledge_text or "未找到相关知识", sources_text or "无记忆匹配"
|
||||
|
||||
@@ -17,6 +17,9 @@ logger = get_module_logger("reply_generator")
|
||||
PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复:
|
||||
|
||||
当前对话目标:{goals_str}
|
||||
|
||||
{knowledge_info_str}
|
||||
|
||||
最近的聊天记录:
|
||||
{chat_history_text}
|
||||
|
||||
@@ -25,7 +28,7 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请
|
||||
1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
|
||||
2. 符合你的性格特征和身份细节
|
||||
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
|
||||
4. 适当利用相关知识,但不要生硬引用
|
||||
4. 可以适当利用相关知识,但不要生硬引用
|
||||
5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容
|
||||
|
||||
请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
|
||||
@@ -39,6 +42,9 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请
|
||||
PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息:
|
||||
|
||||
当前对话目标:{goals_str}
|
||||
|
||||
{knowledge_info_str}
|
||||
|
||||
最近的聊天记录:
|
||||
{chat_history_text}
|
||||
|
||||
@@ -47,7 +53,7 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊
|
||||
1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
|
||||
2. 符合你的性格特征和身份细节
|
||||
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
|
||||
4. 适当利用相关知识,但不要生硬引用
|
||||
4. 可以适当利用相关知识,但不要生硬引用
|
||||
5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容
|
||||
|
||||
请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
|
||||
@@ -131,6 +137,38 @@ class ReplyGenerator:
|
||||
else:
|
||||
goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况
|
||||
|
||||
# --- 新增:构建知识信息字符串 ---
|
||||
knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考
|
||||
try:
|
||||
# 检查 conversation_info 是否有 knowledge_list 并且不为空
|
||||
if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list:
|
||||
# 最多只显示最近的 5 条知识
|
||||
recent_knowledge = conversation_info.knowledge_list[-5:]
|
||||
for i, knowledge_item in enumerate(recent_knowledge):
|
||||
if isinstance(knowledge_item, dict):
|
||||
query = knowledge_item.get("query", "未知查询")
|
||||
knowledge = knowledge_item.get("knowledge", "无知识内容")
|
||||
source = knowledge_item.get("source", "未知来源")
|
||||
# 只取知识内容的前 2000 个字
|
||||
knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge
|
||||
knowledge_info_str += (
|
||||
f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁
|
||||
)
|
||||
else:
|
||||
knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n"
|
||||
|
||||
if not recent_knowledge:
|
||||
knowledge_info_str += "- 暂无。\n" # 更简洁的提示
|
||||
|
||||
else:
|
||||
knowledge_info_str += "- 暂无。\n"
|
||||
except AttributeError:
|
||||
logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。")
|
||||
knowledge_info_str += "- 获取知识列表时出错。\n"
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}")
|
||||
knowledge_info_str += "- 处理知识列表时出错。\n"
|
||||
|
||||
# 获取聊天历史记录 (chat_history_text)
|
||||
chat_history_text = observation_info.chat_history_str
|
||||
if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages:
|
||||
@@ -162,7 +200,10 @@ class ReplyGenerator:
|
||||
|
||||
# --- 格式化最终的 Prompt ---
|
||||
prompt = prompt_template.format(
|
||||
persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text
|
||||
persona_text=persona_text,
|
||||
goals_str=goals_str,
|
||||
chat_history_text=chat_history_text,
|
||||
knowledge_info_str=knowledge_info_str,
|
||||
)
|
||||
|
||||
# --- 调用 LLM 生成 ---
|
||||
|
||||
@@ -99,15 +99,20 @@ class ChatBot:
|
||||
template_group_name = None
|
||||
|
||||
async def preprocess():
|
||||
logger.trace("开始预处理消息...")
|
||||
# 如果在私聊中
|
||||
if groupinfo is None:
|
||||
logger.trace("检测到私聊消息")
|
||||
# 是否在配置信息中开启私聊模式
|
||||
if global_config.enable_friend_chat:
|
||||
logger.trace("私聊模式已启用")
|
||||
# 是否进入PFC
|
||||
if global_config.enable_pfc_chatting:
|
||||
logger.trace("进入PFC私聊处理流程")
|
||||
userinfo = message.message_info.user_info
|
||||
messageinfo = message.message_info
|
||||
# 创建聊天流
|
||||
logger.trace(f"为{userinfo.user_id}创建/获取聊天流")
|
||||
chat = await chat_manager.get_or_create_stream(
|
||||
platform=messageinfo.platform,
|
||||
user_info=userinfo,
|
||||
@@ -118,9 +123,11 @@ class ChatBot:
|
||||
await self._create_pfc_chat(message)
|
||||
# 禁止PFC,进入普通的心流消息处理逻辑
|
||||
else:
|
||||
logger.trace("进入普通心流私聊处理")
|
||||
await self.heartflow_processor.process_message(message_data)
|
||||
# 群聊默认进入心流消息处理逻辑
|
||||
else:
|
||||
logger.trace(f"检测到群聊消息,群ID: {groupinfo.group_id}")
|
||||
await self.heartflow_processor.process_message(message_data)
|
||||
|
||||
if template_group_name:
|
||||
|
||||
@@ -159,16 +159,16 @@ class MessageManager:
|
||||
logger.warning("Processor task already running.")
|
||||
return
|
||||
self._processor_task = asyncio.create_task(self._start_processor_loop())
|
||||
logger.info("MessageManager processor task started.")
|
||||
logger.debug("MessageManager processor task started.")
|
||||
|
||||
def stop(self):
|
||||
"""停止后台处理器任务。"""
|
||||
self._running = False
|
||||
if hasattr(self, "_processor_task") and not self._processor_task.done():
|
||||
self._processor_task.cancel()
|
||||
logger.info("MessageManager processor task stopping.")
|
||||
logger.debug("MessageManager processor task stopping.")
|
||||
else:
|
||||
logger.info("MessageManager processor task not running or already stopped.")
|
||||
logger.debug("MessageManager processor task not running or already stopped.")
|
||||
|
||||
async def get_container(self, chat_id: str) -> MessageContainer:
|
||||
"""获取或创建聊天流的消息容器 (异步,使用锁)"""
|
||||
|
||||
@@ -732,6 +732,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
|
||||
return f"{int(diff / 86400)}天前:\n"
|
||||
else:
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n"
|
||||
elif mode == "lite":
|
||||
# 只返回时分秒格式,喵~
|
||||
return time.strftime("%H:%M:%S", time.localtime(timestamp))
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import hashlib
|
||||
from typing import Optional
|
||||
from PIL import Image
|
||||
import io
|
||||
import numpy as np
|
||||
|
||||
|
||||
from ...common.database import db
|
||||
@@ -231,14 +232,16 @@ class ImageManager:
|
||||
return "[图片]"
|
||||
|
||||
@staticmethod
|
||||
def transform_gif(gif_base64: str) -> str:
|
||||
"""将GIF转换为水平拼接的静态图像
|
||||
def transform_gif(gif_base64: str, similarity_threshold: float = 1000.0, max_frames: int = 15) -> Optional[str]:
|
||||
"""将GIF转换为水平拼接的静态图像, 跳过相似的帧
|
||||
|
||||
Args:
|
||||
gif_base64: GIF的base64编码字符串
|
||||
similarity_threshold: 判定帧相似的阈值 (MSE),越小表示要求差异越大才算不同帧,默认1000.0
|
||||
max_frames: 最大抽取的帧数,默认15
|
||||
|
||||
Returns:
|
||||
str: 拼接后的JPG图像的base64编码字符串
|
||||
Optional[str]: 拼接后的JPG图像的base64编码字符串, 或者在失败时返回None
|
||||
"""
|
||||
try:
|
||||
# 解码base64
|
||||
@@ -246,41 +249,88 @@ class ImageManager:
|
||||
gif = Image.open(io.BytesIO(gif_data))
|
||||
|
||||
# 收集所有帧
|
||||
frames = []
|
||||
all_frames = []
|
||||
try:
|
||||
while True:
|
||||
gif.seek(len(frames))
|
||||
gif.seek(len(all_frames))
|
||||
# 确保是RGB格式方便比较
|
||||
frame = gif.convert("RGB")
|
||||
frames.append(frame.copy())
|
||||
all_frames.append(frame.copy())
|
||||
except EOFError:
|
||||
pass
|
||||
pass # 读完啦
|
||||
|
||||
if not frames:
|
||||
raise ValueError("No frames found in GIF")
|
||||
if not all_frames:
|
||||
logger.warning("GIF中没有找到任何帧")
|
||||
return None # 空的GIF直接返回None
|
||||
|
||||
# 计算需要抽取的帧的索引
|
||||
total_frames = len(frames)
|
||||
if total_frames <= 15:
|
||||
selected_frames = frames
|
||||
else:
|
||||
# 均匀抽取10帧
|
||||
indices = [int(i * (total_frames - 1) / 14) for i in range(15)]
|
||||
selected_frames = [frames[i] for i in indices]
|
||||
# --- 新的帧选择逻辑 ---
|
||||
selected_frames = []
|
||||
last_selected_frame_np = None
|
||||
|
||||
# 获取单帧的尺寸
|
||||
for i, current_frame in enumerate(all_frames):
|
||||
current_frame_np = np.array(current_frame)
|
||||
|
||||
# 第一帧总是要选的
|
||||
if i == 0:
|
||||
selected_frames.append(current_frame)
|
||||
last_selected_frame_np = current_frame_np
|
||||
continue
|
||||
|
||||
# 计算和上一张选中帧的差异(均方误差 MSE)
|
||||
if last_selected_frame_np is not None:
|
||||
mse = np.mean((current_frame_np - last_selected_frame_np) ** 2)
|
||||
# logger.trace(f"帧 {i} 与上一选中帧的 MSE: {mse}") # 可以取消注释来看差异值
|
||||
|
||||
# 如果差异够大,就选它!
|
||||
if mse > similarity_threshold:
|
||||
selected_frames.append(current_frame)
|
||||
last_selected_frame_np = current_frame_np
|
||||
# 检查是不是选够了
|
||||
if len(selected_frames) >= max_frames:
|
||||
# logger.debug(f"已选够 {max_frames} 帧,停止选择。")
|
||||
break
|
||||
# 如果差异不大就跳过这一帧啦
|
||||
|
||||
# --- 帧选择逻辑结束 ---
|
||||
|
||||
# 如果选择后连一帧都没有(比如GIF只有一帧且后续处理失败?)或者原始GIF就没帧,也返回None
|
||||
if not selected_frames:
|
||||
logger.warning("处理后没有选中任何帧")
|
||||
return None
|
||||
|
||||
# logger.debug(f"总帧数: {len(all_frames)}, 选中帧数: {len(selected_frames)}")
|
||||
|
||||
# 获取选中的第一帧的尺寸(假设所有帧尺寸一致)
|
||||
frame_width, frame_height = selected_frames[0].size
|
||||
|
||||
# 计算目标尺寸,保持宽高比
|
||||
target_height = 200 # 固定高度
|
||||
# 防止除以零
|
||||
if frame_height == 0:
|
||||
logger.error("帧高度为0,无法计算缩放尺寸")
|
||||
return None
|
||||
target_width = int((target_height / frame_height) * frame_width)
|
||||
# 宽度也不能是0
|
||||
if target_width == 0:
|
||||
logger.warning(f"计算出的目标宽度为0 (原始尺寸 {frame_width}x{frame_height}),调整为1")
|
||||
target_width = 1
|
||||
|
||||
# 调整所有帧的大小
|
||||
# 调整所有选中帧的大小
|
||||
resized_frames = [
|
||||
frame.resize((target_width, target_height), Image.Resampling.LANCZOS) for frame in selected_frames
|
||||
]
|
||||
|
||||
# 创建拼接图像
|
||||
total_width = target_width * len(resized_frames)
|
||||
# 防止总宽度为0
|
||||
if total_width == 0 and len(resized_frames) > 0:
|
||||
logger.warning("计算出的总宽度为0,但有选中帧,可能目标宽度太小")
|
||||
# 至少给点宽度吧
|
||||
total_width = len(resized_frames)
|
||||
elif total_width == 0:
|
||||
logger.error("计算出的总宽度为0且无选中帧")
|
||||
return None
|
||||
|
||||
combined_image = Image.new("RGB", (total_width, target_height))
|
||||
|
||||
# 水平拼接图像
|
||||
@@ -289,14 +339,17 @@ class ImageManager:
|
||||
|
||||
# 转换为base64
|
||||
buffer = io.BytesIO()
|
||||
combined_image.save(buffer, format="JPEG", quality=85)
|
||||
combined_image.save(buffer, format="JPEG", quality=85) # 保存为JPEG
|
||||
result_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
return result_base64
|
||||
|
||||
except MemoryError:
|
||||
logger.error("GIF转换失败: 内存不足,可能是GIF太大或帧数太多")
|
||||
return None # 内存不够啦
|
||||
except Exception as e:
|
||||
logger.error(f"GIF转换失败: {str(e)}")
|
||||
return None
|
||||
logger.error(f"GIF转换失败: {str(e)}", exc_info=True) # 记录详细错误信息
|
||||
return None # 其他错误也返回None
|
||||
|
||||
|
||||
# 创建全局单例
|
||||
|
||||
@@ -34,9 +34,12 @@ MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换
|
||||
class MaiEmoji:
|
||||
"""定义一个表情包"""
|
||||
|
||||
def __init__(self, filename: str, path: str):
|
||||
self.path = path # 存储目录路径
|
||||
self.filename = filename
|
||||
def __init__(self, full_path: str):
|
||||
if not full_path:
|
||||
raise ValueError("full_path cannot be empty")
|
||||
self.full_path = full_path # 文件的完整路径 (包括文件名)
|
||||
self.path = os.path.dirname(full_path) # 文件所在的目录路径
|
||||
self.filename = os.path.basename(full_path) # 文件名
|
||||
self.embedding = []
|
||||
self.hash = "" # 初始为空,在创建实例时会计算
|
||||
self.description = ""
|
||||
@@ -48,35 +51,58 @@ class MaiEmoji:
|
||||
self.format = ""
|
||||
|
||||
async def initialize_hash_format(self):
|
||||
"""从文件创建表情包实例
|
||||
|
||||
参数:
|
||||
file_path: 文件的完整路径
|
||||
|
||||
返回:
|
||||
MaiEmoji: 创建的表情包实例,如果失败则返回None
|
||||
"""
|
||||
"""从文件创建表情包实例, 计算哈希值和格式"""
|
||||
image_base64 = None
|
||||
image_bytes = None
|
||||
try:
|
||||
file_path = os.path.join(self.path, self.filename)
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"[错误] 表情包文件不存在: {file_path}")
|
||||
# 使用 full_path 检查文件是否存在
|
||||
if not os.path.exists(self.full_path):
|
||||
logger.error(f"[初始化错误] 表情包文件不存在: {self.full_path}")
|
||||
self.is_deleted = True
|
||||
return None
|
||||
|
||||
image_base64 = image_path_to_base64(file_path)
|
||||
# 使用 full_path 读取文件
|
||||
logger.debug(f"[初始化] 正在读取文件: {self.full_path}")
|
||||
image_base64 = image_path_to_base64(self.full_path)
|
||||
if image_base64 is None:
|
||||
logger.error(f"[错误] 无法读取图片: {file_path}")
|
||||
logger.error(f"[初始化错误] 无法读取或转换Base64: {self.full_path}")
|
||||
self.is_deleted = True
|
||||
return None
|
||||
logger.debug(f"[初始化] 文件读取成功 (Base64预览: {image_base64[:50]}...)")
|
||||
|
||||
# 计算哈希值
|
||||
logger.debug(f"[初始化] 正在解码Base64并计算哈希: {self.filename}")
|
||||
image_bytes = base64.b64decode(image_base64)
|
||||
self.hash = hashlib.md5(image_bytes).hexdigest()
|
||||
logger.debug(f"[初始化] 哈希计算成功: {self.hash}")
|
||||
|
||||
# 获取图片格式
|
||||
self.format = Image.open(io.BytesIO(image_bytes)).format.lower()
|
||||
logger.debug(f"[初始化] 正在使用Pillow获取格式: {self.filename}")
|
||||
try:
|
||||
with Image.open(io.BytesIO(image_bytes)) as img:
|
||||
self.format = img.format.lower()
|
||||
logger.debug(f"[初始化] 格式获取成功: {self.format}")
|
||||
except Exception as pil_error:
|
||||
logger.error(f"[初始化错误] Pillow无法处理图片 ({self.filename}): {pil_error}")
|
||||
logger.error(traceback.format_exc())
|
||||
self.is_deleted = True
|
||||
return None
|
||||
|
||||
# 如果所有步骤成功,返回 True
|
||||
return True
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}")
|
||||
self.is_deleted = True
|
||||
return None
|
||||
except base64.binascii.Error as b64_error:
|
||||
logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}")
|
||||
self.is_deleted = True
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 初始化表情包失败: {str(e)}")
|
||||
logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
self.is_deleted = True
|
||||
return None
|
||||
|
||||
async def register_to_db(self):
|
||||
@@ -87,61 +113,72 @@ class MaiEmoji:
|
||||
"""
|
||||
try:
|
||||
# 确保目标目录存在
|
||||
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
|
||||
|
||||
# 源路径是当前实例的完整路径
|
||||
source_path = os.path.join(self.path, self.filename)
|
||||
# 目标路径
|
||||
destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
|
||||
# 源路径是当前实例的完整路径 self.full_path
|
||||
source_full_path = self.full_path
|
||||
# 目标完整路径
|
||||
destination_full_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
|
||||
|
||||
# 检查源文件是否存在
|
||||
if not os.path.exists(source_path):
|
||||
logger.error(f"[错误] 源文件不存在: {source_path}")
|
||||
if not os.path.exists(source_full_path):
|
||||
logger.error(f"[错误] 源文件不存在: {source_full_path}")
|
||||
return False
|
||||
|
||||
# --- 文件移动 ---
|
||||
try:
|
||||
# 如果目标文件已存在,先删除 (确保移动成功)
|
||||
if os.path.exists(destination_path):
|
||||
os.remove(destination_path)
|
||||
if os.path.exists(destination_full_path):
|
||||
os.remove(destination_full_path)
|
||||
|
||||
os.rename(source_path, destination_path)
|
||||
logger.info(f"[移动] 文件从 {source_path} 移动到 {destination_path}")
|
||||
# 更新实例的路径属性为新目录
|
||||
os.rename(source_full_path, destination_full_path)
|
||||
logger.debug(f"[移动] 文件从 {source_full_path} 移动到 {destination_full_path}")
|
||||
# 更新实例的路径属性为新路径
|
||||
self.full_path = destination_full_path
|
||||
self.path = EMOJI_REGISTED_DIR
|
||||
# self.filename 保持不变
|
||||
except Exception as move_error:
|
||||
logger.error(f"[错误] 移动文件失败: {str(move_error)}")
|
||||
return False # 文件移动失败,不继续
|
||||
# 如果移动失败,尝试将实例状态恢复?暂时不处理,仅返回失败
|
||||
return False
|
||||
|
||||
# --- 数据库操作 ---
|
||||
try:
|
||||
# 准备数据库记录 for emoji collection
|
||||
emoji_record = {
|
||||
"filename": self.filename,
|
||||
"path": os.path.join(self.path, self.filename), # 使用更新后的路径
|
||||
"path": self.path, # 存储目录路径
|
||||
"full_path": self.full_path, # 存储完整文件路径
|
||||
"embedding": self.embedding,
|
||||
"description": self.description,
|
||||
"emotion": self.emotion, # 添加情感标签字段
|
||||
"emotion": self.emotion,
|
||||
"hash": self.hash,
|
||||
"format": self.format,
|
||||
"timestamp": int(self.register_time), # 使用实例的注册时间
|
||||
"timestamp": int(self.register_time),
|
||||
"usage_count": self.usage_count,
|
||||
"last_used_time": self.last_used_time,
|
||||
}
|
||||
|
||||
# 使用upsert确保记录存在或被更新
|
||||
db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
|
||||
logger.success(f"[注册] 表情包信息保存到数据库: {self.description}")
|
||||
|
||||
logger.success(f"[注册] 表情包信息保存到数据库: {self.filename} ({self.emotion})")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as db_error:
|
||||
logger.error(f"[错误] 保存数据库失败: {str(db_error)}")
|
||||
# 考虑是否需要将文件移回?为了简化,暂时只记录错误
|
||||
logger.error(f"[错误] 保存数据库失败 ({self.filename}): {str(db_error)}")
|
||||
# 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误
|
||||
# 可以考虑在这里尝试删除已移动的文件,避免残留
|
||||
try:
|
||||
if os.path.exists(self.full_path): # full_path 此时是目标路径
|
||||
os.remove(self.full_path)
|
||||
logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}")
|
||||
except Exception as remove_error:
|
||||
logger.error(f"[错误] 回滚删除文件失败: {remove_error}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 注册表情包失败: {str(e)}")
|
||||
logger.error(f"[错误] 注册表情包失败 ({self.filename}): {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
@@ -155,30 +192,36 @@ class MaiEmoji:
|
||||
"""
|
||||
try:
|
||||
# 1. 删除文件
|
||||
if os.path.exists(os.path.join(self.path, self.filename)):
|
||||
file_to_delete = self.full_path
|
||||
if os.path.exists(file_to_delete):
|
||||
try:
|
||||
os.remove(os.path.join(self.path, self.filename))
|
||||
logger.info(f"[删除] 文件: {os.path.join(self.path, self.filename)}")
|
||||
os.remove(file_to_delete)
|
||||
logger.debug(f"[删除] 文件: {file_to_delete}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}")
|
||||
# 继续执行,即使文件删除失败也尝试删除数据库记录
|
||||
logger.error(f"[错误] 删除文件失败 {file_to_delete}: {str(e)}")
|
||||
# 文件删除失败,但仍然尝试删除数据库记录
|
||||
|
||||
# 2. 删除数据库记录
|
||||
result = db.emoji.delete_one({"hash": self.hash})
|
||||
deleted_in_db = result.deleted_count > 0
|
||||
|
||||
if deleted_in_db:
|
||||
logger.success(f"[删除] 成功删除表情包记录: {self.description}")
|
||||
|
||||
logger.info(f"[删除] 表情包数据库记录 {self.filename} (Hash: {self.hash})")
|
||||
# 3. 标记对象已被删除
|
||||
self.is_deleted = True
|
||||
return True
|
||||
else:
|
||||
logger.error(f"[错误] 删除表情包记录失败: {self.hash}")
|
||||
# 如果数据库记录删除失败,但文件可能已删除,记录一个警告
|
||||
if not os.path.exists(file_to_delete):
|
||||
logger.warning(
|
||||
f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})"
|
||||
)
|
||||
else:
|
||||
logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除表情包失败: {str(e)}")
|
||||
logger.error(f"[错误] 删除表情包失败 ({self.filename}): {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -195,7 +238,7 @@ class EmojiManager:
|
||||
self._scan_task = None
|
||||
self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
|
||||
self.llm_emotion_judge = LLMRequest(
|
||||
model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji"
|
||||
model=global_config.llm_normal, max_tokens=600, request_type="emoji"
|
||||
) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
|
||||
|
||||
self.emoji_num = 0
|
||||
@@ -208,6 +251,7 @@ class EmojiManager:
|
||||
def _ensure_emoji_dir(self):
|
||||
"""确保表情存储目录存在"""
|
||||
os.makedirs(EMOJI_DIR, exist_ok=True)
|
||||
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
|
||||
|
||||
def initialize(self):
|
||||
"""初始化数据库连接和表情目录"""
|
||||
@@ -264,59 +308,71 @@ class EmojiManager:
|
||||
Args:
|
||||
text_emotion: 输入的情感描述文本
|
||||
Returns:
|
||||
Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None
|
||||
Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述),如果没有找到则返回None
|
||||
"""
|
||||
try:
|
||||
self._ensure_db()
|
||||
time_start = time.time()
|
||||
_time_start = time.time()
|
||||
|
||||
# 获取所有表情包
|
||||
# 获取所有表情包 (从内存缓存中获取)
|
||||
all_emojis = self.emoji_objects
|
||||
|
||||
if not all_emojis:
|
||||
logger.warning("数据库中没有任何表情包")
|
||||
logger.warning("内存中没有任何表情包对象")
|
||||
# 可以考虑再查一次数据库?或者依赖定期任务更新
|
||||
return None
|
||||
|
||||
# 计算每个表情包与输入文本的最大情感相似度
|
||||
emoji_similarities = []
|
||||
for emoji in all_emojis:
|
||||
# 跳过已标记为删除的对象
|
||||
if emoji.is_deleted:
|
||||
continue
|
||||
|
||||
emotions = emoji.emotion
|
||||
if not emotions:
|
||||
continue
|
||||
|
||||
# 计算与每个emotion标签的相似度,取最大值
|
||||
max_similarity = 0
|
||||
best_matching_emotion = "" # 记录最匹配的 emotion 喵~
|
||||
for emotion in emotions:
|
||||
# 使用编辑距离计算相似度
|
||||
distance = self._levenshtein_distance(text_emotion, emotion)
|
||||
max_len = max(len(text_emotion), len(emotion))
|
||||
similarity = 1 - (distance / max_len if max_len > 0 else 0)
|
||||
max_similarity = max(max_similarity, similarity)
|
||||
if similarity > max_similarity: # 如果找到更相似的喵~
|
||||
max_similarity = similarity
|
||||
best_matching_emotion = emotion # 就记下这个 emotion 喵~
|
||||
|
||||
emoji_similarities.append((emoji, max_similarity))
|
||||
if best_matching_emotion: # 确保有匹配的情感才添加喵~
|
||||
emoji_similarities.append((emoji, max_similarity, best_matching_emotion)) # 把 emotion 也存起来喵~
|
||||
|
||||
# 按相似度降序排序
|
||||
emoji_similarities.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
# 获取前5个最相似的表情包
|
||||
top_5_emojis = emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
|
||||
# 获取前10个最相似的表情包
|
||||
top_emojis = (
|
||||
emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
|
||||
) # 改个名字,更清晰喵~
|
||||
|
||||
if not top_5_emojis:
|
||||
if not top_emojis:
|
||||
logger.warning("未找到匹配的表情包")
|
||||
return None
|
||||
|
||||
# 从前5个中随机选择一个
|
||||
selected_emoji, similarity = random.choice(top_5_emojis)
|
||||
# 从前几个中随机选择一个
|
||||
selected_emoji, similarity, matched_emotion = random.choice(top_emojis) # 把匹配的 emotion 也拿出来喵~
|
||||
|
||||
# 更新使用次数
|
||||
self.record_usage(selected_emoji.hash)
|
||||
|
||||
time_end = time.time()
|
||||
_time_end = time.time()
|
||||
|
||||
logger.info(
|
||||
f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})"
|
||||
logger.info( # 使用匹配到的 emotion 记录日志喵~
|
||||
f"为[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}"
|
||||
)
|
||||
return selected_emoji.path, f"[ {selected_emoji.description} ]"
|
||||
# 返回完整文件路径和描述
|
||||
return selected_emoji.full_path, f"[ {selected_emoji.description} ]"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 获取表情包失败: {str(e)}")
|
||||
@@ -364,40 +420,50 @@ class EmojiManager:
|
||||
self.emoji_num = total_count
|
||||
removed_count = 0
|
||||
# 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
|
||||
for emoji in self.emoji_objects[:]:
|
||||
objects_to_remove = []
|
||||
for emoji in self.emoji_objects:
|
||||
try:
|
||||
# 跳过已经标记为删除的,避免重复处理
|
||||
if emoji.is_deleted:
|
||||
objects_to_remove.append(emoji) # 收集起来一次性移除
|
||||
continue
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(emoji.path):
|
||||
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
|
||||
if not os.path.exists(emoji.full_path):
|
||||
logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}")
|
||||
# 执行表情包对象的删除方法
|
||||
await emoji.delete()
|
||||
# 从列表中移除该对象
|
||||
self.emoji_objects.remove(emoji)
|
||||
await emoji.delete() # delete 方法现在会标记 is_deleted
|
||||
objects_to_remove.append(emoji) # 标记删除后,也收集起来移除
|
||||
# 更新计数
|
||||
self.emoji_num -= 1
|
||||
removed_count += 1
|
||||
continue
|
||||
|
||||
if emoji.description == None:
|
||||
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
|
||||
# 执行表情包对象的删除方法
|
||||
# 检查描述是否为空 (如果为空也视为无效)
|
||||
if not emoji.description:
|
||||
logger.warning(f"[检查] 表情包描述为空,视为无效: {emoji.filename}")
|
||||
await emoji.delete()
|
||||
# 从列表中移除该对象
|
||||
self.emoji_objects.remove(emoji)
|
||||
# 更新计数
|
||||
objects_to_remove.append(emoji)
|
||||
self.emoji_num -= 1
|
||||
removed_count += 1
|
||||
continue
|
||||
|
||||
except Exception as item_error:
|
||||
logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
|
||||
logger.error(f"[错误] 处理表情包记录时出错 ({emoji.filename}): {str(item_error)}")
|
||||
# 即使出错,也尝试继续检查下一个
|
||||
continue
|
||||
|
||||
# 从 self.emoji_objects 中移除标记的对象
|
||||
if objects_to_remove:
|
||||
self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove]
|
||||
|
||||
# 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件
|
||||
await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
|
||||
|
||||
# 输出清理结果
|
||||
if removed_count > 0:
|
||||
logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
|
||||
logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}")
|
||||
logger.success(f"[清理] 已清理 {removed_count} 个失效/文件丢失的表情包记录")
|
||||
logger.info(f"[统计] 清理前记录数: {total_count} | 清理后有效记录数: {len(self.emoji_objects)}")
|
||||
else:
|
||||
logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
|
||||
|
||||
@@ -460,45 +526,72 @@ class EmojiManager:
|
||||
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
|
||||
|
||||
async def get_all_emoji_from_db(self):
|
||||
"""获取所有表情包并初始化为MaiEmoji类对象
|
||||
|
||||
参数:
|
||||
hash: 可选,如果提供则只返回指定哈希值的表情包
|
||||
|
||||
返回:
|
||||
list[MaiEmoji]: 表情包对象列表
|
||||
"""
|
||||
"""获取所有表情包并初始化为MaiEmoji类对象,更新 self.emoji_objects"""
|
||||
try:
|
||||
self._ensure_db()
|
||||
logger.info("[数据库] 开始加载所有表情包记录...")
|
||||
|
||||
# 获取所有表情包
|
||||
all_emoji_data = list(db.emoji.find())
|
||||
|
||||
# 将数据库记录转换为MaiEmoji对象
|
||||
emoji_objects = []
|
||||
load_errors = 0
|
||||
|
||||
for emoji_data in all_emoji_data:
|
||||
emoji = MaiEmoji(
|
||||
filename=emoji_data.get("filename", ""),
|
||||
path=emoji_data.get("path", ""),
|
||||
)
|
||||
full_path = emoji_data.get("full_path")
|
||||
if not full_path:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
|
||||
load_errors += 1
|
||||
continue # 跳过缺少 full_path 的记录
|
||||
|
||||
# 设置额外属性
|
||||
emoji.hash = emoji_data.get("hash", "")
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
|
||||
emoji.register_time = emoji_data.get("timestamp", time.time())
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
|
||||
emoji_objects.append(emoji)
|
||||
try:
|
||||
# 使用 full_path 初始化 MaiEmoji 对象
|
||||
emoji = MaiEmoji(full_path=full_path)
|
||||
|
||||
# 存储到EmojiManager中
|
||||
# 设置从数据库加载的属性
|
||||
emoji.hash = emoji_data.get("hash", "")
|
||||
# 如果 hash 为空,也跳过?取决于业务逻辑
|
||||
if not emoji.hash:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", [])
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
# 优先使用 last_used_time,否则用 timestamp,最后用当前时间
|
||||
last_used = emoji_data.get("last_used_time")
|
||||
timestamp = emoji_data.get("timestamp")
|
||||
emoji.last_used_time = (
|
||||
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
|
||||
)
|
||||
emoji.register_time = timestamp if timestamp is not None else time.time()
|
||||
emoji.format = emoji_data.get("format", "") # 加载格式
|
||||
|
||||
# 不需要再手动设置 path 和 filename,__init__ 会自动处理
|
||||
|
||||
emoji_objects.append(emoji)
|
||||
|
||||
except ValueError as ve: # 捕获 __init__ 可能的错误
|
||||
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
|
||||
load_errors += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
|
||||
load_errors += 1
|
||||
|
||||
# 更新内存中的列表和数量
|
||||
self.emoji_objects = emoji_objects
|
||||
self.emoji_num = len(emoji_objects)
|
||||
|
||||
logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。")
|
||||
if load_errors > 0:
|
||||
logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
|
||||
logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}")
|
||||
self.emoji_objects = [] # 加载失败则清空列表
|
||||
self.emoji_num = 0
|
||||
|
||||
async def get_emoji_from_db(self, hash=None):
|
||||
"""获取所有表情包并初始化为MaiEmoji类对象
|
||||
"""获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找)
|
||||
|
||||
参数:
|
||||
hash: 可选,如果提供则只返回指定哈希值的表情包
|
||||
@@ -509,50 +602,73 @@ class EmojiManager:
|
||||
try:
|
||||
self._ensure_db()
|
||||
|
||||
# 准备查询条件
|
||||
query = {}
|
||||
if hash:
|
||||
query = {"hash": hash}
|
||||
|
||||
# 获取所有表情包
|
||||
all_emoji_data = list(db.emoji.find(query))
|
||||
|
||||
# 将数据库记录转换为MaiEmoji对象
|
||||
emoji_objects = []
|
||||
for emoji_data in all_emoji_data:
|
||||
emoji = MaiEmoji(
|
||||
filename=emoji_data.get("filename", ""),
|
||||
path=emoji_data.get("path", ""),
|
||||
else:
|
||||
logger.warning(
|
||||
"[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。"
|
||||
)
|
||||
|
||||
# 设置额外属性
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
|
||||
emoji.register_time = emoji_data.get("timestamp", time.time())
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
|
||||
emoji_data_list = list(db.emoji.find(query))
|
||||
emoji_objects = []
|
||||
load_errors = 0
|
||||
|
||||
emoji_objects.append(emoji)
|
||||
for emoji_data in emoji_data_list:
|
||||
full_path = emoji_data.get("full_path")
|
||||
if not full_path:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
# 存储到EmojiManager中
|
||||
self.emoji_objects = emoji_objects
|
||||
try:
|
||||
emoji = MaiEmoji(full_path=full_path)
|
||||
emoji.hash = emoji_data.get("hash", "")
|
||||
if not emoji.hash:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", [])
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
last_used = emoji_data.get("last_used_time")
|
||||
timestamp = emoji_data.get("timestamp")
|
||||
emoji.last_used_time = (
|
||||
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
|
||||
)
|
||||
emoji.register_time = timestamp if timestamp is not None else time.time()
|
||||
emoji.format = emoji_data.get("format", "")
|
||||
emoji_objects.append(emoji)
|
||||
except ValueError as ve:
|
||||
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
|
||||
load_errors += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
|
||||
load_errors += 1
|
||||
|
||||
if load_errors > 0:
|
||||
logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。")
|
||||
|
||||
return emoji_objects
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
|
||||
logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}")
|
||||
return []
|
||||
|
||||
async def get_emoji_from_manager(self, hash) -> MaiEmoji:
|
||||
"""从EmojiManager中获取表情包
|
||||
async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]:
|
||||
"""从内存中的 emoji_objects 列表获取表情包
|
||||
|
||||
参数:
|
||||
hash:如果提供则只返回指定哈希值的表情包
|
||||
hash: 要查找的表情包哈希值
|
||||
返回:
|
||||
MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None
|
||||
"""
|
||||
for emoji in self.emoji_objects:
|
||||
if emoji.hash == hash:
|
||||
# 确保对象未被标记为删除且哈希值匹配
|
||||
if not emoji.is_deleted and emoji.hash == hash:
|
||||
return emoji
|
||||
return None
|
||||
return None # 如果循环结束还没找到,则返回 None
|
||||
|
||||
async def delete_emoji(self, emoji_hash: str) -> bool:
|
||||
"""根据哈希值删除表情包
|
||||
@@ -656,11 +772,11 @@ class EmojiManager:
|
||||
|
||||
# 调用大模型进行决策
|
||||
decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8)
|
||||
logger.info(f"[决策] 大模型决策结果: {decision}")
|
||||
logger.info(f"[决策] 结果: {decision}")
|
||||
|
||||
# 解析决策结果
|
||||
if "不删除" in decision:
|
||||
logger.info("[决策] 决定不删除任何表情包")
|
||||
logger.info("[决策] 不删除任何表情包")
|
||||
return False
|
||||
|
||||
# 尝试从决策中提取表情包编号
|
||||
@@ -673,7 +789,7 @@ class EmojiManager:
|
||||
emoji_to_delete = selected_emojis[emoji_index]
|
||||
|
||||
# 删除选定的表情包
|
||||
logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}")
|
||||
logger.info(f"[决策] 删除表情包: {emoji_to_delete.description}")
|
||||
delete_success = await self.delete_emoji(emoji_to_delete.hash)
|
||||
|
||||
if delete_success:
|
||||
@@ -682,7 +798,7 @@ class EmojiManager:
|
||||
if register_success:
|
||||
self.emoji_objects.append(new_emoji)
|
||||
self.emoji_num += 1
|
||||
logger.success(f"[成功] 注册表情包: {new_emoji.description}")
|
||||
logger.success(f"[成功] 注册: {new_emoji.filename}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}")
|
||||
@@ -719,10 +835,10 @@ class EmojiManager:
|
||||
# 调用AI获取描述
|
||||
if image_format == "gif" or image_format == "GIF":
|
||||
image_base64 = image_manager.transform_gif(image_base64)
|
||||
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,详细描述一下表情包表达的情感和内容,请关注其幽默和讽刺意味"
|
||||
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
|
||||
description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg")
|
||||
else:
|
||||
prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味"
|
||||
prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
|
||||
description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
|
||||
|
||||
# 审核表情包
|
||||
@@ -741,17 +857,22 @@ class EmojiManager:
|
||||
|
||||
# 分析情感含义
|
||||
emotion_prompt = f"""
|
||||
基于这个表情包的描述:'{description}',请列出1-2个可能的情感标签,每个标签用一个词组表示,格式如下:
|
||||
幽默的讽刺
|
||||
悲伤的无奈
|
||||
愤怒的抗议
|
||||
愤怒的讽刺
|
||||
直接输出词组,词组检用逗号分隔。"""
|
||||
请你识别这个表情包的含义和适用场景,给我简短的描述,每个描述不要超过15个字
|
||||
这是一个基于这个表情包的描述:'{description}'
|
||||
你可以关注其幽默和讽刺意味,动用贴吧,微博,小红书的知识,必须从互联网梗,meme的角度去分析
|
||||
请直接输出描述,不要出现任何其他内容,如果有多个描述,可以用逗号分隔
|
||||
"""
|
||||
emotions_text, _ = await self.llm_emotion_judge.generate_response_async(emotion_prompt, temperature=0.7)
|
||||
|
||||
# 处理情感列表
|
||||
emotions = [e.strip() for e in emotions_text.split(",") if e.strip()]
|
||||
|
||||
# 根据情感标签数量随机选择喵~超过5个选3个,超过2个选2个
|
||||
if len(emotions) > 5:
|
||||
emotions = random.sample(emotions, 3)
|
||||
elif len(emotions) > 2:
|
||||
emotions = random.sample(emotions, 2)
|
||||
|
||||
return f"[表情包:{description}]", emotions
|
||||
|
||||
except Exception as e:
|
||||
@@ -767,100 +888,176 @@ class EmojiManager:
|
||||
Returns:
|
||||
bool: 注册是否成功
|
||||
"""
|
||||
file_full_path = os.path.join(EMOJI_DIR, filename)
|
||||
if not os.path.exists(file_full_path):
|
||||
logger.error(f"[注册失败] 文件不存在: {file_full_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 使用MaiEmoji类创建表情包实例
|
||||
new_emoji = MaiEmoji(filename, EMOJI_DIR)
|
||||
await new_emoji.initialize_hash_format()
|
||||
emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename))
|
||||
description, emotions = await self.build_emoji_description(emoji_base64)
|
||||
if description == "" or description == None:
|
||||
# 1. 创建 MaiEmoji 实例并初始化哈希和格式
|
||||
new_emoji = MaiEmoji(full_path=file_full_path)
|
||||
init_result = await new_emoji.initialize_hash_format()
|
||||
if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误
|
||||
logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}")
|
||||
# 是否需要删除源文件?看业务需求,暂时不删
|
||||
return False
|
||||
new_emoji.description = description
|
||||
new_emoji.emotion = emotions
|
||||
|
||||
# 检查是否已经注册过
|
||||
# 对比内存中是否存在相同哈希值的表情包
|
||||
# 2. 检查哈希是否已存在 (在内存中检查)
|
||||
if await self.get_emoji_from_manager(new_emoji.hash):
|
||||
logger.warning(f"[警告] 表情包已存在: {filename}")
|
||||
logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}")
|
||||
# 删除重复的源文件
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除重复的待注册文件: {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除重复文件失败: {str(e)}")
|
||||
return False # 返回 False 表示未注册新表情
|
||||
|
||||
# 3. 构建描述和情感
|
||||
try:
|
||||
emoji_base64 = image_path_to_base64(file_full_path)
|
||||
if emoji_base64 is None: # 再次检查读取
|
||||
logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}")
|
||||
return False
|
||||
description, emotions = await self.build_emoji_description(emoji_base64)
|
||||
if not description: # 检查描述是否成功生成或审核通过
|
||||
logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}")
|
||||
# 删除未能生成描述的文件
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除描述生成失败的文件: {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除描述生成失败文件时出错: {str(e)}")
|
||||
return False
|
||||
new_emoji.description = description
|
||||
new_emoji.emotion = emotions
|
||||
except Exception as build_desc_error:
|
||||
logger.error(f"[注册失败] 生成描述/情感时出错 ({filename}): {build_desc_error}")
|
||||
# 同样考虑删除文件
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除描述生成异常的文件: {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除描述生成异常文件时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
# 4. 检查容量并决定是否替换或直接注册
|
||||
if self.emoji_num >= self.emoji_num_max:
|
||||
logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})")
|
||||
logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),尝试替换...")
|
||||
replaced = await self.replace_a_emoji(new_emoji)
|
||||
if not replaced:
|
||||
logger.error("[错误] 替换表情包失败,无法完成注册")
|
||||
logger.error("[注册失败] 替换表情包失败,无法完成注册")
|
||||
# 替换失败,删除新表情包文件
|
||||
try:
|
||||
os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径
|
||||
logger.info(f"[清理] 删除替换失败的新表情文件: {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}")
|
||||
return False
|
||||
# 替换成功时,replace_a_emoji 内部已处理 new_emoji 的注册和添加到列表
|
||||
return True
|
||||
else:
|
||||
# 修复:等待异步注册完成
|
||||
register_success = await new_emoji.register_to_db()
|
||||
# 直接注册
|
||||
register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB
|
||||
if register_success:
|
||||
# 注册成功后,添加到内存列表
|
||||
self.emoji_objects.append(new_emoji)
|
||||
self.emoji_num += 1
|
||||
logger.success(f"[成功] 注册表情包: {filename}")
|
||||
logger.success(f"[成功] 注册新表情包: {filename} (当前: {self.emoji_num}/{self.emoji_num_max})")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"[错误] 注册表情包到数据库失败: {filename}")
|
||||
logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}")
|
||||
# register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在
|
||||
# 是否需要删除源文件?
|
||||
if os.path.exists(file_full_path):
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除注册失败的源文件: {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 注册表情包失败: {str(e)}")
|
||||
logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 尝试删除源文件以避免循环处理
|
||||
if os.path.exists(file_full_path):
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除处理异常的源文件: {filename}")
|
||||
except Exception as remove_error:
|
||||
logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}")
|
||||
return False
|
||||
|
||||
async def clear_temp_emoji(self):
|
||||
"""每天清理临时表情包
|
||||
"""清理临时表情包
|
||||
清理/data/emoji和/data/image目录下的所有文件
|
||||
当目录中文件数超过50时,会全部删除
|
||||
当目录中文件数超过100时,会全部删除
|
||||
"""
|
||||
|
||||
logger.info("[清理] 开始清理临时表情包...")
|
||||
logger.info("[清理] 开始清理缓存...")
|
||||
|
||||
# 清理emoji目录
|
||||
emoji_dir = os.path.join(BASE_DIR, "emoji")
|
||||
if os.path.exists(emoji_dir):
|
||||
files = os.listdir(emoji_dir)
|
||||
# 如果文件数超过50就全部删除
|
||||
if len(files) > 50:
|
||||
if len(files) > 100:
|
||||
for filename in files:
|
||||
file_path = os.path.join(emoji_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
logger.debug(f"[清理] 删除表情包文件: {filename}")
|
||||
logger.debug(f"[清理] 删除: {filename}")
|
||||
|
||||
# 清理image目录
|
||||
image_dir = os.path.join(BASE_DIR, "image")
|
||||
if os.path.exists(image_dir):
|
||||
files = os.listdir(image_dir)
|
||||
# 如果文件数超过50就全部删除
|
||||
if len(files) > 50:
|
||||
if len(files) > 100:
|
||||
for filename in files:
|
||||
file_path = os.path.join(image_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
logger.debug(f"[清理] 删除图片文件: {filename}")
|
||||
logger.debug(f"[清理] 删除图片: {filename}")
|
||||
|
||||
logger.success("[清理] 临时文件清理完成")
|
||||
logger.success("[清理] 完成")
|
||||
|
||||
async def clean_unused_emojis(self, emoji_dir, emoji_objects):
|
||||
"""清理未使用的表情包文件
|
||||
遍历指定文件夹中的所有文件,删除未在emoji_objects列表中的文件
|
||||
"""
|
||||
# 获取所有表情包路径
|
||||
emoji_paths = {emoji.path for emoji in emoji_objects}
|
||||
"""清理指定目录中未被 emoji_objects 追踪的表情包文件"""
|
||||
if not os.path.exists(emoji_dir):
|
||||
logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
|
||||
return
|
||||
|
||||
# 遍历文件夹中的所有文件
|
||||
for file_name in os.listdir(emoji_dir):
|
||||
file_path = os.path.join(emoji_dir, file_name)
|
||||
try:
|
||||
# 获取内存中所有有效表情包的完整路径集合
|
||||
tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
|
||||
cleaned_count = 0
|
||||
|
||||
# 检查文件是否在表情包路径列表中
|
||||
if file_path not in emoji_paths:
|
||||
try:
|
||||
# 删除未在表情包列表中的文件
|
||||
os.remove(file_path)
|
||||
logger.info(f"[清理] 删除未使用的表情包文件: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除文件时出错: {str(e)}")
|
||||
# 遍历指定目录中的所有文件
|
||||
for file_name in os.listdir(emoji_dir):
|
||||
file_full_path = os.path.join(emoji_dir, file_name)
|
||||
|
||||
# 确保处理的是文件而不是子目录
|
||||
if not os.path.isfile(file_full_path):
|
||||
continue
|
||||
|
||||
# 如果文件不在被追踪的集合中,则删除
|
||||
if file_full_path not in tracked_full_paths:
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
|
||||
cleaned_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
|
||||
|
||||
if cleaned_count > 0:
|
||||
logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
|
||||
else:
|
||||
logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
|
||||
|
||||
|
||||
# 创建全局单例
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import time
|
||||
import traceback
|
||||
import random # <--- 添加导入
|
||||
import json # <--- 确保导入 json
|
||||
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
|
||||
from collections import deque
|
||||
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
|
||||
@@ -14,9 +15,7 @@ from src.plugins.models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
|
||||
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
|
||||
from src.do_tool.tool_use import ToolUser
|
||||
from src.plugins.emoji_system.emoji_manager import emoji_manager
|
||||
from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments
|
||||
from src.heart_flow.sub_mind import SubMind
|
||||
from src.heart_flow.observation import Observation
|
||||
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
|
||||
@@ -30,12 +29,14 @@ from src.plugins.moods.moods import MoodManager
|
||||
from src.individuality.individuality import Individuality
|
||||
|
||||
|
||||
INITIAL_DURATION = 60.0
|
||||
|
||||
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
|
||||
|
||||
EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
|
||||
|
||||
logger = get_logger("interest") # Logger Name Changed
|
||||
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
|
||||
|
||||
|
||||
logger = get_logger("hfc") # Logger Name Changed
|
||||
|
||||
|
||||
# 默认动作定义
|
||||
@@ -117,35 +118,6 @@ class ActionManager:
|
||||
"""重置为默认动作集"""
|
||||
self._available_actions = DEFAULT_ACTIONS.copy()
|
||||
|
||||
def get_planner_tool_definition(self) -> List[Dict[str, Any]]:
|
||||
"""获取当前动作集对应的规划器工具定义"""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "decide_reply_action",
|
||||
"description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": list(self._available_actions.keys()),
|
||||
"description": "决定采取的行动:"
|
||||
+ ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]),
|
||||
},
|
||||
"reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
|
||||
"emoji_query": {
|
||||
"type": "string",
|
||||
"description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
|
||||
},
|
||||
},
|
||||
"required": ["action", "reasoning"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# 在文件开头添加自定义异常类
|
||||
class HeartFCError(Exception):
|
||||
@@ -179,8 +151,6 @@ class HeartFChatting:
|
||||
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
|
||||
"""
|
||||
|
||||
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat_id: str,
|
||||
@@ -222,7 +192,6 @@ class HeartFChatting:
|
||||
max_tokens=256,
|
||||
request_type="response_heartflow",
|
||||
)
|
||||
self.tool_user = ToolUser()
|
||||
self.heart_fc_sender = HeartFCSender()
|
||||
|
||||
# LLM规划器配置
|
||||
@@ -261,7 +230,7 @@ class HeartFChatting:
|
||||
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
|
||||
|
||||
self._initialized = True
|
||||
logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ")
|
||||
logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ")
|
||||
return True
|
||||
|
||||
async def start(self):
|
||||
@@ -292,7 +261,7 @@ class HeartFChatting:
|
||||
pass # 忽略取消或超时错误
|
||||
self._loop_task = None # 清理旧任务引用
|
||||
|
||||
logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
|
||||
logger.debug(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
|
||||
# 创建新的循环任务
|
||||
self._loop_task = asyncio.create_task(self._hfc_loop())
|
||||
# 添加完成回调
|
||||
@@ -470,6 +439,16 @@ class HeartFChatting:
|
||||
|
||||
# execute:执行
|
||||
|
||||
# 在此处添加日志记录
|
||||
if action == "text_reply":
|
||||
action_str = "回复"
|
||||
elif action == "emoji_reply":
|
||||
action_str = "回复表情"
|
||||
else:
|
||||
action_str = "不回复"
|
||||
|
||||
logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'")
|
||||
|
||||
return await self._handle_action(
|
||||
action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
|
||||
)
|
||||
@@ -644,14 +623,14 @@ class HeartFChatting:
|
||||
self._lian_xu_bu_hui_fu_ci_shu += 1
|
||||
self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间
|
||||
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}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
|
||||
f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒"
|
||||
)
|
||||
|
||||
# 检查是否同时达到次数和时间阈值
|
||||
time_threshold = 0.66 * WAITING_TIME_THRESHOLD * self.CONSECUTIVE_NO_REPLY_THRESHOLD
|
||||
time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD
|
||||
if (
|
||||
self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD
|
||||
self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD
|
||||
and self._lian_xu_deng_dai_shi_jian >= time_threshold
|
||||
):
|
||||
logger.info(
|
||||
@@ -661,7 +640,7 @@ class HeartFChatting:
|
||||
)
|
||||
# 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。
|
||||
await self.on_consecutive_no_reply_callback()
|
||||
elif self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD:
|
||||
elif self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD:
|
||||
# 仅次数达到阈值,但时间未达到
|
||||
logger.debug(
|
||||
f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) "
|
||||
@@ -784,41 +763,36 @@ class HeartFChatting:
|
||||
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
|
||||
重构为:让LLM返回结构化JSON文本,然后在代码中解析。
|
||||
|
||||
参数:
|
||||
current_mind: 子思维的当前思考结果
|
||||
cycle_timers: 计时器字典
|
||||
is_re_planned: 是否为重新规划
|
||||
is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑)
|
||||
"""
|
||||
logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器")
|
||||
logger.info(f"{self.log_prefix}开始想要做什么")
|
||||
|
||||
# --- 新增:检查历史动作并调整可用动作 ---
|
||||
lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
|
||||
actions_to_remove_temporarily = []
|
||||
probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断
|
||||
|
||||
# 反向遍历最近的循环历史
|
||||
# --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
|
||||
lian_xu_wen_ben_hui_fu = 0
|
||||
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个循环即可,避免检查过多历史 (如果历史很长)
|
||||
break
|
||||
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% 概率
|
||||
if probability_roll < 0.8:
|
||||
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)")
|
||||
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
|
||||
else:
|
||||
@@ -826,168 +800,179 @@ class HeartFChatting:
|
||||
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% 概率
|
||||
if probability_roll < 0.4:
|
||||
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]
|
||||
if is_re_planned:
|
||||
await observation.observe()
|
||||
# if is_re_planned: # 暂时简化,不处理重新规划
|
||||
# await observation.observe()
|
||||
observed_messages = observation.talking_message
|
||||
observed_messages_str = observation.talking_message_str_truncate
|
||||
|
||||
# --- 使用 LLM 进行决策 --- #
|
||||
reasoning = "默认决策或获取决策失败"
|
||||
llm_error = False # LLM错误标志
|
||||
arguments = None # 初始化参数变量
|
||||
emoji_query = "" # <--- 在这里初始化 emoji_query
|
||||
# --- 使用 LLM 进行决策 (JSON 输出模式) --- #
|
||||
action = "no_reply" # 默认动作
|
||||
reasoning = "规划器初始化默认"
|
||||
emoji_query = ""
|
||||
llm_error = False # LLM 请求或解析错误标志
|
||||
|
||||
# 获取我们将传递给 prompt 构建器和用于验证的当前可用动作
|
||||
current_available_actions = self.action_manager.get_available_actions()
|
||||
|
||||
try:
|
||||
# --- 新增:应用临时动作移除 ---
|
||||
# --- 应用临时动作移除 ---
|
||||
if actions_to_remove_temporarily:
|
||||
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
|
||||
# 更新 current_available_actions 以反映移除后的状态
|
||||
current_available_actions = self.action_manager.get_available_actions()
|
||||
logger.debug(
|
||||
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
|
||||
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
|
||||
)
|
||||
|
||||
# --- 构建提示词 ---
|
||||
replan_prompt_str = ""
|
||||
if is_re_planned:
|
||||
replan_prompt_str = await self._build_replan_prompt(
|
||||
self._current_cycle.action_type, self._current_cycle.reasoning
|
||||
)
|
||||
# --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
|
||||
# replan_prompt_str = "" # 暂时简化
|
||||
# if is_re_planned:
|
||||
# replan_prompt_str = await self._build_replan_prompt(
|
||||
# self._current_cycle.action_type, self._current_cycle.reasoning
|
||||
# )
|
||||
prompt = await self._build_planner_prompt(
|
||||
observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str
|
||||
observed_messages_str,
|
||||
current_mind,
|
||||
self.sub_mind.structured_info,
|
||||
"", # replan_prompt_str,
|
||||
current_available_actions, # <--- 传入当前可用动作
|
||||
)
|
||||
|
||||
# --- 调用 LLM ---
|
||||
# --- 调用 LLM (普通文本生成) ---
|
||||
llm_content = None
|
||||
try:
|
||||
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(
|
||||
prompt=prompt,
|
||||
tools=planner_tools,
|
||||
)
|
||||
logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
|
||||
# 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name)
|
||||
# 我们只需要 content
|
||||
# !! 注意:这里假设 self.planner_llm 有 generate_response 方法
|
||||
# !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改
|
||||
llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
|
||||
logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
|
||||
except Exception as req_e:
|
||||
logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}")
|
||||
action = "error"
|
||||
reasoning = f"LLM请求失败: {req_e}"
|
||||
logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
|
||||
reasoning = f"LLM 请求失败: {req_e}"
|
||||
llm_error = True
|
||||
# 直接返回错误结果
|
||||
return {
|
||||
"action": action,
|
||||
"reasoning": reasoning,
|
||||
"emoji_query": "",
|
||||
"current_mind": current_mind,
|
||||
"observed_messages": observed_messages,
|
||||
"llm_error": llm_error,
|
||||
}
|
||||
# 直接使用默认动作返回错误结果
|
||||
action = "no_reply" # 明确设置为默认值
|
||||
emoji_query = "" # 明确设置为空
|
||||
# 不再立即返回,而是继续执行 finally 块以恢复动作
|
||||
# return { ... }
|
||||
|
||||
# 默认错误状态
|
||||
action = "error"
|
||||
reasoning = "处理工具调用时出错"
|
||||
llm_error = True
|
||||
# --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) ---
|
||||
if not llm_error and llm_content:
|
||||
try:
|
||||
# 尝试去除可能的 markdown 代码块标记
|
||||
cleaned_content = (
|
||||
llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
|
||||
)
|
||||
if not cleaned_content:
|
||||
raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0)
|
||||
parsed_json = json.loads(cleaned_content)
|
||||
|
||||
# 1. 验证工具调用
|
||||
success, valid_tool_calls, error_msg = process_llm_tool_calls(
|
||||
tool_calls, log_prefix=f"{self.log_prefix}[Planner] "
|
||||
)
|
||||
# 提取决策,提供默认值
|
||||
extracted_action = parsed_json.get("action", "no_reply")
|
||||
extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
|
||||
extracted_emoji_query = parsed_json.get("emoji_query", "")
|
||||
|
||||
if success and valid_tool_calls:
|
||||
# 2. 提取第一个调用并获取参数
|
||||
first_tool_call = valid_tool_calls[0]
|
||||
tool_name = first_tool_call.get("function", {}).get("name")
|
||||
arguments = extract_tool_call_arguments(first_tool_call, None)
|
||||
|
||||
# 3. 检查名称和参数
|
||||
expected_tool_name = "decide_reply_action"
|
||||
if tool_name == expected_tool_name and arguments is not None:
|
||||
# 4. 成功,提取决策
|
||||
extracted_action = arguments.get("action", "no_reply")
|
||||
# 验证动作
|
||||
if extracted_action not in self.action_manager.get_available_actions():
|
||||
# 如果LLM返回了一个此时不该用的动作(因为被临时移除了)
|
||||
# 或者完全无效的动作
|
||||
# 验证动作是否在当前可用列表中
|
||||
# !! 使用调用 prompt 时实际可用的动作列表进行验证
|
||||
if extracted_action not in current_available_actions:
|
||||
logger.warning(
|
||||
f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'"
|
||||
f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
|
||||
)
|
||||
action = "no_reply"
|
||||
reasoning = f"LLM返回了当前不可用的动作: {extracted_action}"
|
||||
reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
|
||||
emoji_query = ""
|
||||
llm_error = False # 视为逻辑修正而非 LLM 错误
|
||||
# --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) ---
|
||||
if "no_reply" not in self.action_manager.get_available_actions():
|
||||
# 检查 no_reply 是否也恰好被移除了 (极端情况)
|
||||
if "no_reply" not in current_available_actions:
|
||||
logger.error(
|
||||
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
|
||||
)
|
||||
action = "error" # 回退到错误状态
|
||||
reasoning = "无法执行任何有效动作,包括 no_reply"
|
||||
llm_error = True
|
||||
llm_error = True # 标记为严重错误
|
||||
else:
|
||||
llm_error = False # 视为逻辑修正而非 LLM 错误
|
||||
else:
|
||||
# 动作有效且可用,使用提取的值
|
||||
# 动作有效且可用
|
||||
action = extracted_action
|
||||
reasoning = arguments.get("reasoning", "未提供理由")
|
||||
emoji_query = arguments.get("emoji_query", "")
|
||||
llm_error = False # 成功处理
|
||||
# 记录决策结果
|
||||
reasoning = extracted_reasoning
|
||||
emoji_query = extracted_emoji_query
|
||||
llm_error = False # 解析成功
|
||||
logger.debug(
|
||||
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
|
||||
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
|
||||
)
|
||||
elif tool_name != expected_tool_name:
|
||||
reasoning = f"LLM返回了非预期的工具: {tool_name}"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
else: # arguments is None
|
||||
reasoning = f"无法提取工具 {tool_name} 的参数"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
elif not success:
|
||||
reasoning = f"验证工具调用失败: {error_msg}"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
else: # not valid_tool_calls
|
||||
# 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
|
||||
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,说明在处理过程中有错误发生
|
||||
|
||||
except Exception as llm_e:
|
||||
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}")
|
||||
except json.JSONDecodeError as json_e:
|
||||
logger.warning(
|
||||
f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'"
|
||||
)
|
||||
reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'."
|
||||
action = "no_reply" # 解析失败则默认不回复
|
||||
emoji_query = ""
|
||||
llm_error = True # 标记解析错误
|
||||
except Exception as parse_e:
|
||||
logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}")
|
||||
reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'."
|
||||
action = "no_reply"
|
||||
emoji_query = ""
|
||||
llm_error = True
|
||||
elif not llm_error and not llm_content:
|
||||
# LLM 请求成功但返回空内容
|
||||
logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。")
|
||||
reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'."
|
||||
action = "no_reply"
|
||||
emoji_query = ""
|
||||
llm_error = True # 标记为空响应错误
|
||||
|
||||
# 如果 llm_error 在此阶段为 True,意味着请求成功但解析失败或返回空
|
||||
# 如果 llm_error 在请求阶段就为 True,则跳过了此解析块
|
||||
|
||||
except Exception as outer_e:
|
||||
logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}")
|
||||
logger.error(traceback.format_exc())
|
||||
action = "error"
|
||||
reasoning = f"Planner内部处理错误: {llm_e}"
|
||||
action = "error" # 发生未知错误,标记为 error 动作
|
||||
reasoning = f"Planner 内部处理错误: {outer_e}"
|
||||
emoji_query = ""
|
||||
llm_error = True
|
||||
# --- 新增:确保动作恢复 ---
|
||||
finally:
|
||||
if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复
|
||||
# --- 确保动作恢复 ---
|
||||
# 检查 self._original_actions_backup 是否有值来判断是否需要恢复
|
||||
if self.action_manager._original_actions_backup is not None:
|
||||
self.action_manager.restore_actions()
|
||||
logger.debug(
|
||||
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
|
||||
)
|
||||
# --- 结束:确保动作恢复 ---
|
||||
# --- 结束 LLM 决策 --- #
|
||||
# --- 结束确保动作恢复 ---
|
||||
|
||||
# --- 概率性忽略文本回复附带的表情 (逻辑保持不变) ---
|
||||
if action == "text_reply" and emoji_query:
|
||||
logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'")
|
||||
if random.random() > EMOJI_SEND_PRO:
|
||||
logger.info(
|
||||
f"{self.log_prefix}但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
|
||||
)
|
||||
emoji_query = "" # 清空表情请求
|
||||
else:
|
||||
logger.info(f"{self.log_prefix}好吧,加上表情 '{emoji_query}'")
|
||||
# --- 结束概率性忽略 ---
|
||||
|
||||
# 返回结果字典
|
||||
return {
|
||||
"action": action,
|
||||
"reasoning": reasoning,
|
||||
"emoji_query": emoji_query,
|
||||
"current_mind": current_mind,
|
||||
"observed_messages": observed_messages,
|
||||
"llm_error": llm_error,
|
||||
"llm_error": llm_error, # 返回错误状态
|
||||
}
|
||||
|
||||
async def _get_anchor_message(self) -> Optional[MessageRecv]:
|
||||
@@ -1016,9 +1001,7 @@ class HeartFChatting:
|
||||
}
|
||||
anchor_message = MessageRecv(placeholder_msg_dict)
|
||||
anchor_message.update_chat_stream(self.chat_stream)
|
||||
logger.info(
|
||||
f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
|
||||
)
|
||||
logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
|
||||
return anchor_message
|
||||
|
||||
except Exception as e:
|
||||
@@ -1131,8 +1114,9 @@ class HeartFChatting:
|
||||
current_mind: Optional[str],
|
||||
structured_info: Dict[str, Any],
|
||||
replan_prompt: str,
|
||||
current_available_actions: Dict[str, str],
|
||||
) -> str:
|
||||
"""构建 Planner LLM 的提示词"""
|
||||
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
|
||||
try:
|
||||
# 准备结构化信息块
|
||||
structured_info_block = ""
|
||||
@@ -1148,12 +1132,13 @@ class HeartFChatting:
|
||||
else:
|
||||
chat_content_block = "当前没有观察到新的聊天内容。\n"
|
||||
|
||||
# 准备当前思维块
|
||||
# 准备当前思维块 (修改以匹配模板)
|
||||
current_mind_block = ""
|
||||
if current_mind:
|
||||
current_mind_block = f"{current_mind}"
|
||||
# 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
|
||||
current_mind_block = f"你的内心想法:\n{current_mind}"
|
||||
else:
|
||||
current_mind_block = "[没有特别的想法]"
|
||||
current_mind_block = "你的内心想法:\n[没有特别的想法]"
|
||||
|
||||
# 准备循环信息块 (分析最近的活动循环)
|
||||
recent_active_cycles = []
|
||||
@@ -1193,23 +1178,40 @@ class HeartFChatting:
|
||||
|
||||
# 包装提示块,增加可读性,即使没有连续回复也给个标记
|
||||
if cycle_info_block:
|
||||
# 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
|
||||
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
|
||||
else:
|
||||
# 如果最近的活动循环不是文本回复,或者没有活动循环
|
||||
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
|
||||
|
||||
individuality = Individuality.get_instance()
|
||||
# 模板中占位符是 {prompt_personality}
|
||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||
|
||||
# 获取提示词模板并填充数据
|
||||
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format(
|
||||
# --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
|
||||
action_options_text = "当前你可以选择的行动有:\n"
|
||||
action_keys = list(current_available_actions.keys())
|
||||
for name in action_keys:
|
||||
desc = current_available_actions[name]
|
||||
action_options_text += f"- '{name}': {desc}\n"
|
||||
|
||||
# --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
|
||||
example_action_key = action_keys[0] if action_keys else "no_reply"
|
||||
|
||||
# --- 获取提示词模板 ---
|
||||
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
|
||||
|
||||
# --- 填充模板 ---
|
||||
prompt = planner_prompt_template.format(
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
structured_info_block=structured_info_block,
|
||||
chat_content_block=chat_content_block,
|
||||
current_mind_block=current_mind_block,
|
||||
replan=replan_prompt,
|
||||
replan="", # 暂时留空 replan 信息
|
||||
cycle_info_block=cycle_info_block,
|
||||
action_options_text=action_options_text, # 传入可用动作描述
|
||||
example_action=example_action_key, # 传入示例动作键
|
||||
)
|
||||
|
||||
return prompt
|
||||
@@ -1217,7 +1219,7 @@ class HeartFChatting:
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return ""
|
||||
return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
|
||||
|
||||
# --- 回复器 (Replier) 的定义 --- #
|
||||
async def _replier_work(
|
||||
@@ -1258,7 +1260,7 @@ class HeartFChatting:
|
||||
try:
|
||||
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
||||
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
|
||||
logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
|
||||
# logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
|
||||
# 捕捉 LLM 输出信息
|
||||
info_catcher.catch_after_llm_generated(
|
||||
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
|
||||
|
||||
@@ -47,17 +47,15 @@ def init_prompt():
|
||||
"info_from_tools",
|
||||
)
|
||||
|
||||
# Planner提示词 - 优化版
|
||||
# Planner提示词 - 修改为要求 JSON 输出
|
||||
Prompt(
|
||||
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
|
||||
{structured_info_block}
|
||||
{chat_content_block}
|
||||
你的内心想法:
|
||||
{current_mind_block}
|
||||
{replan}
|
||||
{cycle_info_block}
|
||||
|
||||
请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意:
|
||||
请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
|
||||
|
||||
【回复原则】
|
||||
1. 不回复(no_reply)适用:
|
||||
@@ -69,7 +67,7 @@ def init_prompt():
|
||||
2. 文字回复(text_reply)适用:
|
||||
- 有实质性内容需要表达
|
||||
- 有人提到你,但你还没有回应他
|
||||
- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃")
|
||||
- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合)
|
||||
- 不要追加太多表情
|
||||
|
||||
3. 纯表情回复(emoji_reply)适用:
|
||||
@@ -81,14 +79,34 @@ def init_prompt():
|
||||
- 避免重复或评价自己的发言
|
||||
- 不要和自己聊天
|
||||
|
||||
【必须遵守】
|
||||
- 遵守回复原则
|
||||
- 必须调用工具并包含action和reasoning
|
||||
- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply)
|
||||
- 并不是所有选择都可用
|
||||
- 选择text_reply或emoji_reply时必须提供emoji_query
|
||||
- 保持回复自然,符合日常聊天习惯""",
|
||||
"planner_prompt",
|
||||
【决策任务】
|
||||
{action_options_text}
|
||||
|
||||
你必须从上面列出的可用行动中选择一个,并说明原因。
|
||||
你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
|
||||
JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
|
||||
{{
|
||||
"action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
|
||||
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
|
||||
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
|
||||
}}
|
||||
|
||||
例如:
|
||||
{{
|
||||
"action": "text_reply",
|
||||
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
|
||||
"emoji_query": "微笑"
|
||||
}}
|
||||
或
|
||||
{{
|
||||
"action": "no_reply",
|
||||
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
|
||||
"emoji_query": ""
|
||||
}}
|
||||
|
||||
请输出你的决策 JSON:
|
||||
""", # 使用三引号避免内部引号问题
|
||||
"planner_prompt", # 保持名称不变,替换内容
|
||||
)
|
||||
|
||||
Prompt(
|
||||
@@ -177,7 +195,7 @@ class PromptBuilder:
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.MAX_CONTEXT_SIZE,
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
|
||||
chat_talking_prompt = await build_readable_messages(
|
||||
@@ -246,6 +264,8 @@ class PromptBuilder:
|
||||
sender_name=sender_name,
|
||||
)
|
||||
|
||||
logger.debug(f"focus_chat_prompt: \n{prompt}")
|
||||
|
||||
return prompt
|
||||
|
||||
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
|
||||
@@ -259,15 +279,15 @@ class PromptBuilder:
|
||||
who_chat_in_group += get_recent_group_speaker(
|
||||
chat_stream.stream_id,
|
||||
(chat_stream.user_info.platform, chat_stream.user_info.user_id),
|
||||
limit=global_config.MAX_CONTEXT_SIZE,
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
|
||||
relation_prompt = ""
|
||||
for person in who_chat_in_group:
|
||||
relation_prompt += await relationship_manager.build_relationship_info(person)
|
||||
print(f"relation_prompt: {relation_prompt}")
|
||||
# print(f"relation_prompt: {relation_prompt}")
|
||||
|
||||
print(f"relat11111111ion_prompt: {relation_prompt}")
|
||||
# print(f"relat11111111ion_prompt: {relation_prompt}")
|
||||
|
||||
# 心情
|
||||
mood_manager = MoodManager.get_instance()
|
||||
@@ -318,7 +338,7 @@ class PromptBuilder:
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.MAX_CONTEXT_SIZE,
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
|
||||
chat_talking_prompt = await build_readable_messages(
|
||||
|
||||
@@ -44,6 +44,8 @@ class NormalChat:
|
||||
# 存储此实例的兴趣监控任务
|
||||
self.start_time = time.time()
|
||||
|
||||
self.last_speak_time = 0
|
||||
|
||||
self._chat_task: Optional[asyncio.Task] = None
|
||||
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。")
|
||||
|
||||
@@ -119,6 +121,8 @@ class NormalChat:
|
||||
|
||||
await message_manager.add_message(message_set)
|
||||
|
||||
self.last_speak_time = time.time()
|
||||
|
||||
return first_bot_msg
|
||||
|
||||
# 改为实例方法
|
||||
@@ -354,7 +358,9 @@ class NormalChat:
|
||||
processed_count = 0
|
||||
# --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 ---
|
||||
messages_to_process_initially = list(messages_to_reply) # 创建副本
|
||||
# --- 修改结束 ---
|
||||
# --- 新增:限制最多处理两条消息 ---
|
||||
messages_to_process_initially = messages_to_process_initially[:2]
|
||||
# --- 新增结束 ---
|
||||
for item in messages_to_process_initially: # 使用副本迭代
|
||||
msg_id, (message, interest_value, is_mentioned) = item
|
||||
# --- 修改:在处理前尝试 pop,防止竞争 ---
|
||||
@@ -439,7 +445,7 @@ class NormalChat:
|
||||
logger.error(f"[{self.stream_name}] 任务异常: {exc}")
|
||||
logger.error(traceback.format_exc())
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.stream_name}] 任务已取消")
|
||||
logger.debug(f"[{self.stream_name}] 任务已取消")
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
|
||||
finally:
|
||||
@@ -452,12 +458,12 @@ class NormalChat:
|
||||
"""停止当前实例的兴趣监控任务。"""
|
||||
if self._chat_task and not self._chat_task.done():
|
||||
task = self._chat_task
|
||||
logger.info(f"[{self.stream_name}] 尝试取消聊天任务。")
|
||||
logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。")
|
||||
task.cancel()
|
||||
try:
|
||||
await task # 等待任务响应取消
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.stream_name}] 聊天任务已成功取消。")
|
||||
logger.info(f"[{self.stream_name}] 结束一般聊天模式。")
|
||||
except Exception as e:
|
||||
# 回调函数 _handle_task_completion 会处理异常日志
|
||||
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")
|
||||
|
||||
@@ -29,7 +29,7 @@ class NormalChatGenerator:
|
||||
)
|
||||
|
||||
self.model_sum = LLMRequest(
|
||||
model=global_config.llm_summary_by_topic, temperature=0.7, max_tokens=3000, request_type="relation"
|
||||
model=global_config.llm_summary, temperature=0.7, max_tokens=3000, request_type="relation"
|
||||
)
|
||||
self.current_model_type = "r1" # 默认使用 R1
|
||||
self.current_model_name = "unknown model"
|
||||
@@ -82,12 +82,14 @@ class NormalChatGenerator:
|
||||
sender_name=sender_name,
|
||||
chat_stream=message.chat_stream,
|
||||
)
|
||||
logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
|
||||
logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}")
|
||||
|
||||
try:
|
||||
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
|
||||
|
||||
logger.info(f"prompt:{prompt}\n生成回复:{content}")
|
||||
logger.debug(f"prompt:{prompt}\n生成回复:{content}")
|
||||
|
||||
logger.info(f"对 {message.processed_plain_text} 的回复:{content}")
|
||||
|
||||
info_catcher.catch_after_llm_generated(
|
||||
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
|
||||
|
||||
@@ -11,6 +11,9 @@ from .lpmmconfig import global_config
|
||||
from .utils.dyn_topk import dyn_select_top_k
|
||||
|
||||
|
||||
MAX_KNOWLEDGE_LENGTH = 10000 # 最大知识长度
|
||||
|
||||
|
||||
class QAManager:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -112,8 +115,10 @@ class QAManager:
|
||||
for res in query_res
|
||||
]
|
||||
found_knowledge = "\n".join(
|
||||
[f"第{i + 1}条知识:{k[1]}\n 该条知识对于问题的相关性:{k[0]}" for i, k in enumerate(knowledge)]
|
||||
[f"第{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}" for i, k in enumerate(knowledge)]
|
||||
)
|
||||
if len(found_knowledge) > MAX_KNOWLEDGE_LENGTH:
|
||||
found_knowledge = found_knowledge[:MAX_KNOWLEDGE_LENGTH] + "\n"
|
||||
return found_knowledge
|
||||
else:
|
||||
logger.info("LPMM知识库并未初始化,使用旧版数据库进行检索")
|
||||
|
||||
@@ -189,7 +189,7 @@ class Hippocampus:
|
||||
def __init__(self):
|
||||
self.memory_graph = MemoryGraph()
|
||||
self.llm_topic_judge = None
|
||||
self.llm_summary_by_topic = None
|
||||
self.llm_summary = None
|
||||
self.entorhinal_cortex = None
|
||||
self.parahippocampal_gyrus = None
|
||||
self.config = None
|
||||
@@ -203,7 +203,7 @@ class Hippocampus:
|
||||
# 从数据库加载记忆图
|
||||
self.entorhinal_cortex.sync_memory_from_db()
|
||||
self.llm_topic_judge = LLMRequest(self.config.llm_topic_judge, request_type="memory")
|
||||
self.llm_summary_by_topic = LLMRequest(self.config.llm_summary_by_topic, request_type="memory")
|
||||
self.llm_summary = LLMRequest(self.config.llm_summary, request_type="memory")
|
||||
|
||||
def get_all_node_names(self) -> list:
|
||||
"""获取记忆图中所有节点的名字列表"""
|
||||
@@ -1169,7 +1169,7 @@ class ParahippocampalGyrus:
|
||||
# 调用修改后的 topic_what,不再需要 time_info
|
||||
topic_what_prompt = self.hippocampus.topic_what(input_text, topic)
|
||||
try:
|
||||
task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt)
|
||||
task = self.hippocampus.llm_summary.generate_response_async(topic_what_prompt)
|
||||
tasks.append((topic.strip(), task))
|
||||
except Exception as e:
|
||||
logger.error(f"生成话题 '{topic}' 的摘要时发生错误: {e}")
|
||||
|
||||
@@ -24,7 +24,7 @@ class MemoryConfig:
|
||||
consolidate_memory_interval: int # 记忆整合间隔
|
||||
|
||||
llm_topic_judge: str # 话题判断模型
|
||||
llm_summary_by_topic: str # 话题总结模型
|
||||
llm_summary: str # 话题总结模型
|
||||
|
||||
@classmethod
|
||||
def from_global_config(cls, global_config):
|
||||
@@ -44,7 +44,5 @@ class MemoryConfig:
|
||||
consolidate_memory_percentage=getattr(global_config, "consolidate_memory_percentage", 0.01),
|
||||
consolidate_memory_interval=getattr(global_config, "consolidate_memory_interval", 1000),
|
||||
llm_topic_judge=getattr(global_config, "llm_topic_judge", "default_judge_model"), # 添加默认模型名
|
||||
llm_summary_by_topic=getattr(
|
||||
global_config, "llm_summary_by_topic", "default_summary_model"
|
||||
), # 添加默认模型名
|
||||
llm_summary=getattr(global_config, "llm_summary", "default_summary_model"), # 添加默认模型名
|
||||
)
|
||||
|
||||
@@ -632,7 +632,7 @@ class LLMRequest:
|
||||
**params_copy,
|
||||
}
|
||||
if "max_tokens" not in payload and "max_completion_tokens" not in payload:
|
||||
payload["max_tokens"] = global_config.max_response_length
|
||||
payload["max_tokens"] = global_config.model_max_output_length
|
||||
# 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查
|
||||
if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload:
|
||||
payload["max_completion_tokens"] = payload.pop("max_tokens")
|
||||
|
||||
@@ -282,10 +282,10 @@ class RelationshipManager:
|
||||
if is_id:
|
||||
person_id = person
|
||||
else:
|
||||
print(f"person: {person}")
|
||||
# print(f"person: {person}")
|
||||
person_id = person_info_manager.get_person_id(person[0], person[1])
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
print(f"person_name: {person_name}")
|
||||
# print(f"person_name: {person_name}")
|
||||
relationship_value = await person_info_manager.get_value(person_id, "relationship_value")
|
||||
level_num = self.calculate_level_num(relationship_value)
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ from typing import List
|
||||
|
||||
class InfoCatcher:
|
||||
def __init__(self):
|
||||
self.chat_history = [] # 聊天历史,长度为三倍使用的上下文
|
||||
self.context_length = global_config.MAX_CONTEXT_SIZE
|
||||
self.chat_history_in_thinking = [] # 思考期间的聊天内容
|
||||
self.chat_history_after_response = [] # 回复后的聊天内容,长度为一倍上下文
|
||||
self.chat_history = [] # 聊天历史,长度为三倍使用的上下文喵~
|
||||
self.context_length = global_config.observation_context_size
|
||||
self.chat_history_in_thinking = [] # 思考期间的聊天内容喵~
|
||||
self.chat_history_after_response = [] # 回复后的聊天内容,长度为一倍上下文喵~
|
||||
|
||||
self.chat_id = ""
|
||||
self.response_mode = global_config.response_mode
|
||||
self.trigger_response_text = ""
|
||||
self.response_text = ""
|
||||
|
||||
@@ -36,10 +35,10 @@ class InfoCatcher:
|
||||
"model": "",
|
||||
}
|
||||
|
||||
# 使用字典来存储 reasoning 模式的数据
|
||||
# 使用字典来存储 reasoning 模式的数据喵~
|
||||
self.reasoning_data = {"thinking_log": "", "prompt": "", "response": "", "model": ""}
|
||||
|
||||
# 耗时
|
||||
# 耗时喵~
|
||||
self.timing_results = {
|
||||
"interested_rate_time": 0,
|
||||
"sub_heartflow_observe_time": 0,
|
||||
@@ -73,15 +72,25 @@ class InfoCatcher:
|
||||
self.heartflow_data["sub_heartflow_now"] = current_mind
|
||||
|
||||
def catch_after_llm_generated(self, prompt: str, response: str, reasoning_content: str = "", model_name: str = ""):
|
||||
if self.response_mode == "heart_flow":
|
||||
self.heartflow_data["prompt"] = prompt
|
||||
self.heartflow_data["response"] = response
|
||||
self.heartflow_data["model"] = model_name
|
||||
elif self.response_mode == "reasoning":
|
||||
self.reasoning_data["thinking_log"] = reasoning_content
|
||||
self.reasoning_data["prompt"] = prompt
|
||||
self.reasoning_data["response"] = response
|
||||
self.reasoning_data["model"] = model_name
|
||||
# if self.response_mode == "heart_flow": # 条件判断不需要了喵~
|
||||
# self.heartflow_data["prompt"] = prompt
|
||||
# self.heartflow_data["response"] = response
|
||||
# self.heartflow_data["model"] = model_name
|
||||
# elif self.response_mode == "reasoning": # 条件判断不需要了喵~
|
||||
# self.reasoning_data["thinking_log"] = reasoning_content
|
||||
# self.reasoning_data["prompt"] = prompt
|
||||
# self.reasoning_data["response"] = response
|
||||
# self.reasoning_data["model"] = model_name
|
||||
|
||||
# 直接记录信息喵~
|
||||
self.reasoning_data["thinking_log"] = reasoning_content
|
||||
self.reasoning_data["prompt"] = prompt
|
||||
self.reasoning_data["response"] = response
|
||||
self.reasoning_data["model"] = model_name
|
||||
# 如果 heartflow 数据也需要通用字段,可以取消下面的注释喵~
|
||||
# self.heartflow_data["prompt"] = prompt
|
||||
# self.heartflow_data["response"] = response
|
||||
# self.heartflow_data["model"] = model_name
|
||||
|
||||
self.response_text = response
|
||||
|
||||
@@ -172,13 +181,13 @@ class InfoCatcher:
|
||||
}
|
||||
|
||||
def done_catch(self):
|
||||
"""将收集到的信息存储到数据库的 thinking_log 集合中"""
|
||||
"""将收集到的信息存储到数据库的 thinking_log 集合中喵~"""
|
||||
try:
|
||||
# 将消息对象转换为可序列化的字典
|
||||
# 将消息对象转换为可序列化的字典喵~
|
||||
|
||||
thinking_log_data = {
|
||||
"chat_id": self.chat_id,
|
||||
"response_mode": self.response_mode,
|
||||
# "response_mode": self.response_mode, # 这个也删掉喵~
|
||||
"trigger_text": self.trigger_response_text,
|
||||
"response_text": self.response_text,
|
||||
"trigger_info": {
|
||||
@@ -195,18 +204,20 @@ class InfoCatcher:
|
||||
"chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response),
|
||||
}
|
||||
|
||||
# 根据不同的响应模式添加相应的数据
|
||||
if self.response_mode == "heart_flow":
|
||||
thinking_log_data["mode_specific_data"] = self.heartflow_data
|
||||
elif self.response_mode == "reasoning":
|
||||
thinking_log_data["mode_specific_data"] = self.reasoning_data
|
||||
# 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~
|
||||
# if self.response_mode == "heart_flow":
|
||||
# thinking_log_data["mode_specific_data"] = self.heartflow_data
|
||||
# elif self.response_mode == "reasoning":
|
||||
# thinking_log_data["mode_specific_data"] = self.reasoning_data
|
||||
thinking_log_data["heartflow_data"] = self.heartflow_data
|
||||
thinking_log_data["reasoning_data"] = self.reasoning_data
|
||||
|
||||
# 将数据插入到 thinking_log 集合中
|
||||
# 将数据插入到 thinking_log 集合中喵~
|
||||
db.thinking_log.insert_one(thinking_log_data)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"存储思考日志时出错: {str(e)}")
|
||||
print(f"存储思考日志时出错: {str(e)} 喵~")
|
||||
print(traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, TypeVar, List, Union, Tuple
|
||||
import ast
|
||||
|
||||
# 定义类型变量用于泛型类型提示
|
||||
T = TypeVar("T")
|
||||
@@ -12,6 +13,7 @@ logger = logging.getLogger("json_utils")
|
||||
def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
|
||||
"""
|
||||
安全地解析JSON字符串,出错时返回默认值
|
||||
现在尝试处理单引号和标准JSON
|
||||
|
||||
参数:
|
||||
json_str: 要解析的JSON字符串
|
||||
@@ -20,16 +22,34 @@ def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
|
||||
返回:
|
||||
解析后的Python对象,或在解析失败时返回default_value
|
||||
"""
|
||||
if not json_str:
|
||||
if not json_str or not isinstance(json_str, str):
|
||||
logger.warning(f"safe_json_loads 接收到非字符串输入: {type(json_str)}, 值: {json_str}")
|
||||
return default_value
|
||||
|
||||
try:
|
||||
# 尝试标准的 JSON 解析
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析失败: {e}, JSON字符串: {json_str[:100]}...")
|
||||
return default_value
|
||||
except json.JSONDecodeError:
|
||||
# 如果标准解析失败,尝试将单引号替换为双引号再解析
|
||||
# (注意:这种替换可能不安全,如果字符串内容本身包含引号)
|
||||
# 更安全的方式是用 ast.literal_eval
|
||||
try:
|
||||
# logger.debug(f"标准JSON解析失败,尝试用 ast.literal_eval 解析: {json_str[:100]}...")
|
||||
result = ast.literal_eval(json_str)
|
||||
# 确保结果是字典(因为我们通常期望参数是字典)
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
else:
|
||||
logger.warning(f"ast.literal_eval 解析成功但结果不是字典: {type(result)}, 内容: {result}")
|
||||
return default_value
|
||||
except (ValueError, SyntaxError, MemoryError, RecursionError) as ast_e:
|
||||
logger.error(f"使用 ast.literal_eval 解析失败: {ast_e}, 字符串: {json_str[:100]}...")
|
||||
return default_value
|
||||
except Exception as e:
|
||||
logger.error(f"使用 ast.literal_eval 解析时发生意外错误: {e}, 字符串: {json_str[:100]}...")
|
||||
return default_value
|
||||
except Exception as e:
|
||||
logger.error(f"JSON解析过程中发生意外错误: {e}")
|
||||
logger.error(f"JSON解析过程中发生意外错误: {e}, 字符串: {json_str[:100]}...")
|
||||
return default_value
|
||||
|
||||
|
||||
@@ -177,25 +197,27 @@ def process_llm_tool_calls(
|
||||
if "name" not in func_details or not isinstance(func_details.get("name"), str):
|
||||
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'name'或类型不正确: {func_details}")
|
||||
continue
|
||||
if "arguments" not in func_details or not isinstance(
|
||||
func_details.get("arguments"), str
|
||||
): # 参数是字符串形式的JSON
|
||||
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'或类型不正确: {func_details}")
|
||||
|
||||
# 验证参数 'arguments'
|
||||
args_value = func_details.get("arguments")
|
||||
|
||||
# 1. 检查 arguments 是否存在且是字符串
|
||||
if args_value is None or not isinstance(args_value, str):
|
||||
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'字符串: {func_details}")
|
||||
continue
|
||||
|
||||
# 可选:尝试解析参数JSON,确保其有效
|
||||
args_str = func_details["arguments"]
|
||||
try:
|
||||
json.loads(args_str) # 尝试解析,但不存储结果
|
||||
except json.JSONDecodeError as e:
|
||||
# 2. 尝试安全地解析 arguments 字符串
|
||||
parsed_args = safe_json_loads(args_value, None)
|
||||
|
||||
# 3. 检查解析结果是否为字典
|
||||
if parsed_args is None or not isinstance(parsed_args, dict):
|
||||
logger.warning(
|
||||
f"{log_prefix}工具调用[{i}]的'arguments'不是有效的JSON字符串: {e}, 内容: {args_str[:100]}..."
|
||||
f"{log_prefix}工具调用[{i}]的'arguments'无法解析为有效的JSON字典, "
|
||||
f"原始字符串: {args_value[:100]}..., 解析结果类型: {type(parsed_args).__name__}"
|
||||
)
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"{log_prefix}解析工具调用[{i}]的'arguments'时发生意外错误: {e}, 内容: {args_str[:100]}...")
|
||||
continue
|
||||
|
||||
# 如果检查通过,将原始的 tool_call 加入有效列表
|
||||
valid_tool_calls.append(tool_call)
|
||||
|
||||
if not valid_tool_calls and tool_calls: # 如果原始列表不为空,但验证后为空
|
||||
|
||||
@@ -64,6 +64,9 @@ class ClassicalWillingManager(BaseWillingManager):
|
||||
self.chat_reply_willing[chat_id] = max(0, current_willing - 1.8)
|
||||
|
||||
async def after_generate_reply_handle(self, message_id):
|
||||
if message_id not in self.ongoing_messages:
|
||||
return
|
||||
|
||||
chat_id = self.ongoing_messages[message_id].chat_id
|
||||
current_willing = self.chat_reply_willing.get(chat_id, 0)
|
||||
if current_willing < 1:
|
||||
|
||||
@@ -77,7 +77,7 @@ class BaseWillingManager(ABC):
|
||||
if not issubclass(manager_class, cls):
|
||||
raise TypeError(f"Manager class {manager_class.__name__} is not a subclass of {cls.__name__}")
|
||||
else:
|
||||
logger.info(f"成功载入willing模式:{manager_type}")
|
||||
logger.info(f"普通回复模式:{manager_type}")
|
||||
return manager_class()
|
||||
except (ImportError, AttributeError, TypeError) as e:
|
||||
module = importlib.import_module(".mode_classical", __package__)
|
||||
@@ -110,7 +110,7 @@ class BaseWillingManager(ABC):
|
||||
def delete(self, message_id: str):
|
||||
del_message = self.ongoing_messages.pop(message_id, None)
|
||||
if not del_message:
|
||||
logger.debug(f"删除异常,当前消息{message_id}不存在")
|
||||
logger.debug(f"尝试删除不存在的消息 ID: {message_id},可能已被其他流程处理,喵~")
|
||||
|
||||
@abstractmethod
|
||||
async def async_task_starter(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user