mirror of https://github.com/Mai-with-u/MaiBot.git
266 lines
13 KiB
Python
266 lines
13 KiB
Python
# PFC/pfc_heartflow.py
|
||
import traceback
|
||
import time
|
||
from typing import Optional, Dict, Any
|
||
from src.individuality.individuality import Individuality
|
||
from src.common.logger import get_module_logger
|
||
from src.common.utils_llm import LLMRequest # Assuming utils_model is renamed or refactored
|
||
from ...config.config import global_config # Make sure path is correct
|
||
from .observation_info import ObservationInfo
|
||
from .conversation_info import ConversationInfo
|
||
from maim_message import UserInfo # Assuming maim_message provides UserInfo
|
||
from src.plugins.utils.chat_message_builder import build_readable_messages # Make sure path is correct
|
||
|
||
logger = get_module_logger("pfc_heartflow")
|
||
|
||
# --- Heartflow Generation Prompts ---
|
||
|
||
# Prompt (1): Initial Heartflow Generation (When conversation starts or bot receives first message)
|
||
PROMPT_INITIAL = """{persona_text}
|
||
现在你正在上网,{sender_name} ({sender_id}) 刚刚给你发来了一条QQ私聊消息。
|
||
|
||
【最近的聊天记录】(包括你已成功发送的消息 和 新收到的消息)
|
||
{chat_history_text}
|
||
--- 消息结束 ---
|
||
|
||
请根据以上信息,结合你的性格,设想一下你收到这条消息时的第一反应和真实想法。
|
||
要求:
|
||
- 输出一段连贯、自然、真实的内心独白。
|
||
- 像一个真人在思考,语气可以随意一些,符合你的性格。
|
||
- 不要分点输出,不要包含任何动作描写、表情符号、引号或括号。
|
||
- **只输出内心独白内容,不要任何额外文字或格式。**
|
||
"""
|
||
|
||
# Prompt (2): Heartflow Update After Successfully Sending a Message
|
||
PROMPT_AFTER_SEND = """{persona_text}
|
||
你正在和 {sender_name} ({sender_id}) QQ私聊。
|
||
你刚才的想法是:『{previous_heartflow}』
|
||
刚刚你成功发送了一条消息给对方:"{last_bot_message}"
|
||
|
||
【你们最近的聊天记录】(包括你刚发送的消息)
|
||
{chat_history_text}
|
||
--- 消息结束 ---
|
||
|
||
现在,请结合你刚才的想法、你发送的消息以及聊天记录,继续思考。
|
||
- 你的想法可以是对刚才发送内容的补充、延伸,或者思考对方可能的反应,或者计划下一步说什么。
|
||
- 保持想法的连贯性,但也要注意话题的推进,不要停留在完全相同的想法上,除非你觉得有必要强调。
|
||
- 输出一段连贯、自然、真实的内心独白。
|
||
- 像一个真人在思考,语气可以随意一些,符合你的性格。
|
||
- 不要分点输出,不要包含任何动作描写、表情符号、引号或括号。
|
||
- **只输出内心独白内容,不要任何额外文字或格式。**
|
||
"""
|
||
|
||
# Prompt (3): Heartflow Update After Reply Check Failed (e.g., message rejected)
|
||
PROMPT_AFTER_FAIL = """{persona_text}
|
||
你正在和 {sender_name} ({sender_id}) QQ私聊。
|
||
你之前的想法是:『{previous_heartflow}』
|
||
你本来想发送一条消息:"{failed_message}"
|
||
但是,这个想法/回复因为『{fail_reason}』被你自己否定了/觉得不合适。
|
||
|
||
【你们最近的聊天记录】
|
||
{chat_history_text}
|
||
--- 消息结束 ---
|
||
|
||
现在,请结合你之前的想法、被否定的消息以及失败原因,重新思考。
|
||
- 你可能会反思为什么刚才的想法不合适,或者思考替代的说法,或者决定暂时不回复。
|
||
- 保持想法的连贯性,但要根据失败原因调整思路。
|
||
- 输出一段连贯、自然、真实的内心独白。
|
||
- 像一个真人在思考,语气可以随意一些,符合你的性格。
|
||
- 不要分点输出,不要包含任何动作描写、表情符号、引号或括号。
|
||
- **只输出内心独白内容,不要任何额外文字或格式。**
|
||
"""
|
||
|
||
# Prompt (4): Heartflow Update After Waiting Timeout
|
||
PROMPT_AFTER_TIMEOUT = """{persona_text}
|
||
你正在和 {sender_name} ({sender_id}) QQ私聊。
|
||
你之前的想法是:『{previous_heartflow}』
|
||
你上次发言是 {time_since_last_bot_speak:.1f} 秒前。
|
||
你已经等待了对方 {wait_duration:.1f} 分钟没有回应。 ({timeout_reason})
|
||
|
||
【你们最近的聊天记录】
|
||
{chat_history_text}
|
||
--- 消息结束 ---
|
||
|
||
对方长时间没有回复,你现在的想法是什么?
|
||
- 你可能会思考对方为什么没回,是在忙吗?还是对话结束了?
|
||
- 你可能会考虑是否要发点什么打破沉默,或者就此结束对话。
|
||
- 结合你之前的想法和等待的情况进行思考。
|
||
- 输出一段连贯、自然、真实的内心独白。
|
||
- 像一个真人在思考,语气可以随意一些,符合你的性格。
|
||
- 不要分点输出,不要包含任何动作描写、表情符号、引号或括号。
|
||
- **只输出内心独白内容,不要任何额外文字或格式。**
|
||
"""
|
||
|
||
# Prompt (5): Heartflow Update When Rethinking Goal
|
||
PROMPT_WHEN_RETHINKING = """{persona_text}
|
||
你正在和 {sender_name} ({sender_id}) QQ私聊。
|
||
你之前的想法是:『{previous_heartflow}』
|
||
你觉得现在需要重新思考一下对话的目标或方向了。({rethink_reason})
|
||
|
||
【你们最近的聊天记录】
|
||
{chat_history_text}
|
||
--- 消息结束 ---
|
||
|
||
请结合你之前的想法和需要重新思考目标的原因,梳理一下你现在的思路。
|
||
- 你可能会回顾一下之前的对话,思考当前进展如何。
|
||
- 你可能会考虑开启新的话题,或者如何引导对话到你期望的方向。
|
||
- 输出一段连贯、自然、真实的内心独白。
|
||
- 像一个真人在思考,语气可以随意一些,符合你的性格。
|
||
- 不要分点输出,不要包含任何动作描写、表情符号、引号或括号。
|
||
- **只输出内心独白内容,不要任何额外文字或格式。**
|
||
"""
|
||
|
||
|
||
class HeartflowGenerator:
|
||
"""心流生成器"""
|
||
|
||
def __init__(self, stream_id: str):
|
||
self.stream_id = stream_id
|
||
# 这里假设你在 config.py 中定义了名为 'llm_PFC_heartflow' 的新 LLM 配置
|
||
# 需要确保 global_config 中存在 llm_PFC_heartflow 这个键
|
||
try:
|
||
self.llm = LLMRequest(
|
||
model=global_config.llm_PFC_heartflow, # 使用新的配置
|
||
temperature=global_config.llm_PFC_heartflow.get("temp", 0.8), # 假设温度设置,可调整
|
||
max_tokens=global_config.llm_PFC_heartflow.get("max_tokens", 200), # 限制心流长度,可调整
|
||
request_type="heartflow_generation",
|
||
)
|
||
except AttributeError:
|
||
logger.error("*"*20)
|
||
logger.error("错误:无法找到 'llm_PFC_heartflow' 配置!")
|
||
logger.error("请确保在 config.py 中定义了 llm_PFC_heartflow 的 LLM 配置。")
|
||
logger.error("将使用 llm_normal 作为备用,但这可能不是最佳效果。")
|
||
logger.error("*"*20)
|
||
# 使用备用配置,但这可能不是最优选择
|
||
self.llm = LLMRequest(
|
||
model=global_config.llm_normal,
|
||
temperature=0.8,
|
||
max_tokens=200,
|
||
request_type="heartflow_generation_fallback",
|
||
)
|
||
|
||
self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3)
|
||
self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
|
||
self.name = global_config.BOT_NICKNAME
|
||
self.bot_id = str(global_config.BOT_QQ)
|
||
|
||
|
||
async def generate_heartflow(
|
||
self,
|
||
situation: str, # e.g., "initial", "after_send", "after_fail", "after_timeout", "rethinking"
|
||
observation_info: ObservationInfo,
|
||
conversation_info: ConversationInfo,
|
||
context_data: Optional[Dict[str, Any]] = None # To pass specific data like failed_message, reason, etc.
|
||
) -> str:
|
||
"""根据不同情境生成心流
|
||
|
||
Args:
|
||
situation: 当前情境标识符
|
||
observation_info: 观察信息
|
||
conversation_info: 对话信息
|
||
context_data: 传递特定情境所需的数据 (e.g., {'last_bot_message': '...', 'previous_heartflow': '...'})
|
||
|
||
Returns:
|
||
str: 生成的心流文本
|
||
"""
|
||
if context_data is None:
|
||
context_data = {}
|
||
|
||
logger.info(f"开始生成心流,情境: {situation}")
|
||
|
||
# --- 构建通用 Prompt 参数 ---
|
||
# Persona Text (Character info)
|
||
identity_details_only = self.identity_detail_info
|
||
identity_addon = ""
|
||
if isinstance(identity_details_only, str):
|
||
pronouns = ["你", "我", "他"]
|
||
for p in pronouns:
|
||
if identity_details_only.startswith(p):
|
||
identity_details_only = identity_details_only[len(p) :]
|
||
break
|
||
if identity_details_only.endswith("。"):
|
||
identity_details_only = identity_details_only[:-1]
|
||
cleaned_details = identity_details_only.strip(",, ")
|
||
if cleaned_details:
|
||
identity_addon = f"并且{cleaned_details}"
|
||
persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。"
|
||
|
||
# Chat History
|
||
chat_history_text = "还没有聊天记录。"
|
||
sender_name = "对方"
|
||
sender_id = "未知"
|
||
if hasattr(observation_info, 'chat_history_str') and observation_info.chat_history_str:
|
||
chat_history_text = observation_info.chat_history_str
|
||
# Try to get sender info from the last message if available
|
||
last_msg = observation_info.chat_history[-1] if observation_info.chat_history else None
|
||
if isinstance(last_msg, dict):
|
||
user_info_dict = last_msg.get('user_info')
|
||
if isinstance(user_info_dict, dict):
|
||
user_info = UserInfo.from_dict(user_info_dict)
|
||
# Get info of the other person, not the bot itself
|
||
if str(user_info.user_id) != self.bot_id:
|
||
sender_name = user_info.user_nickname or "对方"
|
||
sender_id = str(user_info.user_id)
|
||
|
||
# Previous Heartflow (handle None case)
|
||
previous_heartflow = context_data.get('previous_heartflow', conversation_info.current_heartflow if hasattr(conversation_info, 'current_heartflow') else '')
|
||
if not previous_heartflow:
|
||
previous_heartflow = "你之前还没来得及形成具体的想法。" # Default if no previous thought
|
||
|
||
# --- 选择并格式化 Prompt ---
|
||
prompt_template = None
|
||
format_params = {
|
||
"persona_text": persona_text,
|
||
"sender_name": sender_name,
|
||
"sender_id": sender_id,
|
||
"chat_history_text": chat_history_text,
|
||
"previous_heartflow": previous_heartflow,
|
||
"bot_name": self.name,
|
||
# Add more common params if needed
|
||
}
|
||
|
||
if situation == "initial":
|
||
prompt_template = PROMPT_INITIAL
|
||
# 'initial' specific params (if any, likely none needed beyond common ones)
|
||
elif situation == "after_send":
|
||
prompt_template = PROMPT_AFTER_SEND
|
||
format_params["last_bot_message"] = context_data.get("last_bot_message", "(未能获取到刚发送的消息)")
|
||
elif situation == "after_fail":
|
||
prompt_template = PROMPT_AFTER_FAIL
|
||
format_params["failed_message"] = context_data.get("failed_message", "(未能获取到失败的消息)")
|
||
format_params["fail_reason"] = context_data.get("fail_reason", "(未知原因)")
|
||
elif situation == "after_timeout":
|
||
prompt_template = PROMPT_AFTER_TIMEOUT
|
||
# Calculate times - requires observation_info to be up-to-date
|
||
now = time.time()
|
||
time_since_last_bot_speak = (now - observation_info.last_bot_speak_time) if observation_info.last_bot_speak_time else float('inf')
|
||
wait_duration_minutes = context_data.get("wait_duration", 0) / 60.0 # Expect duration in seconds from context_data
|
||
format_params["time_since_last_bot_speak"] = time_since_last_bot_speak
|
||
format_params["wait_duration"] = wait_duration_minutes
|
||
format_params["timeout_reason"] = context_data.get("timeout_reason", "长时间未回应")
|
||
elif situation == "rethinking":
|
||
prompt_template = PROMPT_WHEN_RETHINKING
|
||
format_params["rethink_reason"] = context_data.get("rethink_reason", "需要调整对话方向")
|
||
else:
|
||
logger.warning(f"未知的的心流生成情境: {situation},将使用 'initial' 作为默认。")
|
||
prompt_template = PROMPT_INITIAL
|
||
|
||
# --- 调用 LLM 生成 ---
|
||
try:
|
||
final_prompt = prompt_template.format(**format_params)
|
||
logger.debug(f"发送到LLM的心流生成提示词 (情境: {situation}):\n------\n{final_prompt}\n------")
|
||
|
||
heartflow_content, _ = await self.llm.generate_response_async(final_prompt)
|
||
# Clean up potential unwanted prefixes/suffixes if LLM adds them
|
||
heartflow_content = heartflow_content.strip().strip('\"').strip('\'').strip("内心独白:").strip()
|
||
logger.info(f"生成的心流 (情境: {situation}): 『{heartflow_content}』")
|
||
return heartflow_content
|
||
|
||
except KeyError as e:
|
||
logger.error(f"格式化心流 Prompt 时缺少键: {e}")
|
||
logger.error(f"可用参数: {format_params.keys()}")
|
||
return f"(生成内心想法时出错:缺少参数 {e})"
|
||
except Exception as e:
|
||
logger.error(f"生成心流时出错 (情境: {situation}): {e}")
|
||
logger.error(traceback.format_exc())
|
||
return "(生成内心想法时遇到错误)" |