Merge branch 'dev'
This commit is contained in:
@@ -65,6 +65,6 @@ asyncio
|
|||||||
tavily-python
|
tavily-python
|
||||||
google-generativeai
|
google-generativeai
|
||||||
lunar_python
|
lunar_python
|
||||||
|
fuzzywuzzy
|
||||||
python-multipart
|
python-multipart
|
||||||
aiofiles
|
aiofiles
|
||||||
@@ -311,6 +311,10 @@ class CycleProcessor:
|
|||||||
if reply_actions:
|
if reply_actions:
|
||||||
logger.info(f"{self.log_prefix} 正在执行文本回复...")
|
logger.info(f"{self.log_prefix} 正在执行文本回复...")
|
||||||
for action in reply_actions:
|
for action in reply_actions:
|
||||||
|
target_user_id = action.get("action_message",{}).get("chat_info_user_id","")
|
||||||
|
if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self:
|
||||||
|
logger.warning("选取的reply的目标为bot自己,跳过reply action")
|
||||||
|
continue
|
||||||
result = await execute_action(action)
|
result = await execute_action(action)
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
logger.error(f"{self.log_prefix} 回复动作执行异常: {result}")
|
logger.error(f"{self.log_prefix} 回复动作执行异常: {result}")
|
||||||
|
|||||||
@@ -379,6 +379,37 @@ class HeartFChatting:
|
|||||||
self.context.last_message_time = time.time()
|
self.context.last_message_time = time.time()
|
||||||
self.context.last_read_time = time.time()
|
self.context.last_read_time = time.time()
|
||||||
|
|
||||||
|
# --- 专注模式安静群组检查 ---
|
||||||
|
quiet_groups = global_config.chat.focus_mode_quiet_groups
|
||||||
|
if quiet_groups and self.context.chat_stream:
|
||||||
|
is_group_chat = self.context.chat_stream.group_info is not None
|
||||||
|
if is_group_chat:
|
||||||
|
try:
|
||||||
|
platform = self.context.chat_stream.platform
|
||||||
|
group_id = self.context.chat_stream.group_info.group_id
|
||||||
|
|
||||||
|
# 兼容不同QQ适配器的平台名称
|
||||||
|
is_qq_platform = platform in ["qq", "napcat"]
|
||||||
|
|
||||||
|
current_chat_identifier = f"{platform}:{group_id}"
|
||||||
|
config_identifier_for_qq = f"qq:{group_id}"
|
||||||
|
|
||||||
|
is_in_quiet_list = (current_chat_identifier in quiet_groups or
|
||||||
|
(is_qq_platform and config_identifier_for_qq in quiet_groups))
|
||||||
|
|
||||||
|
if is_in_quiet_list:
|
||||||
|
is_mentioned_in_batch = False
|
||||||
|
for msg in recent_messages:
|
||||||
|
if msg.get("is_mentioned"):
|
||||||
|
is_mentioned_in_batch = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_mentioned_in_batch:
|
||||||
|
logger.info(f"{self.context.log_prefix} 在专注安静模式下,因未被提及而忽略了消息。")
|
||||||
|
return True # 消耗消息但不做回复
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{self.context.log_prefix} 检查专注安静群组时出错: {e}")
|
||||||
|
|
||||||
# 处理唤醒度逻辑
|
# 处理唤醒度逻辑
|
||||||
if current_sleep_state in [SleepState.SLEEPING, SleepState.PREPARING_SLEEP, SleepState.INSOMNIA]:
|
if current_sleep_state in [SleepState.SLEEPING, SleepState.PREPARING_SLEEP, SleepState.INSOMNIA]:
|
||||||
self._handle_wakeup_messages(recent_messages)
|
self._handle_wakeup_messages(recent_messages)
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ async def schedule_reminder(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str):
|
async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str, original_message: str = None):
|
||||||
"""执行提醒回调函数"""
|
"""执行提醒回调函数"""
|
||||||
try:
|
try:
|
||||||
# 获取对应的subheartflow实例
|
# 获取对应的subheartflow实例
|
||||||
@@ -219,11 +219,15 @@ async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str):
|
|||||||
# 创建主动思考事件,触发完整的思考流程
|
# 创建主动思考事件,触发完整的思考流程
|
||||||
from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent
|
from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent
|
||||||
|
|
||||||
|
# 使用原始消息来构造reason,如果没有原始消息则使用处理后的内容
|
||||||
|
reason_content = original_message if original_message else reminder_text
|
||||||
|
|
||||||
event = ProactiveTriggerEvent(
|
event = ProactiveTriggerEvent(
|
||||||
source="reminder_system",
|
source="reminder_system",
|
||||||
reason=f"定时提醒:{reminder_text}",
|
reason=f"定时提醒:{reason_content}", # 这里传递完整的原始消息
|
||||||
metadata={
|
metadata={
|
||||||
"reminder_text": reminder_text,
|
"reminder_text": reminder_text,
|
||||||
|
"original_message": original_message,
|
||||||
"trigger_time": datetime.now().isoformat()
|
"trigger_time": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -231,7 +235,7 @@ async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str):
|
|||||||
# 通过subflow的HeartFChatting实例触发主动思考
|
# 通过subflow的HeartFChatting实例触发主动思考
|
||||||
await subflow.heart_fc_instance.proactive_thinker.think(event)
|
await subflow.heart_fc_instance.proactive_thinker.think(event)
|
||||||
|
|
||||||
logger.info(f"已触发提醒的主动思考,内容: {reminder_text}")
|
logger.info(f"已触发提醒的主动思考,内容: {reminder_text},没有传递那条消息吗?{original_message}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"执行提醒回调时发生错误: {e}")
|
logger.error(f"执行提醒回调时发生错误: {e}")
|
||||||
|
|||||||
@@ -120,75 +120,61 @@ class ProactiveThinker:
|
|||||||
trigger_event (ProactiveTriggerEvent): 触发事件。
|
trigger_event (ProactiveTriggerEvent): 触发事件。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 如果是提醒事件,跳过规划器,直接构建默认动作
|
# 如果是提醒事件,直接使用当前上下文执行at_user动作
|
||||||
if trigger_event.source == "reminder_system":
|
if trigger_event.source == "reminder_system":
|
||||||
# 1. 获取上下文信息
|
# 1. 获取上下文信息
|
||||||
metadata = trigger_event.metadata or {}
|
metadata = trigger_event.metadata or {}
|
||||||
action_message = metadata
|
|
||||||
reminder_content = trigger_event.reason.replace("定时提醒:", "").strip()
|
reminder_content = trigger_event.reason.replace("定时提醒:", "").strip()
|
||||||
|
|
||||||
# 2. 确定目标用户名
|
# 2. 使用LLM智能解析目标用户名
|
||||||
target_user_name = None
|
target_user_name = None
|
||||||
match = re.search(r"艾特一下([^,,\s]+)", reminder_content)
|
|
||||||
if match:
|
# 首先尝试从完整的原始信息中解析(如果有的话)
|
||||||
target_user_name = match.group(1)
|
full_content = trigger_event.reason
|
||||||
else:
|
logger.info(f"{self.context.log_prefix} 解析提醒内容: '{full_content}'")
|
||||||
from src.person_info.person_info import get_person_info_manager
|
|
||||||
user_id = metadata.get("user_id")
|
sender_name = metadata.get("sender_name")
|
||||||
platform = metadata.get("platform")
|
target_user_name = await self._extract_target_user_with_llm(full_content, sender_name)
|
||||||
if user_id and platform:
|
|
||||||
person_id = get_person_info_manager().get_person_id(platform, user_id)
|
|
||||||
target_user_name = await get_person_info_manager().get_value(person_id, "person_name")
|
|
||||||
|
|
||||||
if not target_user_name:
|
if not target_user_name:
|
||||||
logger.warning(f"无法从提醒 '{reminder_content}' 中确定目标用户,回退")
|
logger.warning(f"无法从提醒 '{reminder_content}' 中确定目标用户,回退")
|
||||||
raise Exception("无法确定目标用户")
|
# 回退到生成普通提醒消息
|
||||||
|
|
||||||
# 3. 构建动作
|
|
||||||
action_result = {
|
|
||||||
"action_type": "at_user",
|
|
||||||
"reasoning": "执行定时提醒",
|
|
||||||
"action_data": {
|
|
||||||
"user_name": target_user_name,
|
|
||||||
"at_message": reminder_content
|
|
||||||
},
|
|
||||||
"action_message": action_message
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. 执行或回退
|
|
||||||
try:
|
|
||||||
original_chat_id = metadata.get("chat_id")
|
|
||||||
if not original_chat_id:
|
|
||||||
if trigger_event.related_message_id:
|
|
||||||
db_message = await db_get(Messages, {"message_id": trigger_event.related_message_id}, single_result=True) or {}
|
|
||||||
original_chat_id = db_message.get("chat_id")
|
|
||||||
|
|
||||||
if not original_chat_id:
|
|
||||||
raise Exception("提醒事件中缺少chat_id")
|
|
||||||
|
|
||||||
from src.chat.heart_flow.heartflow import heartflow
|
|
||||||
subflow = await heartflow.get_or_create_subheartflow(original_chat_id)
|
|
||||||
if not subflow:
|
|
||||||
raise Exception(f"无法为chat_id {original_chat_id} 获取subflow")
|
|
||||||
|
|
||||||
success, _, _ = await subflow.heart_fc_instance.cycle_processor._handle_action(
|
|
||||||
action=action_result["action_type"],
|
|
||||||
reasoning=action_result["reasoning"],
|
|
||||||
action_data=action_result["action_data"],
|
|
||||||
cycle_timers={},
|
|
||||||
thinking_id="",
|
|
||||||
action_message=action_result["action_message"],
|
|
||||||
)
|
|
||||||
if not success:
|
|
||||||
raise Exception("at_user action failed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"{self.context.log_prefix} at_user动作执行失败: {e},回退到proactive_reply")
|
|
||||||
fallback_action = {
|
fallback_action = {
|
||||||
"action_type": "proactive_reply",
|
"action_type": "proactive_reply",
|
||||||
"action_data": {"topic": trigger_event.reason},
|
"action_data": {"topic": f"定时提醒:{reminder_content}"},
|
||||||
"action_message": action_message
|
"action_message": metadata
|
||||||
}
|
}
|
||||||
await self._generate_proactive_content_and_send(fallback_action, trigger_event)
|
await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 直接使用当前上下文的cycle_processor执行at_user动作
|
||||||
|
try:
|
||||||
|
success, _, _ = await self.cycle_processor._handle_action(
|
||||||
|
action="at_user",
|
||||||
|
reasoning="执行定时提醒",
|
||||||
|
action_data={
|
||||||
|
"user_name": target_user_name,
|
||||||
|
"at_message": reminder_content
|
||||||
|
},
|
||||||
|
cycle_timers={},
|
||||||
|
thinking_id="",
|
||||||
|
action_message=metadata,
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
logger.info(f"{self.context.log_prefix} 成功执行定时提醒艾特用户 {target_user_name}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise Exception("at_user action failed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"{self.context.log_prefix} at_user动作执行失败: {e},回退到专用提醒回复")
|
||||||
|
# 回退到专用的定时提醒回复
|
||||||
|
fallback_action = {
|
||||||
|
"action_type": "proactive_reply",
|
||||||
|
"action_data": {"topic": f"定时提醒:{reminder_content}"},
|
||||||
|
"action_message": metadata
|
||||||
|
}
|
||||||
|
await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content)
|
||||||
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 对于其他来源的主动思考,正常调用规划器
|
# 对于其他来源的主动思考,正常调用规划器
|
||||||
@@ -213,6 +199,149 @@ class ProactiveThinker:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}")
|
logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
async def _extract_target_user_with_llm(self, reminder_content: str, sender_name: str) -> str:
|
||||||
|
"""
|
||||||
|
使用LLM从提醒内容中提取目标用户名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reminder_content: 完整的提醒内容
|
||||||
|
sender_name: 消息发送者的昵称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
提取出的用户名,如果找不到则返回None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from src.llm_models.utils_model import LLMRequest
|
||||||
|
from src.config.config import model_config
|
||||||
|
|
||||||
|
bot_name = global_config.bot.nickname
|
||||||
|
user_extraction_prompt = f'''
|
||||||
|
从以下提醒消息中提取需要被提醒的目标用户名。
|
||||||
|
|
||||||
|
**重要认知**:
|
||||||
|
- 你的名字是"{bot_name}"。当消息中提到"{bot_name}"时,通常是在称呼你。
|
||||||
|
- 消息的发送者是"{sender_name}"。当消息中出现"我"、"咱"等第一人称代词时,指代的就是"{sender_name}"。
|
||||||
|
|
||||||
|
提醒消息: "{reminder_content}"
|
||||||
|
|
||||||
|
规则:
|
||||||
|
1. 分析消息,找出真正需要被提醒的人。
|
||||||
|
2. 如果提醒目标是第一人称(如"我"),那么目标就是发送者"{sender_name}"。
|
||||||
|
3. **绝对不能**提取你自己的名字("{bot_name}")作为目标。
|
||||||
|
4. 只提取最关键的人名,不要包含多余的词语。
|
||||||
|
5. 如果没有明确的提醒目标(既不是其他人,也不是发送者自己),请回答"无"。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- 消息: "定时提醒:{bot_name},10分钟后提醒我去打深渊" -> "{sender_name}"
|
||||||
|
- 消息: "定时提醒:{bot_name},提醒阿范一分钟后去写模组" -> "阿范"
|
||||||
|
- 消息: "定时提醒:一分钟后提醒一闪喝水" -> "一闪"
|
||||||
|
- 消息: "定时提醒:喝水" -> "无"
|
||||||
|
- 消息: "定时提醒:{bot_name},记得休息" -> "无"
|
||||||
|
|
||||||
|
请直接输出提取到的用户名,如果不存在则输出"无"。
|
||||||
|
'''
|
||||||
|
|
||||||
|
llm_request = LLMRequest(
|
||||||
|
model_set=model_config.model_task_config.utils_small,
|
||||||
|
request_type="reminder_user_extraction"
|
||||||
|
)
|
||||||
|
|
||||||
|
response, _ = await llm_request.generate_response_async(prompt=user_extraction_prompt)
|
||||||
|
|
||||||
|
if response and response.strip() != "无":
|
||||||
|
logger.info(f"LLM成功提取目标用户: '{response.strip()}'")
|
||||||
|
return response.strip()
|
||||||
|
else:
|
||||||
|
logger.warning(f"LLM未能从 '{reminder_content}' 中提取目标用户")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"使用LLM提取用户名时出错: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _generate_reminder_proactive_reply(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent, reminder_content: str):
|
||||||
|
"""
|
||||||
|
为定时提醒事件生成专用的主动回复
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_result: 动作结果
|
||||||
|
trigger_event: 触发事件
|
||||||
|
reminder_content: 提醒内容
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"{self.context.log_prefix} 生成定时提醒专用回复: '{reminder_content}'")
|
||||||
|
|
||||||
|
# 获取基本信息
|
||||||
|
bot_name = global_config.bot.nickname
|
||||||
|
personality = global_config.personality
|
||||||
|
identity_block = (
|
||||||
|
f"你的名字是{bot_name}。\n"
|
||||||
|
f"关于你:{personality.personality_core},并且{personality.personality_side}。\n"
|
||||||
|
f"你的身份是{personality.identity},平时说话风格是{personality.reply_style}。"
|
||||||
|
)
|
||||||
|
mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}"
|
||||||
|
|
||||||
|
# 获取日程信息
|
||||||
|
schedule_block = "你今天没有日程安排。"
|
||||||
|
if global_config.planning_system.schedule_enable:
|
||||||
|
if current_activity := schedule_manager.get_current_activity():
|
||||||
|
schedule_block = f"你当前正在:{current_activity}。"
|
||||||
|
|
||||||
|
# 为定时提醒定制的专用提示词
|
||||||
|
reminder_prompt = f"""
|
||||||
|
## 你的角色
|
||||||
|
{identity_block}
|
||||||
|
|
||||||
|
## 你的心情
|
||||||
|
{mood_block}
|
||||||
|
|
||||||
|
## 你今天的日程安排
|
||||||
|
{schedule_block}
|
||||||
|
|
||||||
|
## 定时提醒任务
|
||||||
|
你收到了一个定时提醒:"{reminder_content}"
|
||||||
|
这是一个自动触发的提醒事件,你需要根据提醒内容发送一条友好的提醒消息。
|
||||||
|
|
||||||
|
## 任务要求
|
||||||
|
- 这是一个定时提醒,要体现出你的贴心和关怀
|
||||||
|
- 根据提醒内容的具体情况(如"喝水"、"休息"等)给出相应的提醒
|
||||||
|
- 保持你一贯的温暖、俏皮风格
|
||||||
|
- 可以加上一些鼓励或关心的话语
|
||||||
|
- 直接输出提醒消息,不要解释为什么要提醒
|
||||||
|
|
||||||
|
请生成一条温暖贴心的提醒消息。
|
||||||
|
"""
|
||||||
|
|
||||||
|
response_text = await generator_api.generate_response_custom(
|
||||||
|
chat_stream=self.context.chat_stream,
|
||||||
|
prompt=reminder_prompt,
|
||||||
|
request_type="chat.replyer.reminder",
|
||||||
|
)
|
||||||
|
|
||||||
|
if response_text:
|
||||||
|
response_set = process_human_text(
|
||||||
|
content=response_text,
|
||||||
|
enable_splitter=global_config.response_splitter.enable,
|
||||||
|
enable_chinese_typo=global_config.chinese_typo.enable,
|
||||||
|
)
|
||||||
|
await self.cycle_processor.response_handler.send_response(
|
||||||
|
response_set, time.time(), action_result.get("action_message")
|
||||||
|
)
|
||||||
|
await store_action_info(
|
||||||
|
chat_stream=self.context.chat_stream,
|
||||||
|
action_name="reminder_reply",
|
||||||
|
action_data={"reminder_content": reminder_content, "response": response_text},
|
||||||
|
action_prompt_display=f"定时提醒回复: {reminder_content}",
|
||||||
|
action_done=True,
|
||||||
|
)
|
||||||
|
logger.info(f"{self.context.log_prefix} 成功发送定时提醒回复: {response_text}")
|
||||||
|
else:
|
||||||
|
logger.error(f"{self.context.log_prefix} 定时提醒回复生成失败。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{self.context.log_prefix} 生成定时提醒回复时异常: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
async def _get_reminder_context(self, message_id: str) -> str:
|
async def _get_reminder_context(self, message_id: str) -> str:
|
||||||
"""获取提醒消息的上下文"""
|
"""获取提醒消息的上下文"""
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class ResponseHandler:
|
|||||||
need_reply = new_message_count >= random.randint(2, 4)
|
need_reply = new_message_count >= random.randint(2, 4)
|
||||||
|
|
||||||
reply_text = ""
|
reply_text = ""
|
||||||
is_proactive_thinking = message_data.get("message_type") == "proactive_thinking"
|
is_proactive_thinking = (message_data.get("message_type") == "proactive_thinking") if message_data else True
|
||||||
|
|
||||||
first_replied = False
|
first_replied = False
|
||||||
for reply_seg in reply_set:
|
for reply_seg in reply_set:
|
||||||
|
|||||||
@@ -160,9 +160,13 @@ class HeartFCMessageReceiver:
|
|||||||
from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent
|
from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent
|
||||||
|
|
||||||
reminder_content = metadata.get('content', '提醒时间到了')
|
reminder_content = metadata.get('content', '提醒时间到了')
|
||||||
|
# 使用原始消息内容作为reason,如果没有则使用处理后的内容
|
||||||
|
original_message = metadata.get('original_message', '')
|
||||||
|
reason_content = original_message if original_message else reminder_content
|
||||||
|
|
||||||
event = ProactiveTriggerEvent(
|
event = ProactiveTriggerEvent(
|
||||||
source="reminder_system",
|
source="reminder_system",
|
||||||
reason=f"定时提醒:{reminder_content}",
|
reason=f"定时提醒:{reason_content}",
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
related_message_id=metadata.get("original_message_id")
|
related_message_id=metadata.get("original_message_id")
|
||||||
)
|
)
|
||||||
@@ -195,12 +199,14 @@ class HeartFCMessageReceiver:
|
|||||||
metadata = {
|
metadata = {
|
||||||
"type": "reminder",
|
"type": "reminder",
|
||||||
"user_id": reminder_event.user_id,
|
"user_id": reminder_event.user_id,
|
||||||
|
"sender_name": userinfo.user_nickname, # 添加发送者昵称
|
||||||
"platform": chat.platform,
|
"platform": chat.platform,
|
||||||
"chat_id": chat.stream_id,
|
"chat_id": chat.stream_id,
|
||||||
"content": reminder_event.content,
|
"content": reminder_event.content,
|
||||||
"confidence": reminder_event.confidence,
|
"confidence": reminder_event.confidence,
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
"original_message_id": message.message_info.message_id
|
"original_message_id": message.message_info.message_id,
|
||||||
|
"original_message": message.processed_plain_text # 保存完整的原始消息
|
||||||
}
|
}
|
||||||
|
|
||||||
success = await event_scheduler.schedule_event(
|
success = await event_scheduler.schedule_event(
|
||||||
|
|||||||
@@ -205,9 +205,8 @@ class ActionModifier:
|
|||||||
|
|
||||||
elif activation_type == ActionActivationType.RANDOM:
|
elif activation_type == ActionActivationType.RANDOM:
|
||||||
probability = action_info.random_activation_probability
|
probability = action_info.random_activation_probability
|
||||||
if probability >= 1.0:
|
probability = action_info.random_activation_probability
|
||||||
continue # 概率为100%或更高,直接激活
|
if random.random() >= probability:
|
||||||
if random.random() > probability:
|
|
||||||
reason = f"RANDOM类型未触发(概率{probability})"
|
reason = f"RANDOM类型未触发(概率{probability})"
|
||||||
deactivated_actions.append((action_name, reason))
|
deactivated_actions.append((action_name, reason))
|
||||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}")
|
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}")
|
||||||
|
|||||||
@@ -57,30 +57,64 @@ def init_prompt():
|
|||||||
|
|
||||||
{moderation_prompt}
|
{moderation_prompt}
|
||||||
|
|
||||||
现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息:
|
**任务: 构建一个完整的响应**
|
||||||
|
你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成:
|
||||||
|
1. **主要动作**: 这是响应的核心,通常是 `reply`(文本回复)。
|
||||||
|
2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。
|
||||||
|
|
||||||
|
**决策流程:**
|
||||||
|
1. 首先,决定是否要进行 `reply`。
|
||||||
|
2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。
|
||||||
|
3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。
|
||||||
|
4. 如果用户明确要求了某个动作,请务必优先满足。
|
||||||
|
|
||||||
|
**可用动作:**
|
||||||
{actions_before_now_block}
|
{actions_before_now_block}
|
||||||
|
|
||||||
{no_action_block}
|
{no_action_block}
|
||||||
|
|
||||||
动作:reply
|
动作:reply
|
||||||
动作描述:参与聊天回复,发送文本进行表达
|
动作描述:参与聊天回复,发送文本进行表达
|
||||||
- 你想要闲聊或者随便附
|
- 你想要闲聊或者随便附和
|
||||||
- {mentioned_bonus}
|
- {mentioned_bonus}
|
||||||
- 如果你刚刚进行了回复,不要对同一个话题重复回应
|
- 如果你刚刚进行了回复,不要对同一个话题重复回应
|
||||||
- 不要回复自己发送的消息
|
- 不要回复自己发送的消息
|
||||||
{{
|
{{
|
||||||
"action": "reply",
|
"action": "reply",
|
||||||
"target_message_id":"触发action的消息id",
|
"target_message_id": "触发action的消息id",
|
||||||
"reason":"回复的原因"
|
"reason": "回复的原因"
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{action_options_text}
|
{action_options_text}
|
||||||
|
|
||||||
- 如果用户明确要求使用某个动作,请优先选择该动作。
|
|
||||||
- 当一个动作可以作为另一个动作的补充时,你应该同时选择它们。例如,在回复的同时可以发送表情包(emoji)。
|
|
||||||
你必须从上面列出的可用action中选择一个或多个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字
|
|
||||||
|
|
||||||
请根据动作示例,以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果只选择一个动作,也请将其包含在列表中。如果没有任何合适的动作,返回一个空列表[]。不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容:
|
**输出格式:**
|
||||||
|
你必须以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作,返回一个空列表[]。
|
||||||
|
|
||||||
|
**单动作示例 (仅回复):**
|
||||||
|
[
|
||||||
|
{{
|
||||||
|
"action": "reply",
|
||||||
|
"target_message_id": "m123",
|
||||||
|
"reason": "回答用户的问题"
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
|
||||||
|
**组合动作示例 (回复 + 表情包):**
|
||||||
|
[
|
||||||
|
{{
|
||||||
|
"action": "reply",
|
||||||
|
"target_message_id": "m123",
|
||||||
|
"reason": "回答用户的问题"
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
"action": "emoji",
|
||||||
|
"target_message_id": "m123",
|
||||||
|
"reason": "用一个可爱的表情来缓和气氛"
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
|
||||||
|
不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容:
|
||||||
""",
|
""",
|
||||||
"planner_prompt",
|
"planner_prompt",
|
||||||
)
|
)
|
||||||
@@ -148,9 +182,9 @@ def init_prompt():
|
|||||||
动作描述:{action_description}
|
动作描述:{action_description}
|
||||||
{action_require}
|
{action_require}
|
||||||
{{
|
{{
|
||||||
"action": "{action_name}",{action_parameters},
|
"action": "{action_name}",
|
||||||
"target_message_id":"触发action的消息id",
|
"target_message_id": "触发action的消息id",
|
||||||
"reason":"触发action的原因"
|
"reason": "触发action的原因"{action_parameters}
|
||||||
}}
|
}}
|
||||||
""",
|
""",
|
||||||
"action_prompt",
|
"action_prompt",
|
||||||
@@ -285,7 +319,7 @@ class ActionPlanner:
|
|||||||
action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]}
|
action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]}
|
||||||
|
|
||||||
target_message = None
|
target_message = None
|
||||||
if action not in ["no_action", "no_reply"]:
|
if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]:
|
||||||
if target_message_id := action_json.get("target_message_id"):
|
if target_message_id := action_json.get("target_message_id"):
|
||||||
target_message = self.find_message_by_id(target_message_id, message_id_list)
|
target_message = self.find_message_by_id(target_message_id, message_id_list)
|
||||||
if target_message is None:
|
if target_message is None:
|
||||||
@@ -295,7 +329,7 @@ class ActionPlanner:
|
|||||||
logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id")
|
logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id")
|
||||||
|
|
||||||
available_action_names = [name for name, _ in current_available_actions]
|
available_action_names = [name for name, _ in current_available_actions]
|
||||||
if action not in ["no_action", "no_reply", "reply"] and action not in available_action_names:
|
if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'"
|
f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'"
|
||||||
)
|
)
|
||||||
@@ -409,18 +443,18 @@ class ActionPlanner:
|
|||||||
# --- 3. 后处理 ---
|
# --- 3. 后处理 ---
|
||||||
final_actions = self._filter_no_actions(final_actions)
|
final_actions = self._filter_no_actions(final_actions)
|
||||||
|
|
||||||
# === 强制后处理:确保100%概率的动作在回复时被附带 ===
|
# === 概率模式后处理:根据配置决定是否强制添加 emoji 动作 ===
|
||||||
has_reply_action = any(a.get("action_type") == "reply" for a in final_actions)
|
if global_config.emoji.emoji_activate_type == 'random':
|
||||||
if has_reply_action:
|
has_reply_action = any(a.get("action_type") == "reply" for a in final_actions)
|
||||||
for action_name, action_info in available_actions.items():
|
if has_reply_action:
|
||||||
if action_info.activation_type == ActionActivationType.RANDOM and action_info.random_activation_probability >= 1.0:
|
# 检查此动作是否已被选择
|
||||||
# 检查此动作是否已被选择
|
is_already_chosen = any(a.get("action_type") == 'emoji' for a in final_actions)
|
||||||
is_already_chosen = any(a.get("action_type") == action_name for a in final_actions)
|
if not is_already_chosen:
|
||||||
if not is_already_chosen:
|
if random.random() < global_config.emoji.emoji_chance:
|
||||||
logger.info(f"{self.log_prefix}强制添加100%概率动作: {action_name}")
|
logger.info(f"{self.log_prefix}根据概率 '{global_config.emoji.emoji_chance}' 添加 emoji 动作")
|
||||||
final_actions.append({
|
final_actions.append({
|
||||||
"action_type": action_name,
|
"action_type": 'emoji',
|
||||||
"reasoning": "根据100%概率设置强制添加",
|
"reasoning": f"根据概率 {global_config.emoji.emoji_chance} 自动添加",
|
||||||
"action_data": {},
|
"action_data": {},
|
||||||
"action_message": self.get_latest_message(used_message_id_list),
|
"action_message": self.get_latest_message(used_message_id_list),
|
||||||
"available_actions": available_actions,
|
"available_actions": available_actions,
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def init_prompt():
|
|||||||
4. 不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。
|
4. 不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。
|
||||||
最终请输出一条简短、完整且口语化的回复。
|
最终请输出一条简短、完整且口语化的回复。
|
||||||
|
|
||||||
--------------------------------
|
--------------------------------
|
||||||
{time_block}
|
{time_block}
|
||||||
|
|
||||||
{reply_target_block}
|
{reply_target_block}
|
||||||
@@ -823,6 +823,9 @@ class DefaultReplyer:
|
|||||||
sender, target = self._parse_reply_target(reply_to)
|
sender, target = self._parse_reply_target(reply_to)
|
||||||
else:
|
else:
|
||||||
# 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值
|
# 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值
|
||||||
|
if reply_message is None:
|
||||||
|
logger.warning("reply_message 为 None,无法构建prompt")
|
||||||
|
return ""
|
||||||
platform = reply_message.get("chat_info_platform")
|
platform = reply_message.get("chat_info_platform")
|
||||||
person_id = person_info_manager.get_person_id(
|
person_id = person_info_manager.get_person_id(
|
||||||
platform, # type: ignore
|
platform, # type: ignore
|
||||||
@@ -1021,6 +1024,25 @@ class DefaultReplyer:
|
|||||||
prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters)
|
prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters)
|
||||||
prompt_text = await prompt.build()
|
prompt_text = await prompt.build()
|
||||||
|
|
||||||
|
# --- 动态添加分割指令 ---
|
||||||
|
if global_config.response_splitter.enable and global_config.response_splitter.split_mode == "llm":
|
||||||
|
split_instruction = """
|
||||||
|
## 消息分段艺术
|
||||||
|
为了模仿真实人类的聊天节奏,你可以在需要时将一条回复分成几段发送。
|
||||||
|
|
||||||
|
**核心原则**: 只有当分段能**增强表达效果**或**控制信息节奏**时,才在断句处使用 `[SPLIT]` 标记。
|
||||||
|
|
||||||
|
**参考场景**:
|
||||||
|
- 当你想表达一个转折或停顿时。
|
||||||
|
- 当你想先说结论,再补充说明时。
|
||||||
|
|
||||||
|
**任务**: 请结合你的智慧和人设,自然地决定是否需要分段。如果需要,请在最恰当的位置插入 `[SPLIT]` 标记。
|
||||||
|
"""
|
||||||
|
# 在 "现在,你说:" 之前插入
|
||||||
|
parts = prompt_text.rsplit("现在,你说:", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
prompt_text = f"{parts[0]}{split_instruction}\n现在,你说:{parts[1]}"
|
||||||
|
|
||||||
return prompt_text
|
return prompt_text
|
||||||
|
|
||||||
async def build_prompt_rewrite_context(
|
async def build_prompt_rewrite_context(
|
||||||
|
|||||||
@@ -331,8 +331,23 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese
|
|||||||
)
|
)
|
||||||
|
|
||||||
if global_config.response_splitter.enable and enable_splitter:
|
if global_config.response_splitter.enable and enable_splitter:
|
||||||
split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text)
|
logger.info(f"回复分割器已启用,模式: {global_config.response_splitter.split_mode}。")
|
||||||
|
|
||||||
|
split_mode = global_config.response_splitter.split_mode
|
||||||
|
|
||||||
|
if split_mode == "llm" and "[SPLIT]" in cleaned_text:
|
||||||
|
logger.debug("检测到 [SPLIT] 标记,使用 LLM 自定义分割。")
|
||||||
|
split_sentences_raw = cleaned_text.split("[SPLIT]")
|
||||||
|
split_sentences = [s.strip() for s in split_sentences_raw if s.strip()]
|
||||||
|
else:
|
||||||
|
if split_mode == "llm":
|
||||||
|
logger.debug("未检测到 [SPLIT] 标记,本次不进行分割。")
|
||||||
|
split_sentences = [cleaned_text]
|
||||||
|
else: # mode == "punctuation"
|
||||||
|
logger.debug("使用基于标点的传统模式进行分割。")
|
||||||
|
split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text)
|
||||||
else:
|
else:
|
||||||
|
logger.debug("回复分割器已禁用。")
|
||||||
split_sentences = [cleaned_text]
|
split_sentences = [cleaned_text]
|
||||||
|
|
||||||
sentences = []
|
sentences = []
|
||||||
|
|||||||
@@ -73,8 +73,12 @@ class ChatConfig(ValidatedConfigBase):
|
|||||||
talk_frequency: float = Field(default=1.0, description="聊天频率")
|
talk_frequency: float = Field(default=1.0, description="聊天频率")
|
||||||
mentioned_bot_inevitable_reply: bool = Field(default=False, description="提到机器人的必然回复")
|
mentioned_bot_inevitable_reply: bool = Field(default=False, description="提到机器人的必然回复")
|
||||||
at_bot_inevitable_reply: bool = Field(default=False, description="@机器人的必然回复")
|
at_bot_inevitable_reply: bool = Field(default=False, description="@机器人的必然回复")
|
||||||
|
allow_reply_self: bool = Field(default=False, description="是否允许回复自己说的话")
|
||||||
talk_frequency_adjust: list[list[str]] = Field(default_factory=lambda: [], description="聊天频率调整")
|
talk_frequency_adjust: list[list[str]] = Field(default_factory=lambda: [], description="聊天频率调整")
|
||||||
focus_value: float = Field(default=1.0, description="专注值")
|
focus_value: float = Field(default=1.0, description="专注值")
|
||||||
|
focus_mode_quiet_groups: List[str] = Field(
|
||||||
|
default_factory=list, description='专注模式下需要保持安静的群组列表, 格式: ["platform:group_id1", "platform:group_id2"]'
|
||||||
|
)
|
||||||
force_reply_private: bool = Field(default=False, description="强制回复私聊")
|
force_reply_private: bool = Field(default=False, description="强制回复私聊")
|
||||||
group_chat_mode: Literal["auto", "normal", "focus"] = Field(default="auto", description="群聊模式")
|
group_chat_mode: Literal["auto", "normal", "focus"] = Field(default="auto", description="群聊模式")
|
||||||
timestamp_display_mode: Literal["normal", "normal_no_YMD", "relative"] = Field(
|
timestamp_display_mode: Literal["normal", "normal_no_YMD", "relative"] = Field(
|
||||||
@@ -381,6 +385,7 @@ class EmojiConfig(ValidatedConfigBase):
|
|||||||
content_filtration: bool = Field(default=False, description="内容过滤")
|
content_filtration: bool = Field(default=False, description="内容过滤")
|
||||||
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")
|
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")
|
||||||
enable_emotion_analysis: bool = Field(default=True, description="启用情感分析")
|
enable_emotion_analysis: bool = Field(default=True, description="启用情感分析")
|
||||||
|
max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量,0为全部")
|
||||||
|
|
||||||
|
|
||||||
class MemoryConfig(ValidatedConfigBase):
|
class MemoryConfig(ValidatedConfigBase):
|
||||||
@@ -471,6 +476,7 @@ class ResponseSplitterConfig(ValidatedConfigBase):
|
|||||||
"""回复分割器配置类"""
|
"""回复分割器配置类"""
|
||||||
|
|
||||||
enable: bool = Field(default=True, description="启用")
|
enable: bool = Field(default=True, description="启用")
|
||||||
|
split_mode: str = Field(default="llm", description="分割模式: 'llm' 或 'punctuation'")
|
||||||
max_length: int = Field(default=256, description="最大长度")
|
max_length: int = Field(default=256, description="最大长度")
|
||||||
max_sentence_num: int = Field(default=3, description="最大句子数")
|
max_sentence_num: int = Field(default=3, description="最大句子数")
|
||||||
enable_kaomoji_protection: bool = Field(default=False, description="启用颜文字保护")
|
enable_kaomoji_protection: bool = Field(default=False, description="启用颜文字保护")
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ class AtAction(BaseAction):
|
|||||||
# === 功能描述(必须填写)===
|
# === 功能描述(必须填写)===
|
||||||
action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消息"}
|
action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消息"}
|
||||||
action_require = [
|
action_require = [
|
||||||
"当需要艾特某个用户时使用",
|
"当用户明确要求你去'叫'、'喊'、'提醒'或'艾特'某人时使用",
|
||||||
"当你需要提醒特定用户查看消息时使用",
|
"当你判断,为了让特定的人看到消息,需要代表用户去呼叫他/她时使用",
|
||||||
"在回复中需要明确指向某个用户时使用",
|
"例如:'你去叫一下张三','提醒一下李四开会'",
|
||||||
]
|
]
|
||||||
llm_judge_prompt = """
|
llm_judge_prompt = """
|
||||||
判定是否需要使用艾特用户动作的条件:
|
判定是否需要使用艾特用户动作的条件:
|
||||||
@@ -70,15 +70,49 @@ class AtAction(BaseAction):
|
|||||||
if not member_list:
|
if not member_list:
|
||||||
return False, "群成员列表为空"
|
return False, "群成员列表为空"
|
||||||
|
|
||||||
# 使用模糊匹配找到最接近的用户名
|
# 优化用户匹配逻辑
|
||||||
choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list}
|
best_match = None
|
||||||
best_match, score = process.extractOne(user_name, choices.keys())
|
user_id = None
|
||||||
|
|
||||||
|
# 1. 完全精确匹配
|
||||||
|
for member in member_list:
|
||||||
|
card = member.get("card", "")
|
||||||
|
nickname = member.get("nickname", "")
|
||||||
|
if user_name == card or user_name == nickname:
|
||||||
|
best_match = card if user_name == card else nickname
|
||||||
|
user_id = member["user_id"]
|
||||||
|
logger.info(f"找到完全精确匹配: '{user_name}' -> '{best_match}' (ID: {user_id})")
|
||||||
|
break
|
||||||
|
|
||||||
if score < 30: # 设置一个匹配度阈值
|
# 2. 包含关系匹配
|
||||||
logger.info(f"找不到与 '{user_name}' 高度匹配的用户 (最佳匹配: {best_match}, 分数: {score})")
|
if not best_match:
|
||||||
return False, "用户不存在"
|
containing_matches = []
|
||||||
|
for member in member_list:
|
||||||
|
card = member.get("card", "")
|
||||||
|
nickname = member.get("nickname", "")
|
||||||
|
if user_name in card:
|
||||||
|
containing_matches.append((card, member["user_id"]))
|
||||||
|
elif user_name in nickname:
|
||||||
|
containing_matches.append((nickname, member["user_id"]))
|
||||||
|
|
||||||
user_id = choices[best_match]
|
if containing_matches:
|
||||||
|
# 选择最短的匹配项,因为通常更精确
|
||||||
|
best_match, user_id = min(containing_matches, key=lambda x: len(x[0]))
|
||||||
|
logger.info(f"找到包含关系匹配: '{user_name}' -> '{best_match}' (ID: {user_id})")
|
||||||
|
|
||||||
|
# 3. 模糊匹配作为兜底
|
||||||
|
if not best_match:
|
||||||
|
choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list}
|
||||||
|
fuzzy_match, score = process.extractOne(user_name, choices.keys())
|
||||||
|
if score >= 60: # 维持较高的阈值
|
||||||
|
best_match = fuzzy_match
|
||||||
|
user_id = choices[best_match]
|
||||||
|
logger.info(f"找到模糊匹配: '{user_name}' -> '{best_match}' (ID: {user_id}, Score: {score})")
|
||||||
|
|
||||||
|
if not best_match:
|
||||||
|
logger.warning(f"所有匹配策略都未能找到用户: '{user_name}'")
|
||||||
|
return False, "用户不存在"
|
||||||
|
|
||||||
user_info = {"user_id": user_id, "user_nickname": best_match}
|
user_info = {"user_id": user_id, "user_nickname": best_match}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -93,24 +127,39 @@ class AtAction(BaseAction):
|
|||||||
return False, "聊天流不存在"
|
return False, "聊天流不存在"
|
||||||
|
|
||||||
replyer = DefaultReplyer(chat_stream)
|
replyer = DefaultReplyer(chat_stream)
|
||||||
extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}"
|
# 优化提示词,消除记忆割裂感
|
||||||
|
reminder_task = at_message.replace("定时提醒:", "").strip()
|
||||||
|
extra_info = f"""你之前记下了一个提醒任务:'{reminder_task}'
|
||||||
|
现在时间到了,你需要去提醒用户 '{user_name}'。
|
||||||
|
|
||||||
|
**重要规则**:
|
||||||
|
- 你的任务**只**是生成提醒的**内容**。
|
||||||
|
- **绝对不要**在你的回复中包含任何`@`符号或者目标用户的名字。真正的@操作会由系统自动完成。
|
||||||
|
- 像一个朋友一样,自然地完成这个提醒,而不是生硬地复述任务。
|
||||||
|
|
||||||
|
请直接输出提醒的**内容**。"""
|
||||||
|
|
||||||
success, llm_response, _ = await replyer.generate_reply_with_context(
|
success, llm_response, _ = await replyer.generate_reply_with_context(
|
||||||
reply_to=f"{user_name}:{at_message}",
|
reply_to=f"是时候提醒'{user_name}'了", # 内部上下文,更符合执行任务的语境
|
||||||
extra_info=extra_info,
|
extra_info=extra_info,
|
||||||
enable_tool=False,
|
enable_tool=False,
|
||||||
from_plugin=False
|
from_plugin=True # 标记为插件调用,以便LLM更好地理解上下文
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success or not llm_response:
|
if not success or not llm_response:
|
||||||
logger.error("回复器生成回复失败")
|
logger.error("回复器生成回复失败")
|
||||||
return False, "回复生成失败"
|
return False, "回复生成失败"
|
||||||
|
|
||||||
final_message = llm_response.get("content", "")
|
final_message_raw = llm_response.get("content", "")
|
||||||
if not final_message:
|
if not final_message_raw:
|
||||||
logger.warning("回复器生成了空内容")
|
logger.warning("回复器生成了空内容")
|
||||||
return False, "回复内容为空"
|
return False, "回复内容为空"
|
||||||
|
|
||||||
|
# 对LLM生成的内容进行后处理,解析[SPLIT]标记并将分段消息合并
|
||||||
|
from src.chat.utils.utils import process_llm_response
|
||||||
|
final_message_segments = process_llm_response(final_message_raw, enable_splitter=True, enable_chinese_typo=False)
|
||||||
|
final_message = " ".join(final_message_segments)
|
||||||
|
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
"SEND_AT_MESSAGE",
|
"SEND_AT_MESSAGE",
|
||||||
args={"group_id": self.chat_stream.group_info.group_id, "qq_id": user_id, "text": final_message},
|
args={"group_id": self.chat_stream.group_info.group_id, "qq_id": user_id, "text": final_message},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from collections import deque
|
||||||
|
import json
|
||||||
|
|
||||||
# 导入新插件系统
|
# 导入新插件系统
|
||||||
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||||
@@ -9,7 +11,7 @@ from src.common.logger import get_logger
|
|||||||
|
|
||||||
# 导入API模块 - 标准Python包方式
|
# 导入API模块 - 标准Python包方式
|
||||||
from src.plugin_system.apis import llm_api, message_api
|
from src.plugin_system.apis import llm_api, message_api
|
||||||
from src.chat.emoji_system.emoji_manager import get_emoji_manager
|
from src.chat.emoji_system.emoji_manager import get_emoji_manager, MaiEmoji
|
||||||
from src.chat.utils.utils_image import image_path_to_base64
|
from src.chat.utils.utils_image import image_path_to_base64
|
||||||
from src.config.config import global_config
|
from src.config.config import global_config
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ logger = get_logger("emoji")
|
|||||||
class EmojiAction(BaseAction):
|
class EmojiAction(BaseAction):
|
||||||
"""表情动作 - 发送表情包"""
|
"""表情动作 - 发送表情包"""
|
||||||
|
|
||||||
|
# --- 类级别属性 ---
|
||||||
# 激活设置
|
# 激活设置
|
||||||
if global_config.emoji.emoji_activate_type == "llm":
|
if global_config.emoji.emoji_activate_type == "llm":
|
||||||
activation_type = ActionActivationType.LLM_JUDGE
|
activation_type = ActionActivationType.LLM_JUDGE
|
||||||
@@ -33,6 +36,9 @@ class EmojiAction(BaseAction):
|
|||||||
# 动作基本信息
|
# 动作基本信息
|
||||||
action_name = "emoji"
|
action_name = "emoji"
|
||||||
action_description = "发送表情包辅助表达情绪"
|
action_description = "发送表情包辅助表达情绪"
|
||||||
|
|
||||||
|
# 最近发送表情的历史记录
|
||||||
|
_sent_emoji_history = deque(maxlen=4)
|
||||||
|
|
||||||
# LLM判断提示词
|
# LLM判断提示词
|
||||||
llm_judge_prompt = """
|
llm_judge_prompt = """
|
||||||
@@ -67,104 +73,106 @@ class EmojiAction(BaseAction):
|
|||||||
reason = self.action_data.get("reason", "表达当前情绪")
|
reason = self.action_data.get("reason", "表达当前情绪")
|
||||||
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
|
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
|
||||||
|
|
||||||
# 2. 获取所有表情包
|
# 2. 获取所有有效的表情包对象
|
||||||
emoji_manager = get_emoji_manager()
|
emoji_manager = get_emoji_manager()
|
||||||
all_emojis_obj = [e for e in emoji_manager.emoji_objects if not e.is_deleted]
|
all_emojis_obj: list[MaiEmoji] = [e for e in emoji_manager.emoji_objects if not e.is_deleted and e.description]
|
||||||
if not all_emojis_obj:
|
if not all_emojis_obj:
|
||||||
logger.warning(f"{self.log_prefix} 无法获取任何表情包")
|
logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包")
|
||||||
return False, "无法获取任何表情包"
|
return False, "无法获取任何带有描述的有效表情包"
|
||||||
|
|
||||||
# 3. 准备情感数据和后备列表
|
# 3. 根据新配置项决定抽样数量
|
||||||
emotion_map = {}
|
sample_size = global_config.emoji.max_context_emojis
|
||||||
all_emojis_data = []
|
if sample_size > 0 and len(all_emojis_obj) > sample_size:
|
||||||
|
sampled_emojis = random.sample(all_emojis_obj, sample_size)
|
||||||
for emoji in all_emojis_obj:
|
|
||||||
b64 = image_path_to_base64(emoji.full_path)
|
|
||||||
if not b64:
|
|
||||||
continue
|
|
||||||
|
|
||||||
desc = emoji.description
|
|
||||||
emotions = emoji.emotion
|
|
||||||
all_emojis_data.append((b64, desc))
|
|
||||||
|
|
||||||
for emo in emotions:
|
|
||||||
if emo not in emotion_map:
|
|
||||||
emotion_map[emo] = []
|
|
||||||
emotion_map[emo].append((b64, desc))
|
|
||||||
|
|
||||||
if not all_emojis_data:
|
|
||||||
logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据")
|
|
||||||
return False, "无法加载任何有效的表情包数据"
|
|
||||||
|
|
||||||
available_emotions = list(emotion_map.keys())
|
|
||||||
emoji_base64, emoji_description = "", ""
|
|
||||||
|
|
||||||
if not available_emotions:
|
|
||||||
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
|
|
||||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
|
||||||
else:
|
else:
|
||||||
# 获取最近的5条消息内容用于判断
|
sampled_emojis = all_emojis_obj # 0表示全部
|
||||||
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
|
|
||||||
messages_text = ""
|
|
||||||
if recent_messages:
|
|
||||||
messages_text = message_api.build_readable_messages(
|
|
||||||
messages=recent_messages,
|
|
||||||
timestamp_mode="normal_no_YMD",
|
|
||||||
truncate=False,
|
|
||||||
show_actions=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. 构建prompt让LLM选择情感
|
# 4. 为抽样的表情包创建带编号的描述列表
|
||||||
prompt = f"""
|
prompt_emoji_list = []
|
||||||
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。
|
for i, emoji in enumerate(sampled_emojis):
|
||||||
这是最近的聊天记录:
|
prompt_emoji_list.append(f"{i + 1}. {emoji.description}")
|
||||||
{messages_text}
|
|
||||||
|
prompt_emoji_str = "\n".join(prompt_emoji_list)
|
||||||
这是理由:“{reason}”
|
chosen_emoji_obj: MaiEmoji = None
|
||||||
这里是可用的情感标签:{available_emotions}
|
|
||||||
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
|
|
||||||
"""
|
|
||||||
|
|
||||||
if global_config.debug.show_prompt:
|
# 5. 获取最近的5条消息内容用于判断
|
||||||
logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
|
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
|
||||||
else:
|
messages_text = ""
|
||||||
logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
|
if recent_messages:
|
||||||
|
messages_text = message_api.build_readable_messages(
|
||||||
# 5. 调用LLM
|
messages=recent_messages,
|
||||||
models = llm_api.get_available_models()
|
timestamp_mode="normal_no_YMD",
|
||||||
chat_model_config = models.get("utils_small")
|
truncate=False,
|
||||||
if not chat_model_config:
|
show_actions=False,
|
||||||
logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM")
|
|
||||||
return False, "未找到'utils_small'模型配置"
|
|
||||||
|
|
||||||
success, chosen_emotion, _, _ = await llm_api.generate_with_model(
|
|
||||||
prompt, model_config=chat_model_config, request_type="emoji"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
# 6. 构建prompt让LLM选择编号
|
||||||
logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包")
|
prompt = f"""
|
||||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号。
|
||||||
else:
|
这是最近的聊天记录:
|
||||||
chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "")
|
{messages_text}
|
||||||
logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}")
|
|
||||||
|
这是理由:“{reason}”
|
||||||
|
这里是可用的表情包详细描述列表:
|
||||||
|
{prompt_emoji_str}
|
||||||
|
请直接返回一个包含3个最匹配表情包编号的有序JSON列表,例如:[10, 2, 5],不要进行任何解释或添加其他多余的文字。
|
||||||
|
"""
|
||||||
|
|
||||||
if chosen_emotion in emotion_map:
|
# 7. 调用LLM
|
||||||
emoji_base64, emoji_description = random.choice(emotion_map[chosen_emotion])
|
models = llm_api.get_available_models()
|
||||||
logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' 的表情包: {emoji_description}")
|
chat_model_config = models.get("planner")
|
||||||
|
if not chat_model_config:
|
||||||
|
logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM")
|
||||||
|
return False, "未找到 'planner' 模型配置"
|
||||||
|
|
||||||
|
success, chosen_indices_str, _, _ = await llm_api.generate_with_model(
|
||||||
|
prompt, model_config=chat_model_config, request_type="emoji_selection"
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_emoji_obj = None
|
||||||
|
if success:
|
||||||
|
try:
|
||||||
|
chosen_indices = json.loads(chosen_indices_str)
|
||||||
|
if isinstance(chosen_indices, list):
|
||||||
|
logger.info(f"{self.log_prefix} LLM选择的表情编号候选项: {chosen_indices}")
|
||||||
|
for index in chosen_indices:
|
||||||
|
if isinstance(index, int) and 1 <= index <= len(sampled_emojis):
|
||||||
|
candidate_emoji = sampled_emojis[index - 1]
|
||||||
|
if candidate_emoji.hash not in self._sent_emoji_history:
|
||||||
|
selected_emoji_obj = candidate_emoji
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}")
|
||||||
f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包"
|
except (json.JSONDecodeError, TypeError):
|
||||||
)
|
logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}")
|
||||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
|
||||||
|
|
||||||
# 7. 发送表情包
|
if selected_emoji_obj:
|
||||||
success = await self.send_emoji(emoji_base64)
|
chosen_emoji_obj = selected_emoji_obj
|
||||||
|
logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}")
|
||||||
|
else:
|
||||||
|
if not success:
|
||||||
|
logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包")
|
||||||
|
else:
|
||||||
|
logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择")
|
||||||
|
|
||||||
|
selectable_emojis = [e for e in all_emojis_obj if e.hash not in self._sent_emoji_history]
|
||||||
|
if not selectable_emojis:
|
||||||
|
selectable_emojis = all_emojis_obj
|
||||||
|
chosen_emoji_obj = random.choice(selectable_emojis)
|
||||||
|
|
||||||
if not success:
|
# 8. 发送表情包并更新历史记录
|
||||||
logger.error(f"{self.log_prefix} 表情包发送失败")
|
if chosen_emoji_obj:
|
||||||
return False, "表情包发送失败"
|
emoji_base64 = image_path_to_base64(chosen_emoji_obj.full_path)
|
||||||
|
if emoji_base64:
|
||||||
|
send_success = await self.send_emoji(emoji_base64)
|
||||||
|
if send_success:
|
||||||
|
self._sent_emoji_history.append(chosen_emoji_obj.hash)
|
||||||
|
logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_obj.description}")
|
||||||
|
logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}")
|
||||||
|
return True, f"发送表情包: {chosen_emoji_obj.description}"
|
||||||
|
|
||||||
return True, f"发送表情包: {emoji_description}"
|
logger.error(f"{self.log_prefix} 表情包发送失败")
|
||||||
|
return False, "表情包发送失败"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True)
|
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[inner]
|
[inner]
|
||||||
version = "6.8.0"
|
version = "6.8.4"
|
||||||
|
|
||||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||||
#如果你想要修改配置文件,请递增version的值
|
#如果你想要修改配置文件,请递增version的值
|
||||||
@@ -111,12 +111,20 @@ talk_frequency = 1
|
|||||||
focus_value = 1
|
focus_value = 1
|
||||||
# MoFox-Bot的专注思考能力,越高越容易持续连续对话
|
# MoFox-Bot的专注思考能力,越高越容易持续连续对话
|
||||||
|
|
||||||
|
# 在专注模式下,只在被艾特或提及时才回复的群组列表
|
||||||
|
# 这可以让你在某些群里保持“高冷”,只在被需要时才发言
|
||||||
|
# 格式为: ["platform:group_id1", "platform:group_id2"]
|
||||||
|
# 例如: ["qq:123456789", "qq:987654321"]
|
||||||
|
focus_mode_quiet_groups = []
|
||||||
|
|
||||||
# breaking模式配置
|
# breaking模式配置
|
||||||
enable_breaking_mode = true # 是否启用自动进入breaking模式,关闭后不会自动进入breaking形式
|
enable_breaking_mode = true # 是否启用自动进入breaking模式,关闭后不会自动进入breaking形式
|
||||||
|
|
||||||
# 强制私聊回复
|
# 强制私聊回复
|
||||||
force_reply_private = false # 是否强制私聊回复,开启后私聊将强制回复
|
force_reply_private = false # 是否强制私聊回复,开启后私聊将强制回复
|
||||||
|
|
||||||
|
allow_reply_self = false # 是否允许回复自己说的话
|
||||||
|
|
||||||
max_context_size = 25 # 上下文长度
|
max_context_size = 25 # 上下文长度
|
||||||
thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)
|
thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)
|
||||||
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
|
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
|
||||||
@@ -238,6 +246,7 @@ steal_emoji = true # 是否偷取表情包,让MoFox-Bot可以将一些表情
|
|||||||
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
|
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
|
||||||
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
|
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
|
||||||
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
|
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
|
||||||
|
max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量,0为全部
|
||||||
|
|
||||||
[memory]
|
[memory]
|
||||||
enable_memory = true # 是否启用记忆系统
|
enable_memory = true # 是否启用记忆系统
|
||||||
@@ -313,6 +322,7 @@ word_replace_rate=0.006 # 整词替换概率
|
|||||||
|
|
||||||
[response_splitter]
|
[response_splitter]
|
||||||
enable = true # 是否启用回复分割器
|
enable = true # 是否启用回复分割器
|
||||||
|
split_mode = "punctuation" # 分割模式: "llm" - 由语言模型决定, "punctuation" - 基于标点符号
|
||||||
max_length = 512 # 回复允许的最大长度
|
max_length = 512 # 回复允许的最大长度
|
||||||
max_sentence_num = 8 # 回复允许的最大句子数
|
max_sentence_num = 8 # 回复允许的最大句子数
|
||||||
enable_kaomoji_protection = false # 是否启用颜文字保护
|
enable_kaomoji_protection = false # 是否启用颜文字保护
|
||||||
|
|||||||
Reference in New Issue
Block a user