feat(emoji): 增强消息上下文下的表情选择并修复存储用户信息的问题
该提交对表情操作及相关消息发送 API 进行了重大改进。 现在,表情选择逻辑更加具备上下文感知能力。LLM 提示现在包括机器人的待发送回复内容,使其能够根据对话历史和自身回复选择更合适的表情。用于上下文的近期聊天记录也有所增加。 此外,修复了插件发送 API (`send_api`) 中的一个错误。之前,当机器人发送消息时,消息在数据库中存储的是接收者的用户信息,而非机器人的信息。本次提交通过显式传递并使用机器人的用户信息进行存储,确保消息历史准确反映发送者。 其他更改包括: - 重构表情操作的激活逻辑以提高清晰度。 - 改进 LLM 选择的表情描述匹配算法。- 为配置访问添加必要的类型安全检查。
This commit is contained in:
@@ -13,7 +13,7 @@ from mofox_wire import MessageEnvelope
|
||||
from src.chat.message_receive.message_processor import process_message_from_dict
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.chat.utils.utils import calculate_typing_time, truncate_message
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
from src.common.data_models.database_data_model import DatabaseMessages, DatabaseUserInfo
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
|
||||
@@ -27,13 +27,13 @@ logger = get_logger("sender")
|
||||
|
||||
async def send_envelope(
|
||||
envelope: MessageEnvelope,
|
||||
chat_stream: "ChatStream" | None = None,
|
||||
chat_stream: ChatStream | None = None,
|
||||
db_message: DatabaseMessages | None = None,
|
||||
show_log: bool = True,
|
||||
) -> bool:
|
||||
"""发送消息"""
|
||||
message_preview = truncate_message(
|
||||
(db_message.processed_plain_text if db_message else str(envelope.get("message_segment", ""))),
|
||||
(db_message.processed_plain_text or "" if db_message else str(envelope.get("message_segment", ""))),
|
||||
max_length=120,
|
||||
)
|
||||
|
||||
@@ -81,6 +81,7 @@ class HeartFCSender:
|
||||
show_log: bool = True,
|
||||
thinking_start_time: float = 0.0,
|
||||
display_message: str | None = None,
|
||||
storage_user_info: "DatabaseUserInfo | None" = None,
|
||||
) -> bool:
|
||||
if not chat_stream:
|
||||
logger.error("消息缺少 chat_stream,无法发送")
|
||||
@@ -93,6 +94,13 @@ class HeartFCSender:
|
||||
platform=chat_stream.platform,
|
||||
)
|
||||
|
||||
# 如果提供了用于存储的用户信息,则覆盖
|
||||
if storage_message and storage_user_info:
|
||||
db_message.user_info.user_id = storage_user_info.user_id
|
||||
db_message.user_info.user_nickname = storage_user_info.user_nickname
|
||||
db_message.user_info.user_cardname = storage_user_info.user_cardname
|
||||
db_message.user_info.platform = storage_user_info.platform
|
||||
|
||||
# 使用调用方指定的展示文本
|
||||
if display_message:
|
||||
db_message.display_message = display_message
|
||||
@@ -125,9 +133,13 @@ class HeartFCSender:
|
||||
|
||||
# 将发送的消息写入上下文历史
|
||||
try:
|
||||
if chat_stream.context:
|
||||
if chat_stream and chat_stream.context and global_config.chat:
|
||||
context = chat_stream.context
|
||||
max_context_size = getattr(global_config.chat, "max_context_size", 40)
|
||||
chat_config = global_config.chat
|
||||
if chat_config:
|
||||
max_context_size = getattr(chat_config, "max_context_size", 40)
|
||||
else:
|
||||
max_context_size = 40
|
||||
|
||||
if len(context.history_messages) >= max_context_size:
|
||||
context.history_messages = context.history_messages[1:]
|
||||
|
||||
@@ -192,6 +192,7 @@ def _build_message_envelope(
|
||||
timestamp: float,
|
||||
) -> MessageEnvelope:
|
||||
"""构建发送的 MessageEnvelope 数据结构"""
|
||||
# 这里的 user_info 决定了消息要发给谁,所以在私聊场景下必须是目标用户
|
||||
target_user_info = target_stream.user_info or bot_user_info
|
||||
message_info: dict[str, Any] = {
|
||||
"message_id": message_id,
|
||||
@@ -212,7 +213,7 @@ def _build_message_envelope(
|
||||
"platform": target_stream.group_info.platform,
|
||||
}
|
||||
|
||||
return {
|
||||
return { # type: ignore
|
||||
"id": str(uuid.uuid4()),
|
||||
"direction": "outgoing",
|
||||
"platform": target_stream.platform,
|
||||
@@ -257,9 +258,14 @@ async def _send_to_target(
|
||||
current_time = time.time()
|
||||
message_id = f"send_api_{int(current_time * 1000)}"
|
||||
|
||||
bot_config = global_config.bot
|
||||
if not bot_config:
|
||||
logger.error("机器人配置丢失,无法构建机器人用户信息")
|
||||
return False
|
||||
|
||||
bot_user_info = DatabaseUserInfo(
|
||||
user_id=str(global_config.bot.qq_account),
|
||||
user_nickname=global_config.bot.nickname,
|
||||
user_id=str(bot_config.qq_account),
|
||||
user_nickname=bot_config.nickname,
|
||||
platform=target_stream.platform,
|
||||
)
|
||||
|
||||
@@ -328,6 +334,7 @@ async def _send_to_target(
|
||||
show_log=show_log,
|
||||
thinking_start_time=current_time,
|
||||
display_message=display_message_for_db,
|
||||
storage_user_info=bot_user_info,
|
||||
)
|
||||
|
||||
if sent_msg:
|
||||
|
||||
@@ -49,14 +49,6 @@ class EmojiAction(BaseAction):
|
||||
----------------------------------------
|
||||
"""
|
||||
|
||||
# ========== 以下使用旧的激活配置(已废弃但兼容) ==========
|
||||
# 激活设置
|
||||
if global_config.emoji.emoji_activate_type == "llm":
|
||||
activation_type = ActionActivationType.LLM_JUDGE
|
||||
random_activation_probability = 0
|
||||
else:
|
||||
activation_type = ActionActivationType.RANDOM
|
||||
random_activation_probability = global_config.emoji.emoji_chance
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = True
|
||||
|
||||
@@ -88,6 +80,15 @@ class EmojiAction(BaseAction):
|
||||
# 关联类型
|
||||
associated_types: ClassVar[list[str]] = ["emoji"]
|
||||
|
||||
async def go_activate(self, chat_content: str = "", llm_judge_model=None) -> bool:
|
||||
"""根据配置选择激活方式"""
|
||||
assert global_config is not None
|
||||
if global_config.emoji.emoji_activate_type == "llm":
|
||||
return await self._llm_judge_activation(
|
||||
judge_prompt=self.llm_judge_prompt, llm_judge_model=llm_judge_model
|
||||
)
|
||||
return await self._random_activation(global_config.emoji.emoji_chance)
|
||||
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行表情动作"""
|
||||
logger.info(f"{self.log_prefix} 决定发送表情")
|
||||
@@ -95,6 +96,7 @@ class EmojiAction(BaseAction):
|
||||
try:
|
||||
# 1. 获取发送表情的原因
|
||||
reason = self.action_data.get("reason", "表达当前情绪")
|
||||
main_reply_content = self.action_data.get("main_reply_content", "")
|
||||
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
|
||||
|
||||
# 2. 获取所有有效的表情包对象
|
||||
@@ -108,7 +110,7 @@ class EmojiAction(BaseAction):
|
||||
|
||||
# 3. 根据历史记录筛选表情
|
||||
try:
|
||||
recent_emojis_desc = get_recent_emojis(self.chat_id, limit=10)
|
||||
recent_emojis_desc = get_recent_emojis(self.chat_id, limit=20)
|
||||
if recent_emojis_desc:
|
||||
filtered_emojis = [emoji for emoji in all_emojis_obj if emoji.description not in recent_emojis_desc]
|
||||
if filtered_emojis:
|
||||
@@ -120,8 +122,8 @@ class EmojiAction(BaseAction):
|
||||
logger.error(f"{self.log_prefix} 获取或处理表情发送历史时出错: {e}")
|
||||
|
||||
# 4. 准备情感数据和后备列表
|
||||
emotion_map: ClassVar = {}
|
||||
all_emojis_data: ClassVar = []
|
||||
emotion_map = {}
|
||||
all_emojis_data = []
|
||||
|
||||
for emoji in all_emojis_obj:
|
||||
b64 = image_path_to_base64(emoji.full_path)
|
||||
@@ -146,14 +148,15 @@ class EmojiAction(BaseAction):
|
||||
chosen_emotion = "表情包" # 默认描述,避免变量未定义错误
|
||||
|
||||
# 4. 根据配置选择不同的表情选择模式
|
||||
assert global_config is not None
|
||||
if global_config.emoji.emoji_selection_mode == "emotion":
|
||||
# --- 情感标签选择模式 ---
|
||||
if not available_emotions:
|
||||
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
else:
|
||||
# 获取最近的5条消息内容用于判断
|
||||
recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
|
||||
# 获取最近的20条消息内容用于判断
|
||||
recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=20)
|
||||
messages_text = ""
|
||||
if recent_messages:
|
||||
messages_text = await message_api.build_readable_messages(
|
||||
@@ -164,8 +167,15 @@ class EmojiAction(BaseAction):
|
||||
)
|
||||
|
||||
# 构建prompt让LLM选择情感
|
||||
prompt_addition = ""
|
||||
if main_reply_content:
|
||||
prompt_addition = f"""
|
||||
这是你刚刚生成、准备发送的消息:
|
||||
"{main_reply_content}"
|
||||
"""
|
||||
prompt = f"""
|
||||
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。
|
||||
你是一个正在进行聊天的网友,你需要根据一个理由、最近的聊天记录以及你自己将要发送的消息,从一个情感标签列表中选择最匹配的一个。
|
||||
{prompt_addition}
|
||||
这是最近的聊天记录:
|
||||
{messages_text}
|
||||
|
||||
@@ -174,9 +184,7 @@ class EmojiAction(BaseAction):
|
||||
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
|
||||
"""
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
|
||||
else:
|
||||
assert global_config is not None
|
||||
logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
|
||||
|
||||
# 调用LLM
|
||||
@@ -211,10 +219,11 @@ class EmojiAction(BaseAction):
|
||||
)
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
|
||||
elif global_config.emoji.emoji_selection_mode == "description":
|
||||
assert global_config is not None
|
||||
if global_config.emoji.emoji_selection_mode == "description":
|
||||
# --- 详细描述选择模式 ---
|
||||
# 获取最近的5条消息内容用于判断
|
||||
recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
|
||||
recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=20)
|
||||
messages_text = ""
|
||||
if recent_messages:
|
||||
messages_text = await message_api.build_readable_messages(
|
||||
@@ -234,8 +243,15 @@ class EmojiAction(BaseAction):
|
||||
emoji_descriptions = [extract_refined_info(desc) for _, desc in all_emojis_data]
|
||||
|
||||
# 构建prompt让LLM选择描述
|
||||
prompt_addition = ""
|
||||
if main_reply_content:
|
||||
prompt_addition = f"""
|
||||
这是你刚刚生成、准备发送的消息:
|
||||
"{main_reply_content}"
|
||||
"""
|
||||
prompt = f"""
|
||||
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个表情包描述列表中选择最匹配的一个。
|
||||
你是一个正在进行聊天的网友,你需要根据一个理由、最近的聊天记录以及你自己将要发送的消息,从一个表情包描述列表中选择最匹配的一个。
|
||||
{prompt_addition}
|
||||
这是最近的聊天记录:
|
||||
{messages_text}
|
||||
|
||||
@@ -264,44 +280,22 @@ class EmojiAction(BaseAction):
|
||||
chosen_emotion = chosen_description # 在描述模式下,用描述作为情感标签
|
||||
logger.info(f"{self.log_prefix} LLM选择的描述: {chosen_description}")
|
||||
|
||||
# 优化匹配逻辑:优先在精炼描述中精确匹配,然后进行关键词匹配
|
||||
def extract_refined_info(full_desc: str) -> str:
|
||||
return full_desc.split(" Desc:")[0].strip()
|
||||
|
||||
# 1. 尝试在精炼描述中找到最匹配的表情
|
||||
# 我们假设LLM返回的是精炼描述的一部分或全部
|
||||
# 使用更鲁棒的子字符串匹配逻辑
|
||||
matched_emoji = None
|
||||
best_match_score = 0
|
||||
|
||||
for item in all_emojis_data:
|
||||
refined_info = extract_refined_info(item[1])
|
||||
# 计算一个简单的匹配分数
|
||||
score = 0
|
||||
if chosen_description.lower() in refined_info.lower():
|
||||
score += 2 # 包含匹配
|
||||
if refined_info.lower() in chosen_description.lower():
|
||||
score += 2 # 包含匹配
|
||||
|
||||
# 关键词匹配加分
|
||||
chosen_keywords = re.findall(r"\w+", chosen_description.lower())
|
||||
item_keywords = re.findall(r"\[(.*?)\]", refined_info)
|
||||
if item_keywords:
|
||||
item_keywords_set = {k.strip().lower() for k in item_keywords[0].split(",")}
|
||||
for kw in chosen_keywords:
|
||||
if kw in item_keywords_set:
|
||||
score += 1
|
||||
|
||||
if score > best_match_score:
|
||||
best_match_score = score
|
||||
matched_emoji = item
|
||||
for b64, desc in all_emojis_data:
|
||||
# 检查LLM返回的描述是否是数据库中某个表情完整描述的一部分
|
||||
if chosen_description in desc:
|
||||
matched_emoji = (b64, desc)
|
||||
break
|
||||
|
||||
if matched_emoji:
|
||||
emoji_base64, emoji_description = matched_emoji
|
||||
logger.info(f"{self.log_prefix} 找到匹配描述的表情包: {extract_refined_info(emoji_description)}")
|
||||
logger.info(f"{self.log_prefix} 找到匹配描述的表情包: {emoji_description}")
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} LLM选择的描述无法匹配任何表情包, 将随机选择")
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
else:
|
||||
assert global_config is not None
|
||||
logger.error(f"{self.log_prefix} 无效的表情选择模式: {global_config.emoji.emoji_selection_mode}")
|
||||
return False, "无效的表情选择模式"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user