mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'PFC-test' of https://github.com/smartmita/MaiBot into G-Test
commit
6ede63cae3
|
|
@ -300,3 +300,5 @@ $RECYCLE.BIN/
|
|||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
21
bot.py
21
bot.py
|
|
@ -35,6 +35,23 @@ driver = None
|
|||
app = None
|
||||
loop = None
|
||||
|
||||
# shutdown_requested = False # 新增全局变量
|
||||
|
||||
|
||||
async def request_shutdown() -> bool:
|
||||
"""请求关闭程序"""
|
||||
try:
|
||||
if loop and not loop.is_closed():
|
||||
try:
|
||||
loop.run_until_complete(graceful_shutdown())
|
||||
except Exception as ge: # 捕捉优雅关闭时可能发生的错误
|
||||
logger.error(f"优雅关闭时发生错误: {ge}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"请求关闭程序时发生错误: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def easter_egg():
|
||||
# 彩蛋
|
||||
|
|
@ -267,6 +284,8 @@ if __name__ == "__main__":
|
|||
loop.run_until_complete(graceful_shutdown())
|
||||
except Exception as ge: # 捕捉优雅关闭时可能发生的错误
|
||||
logger.error(f"优雅关闭时发生错误: {ge}")
|
||||
# 新增:检测外部请求关闭
|
||||
|
||||
# except Exception as e: # 将主异常捕获移到外层 try...except
|
||||
# logger.error(f"事件循环内发生错误: {str(e)} {str(traceback.format_exc())}")
|
||||
# exit_code = 1
|
||||
|
|
@ -286,5 +305,5 @@ if __name__ == "__main__":
|
|||
loop.close()
|
||||
logger.info("事件循环已关闭")
|
||||
# 在程序退出前暂停,让你有机会看到输出
|
||||
input("按 Enter 键退出...") # <--- 添加这行
|
||||
# input("按 Enter 键退出...") # <--- 添加这行
|
||||
sys.exit(exit_code) # <--- 使用记录的退出码
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
from src.heart_flow.heartflow import heartflow
|
||||
from src.heart_flow.sub_heartflow import ChatState
|
||||
from src.common.logger_manager import get_logger
|
||||
|
||||
logger = get_logger("api")
|
||||
|
||||
|
||||
async def get_all_subheartflow_ids() -> list:
|
||||
|
|
@ -14,3 +17,21 @@ async def forced_change_subheartflow_status(subheartflow_id: str, status: ChatSt
|
|||
if subheartflow:
|
||||
return await heartflow.force_change_subheartflow_status(subheartflow_id, status)
|
||||
return False
|
||||
|
||||
|
||||
async def get_subheartflow_cycle_info(subheartflow_id: str, history_len: int) -> dict:
|
||||
"""获取子心流的循环信息"""
|
||||
subheartflow_cycle_info = await heartflow.api_get_subheartflow_cycle_info(subheartflow_id, history_len)
|
||||
logger.debug(f"子心流 {subheartflow_id} 循环信息: {subheartflow_cycle_info}")
|
||||
if subheartflow_cycle_info:
|
||||
return subheartflow_cycle_info
|
||||
else:
|
||||
logger.warning(f"子心流 {subheartflow_id} 循环信息未找到")
|
||||
return None
|
||||
|
||||
|
||||
async def get_all_states():
|
||||
"""获取所有状态"""
|
||||
all_states = await heartflow.api_get_all_states()
|
||||
logger.debug(f"所有状态: {all_states}")
|
||||
return all_states
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
from fastapi import APIRouter
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
import os
|
||||
import sys
|
||||
|
||||
# from src.heart_flow.heartflow import heartflow
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
# from src.config.config import BotConfig
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.api.reload_config import reload_config as reload_config_func
|
||||
from src.common.server import global_server
|
||||
from .apiforgui import get_all_subheartflow_ids, forced_change_subheartflow_status
|
||||
from src.api.apiforgui import (
|
||||
get_all_subheartflow_ids,
|
||||
forced_change_subheartflow_status,
|
||||
get_subheartflow_cycle_info,
|
||||
get_all_states,
|
||||
)
|
||||
from src.heart_flow.sub_heartflow import ChatState
|
||||
|
||||
# import uvicorn
|
||||
# import os
|
||||
|
||||
|
|
@ -51,6 +61,42 @@ async def forced_change_subheartflow_status_api(subheartflow_id: str, status: Ch
|
|||
return {"status": "failed"}
|
||||
|
||||
|
||||
@router.get("/stop")
|
||||
async def force_stop_maibot():
|
||||
"""强制停止MAI Bot"""
|
||||
from bot import request_shutdown
|
||||
|
||||
success = await request_shutdown()
|
||||
if success:
|
||||
logger.info("MAI Bot已强制停止")
|
||||
return {"status": "success"}
|
||||
else:
|
||||
logger.error("MAI Bot强制停止失败")
|
||||
return {"status": "failed"}
|
||||
|
||||
|
||||
@router.get("/gui/subheartflow/cycleinfo")
|
||||
async def get_subheartflow_cycle_info_api(subheartflow_id: str, history_len: int):
|
||||
"""获取子心流的循环信息"""
|
||||
cycle_info = await get_subheartflow_cycle_info(subheartflow_id, history_len)
|
||||
if cycle_info:
|
||||
return {"status": "success", "data": cycle_info}
|
||||
else:
|
||||
logger.warning(f"子心流 {subheartflow_id} 循环信息未找到")
|
||||
return {"status": "failed", "reason": "subheartflow not found"}
|
||||
|
||||
|
||||
@router.get("/gui/get_all_states")
|
||||
async def get_all_states_api():
|
||||
"""获取所有状态"""
|
||||
all_states = await get_all_states()
|
||||
if all_states:
|
||||
return {"status": "success", "data": all_states}
|
||||
else:
|
||||
logger.warning("获取所有状态失败")
|
||||
return {"status": "failed", "reason": "failed to get all states"}
|
||||
|
||||
|
||||
def start_api_server():
|
||||
"""启动API服务器"""
|
||||
global_server.register_router(router, prefix="/api/v1")
|
||||
|
|
|
|||
|
|
@ -6,56 +6,6 @@ from types import ModuleType
|
|||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
日志颜色说明:
|
||||
|
||||
1. 主程序(Main)
|
||||
浅黄色标题 | 浅黄色消息
|
||||
|
||||
2. 海马体(Memory)
|
||||
浅黄色标题 | 浅黄色消息
|
||||
|
||||
3. PFC(前额叶皮质)
|
||||
浅绿色标题 | 浅绿色消息
|
||||
|
||||
4. 心情(Mood)
|
||||
品红色标题 | 品红色消息
|
||||
|
||||
5. 工具使用(Tool)
|
||||
品红色标题 | 品红色消息
|
||||
|
||||
6. 关系(Relation)
|
||||
浅品红色标题 | 浅品红色消息
|
||||
|
||||
7. 配置(Config)
|
||||
浅青色标题 | 浅青色消息
|
||||
|
||||
8. 麦麦大脑袋
|
||||
浅绿色标题 | 浅绿色消息
|
||||
|
||||
9. 在干嘛
|
||||
青色标题 | 青色消息
|
||||
|
||||
10. 麦麦组织语言
|
||||
浅绿色标题 | 浅绿色消息
|
||||
|
||||
11. 见闻(Chat)
|
||||
浅蓝色标题 | 绿色消息
|
||||
|
||||
12. 表情包(Emoji)
|
||||
橙色标题 | 橙色消息 fg #FFD700
|
||||
|
||||
13. 子心流
|
||||
|
||||
13. 其他模块
|
||||
模块名标题 | 对应颜色消息
|
||||
|
||||
|
||||
注意:
|
||||
1. 级别颜色遵循loguru默认配置
|
||||
2. 可通过环境变量修改日志级别
|
||||
"""
|
||||
|
||||
|
||||
# 加载 .env 文件
|
||||
env_path = Path(__file__).resolve().parent.parent.parent / ".env"
|
||||
|
|
@ -80,7 +30,8 @@ _custom_style_handlers: dict[Tuple[str, str], List[int]] = {} # 记录自定义
|
|||
|
||||
# 获取日志存储根地址
|
||||
current_file_path = Path(__file__).resolve()
|
||||
LOG_ROOT = "logs"
|
||||
ROOT_PATH = os.path.abspath(os.path.join(current_file_path, "..", ".."))
|
||||
LOG_ROOT = str(ROOT_PATH) + "/" + "logs"
|
||||
|
||||
SIMPLE_OUTPUT = os.getenv("SIMPLE_OUTPUT", "false").strip().lower()
|
||||
if SIMPLE_OUTPUT == "true":
|
||||
|
|
|
|||
|
|
@ -277,6 +277,13 @@ class BotConfig:
|
|||
# enable_think_flow: bool = False # 是否启用思考流程
|
||||
talk_allowed_private = set()
|
||||
enable_pfc_chatting: bool = False # 是否启用PFC聊天
|
||||
enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查
|
||||
|
||||
# idle_conversation
|
||||
enable_idle_conversation: bool = False # 是否启用 pfc 主动发言
|
||||
idle_check_interval: int = 10 # 检查间隔,10分钟检查一次
|
||||
min_idle_time: int = 7200 # 最短无活动时间,2小时 (7200秒)
|
||||
max_idle_time: int = 18000 # 最长无活动时间,5小时 (18000秒)
|
||||
api_polling_max_retries: int = 3 # 神秘小功能
|
||||
|
||||
# Group Nickname
|
||||
|
|
@ -528,7 +535,7 @@ class BotConfig:
|
|||
"llm_heartflow",
|
||||
"llm_PFC_action_planner",
|
||||
"llm_PFC_chat",
|
||||
"llm_PFC_reply_checker",
|
||||
"llm_PFC_relationship_eval",
|
||||
"llm_nickname_mapping",
|
||||
"llm_scheduler_all",
|
||||
"llm_scheduler_doing",
|
||||
|
|
@ -709,6 +716,11 @@ class BotConfig:
|
|||
config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting)
|
||||
if config.INNER_VERSION in SpecifierSet(">=1.6.1.5"):
|
||||
config.api_polling_max_retries = experimental_config.get("api_polling_max_retries", config.api_polling_max_retries)
|
||||
if config.INNER_VERSION in SpecifierSet(">=1.6.2"):
|
||||
config.enable_pfc_reply_checker = experimental_config.get(
|
||||
"enable_pfc_reply_checker", config.enable_pfc_reply_checker
|
||||
)
|
||||
logger.info(f"PFC Reply Checker 状态: {'启用' if config.enable_pfc_reply_checker else '关闭'}")
|
||||
|
||||
def idle_conversation(parent: dict):
|
||||
idle_conversation_config = parent["idle_conversation"]
|
||||
|
|
|
|||
|
|
@ -67,6 +67,23 @@ class Heartflow:
|
|||
# 这里的 message 是可选的,可能是一个消息对象,也可能是其他类型的数据
|
||||
return await self.subheartflow_manager.force_change_state(subheartflow_id, status)
|
||||
|
||||
async def api_get_all_states(self):
|
||||
"""获取所有状态"""
|
||||
return await self.interest_logger.api_get_all_states()
|
||||
|
||||
async def api_get_subheartflow_cycle_info(self, subheartflow_id: str, history_len: int) -> Optional[dict]:
|
||||
"""获取子心流的循环信息"""
|
||||
subheartflow = await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
|
||||
if not subheartflow:
|
||||
logger.warning(f"尝试获取不存在的子心流 {subheartflow_id} 的周期信息")
|
||||
return None
|
||||
heartfc_instance = subheartflow.heart_fc_instance
|
||||
if not heartfc_instance:
|
||||
logger.warning(f"子心流 {subheartflow_id} 没有心流实例,无法获取周期信息")
|
||||
return None
|
||||
|
||||
return heartfc_instance.get_cycle_history(last_n=history_len)
|
||||
|
||||
async def heartflow_start_working(self):
|
||||
"""启动后台任务"""
|
||||
await self.background_task_manager.start_tasks()
|
||||
|
|
|
|||
|
|
@ -158,3 +158,55 @@ class InterestLogger:
|
|||
except Exception as e:
|
||||
logger.error(f"记录状态时发生意外错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def api_get_all_states(self):
|
||||
"""获取主心流和所有子心流的状态。"""
|
||||
try:
|
||||
current_timestamp = time.time()
|
||||
|
||||
# main_mind = self.heartflow.current_mind
|
||||
# 获取 Mai 状态名称
|
||||
mai_state_name = self.heartflow.current_state.get_current_state().name
|
||||
|
||||
all_subflow_states = await self.get_all_subflow_states()
|
||||
|
||||
log_entry_base = {
|
||||
"timestamp": round(current_timestamp, 2),
|
||||
# "main_mind": main_mind,
|
||||
"mai_state": mai_state_name,
|
||||
"subflow_count": len(all_subflow_states),
|
||||
"subflows": [],
|
||||
}
|
||||
|
||||
subflow_details = []
|
||||
items_snapshot = list(all_subflow_states.items())
|
||||
for stream_id, state in items_snapshot:
|
||||
group_name = stream_id
|
||||
try:
|
||||
chat_stream = chat_manager.get_stream(stream_id)
|
||||
if chat_stream:
|
||||
if chat_stream.group_info:
|
||||
group_name = chat_stream.group_info.group_name
|
||||
elif chat_stream.user_info:
|
||||
group_name = f"私聊_{chat_stream.user_info.user_nickname}"
|
||||
except Exception as e:
|
||||
logger.trace(f"无法获取 stream_id {stream_id} 的群组名: {e}")
|
||||
|
||||
interest_state = state.get("interest_state", {})
|
||||
|
||||
subflow_entry = {
|
||||
"stream_id": stream_id,
|
||||
"group_name": group_name,
|
||||
"sub_mind": state.get("current_mind", "未知"),
|
||||
"sub_chat_state": state.get("chat_state", "未知"),
|
||||
"interest_level": interest_state.get("interest_level", 0.0),
|
||||
"start_hfc_probability": interest_state.get("start_hfc_probability", 0.0),
|
||||
# "is_above_threshold": interest_state.get("is_above_threshold", False),
|
||||
}
|
||||
subflow_details.append(subflow_entry)
|
||||
|
||||
log_entry_base["subflows"] = subflow_details
|
||||
return subflow_details
|
||||
except Exception as e:
|
||||
logger.error(f"记录状态时发生意外错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
PFC_idle 包 - 用于空闲时主动聊天的功能模块
|
||||
|
||||
该包包含以下主要组件:
|
||||
- IdleChat: 根据关系和活跃度进行智能主动聊天
|
||||
- IdleChatManager: 管理多个聊天实例的空闲状态
|
||||
- IdleConversation: 处理与空闲聊天相关的功能,与主Conversation类解耦
|
||||
"""
|
||||
|
||||
from .idle_chat import IdleChat
|
||||
from .idle_chat_manager import IdleChatManager
|
||||
from .idle_conversation import IdleConversation, get_idle_conversation_instance, initialize_idle_conversation
|
||||
|
||||
__all__ = [
|
||||
"IdleChat",
|
||||
"IdleChatManager",
|
||||
"IdleConversation",
|
||||
"get_idle_conversation_instance",
|
||||
"initialize_idle_conversation",
|
||||
]
|
||||
|
|
@ -0,0 +1,552 @@
|
|||
from typing import Optional, Dict, Set
|
||||
import asyncio
|
||||
import time
|
||||
import random
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugins.models.utils_model import LLMRequest
|
||||
from src.plugins.utils.prompt_builder import global_prompt_manager
|
||||
from src.plugins.person_info.person_info import person_info_manager
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from ...schedule.schedule_generator import bot_schedule
|
||||
from ..chat_observer import ChatObserver
|
||||
from ..message_sender import DirectMessageSender
|
||||
from src.plugins.chat.chat_stream import ChatStream
|
||||
from maim_message import UserInfo
|
||||
from ..pfc_relationship import PfcRepationshipTranslator
|
||||
from rich.traceback import install
|
||||
|
||||
install(extra_lines=3)
|
||||
|
||||
logger = get_logger("pfc_idle_chat")
|
||||
|
||||
|
||||
class IdleChat:
|
||||
"""主动聊天组件(测试中)
|
||||
|
||||
在以下条件都满足时触发主动聊天:
|
||||
1. 当前没有任何活跃的对话实例
|
||||
2. 在指定的活动时间内(7:00-23:00)
|
||||
3. 根据关系值动态调整触发概率
|
||||
4. 上次触发后已经过了足够的冷却时间
|
||||
"""
|
||||
|
||||
# 单例模式实现
|
||||
_instances: Dict[str, "IdleChat"] = {}
|
||||
|
||||
# 全局共享状态,用于跟踪未回复的用户
|
||||
_pending_replies: Dict[str, float] = {} # 用户名 -> 发送时间
|
||||
_tried_users: Set[str] = set() # 已尝试过的用户集合
|
||||
_global_lock = asyncio.Lock() # 保护共享状态的全局锁
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, stream_id: str, private_name: str) -> "IdleChat":
|
||||
"""获取IdleChat实例(单例模式)
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊用户名称
|
||||
|
||||
Returns:
|
||||
IdleChat: IdleChat实例
|
||||
"""
|
||||
key = f"{private_name}:{stream_id}"
|
||||
if key not in cls._instances:
|
||||
cls._instances[key] = cls(stream_id, private_name)
|
||||
# 创建实例时自动启动检测
|
||||
cls._instances[key].start()
|
||||
logger.info(f"[私聊][{private_name}]创建新的IdleChat实例并启动")
|
||||
return cls._instances[key]
|
||||
|
||||
@classmethod
|
||||
async def register_user_response(cls, private_name: str) -> None:
|
||||
"""注册用户已回复
|
||||
|
||||
当用户回复消息时调用此方法,将用户从待回复列表中移除
|
||||
|
||||
Args:
|
||||
private_name: 私聊用户名称
|
||||
"""
|
||||
async with cls._global_lock:
|
||||
if private_name in cls._pending_replies:
|
||||
del cls._pending_replies[private_name]
|
||||
logger.info(f"[私聊][{private_name}]已回复主动聊天消息,从待回复列表中移除")
|
||||
|
||||
@classmethod
|
||||
async def get_next_available_user(cls) -> Optional[str]:
|
||||
"""获取下一个可用于主动聊天的用户
|
||||
|
||||
优先选择未尝试过的用户,其次是已尝试但超时未回复的用户
|
||||
|
||||
Returns:
|
||||
Optional[str]: 下一个可用的用户名,如果没有则返回None
|
||||
"""
|
||||
async with cls._global_lock:
|
||||
current_time = time.time()
|
||||
timeout_threshold = 7200 # 2小时未回复视为超时
|
||||
|
||||
# 清理超时未回复的用户
|
||||
for user, send_time in list(cls._pending_replies.items()):
|
||||
if current_time - send_time > timeout_threshold:
|
||||
logger.info(f"[私聊][{user}]超过{timeout_threshold}秒未回复,标记为超时")
|
||||
del cls._pending_replies[user]
|
||||
|
||||
# 获取所有实例中的用户
|
||||
all_users = set()
|
||||
for key in cls._instances:
|
||||
user = key.split(":", 1)[0]
|
||||
all_users.add(user)
|
||||
|
||||
# 优先选择未尝试过的用户
|
||||
untried_users = all_users - cls._tried_users
|
||||
if untried_users:
|
||||
next_user = random.choice(list(untried_users))
|
||||
cls._tried_users.add(next_user)
|
||||
return next_user
|
||||
|
||||
# 如果所有用户都已尝试过,重置尝试集合,从头开始
|
||||
if len(cls._tried_users) >= len(all_users):
|
||||
cls._tried_users.clear()
|
||||
logger.info("[私聊]所有用户都已尝试过,重置尝试列表")
|
||||
# 随机选择一个不在待回复列表中的用户
|
||||
available_users = all_users - set(cls._pending_replies.keys())
|
||||
if available_users:
|
||||
next_user = random.choice(list(available_users))
|
||||
cls._tried_users.add(next_user)
|
||||
return next_user
|
||||
|
||||
return None
|
||||
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
"""初始化主动聊天组件
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊用户名称
|
||||
"""
|
||||
self.stream_id = stream_id
|
||||
self.private_name = private_name
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
self.message_sender = DirectMessageSender(private_name)
|
||||
|
||||
# 添加异步锁,保护对共享变量的访问
|
||||
self._lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
# LLM请求对象,用于生成主动对话内容
|
||||
self.llm = LLMRequest(model=global_config.llm_normal, temperature=0.5, max_tokens=500, request_type="idle_chat")
|
||||
|
||||
# 工作状态
|
||||
self.active_instances_count: int = 0
|
||||
self.last_trigger_time: float = time.time() - 1500 # 初始化时减少等待时间
|
||||
self._running: bool = False
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
||||
# 配置参数 - 从global_config加载
|
||||
self.min_cooldown = getattr(
|
||||
global_config, "MIN_IDLE_TIME", 7200
|
||||
) # 最短冷却时间(默认2小时)建议修改长一点,你也不希望你的bot一直骚扰你吧
|
||||
self.max_cooldown = getattr(global_config, "MAX_IDLE_TIME", 14400) # 最长冷却时间(默认4小时)
|
||||
self.min_idle_time = getattr(global_config, "MIN_IDLE_TIME", 3600)
|
||||
self.check_interval = getattr(global_config, "IDLE_CHECK_INTERVAL", 600) # 检查间隔(默认10分钟)
|
||||
self.active_hours_start = 6 # 活动开始时间
|
||||
self.active_hours_end = 24 # 活动结束时间
|
||||
|
||||
# 关系值相关
|
||||
self.base_trigger_probability = 0.3 # 基础触发概率
|
||||
self.relationship_factor = 0.0003 # 关系值影响因子
|
||||
|
||||
def start(self) -> None:
|
||||
"""启动主动聊天检测"""
|
||||
# 检查是否启用了主动聊天功能
|
||||
if not getattr(global_config, "ENABLE_IDLE_CONVERSATION", False):
|
||||
logger.info(f"[私聊][{self.private_name}]主动聊天功能已禁用(配置ENABLE_IDLE_CONVERSATION=False)")
|
||||
return
|
||||
|
||||
if self._running:
|
||||
logger.debug(f"[私聊][{self.private_name}]主动聊天功能已在运行中")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._task = asyncio.create_task(self._check_idle_loop())
|
||||
logger.info(f"[私聊][{self.private_name}]启动主动聊天检测")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止主动聊天检测"""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
self._task = None
|
||||
logger.info(f"[私聊][{self.private_name}]停止主动聊天检测")
|
||||
|
||||
async def increment_active_instances(self) -> None:
|
||||
"""增加活跃实例计数
|
||||
|
||||
当创建新的对话实例时调用此方法
|
||||
"""
|
||||
async with self._lock:
|
||||
self.active_instances_count += 1
|
||||
logger.debug(f"[私聊][{self.private_name}]活跃实例数+1,当前:{self.active_instances_count}")
|
||||
|
||||
async def decrement_active_instances(self) -> None:
|
||||
"""减少活跃实例计数
|
||||
|
||||
当对话实例结束时调用此方法
|
||||
"""
|
||||
async with self._lock:
|
||||
self.active_instances_count = max(0, self.active_instances_count - 1)
|
||||
logger.debug(f"[私聊][{self.private_name}]活跃实例数-1,当前:{self.active_instances_count}")
|
||||
|
||||
async def update_last_message_time(self, message_time: Optional[float] = None) -> None:
|
||||
"""更新最后一条消息的时间
|
||||
|
||||
Args:
|
||||
message_time: 消息时间戳,如果为None则使用当前时间
|
||||
"""
|
||||
async with self._lock:
|
||||
self.last_trigger_time = message_time or time.time()
|
||||
logger.debug(f"[私聊][{self.private_name}]更新最后消息时间: {self.last_trigger_time:.2f}")
|
||||
|
||||
# 当用户发送消息时,也应该注册响应
|
||||
await self.__class__.register_user_response(self.private_name)
|
||||
|
||||
def _is_active_hours(self) -> bool:
|
||||
"""检查是否在活动时间内"""
|
||||
current_hour = datetime.now().hour
|
||||
return self.active_hours_start <= current_hour < self.active_hours_end
|
||||
|
||||
async def _should_trigger(self) -> bool:
|
||||
"""检查是否应该触发主动聊天"""
|
||||
async with self._lock:
|
||||
# 确保计数不会出错,重置为0如果发现是负数
|
||||
if self.active_instances_count < 0:
|
||||
logger.warning(f"[私聊][{self.private_name}]检测到活跃实例数为负数,重置为0")
|
||||
self.active_instances_count = 0
|
||||
|
||||
# 检查是否有活跃实例
|
||||
if self.active_instances_count > 0:
|
||||
logger.debug(f"[私聊][{self.private_name}]存在活跃实例({self.active_instances_count}),不触发主动聊天")
|
||||
return False
|
||||
|
||||
# 检查是否在活动时间内
|
||||
if not self._is_active_hours():
|
||||
logger.debug(f"[私聊][{self.private_name}]不在活动时间内,不触发主动聊天")
|
||||
return False
|
||||
|
||||
# 检查冷却时间
|
||||
current_time = time.time()
|
||||
time_since_last_trigger = current_time - self.last_trigger_time
|
||||
if time_since_last_trigger < self.min_cooldown:
|
||||
time_left = self.min_cooldown - time_since_last_trigger
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]冷却时间未到(已过{time_since_last_trigger:.0f}秒/需要{self.min_cooldown}秒),还需等待{time_left:.0f}秒,不触发主动聊天"
|
||||
)
|
||||
return False
|
||||
|
||||
# 强制触发检查 - 如果超过最大冷却时间,增加触发概率
|
||||
force_trigger = False
|
||||
if time_since_last_trigger > self.max_cooldown * 2: # 如果超过最大冷却时间的两倍
|
||||
force_probability = min(0.6, self.base_trigger_probability * 2) # 增加概率但不超过0.6
|
||||
random_force = random.random()
|
||||
force_trigger = random_force < force_probability
|
||||
if force_trigger:
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}]超过最大冷却时间({time_since_last_trigger:.0f}秒),强制触发主动聊天"
|
||||
)
|
||||
return True
|
||||
|
||||
# 获取关系值
|
||||
relationship_value = 0
|
||||
try:
|
||||
# 导入relationship_manager以使用ensure_float方法
|
||||
from src.plugins.person_info.relationship_manager import relationship_manager
|
||||
|
||||
# 尝试获取person_id
|
||||
person_id = None
|
||||
try:
|
||||
# 先尝试通过昵称获取person_id
|
||||
platform = "qq" # 默认平台
|
||||
person_id = person_info_manager.get_person_id(platform, self.private_name)
|
||||
|
||||
# 如果通过昵称获取失败,尝试通过stream_id解析
|
||||
if not person_id:
|
||||
parts = self.stream_id.split("_")
|
||||
if len(parts) >= 2 and parts[0] == "private":
|
||||
user_id = parts[1]
|
||||
platform = parts[2] if len(parts) >= 3 else "qq"
|
||||
try:
|
||||
person_id = person_info_manager.get_person_id(platform, int(user_id))
|
||||
except ValueError:
|
||||
# 如果user_id不是整数,尝试作为字符串使用
|
||||
person_id = person_info_manager.get_person_id(platform, user_id)
|
||||
except Exception as e2:
|
||||
logger.warning(f"[私聊][{self.private_name}]尝试获取person_id失败: {str(e2)}")
|
||||
|
||||
# 获取关系值
|
||||
if person_id:
|
||||
raw_value = await person_info_manager.get_value(person_id, "relationship_value")
|
||||
relationship_value = relationship_manager.ensure_float(raw_value, person_id)
|
||||
logger.debug(f"[私聊][{self.private_name}]成功获取关系值: {relationship_value}")
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}]无法获取person_id,使用默认关系值0")
|
||||
|
||||
# 使用PfcRepationshipTranslator获取关系描述
|
||||
relationship_translator = PfcRepationshipTranslator(self.private_name)
|
||||
relationship_level = relationship_translator._calculate_relationship_level_num(
|
||||
relationship_value, self.private_name
|
||||
)
|
||||
|
||||
# 基于关系等级调整触发概率
|
||||
# 关系越好,主动聊天概率越高
|
||||
level_probability_factors = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5] # 每个等级对应的基础概率因子
|
||||
base_probability = level_probability_factors[relationship_level]
|
||||
|
||||
# 基础概率因子
|
||||
trigger_probability = base_probability
|
||||
trigger_probability = max(0.05, min(0.6, trigger_probability)) # 限制在0.05-0.6之间
|
||||
|
||||
# 最大冷却时间调整 - 随着冷却时间增加,逐渐增加触发概率
|
||||
if time_since_last_trigger > self.max_cooldown:
|
||||
# 计算额外概率 - 每超过最大冷却时间的10%,增加1%的概率,最多增加30%
|
||||
extra_time_factor = min(
|
||||
0.3, (time_since_last_trigger - self.max_cooldown) / (self.max_cooldown * 10)
|
||||
)
|
||||
trigger_probability += extra_time_factor
|
||||
logger.debug(f"[私聊][{self.private_name}]超过标准冷却时间,额外增加概率: +{extra_time_factor:.2f}")
|
||||
|
||||
# 随机判断是否触发
|
||||
random_value = random.random()
|
||||
should_trigger = random_value < trigger_probability
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]触发概率计算: 基础({base_probability:.2f}) + 关系值({relationship_value})影响 = {trigger_probability:.2f},随机值={random_value:.2f}, 结果={should_trigger}"
|
||||
)
|
||||
|
||||
# 如果决定触发,记录详细日志
|
||||
if should_trigger:
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}]决定触发主动聊天: 触发概率={trigger_probability:.2f}, 距上次已过{time_since_last_trigger:.0f}秒"
|
||||
)
|
||||
|
||||
return should_trigger
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]获取关系值失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 即使获取关系值失败,仍有一个基础的几率触发
|
||||
# 这确保即使数据库有问题,主动聊天功能仍然可用
|
||||
base_fallback_probability = 0.1 # 较低的基础几率
|
||||
random_fallback = random.random()
|
||||
fallback_trigger = random_fallback < base_fallback_probability
|
||||
if fallback_trigger:
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}]获取关系值失败,使用后备触发机制: 概率={base_fallback_probability:.2f}, 决定={fallback_trigger}"
|
||||
)
|
||||
return fallback_trigger
|
||||
|
||||
async def _check_idle_loop(self) -> None:
|
||||
"""检查空闲状态的循环"""
|
||||
try:
|
||||
while self._running:
|
||||
# 检查是否启用了主动聊天功能
|
||||
if not getattr(global_config, "ENABLE_IDLE_CONVERSATION", False):
|
||||
# 如果禁用了功能,等待一段时间后再次检查配置
|
||||
await asyncio.sleep(60) # 每分钟检查一次配置变更
|
||||
continue
|
||||
|
||||
# 检查当前用户是否应该触发主动聊天
|
||||
should_trigger = await self._should_trigger()
|
||||
|
||||
# 如果当前用户不触发,检查是否有其他用户已经超时未回复
|
||||
if not should_trigger:
|
||||
async with self.__class__._global_lock:
|
||||
current_time = time.time()
|
||||
pending_timeout = 1800 # 30分钟未回复检查
|
||||
|
||||
# 检查此用户是否在等待回复列表中
|
||||
if self.private_name in self.__class__._pending_replies:
|
||||
logger.debug(f"[私聊][{self.private_name}]当前用户在等待回复列表中,不进行额外检查")
|
||||
else:
|
||||
# 查找所有超过30分钟未回复的用户
|
||||
timed_out_users = []
|
||||
for user, send_time in self.__class__._pending_replies.items():
|
||||
if current_time - send_time > pending_timeout:
|
||||
timed_out_users.append(user)
|
||||
|
||||
# 如果有超时未回复的用户,尝试找下一个用户
|
||||
if timed_out_users:
|
||||
logger.info(f"[私聊]发现{len(timed_out_users)}个用户超过{pending_timeout}秒未回复")
|
||||
next_user = await self.__class__.get_next_available_user()
|
||||
|
||||
if next_user and next_user != self.private_name:
|
||||
logger.info(f"[私聊]选择下一个用户[{next_user}]进行主动聊天")
|
||||
# 查找该用户的实例并触发聊天
|
||||
for key, instance in self.__class__._instances.items():
|
||||
if key.startswith(f"{next_user}:"):
|
||||
logger.info(f"[私聊]为用户[{next_user}]触发主动聊天")
|
||||
# 触发该实例的主动聊天
|
||||
asyncio.create_task(instance._initiate_chat())
|
||||
break
|
||||
|
||||
# 如果当前用户应该触发主动聊天
|
||||
if should_trigger:
|
||||
try:
|
||||
await self._initiate_chat()
|
||||
# 更新上次触发时间
|
||||
async with self._lock:
|
||||
self.last_trigger_time = time.time()
|
||||
|
||||
# 将此用户添加到等待回复列表中
|
||||
async with self.__class__._global_lock:
|
||||
self.__class__._pending_replies[self.private_name] = time.time()
|
||||
self.__class__._tried_users.add(self.private_name)
|
||||
logger.info(f"[私聊][{self.private_name}]已添加到等待回复列表中")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]执行主动聊天过程出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 等待下一次检查
|
||||
check_interval = self.check_interval # 使用配置的检查间隔
|
||||
logger.debug(f"[私聊][{self.private_name}]等待{check_interval}秒后进行下一次主动聊天检查")
|
||||
await asyncio.sleep(check_interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"[私聊][{self.private_name}]主动聊天检测任务被取消")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]主动聊天检测出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 尝试重新启动检测循环
|
||||
if self._running:
|
||||
logger.info(f"[私聊][{self.private_name}]尝试重新启动主动聊天检测")
|
||||
self._task = asyncio.create_task(self._check_idle_loop())
|
||||
|
||||
async def _get_chat_stream(self) -> Optional[ChatStream]:
|
||||
"""获取聊天流实例"""
|
||||
try:
|
||||
# 尝试从全局聊天管理器获取现有的聊天流
|
||||
from src.plugins.chat.chat_stream import chat_manager
|
||||
|
||||
existing_chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
if existing_chat_stream:
|
||||
logger.debug(f"[私聊][{self.private_name}]从chat_manager找到现有聊天流")
|
||||
return existing_chat_stream
|
||||
|
||||
# 如果没有现有聊天流,则创建新的
|
||||
logger.debug(f"[私聊][{self.private_name}]未找到现有聊天流,创建新聊天流")
|
||||
# 创建用户信息对象
|
||||
user_info = UserInfo(
|
||||
user_id=self.private_name, # 使用私聊用户的ID
|
||||
user_nickname=self.private_name, # 使用私聊用户的名称
|
||||
platform="qq",
|
||||
)
|
||||
# 创建聊天流
|
||||
new_stream = ChatStream(self.stream_id, "qq", user_info)
|
||||
# 将新创建的聊天流添加到管理器中
|
||||
chat_manager.register_stream(new_stream)
|
||||
logger.debug(f"[私聊][{self.private_name}]成功创建并注册新聊天流")
|
||||
return new_stream
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]创建/获取聊天流失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
async def _initiate_chat(self) -> None:
|
||||
"""生成并发送主动聊天消息"""
|
||||
try:
|
||||
# 获取聊天历史记录
|
||||
messages = self.chat_observer.get_cached_messages(limit=12)
|
||||
chat_history_text = await build_readable_messages(
|
||||
messages, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", read_mark=0.0
|
||||
)
|
||||
|
||||
# 获取关系信息
|
||||
from src.plugins.person_info.relationship_manager import relationship_manager
|
||||
|
||||
# 获取关系值
|
||||
relationship_value = 0
|
||||
try:
|
||||
platform = "qq"
|
||||
person_id = person_info_manager.get_person_id(platform, self.private_name)
|
||||
if person_id:
|
||||
raw_value = await person_info_manager.get_value(person_id, "relationship_value")
|
||||
relationship_value = relationship_manager.ensure_float(raw_value, person_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]获取关系值失败,使用默认值: {e}")
|
||||
|
||||
# 使用PfcRepationshipTranslator获取关系描述
|
||||
relationship_translator = PfcRepationshipTranslator(self.private_name)
|
||||
full_relationship_text = await relationship_translator.translate_relationship_value_to_text(
|
||||
relationship_value
|
||||
)
|
||||
|
||||
# 提取纯关系描述(去掉"你们的关系是:"前缀)
|
||||
relationship_description = "普通" # 默认值
|
||||
if ":" in full_relationship_text:
|
||||
relationship_description = full_relationship_text.split(":")[1].replace("。", "")
|
||||
|
||||
if global_config.ENABLE_SCHEDULE_GEN:
|
||||
schedule_prompt = await global_prompt_manager.format_prompt(
|
||||
"schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False)
|
||||
)
|
||||
else:
|
||||
schedule_prompt = ""
|
||||
|
||||
# 构建提示词
|
||||
current_time = datetime.now().strftime("%H:%M")
|
||||
prompt = f"""你是{global_config.BOT_NICKNAME}。
|
||||
你正在与用户{self.private_name}进行QQ私聊,你们的关系是{relationship_description}
|
||||
现在时间{current_time}
|
||||
这是你的日程{schedule_prompt}
|
||||
你想要主动发起对话。
|
||||
请基于以下之前的对话历史,生成一条自然、友好、符合关系程度的主动对话消息。
|
||||
这条消息应能够引起用户的兴趣,重新开始对话。
|
||||
最近的对话历史(并不是现在的对话):
|
||||
{chat_history_text}
|
||||
请你严格根据对话历史决定是告诉对方你正在做的事情,还是询问对方正在做的事情
|
||||
请直接输出一条消息,不要有任何额外的解释或引导文字
|
||||
消息内容尽量简短
|
||||
"""
|
||||
|
||||
# 生成回复
|
||||
logger.debug(f"[私聊][{self.private_name}]开始生成主动聊天内容")
|
||||
try:
|
||||
content, _ = await asyncio.wait_for(self.llm.generate_response_async(prompt), timeout=30)
|
||||
logger.debug(f"[私聊][{self.private_name}]成功生成主动聊天内容: {content}")
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"[私聊][{self.private_name}]生成主动聊天内容超时")
|
||||
return
|
||||
except Exception as llm_err:
|
||||
logger.error(f"[私聊][{self.private_name}]生成主动聊天内容失败: {str(llm_err)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return
|
||||
|
||||
# 清理结果
|
||||
content = content.strip()
|
||||
content = content.strip("\"'")
|
||||
|
||||
if not content:
|
||||
logger.error(f"[私聊][{self.private_name}]生成的主动聊天内容为空")
|
||||
return
|
||||
|
||||
# 获取聊天流
|
||||
chat_stream = await self._get_chat_stream()
|
||||
if not chat_stream:
|
||||
logger.error(f"[私聊][{self.private_name}]无法获取有效的聊天流,取消发送主动消息")
|
||||
return
|
||||
|
||||
# 发送消息
|
||||
try:
|
||||
logger.debug(f"[私聊][{self.private_name}]准备发送主动聊天消息: {content}")
|
||||
await self.message_sender.send_message(chat_stream=chat_stream, content=content, reply_to_message=None)
|
||||
logger.info(f"[私聊][{self.private_name}]成功主动发起聊天: {content}")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]发送主动聊天消息失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]主动发起聊天过程中发生未预期的错误: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
from typing import Dict, Optional
|
||||
import asyncio
|
||||
from src.common.logger_manager import get_logger
|
||||
from .idle_chat import IdleChat
|
||||
import traceback
|
||||
|
||||
logger = get_logger("pfc_idle_chat_manager")
|
||||
|
||||
|
||||
class IdleChatManager:
|
||||
"""空闲聊天管理器
|
||||
|
||||
用于管理所有私聊用户的空闲聊天实例。
|
||||
采用单例模式,确保全局只有一个管理器实例。
|
||||
"""
|
||||
|
||||
_instance: Optional["IdleChatManager"] = None
|
||||
_lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
def __init__(self):
|
||||
"""初始化空闲聊天管理器"""
|
||||
self._idle_chats: Dict[str, IdleChat] = {} # stream_id -> IdleChat
|
||||
self._active_conversations_count: Dict[str, int] = {} # stream_id -> count
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "IdleChatManager":
|
||||
"""获取管理器单例 (同步版本)
|
||||
|
||||
Returns:
|
||||
IdleChatManager: 管理器实例
|
||||
"""
|
||||
if not cls._instance:
|
||||
# 在同步环境中创建实例
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
async def get_instance_async(cls) -> "IdleChatManager":
|
||||
"""获取管理器单例 (异步版本)
|
||||
|
||||
Returns:
|
||||
IdleChatManager: 管理器实例
|
||||
"""
|
||||
if not cls._instance:
|
||||
async with cls._lock:
|
||||
if not cls._instance:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def get_or_create_idle_chat(self, stream_id: str, private_name: str) -> IdleChat:
|
||||
"""获取或创建空闲聊天实例
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊用户名称
|
||||
|
||||
Returns:
|
||||
IdleChat: 空闲聊天实例
|
||||
"""
|
||||
if stream_id not in self._idle_chats:
|
||||
idle_chat = IdleChat(stream_id, private_name)
|
||||
self._idle_chats[stream_id] = idle_chat
|
||||
# 初始化活跃对话计数
|
||||
if stream_id not in self._active_conversations_count:
|
||||
self._active_conversations_count[stream_id] = 0
|
||||
idle_chat.start() # 启动空闲检测
|
||||
logger.info(f"[私聊][{private_name}]创建并启动新的空闲聊天实例")
|
||||
return self._idle_chats[stream_id]
|
||||
|
||||
async def remove_idle_chat(self, stream_id: str) -> None:
|
||||
"""移除空闲聊天实例
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
"""
|
||||
if stream_id in self._idle_chats:
|
||||
idle_chat = self._idle_chats[stream_id]
|
||||
idle_chat.stop() # 停止空闲检测
|
||||
del self._idle_chats[stream_id]
|
||||
if stream_id in self._active_conversations_count:
|
||||
del self._active_conversations_count[stream_id]
|
||||
logger.info(f"[私聊][{idle_chat.private_name}]移除空闲聊天实例")
|
||||
|
||||
async def notify_conversation_start(self, stream_id: str) -> None:
|
||||
"""通知对话开始
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
"""
|
||||
try:
|
||||
if stream_id not in self._idle_chats:
|
||||
logger.warning(f"对话开始通知: {stream_id} 没有对应的IdleChat实例,将创建一个")
|
||||
# 从stream_id尝试提取private_name
|
||||
private_name = stream_id
|
||||
if stream_id.startswith("private_"):
|
||||
parts = stream_id.split("_")
|
||||
if len(parts) >= 2:
|
||||
private_name = parts[1] # 取第二部分作为名称
|
||||
await self.get_or_create_idle_chat(stream_id, private_name)
|
||||
|
||||
if stream_id not in self._active_conversations_count:
|
||||
self._active_conversations_count[stream_id] = 0
|
||||
|
||||
# 增加计数前记录当前值,用于日志
|
||||
old_count = self._active_conversations_count[stream_id]
|
||||
self._active_conversations_count[stream_id] += 1
|
||||
new_count = self._active_conversations_count[stream_id]
|
||||
|
||||
# 确保IdleChat实例存在
|
||||
idle_chat = self._idle_chats.get(stream_id)
|
||||
if idle_chat:
|
||||
await idle_chat.increment_active_instances()
|
||||
logger.debug(f"对话开始通知: {stream_id}, 计数从{old_count}增加到{new_count}")
|
||||
else:
|
||||
logger.error(f"对话开始通知: {stream_id}, 计数增加但IdleChat不存在! 计数:{old_count}->{new_count}")
|
||||
except Exception as e:
|
||||
logger.error(f"对话开始通知处理失败: {stream_id}, 错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def notify_conversation_end(self, stream_id: str) -> None:
|
||||
"""通知对话结束
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
"""
|
||||
try:
|
||||
# 记录当前计数用于日志
|
||||
old_count = self._active_conversations_count.get(stream_id, 0)
|
||||
|
||||
# 安全减少计数,避免负数
|
||||
if stream_id in self._active_conversations_count and self._active_conversations_count[stream_id] > 0:
|
||||
self._active_conversations_count[stream_id] -= 1
|
||||
else:
|
||||
# 如果计数已经为0或不存在,设置为0
|
||||
self._active_conversations_count[stream_id] = 0
|
||||
|
||||
new_count = self._active_conversations_count.get(stream_id, 0)
|
||||
|
||||
# 确保IdleChat实例存在
|
||||
idle_chat = self._idle_chats.get(stream_id)
|
||||
if idle_chat:
|
||||
await idle_chat.decrement_active_instances()
|
||||
logger.debug(f"对话结束通知: {stream_id}, 计数从{old_count}减少到{new_count}")
|
||||
else:
|
||||
logger.warning(f"对话结束通知: {stream_id}, 计数减少但IdleChat不存在! 计数:{old_count}->{new_count}")
|
||||
|
||||
# 检查是否所有对话都结束了,帮助调试
|
||||
all_counts = sum(self._active_conversations_count.values())
|
||||
if all_counts == 0:
|
||||
logger.info("所有对话实例都已结束,当前总活跃计数为0")
|
||||
except Exception as e:
|
||||
logger.error(f"对话结束通知处理失败: {stream_id}, 错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def get_idle_chat(self, stream_id: str) -> Optional[IdleChat]:
|
||||
"""获取空闲聊天实例
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
|
||||
Returns:
|
||||
Optional[IdleChat]: 空闲聊天实例,如果不存在则返回None
|
||||
"""
|
||||
return self._idle_chats.get(stream_id)
|
||||
|
||||
def get_active_conversations_count(self, stream_id: str) -> int:
|
||||
"""获取指定流的活跃对话计数
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
|
||||
Returns:
|
||||
int: 活跃对话计数
|
||||
"""
|
||||
return self._active_conversations_count.get(stream_id, 0)
|
||||
|
||||
def get_all_active_conversations_count(self) -> int:
|
||||
"""获取所有活跃对话总计数
|
||||
|
||||
Returns:
|
||||
int: 活跃对话总计数
|
||||
"""
|
||||
return sum(self._active_conversations_count.values())
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
import traceback
|
||||
import asyncio
|
||||
from typing import Optional, Dict
|
||||
from src.common.logger_manager import get_logger
|
||||
import time
|
||||
|
||||
logger = get_logger("pfc_idle_conversation")
|
||||
|
||||
|
||||
class IdleConversation:
|
||||
"""
|
||||
处理Idle聊天相关的功能,将这些功能从主Conversation类中分离出来,
|
||||
以减少代码量并方便维护。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化IdleConversation实例"""
|
||||
self._idle_chat_manager = None
|
||||
self._running = False
|
||||
self._active_streams: Dict[str, bool] = {} # 跟踪活跃的流
|
||||
self._monitor_task = None # 用于后台监控的任务
|
||||
self._lock = asyncio.Lock() # 用于线程安全操作
|
||||
self._initialization_in_progress = False # 防止并发初始化
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化Idle聊天管理器"""
|
||||
# 防止并发初始化
|
||||
if self._initialization_in_progress:
|
||||
logger.debug("IdleConversation正在初始化中,等待完成")
|
||||
return False
|
||||
|
||||
if self._idle_chat_manager is not None:
|
||||
logger.debug("IdleConversation已初始化,无需重复操作")
|
||||
return True
|
||||
|
||||
# 标记开始初始化
|
||||
self._initialization_in_progress = True
|
||||
|
||||
try:
|
||||
# 从PFCManager获取IdleChatManager实例
|
||||
from ..pfc_manager import PFCManager
|
||||
|
||||
pfc_manager = PFCManager.get_instance()
|
||||
self._idle_chat_manager = pfc_manager.get_idle_chat_manager()
|
||||
logger.debug("IdleConversation初始化完成,已获取IdleChatManager实例")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"初始化IdleConversation时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
# 无论成功或失败,都清除初始化标志
|
||||
self._initialization_in_progress = False
|
||||
|
||||
async def start(self):
|
||||
"""启动IdleConversation,创建后台监控任务"""
|
||||
if self._running:
|
||||
logger.debug("IdleConversation已经在运行")
|
||||
return False
|
||||
|
||||
if not self._idle_chat_manager:
|
||||
success = await self.initialize()
|
||||
if not success:
|
||||
logger.error("无法启动IdleConversation:初始化失败")
|
||||
return False
|
||||
|
||||
try:
|
||||
self._running = True
|
||||
# 创建后台监控任务,使用try-except块来捕获可能的异常
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
if loop.is_running():
|
||||
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
||||
logger.info("IdleConversation启动成功,后台监控任务已创建")
|
||||
else:
|
||||
logger.warning("事件循环不活跃,跳过监控任务创建")
|
||||
except RuntimeError:
|
||||
# 如果没有活跃的事件循环,记录警告但继续执行
|
||||
logger.warning("没有活跃的事件循环,IdleConversation将不会启动监控任务")
|
||||
# 尽管没有监控任务,但仍然将running设为True表示IdleConversation已启动
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self._running = False
|
||||
logger.error(f"启动IdleConversation失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
async def stop(self):
|
||||
"""停止IdleConversation的后台任务"""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
if self._monitor_task and not self._monitor_task.done():
|
||||
try:
|
||||
self._monitor_task.cancel()
|
||||
try:
|
||||
await asyncio.wait_for(self._monitor_task, timeout=2.0)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("停止IdleConversation监控任务超时")
|
||||
except asyncio.CancelledError:
|
||||
pass # 正常取消
|
||||
except Exception as e:
|
||||
logger.error(f"停止IdleConversation监控任务时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
self._monitor_task = None
|
||||
logger.info("IdleConversation已停止")
|
||||
|
||||
async def _monitor_loop(self):
|
||||
"""后台监控循环,定期检查活跃的会话并执行必要的操作"""
|
||||
try:
|
||||
while self._running:
|
||||
try:
|
||||
# 同步活跃流计数到IdleChatManager
|
||||
if self._idle_chat_manager:
|
||||
await self._sync_active_streams_to_manager()
|
||||
|
||||
# 这里可以添加定期检查逻辑,如查询空闲状态等
|
||||
active_count = len(self._active_streams)
|
||||
logger.debug(f"IdleConversation监控中,当前活跃流数量: {active_count}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"IdleConversation监控循环出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 每30秒执行一次监控
|
||||
await asyncio.sleep(30)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("IdleConversation监控任务已取消")
|
||||
except Exception as e:
|
||||
logger.error(f"IdleConversation监控任务异常退出: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
self._running = False
|
||||
|
||||
async def _sync_active_streams_to_manager(self):
|
||||
"""同步活跃流计数到IdleChatManager和IdleChat"""
|
||||
try:
|
||||
if not self._idle_chat_manager:
|
||||
return
|
||||
|
||||
# 获取当前的活跃流列表
|
||||
async with self._lock:
|
||||
active_streams = list(self._active_streams.keys())
|
||||
|
||||
# 对每个活跃流,确保IdleChatManager和IdleChat中的计数是正确的
|
||||
for stream_id in active_streams:
|
||||
# 获取当前IdleChatManager中的计数
|
||||
manager_count = self._idle_chat_manager.get_active_conversations_count(stream_id)
|
||||
|
||||
# 由于我们的活跃流字典只记录是否活跃(值为True),所以计数应该是1
|
||||
if manager_count != 1:
|
||||
# 修正IdleChatManager中的计数
|
||||
old_count = manager_count
|
||||
self._idle_chat_manager._active_conversations_count[stream_id] = 1
|
||||
logger.warning(f"同步调整IdleChatManager中的计数: stream_id={stream_id}, {old_count}->1")
|
||||
|
||||
# 同时修正IdleChat中的计数
|
||||
idle_chat = self._idle_chat_manager.get_idle_chat(stream_id)
|
||||
if idle_chat:
|
||||
if getattr(idle_chat, "active_instances_count", 0) != 1:
|
||||
old_count = getattr(idle_chat, "active_instances_count", 0)
|
||||
idle_chat.active_instances_count = 1
|
||||
logger.warning(f"同步调整IdleChat中的计数: stream_id={stream_id}, {old_count}->1")
|
||||
|
||||
# 检查IdleChatManager中有没有多余的计数(conversation中已不存在但manager中还有)
|
||||
for stream_id, count in list(self._idle_chat_manager._active_conversations_count.items()):
|
||||
if count > 0 and stream_id not in active_streams:
|
||||
# 重置为0
|
||||
self._idle_chat_manager._active_conversations_count[stream_id] = 0
|
||||
logger.warning(f"重置IdleChatManager中的多余计数: stream_id={stream_id}, {count}->0")
|
||||
|
||||
# 同时修正IdleChat中的计数
|
||||
idle_chat = self._idle_chat_manager.get_idle_chat(stream_id)
|
||||
if idle_chat and getattr(idle_chat, "active_instances_count", 0) > 0:
|
||||
old_count = getattr(idle_chat, "active_instances_count", 0)
|
||||
idle_chat.active_instances_count = 0
|
||||
logger.warning(f"同步重置IdleChat中的计数: stream_id={stream_id}, {old_count}->0")
|
||||
|
||||
# 日志记录同步结果
|
||||
total_active = len(active_streams)
|
||||
total_manager = sum(self._idle_chat_manager._active_conversations_count.values())
|
||||
logger.debug(f"同步后的计数: IdleConversation活跃流={total_active}, IdleChatManager总计数={total_manager}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步活跃流计数失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def get_or_create_idle_chat(self, stream_id: str, private_name: str):
|
||||
"""
|
||||
获取或创建IdleChat实例
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊对象名称,用于日志
|
||||
|
||||
Returns:
|
||||
bool: 操作是否成功
|
||||
"""
|
||||
# 确保IdleConversation已启动
|
||||
if not self._running:
|
||||
await self.start()
|
||||
|
||||
if not self._idle_chat_manager:
|
||||
# 如果尚未初始化,尝试初始化
|
||||
success = await self.initialize()
|
||||
if not success:
|
||||
logger.warning(f"[私聊][{private_name}] 获取或创建IdleChat失败:IdleChatManager未初始化")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 创建IdleChat实例
|
||||
_idle_chat = await self._idle_chat_manager.get_or_create_idle_chat(stream_id, private_name)
|
||||
logger.debug(f"[私聊][{private_name}] 已创建或获取IdleChat实例")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{private_name}] 创建或获取IdleChat实例失败: {e}")
|
||||
logger.warning(traceback.format_exc())
|
||||
return False
|
||||
|
||||
async def notify_conversation_start(self, stream_id: str, private_name: str) -> bool:
|
||||
"""
|
||||
通知空闲聊天管理器对话开始
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊对象名称,用于日志
|
||||
|
||||
Returns:
|
||||
bool: 通知是否成功
|
||||
"""
|
||||
try:
|
||||
# 确保IdleConversation已启动
|
||||
if not self._running:
|
||||
success = await self.start()
|
||||
if not success:
|
||||
logger.warning(f"[私聊][{private_name}] 启动IdleConversation失败,无法通知对话开始")
|
||||
return False
|
||||
|
||||
if not self._idle_chat_manager:
|
||||
# 如果尚未初始化,尝试初始化
|
||||
success = await self.initialize()
|
||||
if not success:
|
||||
logger.warning(f"[私聊][{private_name}] 通知对话开始失败:IdleChatManager未初始化")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 确保IdleChat实例已创建 - 这是关键步骤,要先创建IdleChat
|
||||
await self.get_or_create_idle_chat(stream_id, private_name)
|
||||
|
||||
# 先记录活跃状态 - 这是权威源
|
||||
async with self._lock:
|
||||
self._active_streams[stream_id] = True
|
||||
|
||||
# 然后同步到IdleChatManager
|
||||
if self._idle_chat_manager:
|
||||
await self._idle_chat_manager.notify_conversation_start(stream_id)
|
||||
logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话开始")
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已记录活跃状态")
|
||||
|
||||
# 立即进行一次同步,确保数据一致性
|
||||
await self._sync_active_streams_to_manager()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{private_name}] 通知空闲聊天管理器对话开始失败: {e}")
|
||||
logger.warning(traceback.format_exc())
|
||||
# 即使通知失败,也应记录活跃状态
|
||||
async with self._lock:
|
||||
self._active_streams[stream_id] = True
|
||||
return False
|
||||
except Exception as outer_e:
|
||||
logger.error(f"[私聊][{private_name}] 处理对话开始通知时发生严重错误: {outer_e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
async def notify_conversation_end(self, stream_id: str, private_name: str) -> bool:
|
||||
"""
|
||||
通知空闲聊天管理器对话结束
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊对象名称,用于日志
|
||||
|
||||
Returns:
|
||||
bool: 通知是否成功
|
||||
"""
|
||||
try:
|
||||
# 先从自身的活跃流中移除 - 这是权威源
|
||||
was_active = False
|
||||
async with self._lock:
|
||||
if stream_id in self._active_streams:
|
||||
del self._active_streams[stream_id]
|
||||
was_active = True
|
||||
logger.debug(f"[私聊][{private_name}] 已从活跃流中移除 {stream_id}")
|
||||
|
||||
if not self._idle_chat_manager:
|
||||
# 如果尚未初始化,尝试初始化
|
||||
success = await self.initialize()
|
||||
if not success:
|
||||
logger.warning(f"[私聊][{private_name}] 通知对话结束失败:IdleChatManager未初始化")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 然后同步到IdleChatManager
|
||||
if self._idle_chat_manager:
|
||||
# 无论如何都尝试通知
|
||||
await self._idle_chat_manager.notify_conversation_end(stream_id)
|
||||
|
||||
# 立即进行一次同步,确保数据一致性
|
||||
await self._sync_active_streams_to_manager()
|
||||
|
||||
logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话结束")
|
||||
|
||||
# 检查当前活跃流数量
|
||||
active_count = len(self._active_streams)
|
||||
if active_count == 0:
|
||||
logger.info(f"[私聊][{private_name}] 当前无活跃流,可能会触发主动聊天")
|
||||
|
||||
# 额外调用:如果实例存在且只有在确实移除了活跃流的情况下才触发检查
|
||||
if was_active:
|
||||
idle_chat = self._idle_chat_manager.get_idle_chat(stream_id)
|
||||
if idle_chat:
|
||||
# 直接触发IdleChat检查,而不是等待下一个循环
|
||||
logger.info(f"[私聊][{private_name}] 对话结束,手动触发一次主动聊天检查")
|
||||
asyncio.create_task(self._trigger_idle_chat_check(idle_chat, stream_id, private_name))
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已更新活跃状态")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{private_name}] 通知空闲聊天管理器对话结束失败: {e}")
|
||||
logger.warning(traceback.format_exc())
|
||||
return False
|
||||
except Exception as outer_e:
|
||||
logger.error(f"[私聊][{private_name}] 处理对话结束通知时发生严重错误: {outer_e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
async def _trigger_idle_chat_check(self, idle_chat, stream_id: str, private_name: str):
|
||||
"""在对话结束后,手动触发一次IdleChat的检查"""
|
||||
try:
|
||||
# 确保活跃计数与IdleConversation一致
|
||||
async with self._lock:
|
||||
is_active_in_conversation = stream_id in self._active_streams
|
||||
|
||||
# 强制使IdleChat的计数与IdleConversation一致
|
||||
if is_active_in_conversation:
|
||||
# 如果在IdleConversation中是活跃的,IdleChat的计数应该是1
|
||||
if idle_chat.active_instances_count != 1:
|
||||
old_count = idle_chat.active_instances_count
|
||||
idle_chat.active_instances_count = 1
|
||||
logger.warning(f"[私聊][{private_name}] 修正IdleChat计数: {old_count}->1")
|
||||
else:
|
||||
# 如果在IdleConversation中不是活跃的,IdleChat的计数应该是0
|
||||
if idle_chat.active_instances_count != 0:
|
||||
old_count = idle_chat.active_instances_count
|
||||
idle_chat.active_instances_count = 0
|
||||
logger.warning(f"[私聊][{private_name}] 修正IdleChat计数: {old_count}->0")
|
||||
|
||||
# 等待1秒,让任何正在进行的处理完成
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 只有当stream不再活跃时才触发检查
|
||||
if not is_active_in_conversation:
|
||||
# 尝试触发一次检查
|
||||
if hasattr(idle_chat, "_should_trigger"):
|
||||
should_trigger = await idle_chat._should_trigger()
|
||||
logger.info(f"[私聊][{private_name}] 手动触发主动聊天检查结果: {should_trigger}")
|
||||
|
||||
# 如果应该触发,直接调用_initiate_chat
|
||||
if should_trigger and hasattr(idle_chat, "_initiate_chat"):
|
||||
logger.info(f"[私聊][{private_name}] 手动触发主动聊天")
|
||||
await idle_chat._initiate_chat()
|
||||
# 更新最后触发时间
|
||||
idle_chat.last_trigger_time = time.time()
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] IdleChat没有_should_trigger方法,无法触发检查")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{private_name}] 手动触发主动聊天检查时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def is_stream_active(self, stream_id: str) -> bool:
|
||||
"""检查指定的stream是否活跃"""
|
||||
return stream_id in self._active_streams
|
||||
|
||||
def get_active_streams_count(self) -> int:
|
||||
"""获取当前活跃的stream数量"""
|
||||
return len(self._active_streams)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""检查IdleConversation是否正在运行"""
|
||||
return self._running
|
||||
|
||||
@property
|
||||
def idle_chat_manager(self):
|
||||
"""获取IdleChatManager实例"""
|
||||
return self._idle_chat_manager
|
||||
|
||||
|
||||
# 创建单例实例
|
||||
_instance: Optional[IdleConversation] = None
|
||||
_instance_lock = asyncio.Lock()
|
||||
_initialization_in_progress = False # 防止并发初始化
|
||||
|
||||
|
||||
async def initialize_idle_conversation() -> IdleConversation:
|
||||
"""初始化并启动IdleConversation单例实例"""
|
||||
global _initialization_in_progress
|
||||
|
||||
# 防止并发初始化
|
||||
if _initialization_in_progress:
|
||||
logger.debug("IdleConversation全局初始化正在进行中,等待完成")
|
||||
return get_idle_conversation_instance()
|
||||
|
||||
# 标记正在初始化
|
||||
_initialization_in_progress = True
|
||||
|
||||
try:
|
||||
instance = get_idle_conversation_instance()
|
||||
|
||||
# 如果实例已经在运行,避免重复初始化
|
||||
if getattr(instance, "_running", False):
|
||||
logger.debug("IdleConversation已在运行状态,无需重新初始化")
|
||||
_initialization_in_progress = False
|
||||
return instance
|
||||
|
||||
# 初始化实例
|
||||
success = await instance.initialize()
|
||||
if not success:
|
||||
logger.error("IdleConversation初始化失败")
|
||||
_initialization_in_progress = False
|
||||
return instance
|
||||
|
||||
# 启动实例
|
||||
success = await instance.start()
|
||||
if not success:
|
||||
logger.error("IdleConversation启动失败")
|
||||
else:
|
||||
# 启动成功,进行初始检查
|
||||
logger.info("IdleConversation启动成功,执行初始化后检查")
|
||||
# 这里可以添加一些启动后的检查,如果需要
|
||||
|
||||
# 创建一个异步任务,定期检查系统状态
|
||||
asyncio.create_task(periodic_system_check(instance))
|
||||
|
||||
return instance
|
||||
except Exception as e:
|
||||
logger.error(f"初始化并启动IdleConversation时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 重置标志,允许下次再试
|
||||
_initialization_in_progress = False
|
||||
return get_idle_conversation_instance() # 返回实例,即使初始化失败
|
||||
finally:
|
||||
# 清除初始化标志
|
||||
_initialization_in_progress = False
|
||||
|
||||
|
||||
async def periodic_system_check(instance: IdleConversation):
|
||||
"""定期检查系统状态,确保主动聊天功能正常工作"""
|
||||
try:
|
||||
# 等待10秒,让系统完全启动
|
||||
await asyncio.sleep(10)
|
||||
|
||||
while getattr(instance, "_running", False):
|
||||
try:
|
||||
# 检查活跃流数量
|
||||
active_streams_count = len(getattr(instance, "_active_streams", {}))
|
||||
|
||||
# 如果IdleChatManager存在,检查其中的活跃对话计数
|
||||
idle_chat_manager = getattr(instance, "_idle_chat_manager", None)
|
||||
if idle_chat_manager and hasattr(idle_chat_manager, "get_all_active_conversations_count"):
|
||||
manager_count = idle_chat_manager.get_all_active_conversations_count()
|
||||
|
||||
# 如果两者不一致,记录警告
|
||||
if active_streams_count != manager_count:
|
||||
logger.warning(
|
||||
f"检测到计数不一致: IdleConversation记录的活跃流数量({active_streams_count}) 与 IdleChatManager记录的活跃对话数({manager_count})不匹配"
|
||||
)
|
||||
|
||||
# 如果IdleChatManager记录的计数为0但自己的记录不为0,进行修正
|
||||
if manager_count == 0 and active_streams_count > 0:
|
||||
logger.warning("检测到可能的计数错误,尝试修正:清空IdleConversation的活跃流记录")
|
||||
async with instance._lock:
|
||||
instance._active_streams.clear()
|
||||
|
||||
# 检查计数如果为0,帮助日志输出
|
||||
if active_streams_count == 0:
|
||||
logger.debug("当前没有活跃的对话流,应该可以触发主动聊天")
|
||||
|
||||
except Exception as check_err:
|
||||
logger.error(f"执行系统检查时出错: {check_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 每60秒检查一次
|
||||
await asyncio.sleep(60)
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("系统检查任务被取消")
|
||||
except Exception as e:
|
||||
logger.error(f"系统检查任务异常退出: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def get_idle_conversation_instance() -> IdleConversation:
|
||||
"""获取IdleConversation的单例实例"""
|
||||
global _instance
|
||||
if _instance is None:
|
||||
_instance = IdleConversation()
|
||||
return _instance
|
||||
|
|
@ -1,21 +1,31 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
import asyncio
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
from src.common.logger import get_module_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from .chat_observer import ChatObserver
|
||||
from .message_sender import DirectMessageSender
|
||||
from ..chat.chat_stream import ChatStream
|
||||
from maim_message import UserInfo
|
||||
from src.plugins.chat.chat_stream import chat_manager, ChatStream
|
||||
from src.individuality.individuality import Individuality
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from maim_message import UserInfo
|
||||
|
||||
from ..chat_observer import ChatObserver
|
||||
from ..message_sender import DirectMessageSender
|
||||
|
||||
# 导入富文本回溯,用于更好的错误展示
|
||||
from rich.traceback import install
|
||||
|
||||
# 使用TYPE_CHECKING避免循环导入
|
||||
if TYPE_CHECKING:
|
||||
from .conversation import Conversation
|
||||
from ..conversation import Conversation
|
||||
|
||||
logger = get_module_logger("pfc_idle")
|
||||
install(extra_lines=3)
|
||||
|
||||
# 获取当前模块的日志记录器
|
||||
logger = get_logger("idle_conversation_starter")
|
||||
|
||||
|
||||
class IdleConversationStarter:
|
||||
|
|
@ -125,6 +135,7 @@ class IdleConversationStarter:
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]重新加载配置时出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def _check_idle_loop(self) -> None:
|
||||
"""检查空闲状态的循环
|
||||
|
|
@ -167,6 +178,7 @@ class IdleConversationStarter:
|
|||
logger.debug(f"[私聊][{self.private_name}]空闲对话检测任务被取消")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]空闲对话检测出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 尝试重新启动检测循环
|
||||
if self._running:
|
||||
logger.info(f"[私聊][{self.private_name}]尝试重新启动空闲对话检测")
|
||||
|
|
@ -175,7 +187,7 @@ class IdleConversationStarter:
|
|||
async def _initiate_conversation(self) -> None:
|
||||
"""生成并发送主动对话内容
|
||||
|
||||
获取聊天历史记录,使用LLM生成合适的开场白,然后发送消息。
|
||||
获取聊天历史记录,使用LLM生成合适的开场白(大概),然后发送消息。
|
||||
"""
|
||||
try:
|
||||
# 获取聊天历史记录,用于生成更合适的开场白
|
||||
|
|
@ -212,6 +224,7 @@ class IdleConversationStarter:
|
|||
return
|
||||
except Exception as llm_err:
|
||||
logger.error(f"[私聊][{self.private_name}]生成主动对话内容失败: {str(llm_err)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return
|
||||
|
||||
# 清理结果
|
||||
|
|
@ -225,9 +238,10 @@ class IdleConversationStarter:
|
|||
# 统一错误处理,从这里开始所有操作都在同一个try-except块中
|
||||
logger.debug(f"[私聊][{self.private_name}]成功生成主动对话内容: {content},准备发送")
|
||||
|
||||
from .pfc_manager import PFCManager
|
||||
# 在函数内部导入PFCManager,避免循环导入
|
||||
from ..pfc_manager import PFCManager
|
||||
|
||||
# 获取当前实例
|
||||
# 获取当前实例 - 注意这是同步方法,不需要await
|
||||
pfc_manager = PFCManager.get_instance()
|
||||
|
||||
# 结束当前对话实例(如果存在)
|
||||
|
|
@ -239,6 +253,7 @@ class IdleConversationStarter:
|
|||
await pfc_manager.remove_conversation(self.stream_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]结束当前对话实例时出错: {str(e)},继续创建新实例")
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
# 创建新的对话实例
|
||||
logger.info(f"[私聊][{self.private_name}]创建新的对话实例以发送主动消息")
|
||||
|
|
@ -247,6 +262,7 @@ class IdleConversationStarter:
|
|||
new_conversation = await pfc_manager.get_or_create_conversation(self.stream_id, self.private_name)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]创建新对话实例失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return
|
||||
|
||||
# 确保新对话实例已初始化完成
|
||||
|
|
@ -269,14 +285,17 @@ class IdleConversationStarter:
|
|||
new_conversation.chat_observer.trigger_update()
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]触发聊天观察者更新失败: {str(e)}")
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
logger.success(f"[私聊][{self.private_name}]成功主动发起对话: {content}")
|
||||
logger.info(f"[私聊][{self.private_name}]成功主动发起对话: {content}")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]发送主动对话消息失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
except Exception as e:
|
||||
# 顶级异常处理,确保任何未捕获的异常都不会导致整个进程崩溃
|
||||
logger.error(f"[私聊][{self.private_name}]主动发起对话过程中发生未预期的错误: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def _get_chat_stream(self, conversation: Optional["Conversation"] = None) -> Optional[ChatStream]:
|
||||
"""获取可用的聊天流
|
||||
|
|
@ -313,8 +332,6 @@ class IdleConversationStarter:
|
|||
return conversation.chat_stream
|
||||
|
||||
# 2. 尝试从聊天管理器获取
|
||||
from src.plugins.chat.chat_stream import chat_manager
|
||||
|
||||
try:
|
||||
logger.info(f"[私聊][{self.private_name}]尝试从chat_manager获取聊天流")
|
||||
chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
|
|
@ -322,6 +339,7 @@ class IdleConversationStarter:
|
|||
return chat_stream
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]从chat_manager获取聊天流失败: {str(e)}")
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
# 3. 创建新的聊天流
|
||||
try:
|
||||
|
|
@ -332,4 +350,5 @@ class IdleConversationStarter:
|
|||
return ChatStream(self.stream_id, "qq", user_info)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]创建新聊天流失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
|
@ -3,13 +3,13 @@ import traceback
|
|||
from typing import Tuple, Optional, Dict, Any, List
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.individuality.individuality import Individuality
|
||||
# from src.individuality.individuality import Individuality
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from src.config.config import global_config
|
||||
|
||||
# 确保导入路径正确
|
||||
from .pfc_utils import get_items_from_json, retrieve_contextual_info
|
||||
from .pfc_utils import get_items_from_json
|
||||
from .chat_observer import ChatObserver
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
|
|
@ -20,91 +20,139 @@ logger = get_logger("pfc_action_planner")
|
|||
# --- 定义 Prompt 模板 ---
|
||||
|
||||
# Prompt(1): 首次回复或非连续回复时的决策 Prompt
|
||||
PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方:
|
||||
PROMPT_INITIAL_REPLY = """
|
||||
当前时间:{current_time_str}
|
||||
现在{persona_text}正在与{sender_name}在qq上私聊
|
||||
他们的关系是:{relationship_text}
|
||||
{persona_text}现在的心情是是:{current_emotion_text}
|
||||
你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程,可以回复,可以倾听,甚至可以屏蔽对方:
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str}
|
||||
【最近行动历史概要】
|
||||
{action_history_summary}
|
||||
【你想起来的相关知识】
|
||||
{retrieved_knowledge_str}
|
||||
【上一次行动的详细情况和结果】
|
||||
{last_action_context}
|
||||
【时间和超时提示】
|
||||
{time_since_last_bot_message_info}{timeout_context}
|
||||
【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息)
|
||||
{chat_history_text}
|
||||
【你的的回忆】
|
||||
{retrieved_memory_str}
|
||||
|
||||
{spam_warning_info}
|
||||
|
||||
------
|
||||
可选行动类型以及解释:
|
||||
listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择
|
||||
direct_reply: 直接回复对方 (当有新消息需要处理时,通常应选择此项)
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择
|
||||
end_conversation: 结束对话,对方长时间没回复,繁忙,或者当你觉得对话告一段落时可以选择
|
||||
block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择
|
||||
|
||||
请以JSON格式输出你的决策:
|
||||
{{
|
||||
"action": "选择的行动类型 (必须是上面列表中的一个)",
|
||||
"reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的)"
|
||||
"reason": "选择该行动的原因 "
|
||||
}}
|
||||
|
||||
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
|
||||
|
||||
# Prompt(2): 上一次成功回复后,决定继续发言时的决策 Prompt
|
||||
PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚你已经回复了对方,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方:
|
||||
PROMPT_FOLLOW_UP = """
|
||||
当前时间:{current_time_str}
|
||||
现在{persona_text}正在与{sender_name}在qq上私聊,**并且刚刚{persona_text}已经回复了对方**
|
||||
他们的关系是:{relationship_text}
|
||||
{persona_text}现在的心情是是:{current_emotion_text}
|
||||
你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程,可以发送新消息,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方:
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str}
|
||||
【最近行动历史概要】
|
||||
{action_history_summary}
|
||||
【你想起来的相关知识】
|
||||
{retrieved_knowledge_str}
|
||||
【上一次行动的详细情况和结果】
|
||||
{last_action_context}
|
||||
【时间和超时提示】
|
||||
{time_since_last_bot_message_info}{timeout_context}
|
||||
【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息)
|
||||
{chat_history_text}
|
||||
【你的的回忆】
|
||||
{retrieved_memory_str}
|
||||
|
||||
{spam_warning_info}
|
||||
|
||||
------
|
||||
可选行动类型以及解释:
|
||||
wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择)。**重要:仅当没有未读消息时才能选择此项。**
|
||||
wait: 暂时不说话,留给对方交互空间,等待对方回复。
|
||||
listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个)
|
||||
send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题。**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言**
|
||||
send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题(但是注意看对话记录,如果对方已经没有回复你,end_conversation或wait可能更合适)。
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择
|
||||
end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、已经不再回复你消息、明显暗示或表达想结束聊天时,可以果断选择
|
||||
block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择
|
||||
|
||||
请以JSON格式输出你的决策:
|
||||
{{
|
||||
"action": "选择的行动类型 (必须是上面列表中的一个)",
|
||||
"reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。请说明你为什么选择继续发言而不是等待,以及打算发送什么类型的新消息连续发言,必须记录已经发言了几次)"
|
||||
"reason": "选择该行动的原因"
|
||||
}}
|
||||
|
||||
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
|
||||
|
||||
# 新增:Prompt(3): 决定是否在结束对话前发送告别语
|
||||
PROMPT_END_DECISION = """{persona_text}。刚刚你决定结束一场 QQ 私聊。
|
||||
PROMPT_END_DECISION = """
|
||||
当前时间:{current_time_str}
|
||||
现在{persona_text}与{sender_name}刚刚结束了一场qq私聊
|
||||
他们的关系是:{relationship_text}
|
||||
你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程:
|
||||
|
||||
【你们之前的聊天记录】
|
||||
|
||||
【他们之前的聊天记录】
|
||||
{chat_history_text}
|
||||
|
||||
你觉得你们的对话已经完整结束了吗?有时候,在对话自然结束后再说点什么可能会有点奇怪,但有时也可能需要一条简短的消息来圆满结束。
|
||||
如果觉得确实有必要再发一条简短、自然、符合你人设的告别消息(比如 "好,下次再聊~" 或 "嗯,先这样吧"),就输出 "yes"。
|
||||
你觉得他们的对话已经完整结束了吗?有时候,在对话自然结束后再说点什么可能会有点奇怪,但有时也可能需要一条简短的消息来圆满结束。
|
||||
如果觉得确实有必要再发一条简短、自然的告别消息(比如 "好,下次再聊~" 或 "嗯,先这样吧"),就输出 "yes"。
|
||||
如果觉得当前状态下直接结束对话更好,没有必要再发消息,就输出 "no"。
|
||||
|
||||
请以 JSON 格式输出你的选择:
|
||||
{{
|
||||
"say_bye": "yes/no",
|
||||
"reason": "选择 yes 或 no 的原因和内心想法 (简要说明)"
|
||||
"reason": "选择 yes 或 no 的原因和 (简要说明)"
|
||||
}}
|
||||
|
||||
注意:请严格按照 JSON 格式输出,不要包含任何其他内容。"""
|
||||
|
||||
# Prompt(4): 当 reply_generator 决定不发送消息后的反思决策 Prompt
|
||||
PROMPT_REFLECT_AND_ACT = """
|
||||
当前时间:{current_time_str}
|
||||
现在{persona_text}正在与{sender_name}在qq上私聊,刚刚{persona_text}打算发一条新消息,想了想还是不发了
|
||||
他们的关系是:{relationship_text}
|
||||
{persona_text}现在的心情是是:{current_emotion_text}
|
||||
你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方:
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str}
|
||||
【最近行动历史概要】
|
||||
{action_history_summary}
|
||||
【上一次行动的详细情况和结果】
|
||||
{last_action_context}
|
||||
【时间和超时提示】
|
||||
{time_since_last_bot_message_info}{timeout_context}
|
||||
【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息)
|
||||
{chat_history_text}
|
||||
|
||||
{spam_warning_info}
|
||||
|
||||
------
|
||||
可选行动类型以及解释:
|
||||
wait: 等待,暂时不说话。
|
||||
listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个)
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、已经不再回复你消息、明显暗示或表达想结束聊天时,可以果断选择
|
||||
block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择
|
||||
|
||||
请以JSON格式输出你的决策:
|
||||
{{
|
||||
"action": "选择的行动类型 (必须是上面列表中的一个)",
|
||||
"reason": "选择该行动的原因"
|
||||
}}
|
||||
|
||||
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
|
||||
|
||||
class ActionPlanner:
|
||||
"""行动规划器"""
|
||||
|
|
@ -133,7 +181,7 @@ class ActionPlanner:
|
|||
raise
|
||||
|
||||
# 获取个性化信息和机器人名称
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
# self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
# 获取 ChatObserver 实例 (单例模式)
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
|
|
@ -143,6 +191,7 @@ class ActionPlanner:
|
|||
observation_info: ObservationInfo,
|
||||
conversation_info: ConversationInfo,
|
||||
last_successful_reply_action: Optional[str],
|
||||
use_reflect_prompt: bool = False, # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
规划下一步行动。
|
||||
|
|
@ -164,14 +213,22 @@ class ActionPlanner:
|
|||
timeout_context = self._get_timeout_context(conversation_info)
|
||||
goals_str = self._build_goals_string(conversation_info)
|
||||
chat_history_text = await self._build_chat_history_text(observation_info)
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
# 获取 sender_name, relationship_text, current_emotion_text
|
||||
sender_name_str = getattr(observation_info, 'sender_name', '对方') # 从 observation_info 获取
|
||||
if not sender_name_str: sender_name_str = '对方' # 再次确保有默认值
|
||||
|
||||
relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。')
|
||||
current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。')
|
||||
|
||||
|
||||
persona_text = f"{self.name}。"
|
||||
action_history_summary, last_action_context = self._build_action_history_context(conversation_info)
|
||||
retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info(
|
||||
chat_history_text, self.private_name
|
||||
)
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] (ActionPlanner) 检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str and '出错' not in retrieved_knowledge_str else '无'}"
|
||||
)
|
||||
# retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info(
|
||||
# chat_history_text, self.private_name
|
||||
# )
|
||||
# logger.info(
|
||||
# f"[私聊][{self.private_name}] (ActionPlanner) 检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str and '出错' not in retrieved_knowledge_str else '无'}"
|
||||
# )
|
||||
except Exception as prep_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 准备 Prompt 输入时出错: {prep_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
|
@ -179,14 +236,40 @@ class ActionPlanner:
|
|||
|
||||
# --- 2. 选择并格式化 Prompt ---
|
||||
try:
|
||||
if last_successful_reply_action in ["direct_reply", "send_new_message"]:
|
||||
if use_reflect_prompt: # 新增的判断
|
||||
prompt_template = PROMPT_REFLECT_AND_ACT
|
||||
log_msg = "使用 PROMPT_REFLECT_AND_ACT (反思决策)"
|
||||
# 对于 PROMPT_REFLECT_AND_ACT,它不包含 send_new_message 选项,所以 spam_warning_message 中的相关提示可以调整或省略
|
||||
# 但为了保持占位符填充的一致性,我们仍然计算它
|
||||
spam_warning_message = ""
|
||||
if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少
|
||||
spam_warning_message = f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**"
|
||||
elif conversation_info.my_message_count > 2:
|
||||
spam_warning_message = f"💬【提示】**你之前已连续发送{str(conversation_info.my_message_count)}条消息。请注意保持对话平衡。**"
|
||||
|
||||
elif last_successful_reply_action in ["direct_reply", "send_new_message"]:
|
||||
prompt_template = PROMPT_FOLLOW_UP
|
||||
log_msg = "使用 PROMPT_FOLLOW_UP (追问决策)"
|
||||
spam_warning_message = ""
|
||||
if conversation_info.my_message_count > 5:
|
||||
spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**"
|
||||
elif conversation_info.my_message_count > 2:
|
||||
spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**"
|
||||
|
||||
else:
|
||||
prompt_template = PROMPT_INITIAL_REPLY
|
||||
log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)"
|
||||
spam_warning_message = "" # 初始回复时通常不需要刷屏警告
|
||||
|
||||
logger.debug(f"[私聊][{self.private_name}] {log_msg}")
|
||||
|
||||
current_time_value = "获取时间失败"
|
||||
if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str:
|
||||
current_time_value = observation_info.current_time_str
|
||||
|
||||
if spam_warning_message:
|
||||
spam_warning_message = f"\n{spam_warning_message}\n"
|
||||
|
||||
prompt = prompt_template.format(
|
||||
persona_text=persona_text,
|
||||
goals_str=goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。",
|
||||
|
|
@ -195,8 +278,13 @@ 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 "还没有聊天记录。",
|
||||
retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。",
|
||||
retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。",
|
||||
# retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。",
|
||||
# retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。",
|
||||
current_time_str=current_time_value,
|
||||
spam_warning_info=spam_warning_message,
|
||||
sender_name=sender_name_str,
|
||||
relationship_text=relationship_text_str,
|
||||
current_emotion_text=current_emotion_text_str
|
||||
)
|
||||
logger.debug(f"[私聊][{self.private_name}] 发送到LLM的最终提示词:\n------\n{prompt}\n------")
|
||||
except KeyError as fmt_key_err:
|
||||
|
|
@ -235,8 +323,19 @@ class ActionPlanner:
|
|||
|
||||
if initial_action == "end_conversation":
|
||||
try:
|
||||
time_str_for_end_decision = "获取时间失败"
|
||||
if (
|
||||
observation_info
|
||||
and hasattr(observation_info, "current_time_str")
|
||||
and observation_info.current_time_str
|
||||
):
|
||||
time_str_for_end_decision = observation_info.current_time_str
|
||||
final_action, final_reason = await self._handle_end_conversation_decision(
|
||||
persona_text, chat_history_text, initial_reason
|
||||
persona_text,
|
||||
chat_history_text, initial_reason,
|
||||
time_str_for_end_decision,
|
||||
sender_name_str=sender_name_str,
|
||||
relationship_text_str=relationship_text_str
|
||||
)
|
||||
except Exception as end_dec_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 处理结束对话决策时出错: {end_dec_err}")
|
||||
|
|
@ -251,7 +350,7 @@ class ActionPlanner:
|
|||
# final_reason = initial_reason
|
||||
|
||||
# --- 5. 验证最终行动类型 ---
|
||||
valid_actions = [
|
||||
valid_actions_default = [
|
||||
"direct_reply",
|
||||
"send_new_message",
|
||||
"wait",
|
||||
|
|
@ -261,7 +360,19 @@ class ActionPlanner:
|
|||
"block_and_ignore",
|
||||
"say_goodbye",
|
||||
]
|
||||
if final_action not in valid_actions:
|
||||
valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作
|
||||
"wait",
|
||||
"listening",
|
||||
"rethink_goal",
|
||||
"end_conversation",
|
||||
"block_and_ignore",
|
||||
# PROMPT_REFLECT_AND_ACT 也可以 end_conversation,然后也可能触发 say_goodbye
|
||||
"say_goodbye",
|
||||
]
|
||||
|
||||
current_valid_actions = valid_actions_reflect if use_reflect_prompt else valid_actions_default
|
||||
|
||||
if final_action not in current_valid_actions:
|
||||
logger.warning(f"[私聊][{self.private_name}] LLM 返回了未知的行动类型: '{final_action}',强制改为 wait")
|
||||
final_reason = f"(原始行动'{final_action}'无效,已强制改为wait) {final_reason}"
|
||||
final_action = "wait" # 遇到无效动作,默认等待
|
||||
|
|
@ -317,7 +428,7 @@ class ActionPlanner:
|
|||
and "思考接下来要做什么" in last_goal_text
|
||||
):
|
||||
wait_time_str = last_goal_text.split("分钟,")[0].replace("你等待了", "").strip()
|
||||
timeout_context = f"重要提示:对方已经长时间(约 {wait_time_str} 分钟)没有回复你的消息了,请基于此情况规划下一步。\n"
|
||||
timeout_context = f"重要提示:对方已经长时间(约 {wait_time_str} 分钟)没有回复你的消息了,对方可能去忙了,也可能在对方看来对话已经结束。请基于此情况规划下一步。\n"
|
||||
logger.debug(f"[私聊][{self.private_name}] 检测到超时目标: {last_goal_text}")
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 检查超时目标时属性错误: {e}")
|
||||
|
|
@ -389,6 +500,10 @@ class ActionPlanner:
|
|||
f"\n--- 以下是 {other_unread_count} 条你需要处理的新消息 ---\n{new_messages_str}\n------\n"
|
||||
)
|
||||
logger.debug(f"[私聊][{self.private_name}] 向 LLM 追加了 {other_unread_count} 条未读消息。")
|
||||
else:
|
||||
chat_history_text += (
|
||||
f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n"
|
||||
)
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 构建聊天记录文本时属性错误: {e}")
|
||||
chat_history_text = "[获取聊天记录时出错]\n"
|
||||
|
|
@ -446,11 +561,11 @@ class ActionPlanner:
|
|||
# --- Helper method for handling end_conversation decision ---
|
||||
|
||||
async def _handle_end_conversation_decision(
|
||||
self, persona_text: str, chat_history_text: str, initial_reason: str
|
||||
self, persona_text: str, chat_history_text: str, initial_reason: str, current_time_str: str, sender_name_str: str, relationship_text_str: str
|
||||
) -> Tuple[str, str]:
|
||||
"""处理结束对话前的告别决策"""
|
||||
logger.info(f"[私聊][{self.private_name}] 初步规划结束对话,进入告别决策...")
|
||||
end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text)
|
||||
end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text,current_time_str=current_time_str,sender_name = sender_name_str, relationship_text = relationship_text_str)
|
||||
logger.debug(f"[私聊][{self.private_name}] 发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------")
|
||||
llm_start_time = time.time()
|
||||
end_content, _ = await self.llm.generate_response_async(end_decision_prompt)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,779 @@
|
|||
import time
|
||||
import asyncio
|
||||
import datetime
|
||||
import traceback
|
||||
import json
|
||||
from typing import Optional, Set, TYPE_CHECKING
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from .pfc_types import ConversationState
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .conversation import Conversation # 用于类型提示以避免循环导入
|
||||
|
||||
logger = get_logger("pfc_actions")
|
||||
|
||||
|
||||
async def _send_reply_internal(conversation_instance: "Conversation") -> bool:
|
||||
"""
|
||||
内部辅助函数,用于发送 conversation_instance.generated_reply 中的内容。
|
||||
这之前是 Conversation 类中的 _send_reply 方法。
|
||||
"""
|
||||
# 检查是否有内容可发送
|
||||
if not conversation_instance.generated_reply:
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] 没有生成回复内容,无法发送。")
|
||||
return False
|
||||
# 检查发送器和聊天流是否已初始化
|
||||
if not conversation_instance.direct_sender:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] DirectMessageSender 未初始化,无法发送。")
|
||||
return False
|
||||
if not conversation_instance.chat_stream:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] ChatStream 未初始化,无法发送。")
|
||||
return False
|
||||
|
||||
try:
|
||||
reply_content = conversation_instance.generated_reply
|
||||
# 调用发送器发送消息,不指定回复对象
|
||||
await conversation_instance.direct_sender.send_message(
|
||||
chat_stream=conversation_instance.chat_stream,
|
||||
content=reply_content,
|
||||
reply_to_message=None, # 私聊通常不需要引用回复
|
||||
)
|
||||
# 自身发言数量累计 +1
|
||||
if conversation_instance.conversation_info: # 确保 conversation_info 存在
|
||||
conversation_instance.conversation_info.my_message_count += 1
|
||||
# 发送成功后,将状态设置回分析,准备下一轮规划
|
||||
conversation_instance.state = ConversationState.ANALYZING
|
||||
return True # 返回成功
|
||||
except Exception as e:
|
||||
# 捕获发送过程中的异常
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 发送消息时失败: {str(e)}")
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] {traceback.format_exc()}")
|
||||
conversation_instance.state = ConversationState.ERROR # 发送失败标记错误状态
|
||||
return False # 返回失败
|
||||
|
||||
|
||||
async def handle_action(
|
||||
conversation_instance: "Conversation",
|
||||
action: str,
|
||||
reason: str,
|
||||
observation_info: Optional[ObservationInfo],
|
||||
conversation_info: Optional[ConversationInfo],
|
||||
):
|
||||
"""
|
||||
处理由 ActionPlanner 规划出的具体行动。
|
||||
这之前是 Conversation 类中的 _handle_action 方法。
|
||||
"""
|
||||
# 检查初始化状态
|
||||
if not conversation_instance._initialized:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下处理动作 '{action}'。")
|
||||
return
|
||||
|
||||
# 确保 observation_info 和 conversation_info 不为 None
|
||||
if not observation_info:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] ObservationInfo 为空,无法处理动作 '{action}'。")
|
||||
# 在 conversation_info 和 done_action 存在时更新状态
|
||||
if conversation_info and hasattr(conversation_info, "done_action") and conversation_info.done_action:
|
||||
conversation_info.done_action[-1].update(
|
||||
{
|
||||
"status": "error",
|
||||
"final_reason": "ObservationInfo is None",
|
||||
}
|
||||
)
|
||||
conversation_instance.state = ConversationState.ERROR
|
||||
return
|
||||
if not conversation_info: # conversation_info 在这里是必需的
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] ConversationInfo 为空,无法处理动作 '{action}'。")
|
||||
conversation_instance.state = ConversationState.ERROR
|
||||
return
|
||||
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 开始处理动作: {action}, 原因: {reason}")
|
||||
action_start_time = time.time() # 记录动作开始时间
|
||||
|
||||
# --- 准备动作历史记录条目 ---
|
||||
current_action_record = {
|
||||
"action": action,
|
||||
"plan_reason": reason, # 记录规划时的原因
|
||||
"status": "start", # 初始状态为"开始"
|
||||
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 记录开始时间
|
||||
"final_reason": None, # 最终结果的原因,将在 finally 中设置
|
||||
}
|
||||
# 安全地添加到历史记录列表
|
||||
if not hasattr(conversation_info, "done_action") or conversation_info.done_action is None: # 防御性检查
|
||||
conversation_info.done_action = []
|
||||
conversation_info.done_action.append(current_action_record)
|
||||
# 获取当前记录在列表中的索引,方便后续更新状态
|
||||
action_index = len(conversation_info.done_action) - 1
|
||||
|
||||
# --- 初始化动作执行状态变量 ---
|
||||
action_successful: bool = False # 标记动作是否成功执行
|
||||
final_status: str = "recall" # 动作最终状态,默认为 recall (表示未成功或需重试)
|
||||
final_reason: str = "动作未成功执行" # 动作最终原因
|
||||
|
||||
# 在此声明变量以避免 UnboundLocalError
|
||||
is_suitable: bool = False
|
||||
generated_content_for_check_or_send: str = ""
|
||||
check_reason: str = "未进行检查"
|
||||
need_replan_from_checker: bool = False
|
||||
should_send_reply: bool = True # 默认需要发送 (对于 direct_reply)
|
||||
is_send_decision_from_rg: bool = False # 标记 send_new_message 的决策是否来自 ReplyGenerator
|
||||
|
||||
try:
|
||||
# --- 根据不同的 action 类型执行相应的逻辑 ---
|
||||
|
||||
# 1. 处理需要生成、检查、发送的动作
|
||||
if action in ["direct_reply", "send_new_message"]:
|
||||
max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) # 最多尝试次数 (可配置)
|
||||
reply_attempt_count: int = 0
|
||||
# is_suitable, generated_content_for_check_or_send, check_reason, need_replan_from_checker, should_send_reply, is_send_decision_from_rg 已在外部声明
|
||||
|
||||
while reply_attempt_count < max_reply_attempts and not is_suitable and not need_replan_from_checker:
|
||||
reply_attempt_count += 1
|
||||
log_prefix = f"[私聊][{conversation_instance.private_name}] 尝试生成/检查 '{action}' 回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..."
|
||||
logger.info(log_prefix)
|
||||
|
||||
conversation_instance.state = ConversationState.GENERATING
|
||||
if not conversation_instance.reply_generator:
|
||||
raise RuntimeError("ReplyGenerator 未初始化")
|
||||
|
||||
raw_llm_output = await conversation_instance.reply_generator.generate(
|
||||
observation_info, conversation_info, action_type=action
|
||||
)
|
||||
logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'")
|
||||
|
||||
text_to_process = raw_llm_output # 默认情况下,处理原始输出
|
||||
|
||||
if action == "send_new_message":
|
||||
is_send_decision_from_rg = True # 标记这是 send_new_message 的决策过程
|
||||
parsed_json = None
|
||||
try:
|
||||
# 尝试解析JSON
|
||||
parsed_json = json.loads(raw_llm_output)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"{log_prefix} ReplyGenerator 返回的不是有效的JSON: {raw_llm_output}")
|
||||
# 如果JSON解析失败,视为RG决定不发送,并给出原因
|
||||
conversation_info.last_reply_rejection_reason = "回复生成器未返回有效JSON"
|
||||
conversation_info.last_rejected_reply_content = raw_llm_output
|
||||
should_send_reply = False
|
||||
text_to_process = "no" # 或者一个特定的错误标记
|
||||
|
||||
if parsed_json: # 如果成功解析
|
||||
send_decision = parsed_json.get("send", "no").lower()
|
||||
generated_text_from_json = parsed_json.get("txt", "no")
|
||||
|
||||
if send_decision == "yes":
|
||||
should_send_reply = True
|
||||
text_to_process = generated_text_from_json
|
||||
logger.info(f"{log_prefix} ReplyGenerator 决定发送消息。内容: '{text_to_process[:100]}...'")
|
||||
else: # send_decision is "no"
|
||||
should_send_reply = False
|
||||
text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no"
|
||||
logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。")
|
||||
# 既然RG决定不发送,就直接跳出重试循环
|
||||
break
|
||||
|
||||
# 如果 ReplyGenerator 在 send_new_message 动作中决定不发送,则跳出重试循环
|
||||
if action == "send_new_message" and not should_send_reply:
|
||||
break
|
||||
|
||||
generated_content_for_check_or_send = text_to_process
|
||||
|
||||
# 检查生成的内容是否有效
|
||||
if (
|
||||
not generated_content_for_check_or_send
|
||||
or generated_content_for_check_or_send.startswith("抱歉")
|
||||
or generated_content_for_check_or_send.strip() == ""
|
||||
or (
|
||||
action == "send_new_message"
|
||||
and generated_content_for_check_or_send == "no"
|
||||
and should_send_reply
|
||||
)
|
||||
): # RG决定发送但文本为"no"或空
|
||||
warning_msg = f"{log_prefix} 生成内容无效或为错误提示"
|
||||
if action == "send_new_message" and generated_content_for_check_or_send == "no": # 特殊情况日志
|
||||
warning_msg += " (ReplyGenerator决定发送但文本为'no')"
|
||||
|
||||
logger.warning(warning_msg + ",将进行下一次尝试 (如果适用)。")
|
||||
check_reason = "生成内容无效或选择不发送" # 统一原因
|
||||
conversation_info.last_reply_rejection_reason = check_reason
|
||||
conversation_info.last_rejected_reply_content = generated_content_for_check_or_send
|
||||
|
||||
await asyncio.sleep(0.5) # 暂停一下
|
||||
continue # 直接进入下一次循环尝试
|
||||
|
||||
# --- 内容检查 ---
|
||||
conversation_instance.state = ConversationState.CHECKING
|
||||
if not conversation_instance.reply_checker:
|
||||
raise RuntimeError("ReplyChecker 未初始化")
|
||||
|
||||
# 准备检查器所需参数
|
||||
current_goal_str = ""
|
||||
if conversation_info.goal_list: # 确保 goal_list 存在且不为空
|
||||
goal_item = conversation_info.goal_list[-1]
|
||||
if isinstance(goal_item, dict):
|
||||
current_goal_str = goal_item.get("goal", "")
|
||||
elif isinstance(goal_item, str):
|
||||
current_goal_str = goal_item
|
||||
|
||||
chat_history_for_check = getattr(observation_info, "chat_history", [])
|
||||
chat_history_text_for_check = getattr(observation_info, "chat_history_str", "")
|
||||
current_retry_for_checker = reply_attempt_count - 1 # retry_count 从0开始
|
||||
current_time_value_for_check = observation_info.current_time_str or "获取时间失败"
|
||||
|
||||
# 调用检查器
|
||||
if global_config.enable_pfc_reply_checker:
|
||||
logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...")
|
||||
(
|
||||
is_suitable,
|
||||
check_reason,
|
||||
need_replan_from_checker,
|
||||
) = await conversation_instance.reply_checker.check(
|
||||
reply=generated_content_for_check_or_send,
|
||||
goal=current_goal_str,
|
||||
chat_history=chat_history_for_check, # 使用完整的历史记录列表
|
||||
chat_history_text=chat_history_text_for_check, # 可以是截断的文本
|
||||
current_time_str=current_time_value_for_check,
|
||||
retry_count=current_retry_for_checker, # 传递当前重试次数
|
||||
)
|
||||
logger.info(
|
||||
f"{log_prefix} ReplyChecker 结果: 合适={is_suitable}, 原因='{check_reason}', 需重规划={need_replan_from_checker}"
|
||||
)
|
||||
else: # 如果配置关闭
|
||||
is_suitable = True
|
||||
check_reason = "ReplyChecker 已通过配置关闭"
|
||||
need_replan_from_checker = False
|
||||
logger.debug(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。")
|
||||
|
||||
# 处理检查结果
|
||||
if not is_suitable:
|
||||
conversation_info.last_reply_rejection_reason = check_reason
|
||||
conversation_info.last_rejected_reply_content = generated_content_for_check_or_send
|
||||
|
||||
# 如果是机器人自身复读,且检查器认为不需要重规划 (这是新版 ReplyChecker 的逻辑)
|
||||
if check_reason == "机器人尝试发送重复消息" and not need_replan_from_checker:
|
||||
logger.warning(
|
||||
f"{log_prefix} 回复因自身重复被拒绝: {check_reason}。将使用相同 Prompt 类型重试。"
|
||||
)
|
||||
if reply_attempt_count < max_reply_attempts: # 还有尝试次数
|
||||
await asyncio.sleep(0.5) # 暂停一下
|
||||
continue # 进入下一次重试
|
||||
else: # 达到最大次数
|
||||
logger.warning(f"{log_prefix} 即使是复读,也已达到最大尝试次数。")
|
||||
break # 结束循环,按失败处理
|
||||
elif (
|
||||
not need_replan_from_checker and reply_attempt_count < max_reply_attempts
|
||||
): # 其他不合适原因,但无需重规划,且可重试
|
||||
logger.warning(f"{log_prefix} 回复不合适,原因: {check_reason}。将进行下一次尝试。")
|
||||
await asyncio.sleep(0.5) # 暂停一下
|
||||
continue # 进入下一次重试
|
||||
else: # 需要重规划,或达到最大次数
|
||||
logger.warning(f"{log_prefix} 回复不合适且(需要重规划或已达最大次数)。原因: {check_reason}")
|
||||
break # 结束循环,将在循环外部处理
|
||||
else: # is_suitable is True
|
||||
# 找到了合适的回复
|
||||
conversation_info.last_reply_rejection_reason = None # 清除之前的拒绝原因
|
||||
conversation_info.last_rejected_reply_content = None
|
||||
break # 成功,跳出循环
|
||||
|
||||
# --- 循环结束后处理 ---
|
||||
if action == "send_new_message" and not should_send_reply and is_send_decision_from_rg:
|
||||
# 这是 reply_generator 决定不发送的情况
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': ReplyGenerator 决定不发送消息。"
|
||||
)
|
||||
final_status = "done_no_reply" # 一个新的状态,表示动作完成但无回复
|
||||
final_reason = "回复生成器决定不发送消息"
|
||||
action_successful = True # 动作本身(决策)是成功的
|
||||
|
||||
# 清除追问状态,因为没有实际发送
|
||||
conversation_info.last_successful_reply_action = None
|
||||
conversation_info.my_message_count = 0 # 重置连续发言计数
|
||||
# 后续的 plan 循环会检测到这个 "done_no_reply" 状态并使用反思 prompt
|
||||
|
||||
elif is_suitable: # 适用于 direct_reply 或 (send_new_message 且 RG决定发送并通过检查)
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 找到合适的回复,准备发送。")
|
||||
# conversation_info.last_reply_rejection_reason = None # 已在循环内清除
|
||||
# conversation_info.last_rejected_reply_content = None
|
||||
conversation_instance.generated_reply = generated_content_for_check_or_send # 使用检查通过的内容
|
||||
timestamp_before_sending = time.time()
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}"
|
||||
)
|
||||
conversation_instance.state = ConversationState.SENDING
|
||||
send_success = await _send_reply_internal(conversation_instance) # 调用重构后的发送函数
|
||||
send_end_time = time.time() # 记录发送完成时间
|
||||
|
||||
if send_success:
|
||||
action_successful = True
|
||||
final_status = "done" # 明确设置 final_status
|
||||
final_reason = "成功发送" # 明确设置 final_reason
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 成功发送回复.")
|
||||
|
||||
# --- 新增:将机器人发送的消息添加到 ObservationInfo 的 chat_history ---
|
||||
if (
|
||||
observation_info and conversation_instance.bot_qq_str
|
||||
): # 确保 observation_info 和 bot_qq_str 存在
|
||||
bot_message_dict = {
|
||||
"message_id": f"bot_sent_{send_end_time}", # 生成一个唯一ID
|
||||
"time": send_end_time,
|
||||
"user_info": { # 构造机器人的 UserInfo
|
||||
"user_id": conversation_instance.bot_qq_str,
|
||||
"user_nickname": global_config.BOT_NICKNAME, # 或者 conversation_instance.name
|
||||
"platform": conversation_instance.chat_stream.platform
|
||||
if conversation_instance.chat_stream
|
||||
else "unknown_platform",
|
||||
},
|
||||
"processed_plain_text": conversation_instance.generated_reply,
|
||||
"detailed_plain_text": conversation_instance.generated_reply, # 简单处理
|
||||
# 根据你的消息字典结构,可能还需要其他字段
|
||||
}
|
||||
observation_info.chat_history.append(bot_message_dict)
|
||||
observation_info.chat_history_count = len(observation_info.chat_history)
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] {global_config.BOT_NICKNAME}发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}"
|
||||
)
|
||||
|
||||
# 可选:如果 chat_history 过长,进行修剪 (例如,保留最近N条)
|
||||
max_history_len = getattr(global_config, "pfc_max_chat_history_for_checker", 50) # 例如,可配置
|
||||
if len(observation_info.chat_history) > max_history_len:
|
||||
observation_info.chat_history = observation_info.chat_history[-max_history_len:]
|
||||
observation_info.chat_history_count = len(observation_info.chat_history) # 更新计数
|
||||
|
||||
# 更新 chat_history_str (如果 ReplyChecker 也依赖这个字符串)
|
||||
# 这个更新可能比较消耗资源,如果 checker 只用列表,可以考虑优化此处
|
||||
history_slice_for_str = observation_info.chat_history[-30:] # 例如最近30条
|
||||
try:
|
||||
observation_info.chat_history_str = await build_readable_messages(
|
||||
history_slice_for_str,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
except Exception as e_build_hist:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str 时出错: {e_build_hist}"
|
||||
)
|
||||
observation_info.chat_history_str = "[构建聊天记录出错]"
|
||||
# --- 新增结束 ---
|
||||
|
||||
# 更新 idle_conversation_starter 的最后消息时间
|
||||
# (避免在发送消息后很快触发主动聊天)
|
||||
if conversation_instance.idle_chat:
|
||||
await conversation_instance.idle_chat.update_last_message_time(send_end_time)
|
||||
|
||||
# 清理已处理的未读消息 (只清理在发送这条回复之前的、来自他人的消息)
|
||||
current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", [])
|
||||
message_ids_to_clear: Set[str] = set()
|
||||
for msg in current_unprocessed_messages:
|
||||
msg_time = msg.get("time")
|
||||
msg_id = msg.get("message_id")
|
||||
sender_id_info = msg.get("user_info", {}) # 安全获取 user_info
|
||||
sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None # 安全获取 sender_id
|
||||
|
||||
if (
|
||||
msg_id # 确保 msg_id 存在
|
||||
and msg_time # 确保 msg_time 存在
|
||||
and sender_id != conversation_instance.bot_qq_str # 确保是对方的消息
|
||||
and msg_time < timestamp_before_sending # 只清理发送前的
|
||||
):
|
||||
message_ids_to_clear.add(msg_id)
|
||||
|
||||
if message_ids_to_clear:
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 准备清理 {len(message_ids_to_clear)} 条发送前(他人)消息: {message_ids_to_clear}"
|
||||
)
|
||||
await observation_info.clear_processed_messages(message_ids_to_clear)
|
||||
else:
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 没有需要清理的发送前(他人)消息。")
|
||||
|
||||
# 更新追问状态 和 关系/情绪状态
|
||||
other_new_msg_count_during_planning = getattr(
|
||||
conversation_info, "other_new_messages_during_planning_count", 0
|
||||
)
|
||||
|
||||
# 如果是 direct_reply 且规划期间有他人新消息,则下次不追问
|
||||
if other_new_msg_count_during_planning > 0 and action == "direct_reply":
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。"
|
||||
)
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# conversation_info.my_message_count 不在此处重置,因为它刚发了一条
|
||||
elif action == "direct_reply" or action == "send_new_message": # 成功发送后
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。"
|
||||
)
|
||||
conversation_info.last_successful_reply_action = action
|
||||
|
||||
# 更新实例消息计数和关系/情绪
|
||||
if conversation_info: # 再次确认
|
||||
conversation_info.current_instance_message_count += 1
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 实例消息计数({global_config.BOT_NICKNAME}发送后)增加到: {conversation_info.current_instance_message_count}"
|
||||
)
|
||||
|
||||
if conversation_instance.relationship_updater: # 确保存在
|
||||
await conversation_instance.relationship_updater.update_relationship_incremental(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
)
|
||||
|
||||
sent_reply_summary = (
|
||||
conversation_instance.generated_reply[:50]
|
||||
if conversation_instance.generated_reply
|
||||
else "空回复"
|
||||
)
|
||||
event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'"
|
||||
if conversation_instance.emotion_updater: # 确保存在
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
event_description=event_for_emotion_update,
|
||||
)
|
||||
else: # 发送失败
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 发送回复失败。")
|
||||
final_status = "recall" # 标记为 recall 或 error
|
||||
final_reason = "发送回复时失败"
|
||||
action_successful = False # 确保 action_successful 为 False
|
||||
# 发送失败,重置追问状态和计数
|
||||
conversation_info.last_successful_reply_action = None
|
||||
conversation_info.my_message_count = 0
|
||||
|
||||
elif need_replan_from_checker: # 如果检查器要求重规划
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 因 ReplyChecker 要求而被取消,将重新规划。原因: {check_reason}"
|
||||
)
|
||||
final_status = "recall" # 标记为 recall
|
||||
final_reason = f"回复检查要求重新规划: {check_reason}"
|
||||
# 重置追问状态,因为没有成功发送
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# my_message_count 保持不变,因为没有成功发送
|
||||
|
||||
else: # 达到最大尝试次数仍未找到合适回复 (is_suitable is False and not need_replan_from_checker)
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 达到最大尝试次数 ({max_reply_attempts}),未能生成/检查通过合适的回复。最终原因: {check_reason}"
|
||||
)
|
||||
final_status = "recall" # 标记为 recall
|
||||
final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}"
|
||||
action_successful = False # 确保 action_successful 为 False
|
||||
# 重置追问状态
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# my_message_count 保持不变
|
||||
|
||||
# 2. 处理发送告别语动作 (保持简单,不加重试)
|
||||
elif action == "say_goodbye":
|
||||
conversation_instance.state = ConversationState.GENERATING
|
||||
if not conversation_instance.reply_generator:
|
||||
raise RuntimeError("ReplyGenerator 未初始化")
|
||||
# 生成告别语
|
||||
generated_content = await conversation_instance.reply_generator.generate(
|
||||
observation_info,
|
||||
conversation_info,
|
||||
action_type=action, # action_type='say_goodbye'
|
||||
)
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 生成内容: '{generated_content[:100]}...'"
|
||||
)
|
||||
|
||||
# 检查生成内容
|
||||
if not generated_content or generated_content.startswith("抱歉"):
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 生成内容为空或为错误提示,取消发送。"
|
||||
)
|
||||
final_reason = "生成内容无效"
|
||||
# 即使生成失败,也按计划结束对话
|
||||
final_status = "done" # 标记为 done,因为目的是结束
|
||||
conversation_instance.should_continue = False # 停止对话
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 告别语生成失败,仍按计划结束对话。")
|
||||
else:
|
||||
# 发送告别语
|
||||
conversation_instance.generated_reply = generated_content
|
||||
timestamp_before_sending = time.time()
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}"
|
||||
)
|
||||
conversation_instance.state = ConversationState.SENDING
|
||||
send_success = await _send_reply_internal(conversation_instance) # 调用重构后的发送函数
|
||||
send_end_time = time.time()
|
||||
|
||||
if send_success:
|
||||
action_successful = True # 标记成功
|
||||
# final_status 和 final_reason 会在 finally 中设置
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 成功发送告别语,即将停止对话实例。")
|
||||
# 更新 idle_conversation_starter 的最后消息时间
|
||||
# (避免在发送消息后很快触发主动聊天)
|
||||
if conversation_instance.idle_chat:
|
||||
await conversation_instance.idle_chat.update_last_message_time(send_end_time)
|
||||
# 清理发送前的消息 (虽然通常是最后一条,但保持逻辑一致)
|
||||
current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", [])
|
||||
message_ids_to_clear: Set[str] = set()
|
||||
for msg in current_unprocessed_messages:
|
||||
msg_time = msg.get("time")
|
||||
msg_id = msg.get("message_id")
|
||||
sender_id_info = msg.get("user_info", {})
|
||||
sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None
|
||||
if (
|
||||
msg_id
|
||||
and msg_time
|
||||
and sender_id != conversation_instance.bot_qq_str # 不是自己的消息
|
||||
and msg_time < timestamp_before_sending # 发送前
|
||||
):
|
||||
message_ids_to_clear.add(msg_id)
|
||||
if message_ids_to_clear:
|
||||
await observation_info.clear_processed_messages(message_ids_to_clear)
|
||||
|
||||
# 更新关系和情绪
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.current_instance_message_count += 1
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 实例消息计数(告别语后)增加到: {conversation_info.current_instance_message_count}"
|
||||
)
|
||||
|
||||
sent_reply_summary = (
|
||||
conversation_instance.generated_reply[:50]
|
||||
if conversation_instance.generated_reply
|
||||
else "空回复"
|
||||
)
|
||||
event_for_emotion_update = f"你发送了告别消息: '{sent_reply_summary}...'"
|
||||
if conversation_instance.emotion_updater: # 确保存在
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
event_description=event_for_emotion_update,
|
||||
)
|
||||
# 发送成功后结束对话
|
||||
conversation_instance.should_continue = False
|
||||
else:
|
||||
# 发送失败
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 发送告别语失败。")
|
||||
final_status = "recall" # 或 "error"
|
||||
final_reason = "发送告别语失败"
|
||||
# 发送失败不能结束对话,让其自然流转或由其他逻辑结束
|
||||
conversation_instance.should_continue = True # 保持 should_continue
|
||||
|
||||
# 3. 处理重新思考目标动作
|
||||
elif action == "rethink_goal":
|
||||
conversation_instance.state = ConversationState.RETHINKING
|
||||
if not conversation_instance.goal_analyzer:
|
||||
raise RuntimeError("GoalAnalyzer 未初始化")
|
||||
# 调用 GoalAnalyzer 分析并更新目标
|
||||
await conversation_instance.goal_analyzer.analyze_goal(conversation_info, observation_info)
|
||||
action_successful = True # 标记成功
|
||||
event_for_emotion_update = "你重新思考了对话目标和方向"
|
||||
if (
|
||||
conversation_instance.emotion_updater and conversation_info and observation_info
|
||||
): # 确保updater和info都存在
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
event_description=event_for_emotion_update,
|
||||
)
|
||||
|
||||
# 4. 处理倾听动作
|
||||
elif action == "listening":
|
||||
conversation_instance.state = ConversationState.LISTENING
|
||||
if not conversation_instance.waiter:
|
||||
raise RuntimeError("Waiter 未初始化")
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'listening': 进入倾听状态...")
|
||||
# 调用 Waiter 的倾听等待方法,内部会处理超时
|
||||
await conversation_instance.waiter.wait_listening(conversation_info) # 直接传递 conversation_info
|
||||
action_successful = True # listening 动作本身执行即视为成功,后续由新消息或超时驱动
|
||||
event_for_emotion_update = "你决定耐心倾听对方的发言"
|
||||
if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
event_description=event_for_emotion_update,
|
||||
)
|
||||
|
||||
# 5. 处理结束对话动作
|
||||
elif action == "end_conversation":
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话..."
|
||||
)
|
||||
action_successful = True # 标记成功
|
||||
conversation_instance.should_continue = False # 设置标志以退出循环
|
||||
|
||||
# 6. 处理屏蔽忽略动作
|
||||
elif action == "block_and_ignore":
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'block_and_ignore': 不想再理你了...")
|
||||
ignore_duration_seconds = 10 * 60 # 忽略 10 分钟,可配置
|
||||
conversation_instance.ignore_until_timestamp = time.time() + ignore_duration_seconds
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 将忽略此对话直到: {datetime.datetime.fromtimestamp(conversation_instance.ignore_until_timestamp)}"
|
||||
)
|
||||
conversation_instance.state = ConversationState.IGNORED # 设置忽略状态
|
||||
action_successful = True # 标记成功
|
||||
event_for_emotion_update = "当前对话让你感到不适,你决定暂时不再理会对方"
|
||||
if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
event_description=event_for_emotion_update,
|
||||
)
|
||||
|
||||
# 7. 处理等待动作
|
||||
elif action == "wait":
|
||||
conversation_instance.state = ConversationState.WAITING
|
||||
if not conversation_instance.waiter:
|
||||
raise RuntimeError("Waiter 未初始化")
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'wait': 进入等待状态...")
|
||||
# 调用 Waiter 的常规等待方法,内部处理超时
|
||||
# wait 方法返回是否超时 (True=超时, False=未超时/被新消息中断)
|
||||
timeout_occurred = await conversation_instance.waiter.wait(conversation_info) # 直接传递 conversation_info
|
||||
action_successful = True # wait 动作本身执行即视为成功
|
||||
event_for_emotion_update = ""
|
||||
if timeout_occurred: # 假设 timeout_occurred 能正确反映是否超时
|
||||
event_for_emotion_update = "你等待对方回复,但对方长时间没有回应"
|
||||
else:
|
||||
event_for_emotion_update = "你选择等待对方的回复(对方可能很快回复了)"
|
||||
|
||||
if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_info,
|
||||
observation_info=observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在
|
||||
event_description=event_for_emotion_update,
|
||||
)
|
||||
# wait 动作完成后不需要清理消息,等待新消息或超时触发重新规划
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] Wait 动作完成,无需在此清理消息。")
|
||||
|
||||
# 8. 处理未知的动作类型
|
||||
else:
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] 未知的动作类型: {action}")
|
||||
final_status = "recall" # 未知动作标记为 recall
|
||||
final_reason = f"未知的动作类型: {action}"
|
||||
|
||||
# --- 重置非回复动作的追问状态 ---
|
||||
# 确保执行完非回复动作后,下一次规划不会错误地进入追问逻辑
|
||||
if action not in ["direct_reply", "send_new_message", "say_goodbye"]:
|
||||
conversation_info.last_successful_reply_action = None
|
||||
# 清理可能残留的拒绝信息
|
||||
conversation_info.last_reply_rejection_reason = None
|
||||
conversation_info.last_rejected_reply_content = None
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# 处理任务被取消的异常
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时被取消。")
|
||||
final_status = "cancelled"
|
||||
final_reason = "动作处理被取消"
|
||||
# 取消时也重置追问状态
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.last_successful_reply_action = None
|
||||
raise # 重新抛出 CancelledError,让上层知道任务被取消
|
||||
except Exception as handle_err:
|
||||
# 捕获处理动作过程中的其他所有异常
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时出错: {handle_err}")
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] {traceback.format_exc()}")
|
||||
final_status = "error" # 标记为错误状态
|
||||
final_reason = f"处理动作时出错: {handle_err}"
|
||||
conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误
|
||||
# 出错时重置追问状态
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.last_successful_reply_action = None
|
||||
|
||||
finally:
|
||||
# --- 无论成功与否,都执行 ---
|
||||
|
||||
# 1. 重置临时存储的计数值
|
||||
if conversation_info: # 确保 conversation_info 存在
|
||||
conversation_info.other_new_messages_during_planning_count = 0
|
||||
|
||||
# 2. 更新动作历史记录的最终状态和原因
|
||||
# 优化:如果动作成功但状态仍是默认的 recall,则更新为 done
|
||||
if action_successful:
|
||||
# 如果动作标记为成功,但 final_status 仍然是初始的 "recall" 或者 "start"
|
||||
# (因为可能在try块中成功执行了但没有显式更新 final_status 为 "done")
|
||||
# 或者是 "done_no_reply" 这种特殊的成功状态
|
||||
if (
|
||||
final_status in ["recall", "start"] and action != "send_new_message"
|
||||
): # send_new_message + no_reply 是特殊成功
|
||||
final_status = "done"
|
||||
if not final_reason or final_reason == "动作未成功执行": # 避免覆盖已有的具体成功原因
|
||||
# 为不同类型的成功动作提供更具体的默认成功原因
|
||||
if action == "wait":
|
||||
# 检查 conversation_info.goal_list 是否存在且不为空
|
||||
timeout_occurred = (
|
||||
any(
|
||||
"分钟," in g.get("goal", "")
|
||||
for g in conversation_info.goal_list
|
||||
if isinstance(g, dict)
|
||||
)
|
||||
if conversation_info and conversation_info.goal_list
|
||||
else False
|
||||
)
|
||||
final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)")
|
||||
elif action == "listening":
|
||||
final_reason = "进入倾听状态"
|
||||
elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]:
|
||||
final_reason = f"成功执行 {action}"
|
||||
elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case
|
||||
final_reason = "成功发送"
|
||||
else:
|
||||
final_reason = f"动作 {action} 成功完成"
|
||||
# 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason
|
||||
|
||||
else: # action_successful is False
|
||||
# 如果动作标记为失败,且 final_status 还是 "recall" (初始值) 或 "start"
|
||||
if final_status in ["recall", "start"]:
|
||||
# 尝试从 conversation_info 中获取更具体的失败原因(例如 checker 的原因)
|
||||
# 这个 specific_rejection_reason 是在 try 块中被设置的
|
||||
specific_rejection_reason = getattr(conversation_info, "last_reply_rejection_reason", None)
|
||||
rejected_content = getattr(conversation_info, "last_rejected_reply_content", None)
|
||||
|
||||
if specific_rejection_reason: # 如果有更具体的原因
|
||||
final_reason = f"执行失败: {specific_rejection_reason}"
|
||||
if (
|
||||
rejected_content and specific_rejection_reason == "机器人尝试发送重复消息"
|
||||
): # 对复读提供更清晰的日志
|
||||
final_reason += f" (内容: '{rejected_content[:30]}...')"
|
||||
elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因,且当前原因还是默认的
|
||||
final_reason = f"动作 {action} 执行失败或被意外中止"
|
||||
# 如果 final_status 已经是 "error" 或 "cancelled",则保留它们和它们对应的 final_reason
|
||||
|
||||
# 更新 done_action 中的记录
|
||||
# 防御性检查,确保 conversation_info, done_action 存在,并且索引有效
|
||||
if (
|
||||
conversation_info
|
||||
and hasattr(conversation_info, "done_action")
|
||||
and conversation_info.done_action
|
||||
and action_index < len(conversation_info.done_action)
|
||||
):
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": final_status,
|
||||
"time_completed": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"final_reason": final_reason,
|
||||
"duration_ms": int((time.time() - action_start_time) * 1000),
|
||||
}
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。"
|
||||
)
|
||||
|
||||
# 最终日志输出
|
||||
log_final_reason = final_reason if final_reason else "无明确原因"
|
||||
# 为成功发送的动作添加发送内容摘要
|
||||
if (
|
||||
final_status == "done"
|
||||
and action_successful
|
||||
and action in ["direct_reply", "send_new_message"]
|
||||
and hasattr(conversation_instance, "generated_reply")
|
||||
and conversation_instance.generated_reply
|
||||
):
|
||||
log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')"
|
||||
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}"
|
||||
)
|
||||
|
|
@ -101,6 +101,35 @@ class ChatObserver:
|
|||
message: 消息数据
|
||||
"""
|
||||
try:
|
||||
if isinstance(message, dict): # 确保是字典才添加
|
||||
self.message_cache.append(message)
|
||||
# 可选:限制 message_cache 的大小,例如只保留最近 N 条
|
||||
# 你可以根据你的需求调整 MAX_CACHE_SIZE
|
||||
# 对于情绪判断,可能不需要太长的历史,例如 5-10 条可能就足够了
|
||||
# 但 ChatObserver 的 get_cached_messages 也可能被其他地方用于获取更长的历史
|
||||
# 所以这里的 MAX_CACHE_SIZE 需要权衡,或者让调用者自己决定 limit
|
||||
MAX_CACHE_SIZE = 30 # 例如,保留最近30条作为通用缓存
|
||||
if len(self.message_cache) > MAX_CACHE_SIZE:
|
||||
self.message_cache = self.message_cache[-MAX_CACHE_SIZE:]
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}"
|
||||
)
|
||||
|
||||
# 检查是否用户发送的消息(而非机器人自己)
|
||||
try:
|
||||
from .PFC_idle.idle_chat import IdleChat
|
||||
|
||||
# 获取消息的发送者
|
||||
user_info = message.get("user_info", {})
|
||||
if user_info and str(user_info.get("user_id")) != str(global_config.BOT_QQ):
|
||||
# 用户发送了消息,通知IdleChat
|
||||
asyncio.create_task(IdleChat.register_user_response(self.private_name))
|
||||
logger.debug(f"[私聊][{self.private_name}] 检测到用户消息,已通知IdleChat更新用户响应状态")
|
||||
except Exception as e_idle:
|
||||
logger.warning(f"[私聊][{self.private_name}] 通知IdleChat用户响应状态失败: {e_idle}")
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}] 尝试向 message_cache 添加非字典类型消息: {type(message)}")
|
||||
|
||||
# 发送新消息通知
|
||||
notification = create_new_message_notification(
|
||||
sender="chat_observer", target="observation_info", message=message
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,20 @@
|
|||
from typing import Optional
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
class ConversationInfo:
|
||||
def __init__(self):
|
||||
self.done_action = []
|
||||
self.goal_list = []
|
||||
self.knowledge_list = []
|
||||
self.memory_list = []
|
||||
self.done_action: List[Dict[str, Any]] = [] # 建议明确类型
|
||||
self.goal_list: List[Dict[str, Any]] = [] # 建议明确类型
|
||||
self.knowledge_list: List[Any] = [] # 建议明确类型
|
||||
self.memory_list: List[Any] = [] # 建议明确类型
|
||||
self.last_successful_reply_action: Optional[str] = None
|
||||
self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因
|
||||
self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容
|
||||
self.my_message_count: int = 0 # 用于存储连续发送了多少条消息
|
||||
|
||||
# --- 新增字段 ---
|
||||
self.person_id: Optional[str] = None # 私聊对象的唯一ID
|
||||
self.relationship_text: Optional[str] = "你们还不熟悉。" # 与当前对话者的关系描述文本
|
||||
self.current_emotion_text: Optional[str] = "心情平静。" # 机器人当前的情绪描述文本
|
||||
self.current_instance_message_count: int = 0 # 当前私聊实例中的消息计数
|
||||
# --- 新增字段结束 ---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,335 @@
|
|||
import time
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
from maim_message import UserInfo
|
||||
from src.plugins.chat.chat_stream import chat_manager
|
||||
from src.config.config import global_config
|
||||
|
||||
# 导入 PFC 内部组件和类型
|
||||
from .pfc_types import ConversationState
|
||||
from .pfc import GoalAnalyzer
|
||||
from .chat_observer import ChatObserver
|
||||
from .message_sender import DirectMessageSender
|
||||
from .action_planner import ActionPlanner
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
from .reply_generator import ReplyGenerator
|
||||
from .PFC_idle.idle_chat import IdleChat
|
||||
from .pfc_KnowledgeFetcher import KnowledgeFetcher # 修正大小写
|
||||
from .waiter import Waiter
|
||||
from .pfc_utils import get_person_id
|
||||
from .reply_checker import ReplyChecker
|
||||
from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator
|
||||
from .pfc_emotion import PfcEmotionUpdater
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .conversation import Conversation # 用于类型提示以避免循环导入
|
||||
|
||||
logger = get_logger("pfc_initializer")
|
||||
|
||||
|
||||
async def load_initial_history(conversation_instance: "Conversation"):
|
||||
"""
|
||||
加载并处理初始的聊天记录。
|
||||
之前是 Conversation 类中的 _load_initial_history 方法。
|
||||
"""
|
||||
if not conversation_instance.observation_info: # 确保 ObservationInfo 已初始化
|
||||
logger.warning(f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法加载历史记录。")
|
||||
return
|
||||
|
||||
try:
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] 为 {conversation_instance.stream_id} 加载初始聊天记录..."
|
||||
)
|
||||
# 从聊天核心获取原始消息列表
|
||||
initial_messages = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=conversation_instance.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=30, # limit 可以根据需要调整或配置
|
||||
)
|
||||
|
||||
if initial_messages:
|
||||
# 更新 ObservationInfo 中的历史记录列表和计数
|
||||
conversation_instance.observation_info.chat_history = initial_messages
|
||||
conversation_instance.observation_info.chat_history_count = len(initial_messages)
|
||||
|
||||
# 获取最后一条消息的信息
|
||||
last_msg = initial_messages[-1]
|
||||
conversation_instance.observation_info.last_message_time = last_msg.get("time")
|
||||
conversation_instance.observation_info.last_message_id = last_msg.get("message_id")
|
||||
|
||||
# 安全地解析最后一条消息的发送者信息
|
||||
last_user_info_dict = last_msg.get("user_info", {})
|
||||
if isinstance(last_user_info_dict, dict):
|
||||
try:
|
||||
last_user_info = UserInfo.from_dict(last_user_info_dict)
|
||||
# 存储发送者的 user_id 字符串
|
||||
conversation_instance.observation_info.last_message_sender = (
|
||||
str(last_user_info.user_id) if last_user_info else None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] 解析最后一条消息的用户信息时出错: {e}"
|
||||
)
|
||||
conversation_instance.observation_info.last_message_sender = None
|
||||
else:
|
||||
# 如果 user_info 不是字典,也标记为未知
|
||||
conversation_instance.observation_info.last_message_sender = None
|
||||
|
||||
# 存储最后一条消息的文本内容
|
||||
conversation_instance.observation_info.last_message_content = last_msg.get("processed_plain_text", "")
|
||||
|
||||
# 构建用于 Prompt 的历史记录字符串 (只使用最近的一部分)
|
||||
history_slice_for_str = initial_messages[-30:] # 可配置
|
||||
conversation_instance.observation_info.chat_history_str = await build_readable_messages(
|
||||
history_slice_for_str,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0, # read_mark 可能需要根据实际情况调整
|
||||
)
|
||||
|
||||
# 更新 ChatObserver 和 IdleChat 的时间戳
|
||||
if conversation_instance.chat_observer:
|
||||
# 更新观察者的最后消息时间,避免重复处理这些初始消息
|
||||
conversation_instance.chat_observer.last_message_time = (
|
||||
conversation_instance.observation_info.last_message_time
|
||||
)
|
||||
if conversation_instance.idle_chat and conversation_instance.observation_info.last_message_time:
|
||||
# 更新空闲计时器的起始时间
|
||||
await conversation_instance.idle_chat.update_last_message_time(
|
||||
conversation_instance.observation_info.last_message_time
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {conversation_instance.observation_info.last_message_time}"
|
||||
)
|
||||
else:
|
||||
# 如果没有历史记录
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] 没有找到初始聊天记录。")
|
||||
conversation_instance.observation_info.chat_history_str = "还没有聊天记录。" # 设置默认提示
|
||||
|
||||
except Exception as load_err:
|
||||
# 捕获加载过程中的异常
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 加载初始聊天记录时出错: {load_err}")
|
||||
# 即使出错,也设置一个提示,避免后续使用 None 值
|
||||
if conversation_instance.observation_info:
|
||||
conversation_instance.observation_info.chat_history_str = "[加载聊天记录出错]"
|
||||
|
||||
|
||||
async def initialize_core_components(conversation_instance: "Conversation"):
|
||||
"""
|
||||
异步初始化对话实例及其所有依赖的核心组件。
|
||||
之前是 Conversation 类中的 _initialize 方法。
|
||||
"""
|
||||
# 防止重复初始化 (在 PFCManager层面已经有 _initializing 标志,这里可以简化或移除)
|
||||
# if conversation_instance._initialized or conversation_instance._initializing_flag_from_manager: # 假设 manager 设置了一个标志
|
||||
# logger.warning(f"[私聊][{conversation_instance.private_name}] 尝试重复初始化或正在初始化中 (initializer)。")
|
||||
# return
|
||||
|
||||
# conversation_instance._initializing_flag_from_manager = True # 标记开始初始化
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 开始初始化对话实例核心组件: {conversation_instance.stream_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. 初始化核心功能组件
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ActionPlanner...")
|
||||
conversation_instance.action_planner = ActionPlanner(
|
||||
conversation_instance.stream_id, conversation_instance.private_name
|
||||
)
|
||||
|
||||
conversation_instance.relationship_updater = PfcRelationshipUpdater(
|
||||
private_name=conversation_instance.private_name, bot_name=global_config.BOT_NICKNAME
|
||||
)
|
||||
conversation_instance.relationship_translator = PfcRepationshipTranslator(
|
||||
private_name=conversation_instance.private_name
|
||||
)
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcRelationship 初始化完成。")
|
||||
|
||||
conversation_instance.emotion_updater = PfcEmotionUpdater(
|
||||
private_name=conversation_instance.private_name, bot_name=global_config.BOT_NICKNAME
|
||||
)
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcEmotion 初始化完成。")
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 GoalAnalyzer...")
|
||||
conversation_instance.goal_analyzer = GoalAnalyzer(
|
||||
conversation_instance.stream_id, conversation_instance.private_name
|
||||
)
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ReplyGenerator...")
|
||||
conversation_instance.reply_generator = ReplyGenerator(
|
||||
conversation_instance.stream_id, conversation_instance.private_name
|
||||
)
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 KnowledgeFetcher...")
|
||||
conversation_instance.knowledge_fetcher = KnowledgeFetcher(conversation_instance.private_name)
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 Waiter...")
|
||||
conversation_instance.waiter = Waiter(conversation_instance.stream_id, conversation_instance.private_name)
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 DirectMessageSender...")
|
||||
conversation_instance.direct_sender = DirectMessageSender(conversation_instance.private_name)
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ReplyChecker...")
|
||||
conversation_instance.reply_checker = ReplyChecker(
|
||||
conversation_instance.stream_id, conversation_instance.private_name
|
||||
)
|
||||
|
||||
# 获取关联的 ChatStream
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatStream...")
|
||||
conversation_instance.chat_stream = chat_manager.get_stream(conversation_instance.stream_id)
|
||||
if not conversation_instance.chat_stream:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化错误:无法从 chat_manager 获取 stream_id {conversation_instance.stream_id} 的 ChatStream。"
|
||||
)
|
||||
raise ValueError(f"无法获取 stream_id {conversation_instance.stream_id} 的 ChatStream")
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 IdleChat...")
|
||||
conversation_instance.idle_chat = IdleChat.get_instance(
|
||||
conversation_instance.stream_id, conversation_instance.private_name
|
||||
)
|
||||
await conversation_instance.idle_chat.increment_active_instances()
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已获取并增加活跃计数")
|
||||
|
||||
# 2. 初始化信息存储和观察组件
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatObserver 实例...")
|
||||
conversation_instance.chat_observer = ChatObserver.get_instance(
|
||||
conversation_instance.stream_id, conversation_instance.private_name
|
||||
)
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ObservationInfo...")
|
||||
conversation_instance.observation_info = ObservationInfo(conversation_instance.private_name)
|
||||
if not conversation_instance.observation_info.bot_id: # 确保 ObservationInfo 知道机器人的 ID
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) ObservationInfo 未能自动获取 bot_id,尝试手动设置。"
|
||||
)
|
||||
conversation_instance.observation_info.bot_id = conversation_instance.bot_qq_str
|
||||
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ConversationInfo...")
|
||||
conversation_instance.conversation_info = ConversationInfo()
|
||||
|
||||
# 3. 绑定观察者和信息处理器
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 绑定 ObservationInfo 到 ChatObserver..."
|
||||
)
|
||||
if conversation_instance.observation_info and conversation_instance.chat_observer: # 确保二者都存在
|
||||
conversation_instance.observation_info.bind_to_chat_observer(conversation_instance.chat_observer)
|
||||
|
||||
# 4. 加载初始聊天记录 (调用本文件内的函数)
|
||||
await load_initial_history(conversation_instance)
|
||||
|
||||
# 4.1 加载用户数据
|
||||
if (
|
||||
conversation_instance.conversation_info and conversation_instance.chat_stream
|
||||
): # 确保 conversation_info 和 chat_stream 都存在
|
||||
person_id_tuple = await get_person_id(
|
||||
private_name=conversation_instance.private_name,
|
||||
chat_stream=conversation_instance.chat_stream,
|
||||
)
|
||||
if person_id_tuple: # 确保元组不为空
|
||||
conversation_instance.conversation_info.person_id = person_id_tuple[0] # 第一个元素是 person_id
|
||||
private_platform_str = person_id_tuple[1]
|
||||
private_user_id_str = person_id_tuple[2]
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 获取到 person_id: {conversation_instance.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 未能从 get_person_id 获取到 person_id 相关信息。"
|
||||
)
|
||||
|
||||
# 5. 启动需要后台运行的组件
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 ChatObserver...")
|
||||
if conversation_instance.chat_observer: # 确保存在
|
||||
conversation_instance.chat_observer.start()
|
||||
|
||||
if conversation_instance.idle_chat:
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 IdleChat...")
|
||||
# 不需要再次启动,只需确保已初始化
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已初始化")
|
||||
|
||||
if (
|
||||
conversation_instance.mood_mng
|
||||
and hasattr(conversation_instance.mood_mng, "start_mood_update")
|
||||
and not conversation_instance.mood_mng._running
|
||||
): # type: ignore
|
||||
conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。"
|
||||
)
|
||||
elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。")
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。"
|
||||
)
|
||||
|
||||
if (
|
||||
conversation_instance.conversation_info
|
||||
and conversation_instance.conversation_info.person_id
|
||||
and conversation_instance.relationship_translator
|
||||
and conversation_instance.person_info_mng
|
||||
): # 确保都存在
|
||||
try:
|
||||
numeric_relationship_value = await conversation_instance.person_info_mng.get_value(
|
||||
conversation_instance.conversation_info.person_id, "relationship_value"
|
||||
)
|
||||
if not isinstance(numeric_relationship_value, (int, float)):
|
||||
from bson.decimal128 import Decimal128
|
||||
|
||||
if isinstance(numeric_relationship_value, Decimal128):
|
||||
numeric_relationship_value = float(numeric_relationship_value.to_decimal())
|
||||
else:
|
||||
numeric_relationship_value = 0.0
|
||||
conversation_instance.conversation_info.relationship_text = (
|
||||
await conversation_instance.relationship_translator.translate_relationship_value_to_text(
|
||||
numeric_relationship_value
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本: {conversation_instance.conversation_info.relationship_text}"
|
||||
)
|
||||
except Exception as e_init_rel:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本出错: {e_init_rel}"
|
||||
)
|
||||
conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。"
|
||||
|
||||
if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在
|
||||
try:
|
||||
conversation_instance.conversation_info.current_emotion_text = (
|
||||
conversation_instance.mood_mng.get_prompt()
|
||||
) # type: ignore
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}"
|
||||
)
|
||||
except Exception as e_init_emo:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本出错: {e_init_emo}"
|
||||
)
|
||||
# 保留 ConversationInfo 中的默认值
|
||||
|
||||
# 6. 标记初始化成功并设置运行状态 (这些标志由PFCManager控制和检查)
|
||||
# conversation_instance._initialized = True -> 由 manager 设置
|
||||
# conversation_instance.should_continue = True -> 由 manager 设置
|
||||
conversation_instance.state = ConversationState.ANALYZING # 设置初始状态为分析
|
||||
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] (Initializer) 对话实例 {conversation_instance.stream_id} 核心组件初始化完成。"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化对话实例核心组件失败: {e}")
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) {traceback.format_exc()}")
|
||||
# conversation_instance.should_continue = False # 由 manager 处理
|
||||
# conversation_instance._initialized = False # 由 manager 处理
|
||||
# 外部(PFCManager)会捕获这个异常并处理 should_continue 和 _initialized 标志
|
||||
# 以及调用 conversation_instance.stop()
|
||||
raise # 将异常重新抛出,通知 PFCManager 初始化失败
|
||||
# finally:
|
||||
# conversation_instance._initializing_flag_from_manager = False # 清除标志
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
import time
|
||||
import asyncio
|
||||
import datetime
|
||||
import traceback
|
||||
from typing import Dict, Any, List, TYPE_CHECKING
|
||||
from dateutil import tz
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.config.config import global_config
|
||||
from .pfc_types import ConversationState # 需要导入 ConversationState
|
||||
from . import actions # 需要导入 actions 模块
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .conversation import Conversation
|
||||
|
||||
logger = get_logger("pfc_loop")
|
||||
|
||||
# 时区配置 (从 conversation.py 移过来,或者考虑放到更全局的配置模块)
|
||||
configured_tz = getattr(global_config, "TIME_ZONE", "Asia/Shanghai")
|
||||
TIME_ZONE = tz.gettz(configured_tz)
|
||||
if TIME_ZONE is None:
|
||||
logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'")
|
||||
TIME_ZONE = tz.gettz("Asia/Shanghai")
|
||||
|
||||
|
||||
async def run_conversation_loop(conversation_instance: "Conversation"):
|
||||
"""
|
||||
核心的规划与行动循环 (PFC Loop)。
|
||||
之前是 Conversation 类中的 _plan_and_action_loop 方法。
|
||||
"""
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 进入 run_conversation_loop 循环。")
|
||||
|
||||
if not conversation_instance._initialized:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下运行规划循环,退出。")
|
||||
return
|
||||
|
||||
force_reflect_and_act = False # 用于强制使用反思 prompt 的标志
|
||||
|
||||
while conversation_instance.should_continue:
|
||||
loop_iter_start_time = time.time()
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 开始新一轮循环迭代 ({loop_iter_start_time:.2f})")
|
||||
|
||||
# 更新当前时间
|
||||
try:
|
||||
global TIME_ZONE # 引用全局 TIME_ZONE
|
||||
if TIME_ZONE is None: # 如果还未加载成功
|
||||
configured_tz_loop = getattr(global_config, "TIME_ZONE", "Asia/Shanghai")
|
||||
TIME_ZONE = tz.gettz(configured_tz_loop)
|
||||
if TIME_ZONE is None:
|
||||
logger.error(f"循环中: 配置的时区 '{configured_tz_loop}' 无效,将使用 'Asia/Shanghai'")
|
||||
TIME_ZONE = tz.gettz("Asia/Shanghai")
|
||||
|
||||
current_time_dt = datetime.datetime.now(TIME_ZONE)
|
||||
if conversation_instance.observation_info:
|
||||
time_str = current_time_dt.strftime("%Y-%m-%d %H:%M:%S %Z%z")
|
||||
conversation_instance.observation_info.current_time_str = time_str
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间: {time_str}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法更新当前时间。"
|
||||
)
|
||||
except Exception as time_update_err:
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}"
|
||||
)
|
||||
|
||||
# 处理忽略状态
|
||||
if (
|
||||
conversation_instance.ignore_until_timestamp
|
||||
and loop_iter_start_time < conversation_instance.ignore_until_timestamp
|
||||
):
|
||||
if conversation_instance.idle_chat and conversation_instance.idle_chat._running:
|
||||
# 不直接停止服务,改为暂时忽略此用户
|
||||
# 虽然我们仍然可以通过active_instances_count来决定是否触发主动聊天
|
||||
# 但为了安全起见,我们只记录一个日志
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] 对话被暂时忽略,暂停对该用户的主动聊天")
|
||||
sleep_duration = min(30, conversation_instance.ignore_until_timestamp - loop_iter_start_time)
|
||||
await asyncio.sleep(sleep_duration)
|
||||
continue
|
||||
elif (
|
||||
conversation_instance.ignore_until_timestamp
|
||||
and loop_iter_start_time >= conversation_instance.ignore_until_timestamp
|
||||
):
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] 忽略时间已到 {conversation_instance.stream_id},准备结束对话。"
|
||||
)
|
||||
conversation_instance.ignore_until_timestamp = None
|
||||
await conversation_instance.stop() # 调用 Conversation 实例的 stop 方法
|
||||
continue
|
||||
else:
|
||||
# 忽略状态结束,这里不需要任何特殊处理
|
||||
# IdleChat会通过active_instances_count自动决定是否触发
|
||||
pass
|
||||
|
||||
# 核心规划与行动逻辑
|
||||
try:
|
||||
# 更新关系和情绪文本 (在每次循环开始时进行)
|
||||
if conversation_instance.conversation_info and conversation_instance._initialized:
|
||||
# 更新关系
|
||||
if (
|
||||
conversation_instance.conversation_info.person_id
|
||||
and conversation_instance.relationship_translator
|
||||
and conversation_instance.person_info_mng
|
||||
):
|
||||
try:
|
||||
numeric_relationship_value = await conversation_instance.person_info_mng.get_value(
|
||||
conversation_instance.conversation_info.person_id, "relationship_value"
|
||||
)
|
||||
if not isinstance(numeric_relationship_value, (int, float)):
|
||||
from bson.decimal128 import Decimal128
|
||||
|
||||
if isinstance(numeric_relationship_value, Decimal128):
|
||||
numeric_relationship_value = float(numeric_relationship_value.to_decimal())
|
||||
else:
|
||||
numeric_relationship_value = 0.0
|
||||
conversation_instance.conversation_info.relationship_text = (
|
||||
await conversation_instance.relationship_translator.translate_relationship_value_to_text(
|
||||
numeric_relationship_value
|
||||
)
|
||||
)
|
||||
except Exception as e_rel:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) 更新关系文本时出错: {e_rel}")
|
||||
conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。"
|
||||
# 更新情绪
|
||||
if conversation_instance.mood_mng:
|
||||
conversation_instance.conversation_info.current_emotion_text = (
|
||||
conversation_instance.mood_mng.get_prompt()
|
||||
) # type: ignore
|
||||
|
||||
# 检查核心组件
|
||||
if not all(
|
||||
[
|
||||
conversation_instance.action_planner,
|
||||
conversation_instance.observation_info,
|
||||
conversation_instance.conversation_info,
|
||||
]
|
||||
):
|
||||
logger.error(
|
||||
f"[私聊][{conversation_instance.private_name}] 核心组件未初始化,无法继续规划循环。将等待5秒后重试..."
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
||||
# 规划
|
||||
planning_start_time = time.time()
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] --- (Loop) 开始规划 ({planning_start_time:.2f}) ---"
|
||||
)
|
||||
if conversation_instance.conversation_info:
|
||||
conversation_instance.conversation_info.other_new_messages_during_planning_count = 0
|
||||
|
||||
action, reason = await conversation_instance.action_planner.plan(
|
||||
conversation_instance.observation_info,
|
||||
conversation_instance.conversation_info,
|
||||
conversation_instance.conversation_info.last_successful_reply_action
|
||||
if conversation_instance.conversation_info
|
||||
else None,
|
||||
use_reflect_prompt=force_reflect_and_act,
|
||||
)
|
||||
force_reflect_and_act = False
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Loop) ActionPlanner.plan 完成,初步规划动作: {action}"
|
||||
)
|
||||
|
||||
# 检查中断
|
||||
current_unprocessed_messages = getattr(conversation_instance.observation_info, "unprocessed_messages", [])
|
||||
new_messages_during_planning: List[Dict[str, Any]] = []
|
||||
other_new_messages_during_planning: List[Dict[str, Any]] = []
|
||||
|
||||
for msg in current_unprocessed_messages:
|
||||
msg_time = msg.get("time")
|
||||
sender_id_info = msg.get("user_info", {})
|
||||
sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None
|
||||
if msg_time and msg_time >= planning_start_time:
|
||||
new_messages_during_planning.append(msg)
|
||||
if sender_id != conversation_instance.bot_qq_str:
|
||||
other_new_messages_during_planning.append(msg)
|
||||
|
||||
new_msg_count = len(new_messages_during_planning)
|
||||
other_new_msg_count = len(other_new_messages_during_planning)
|
||||
|
||||
if conversation_instance.conversation_info and other_new_msg_count > 0:
|
||||
conversation_instance.conversation_info.current_instance_message_count += other_new_msg_count
|
||||
# 触发关系和情绪更新(如果需要)
|
||||
if (
|
||||
conversation_instance.relationship_updater
|
||||
and conversation_instance.observation_info
|
||||
and conversation_instance.chat_observer
|
||||
):
|
||||
await conversation_instance.relationship_updater.update_relationship_incremental(
|
||||
conversation_info=conversation_instance.conversation_info,
|
||||
observation_info=conversation_instance.observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer,
|
||||
)
|
||||
if (
|
||||
conversation_instance.emotion_updater
|
||||
and other_new_messages_during_planning
|
||||
and conversation_instance.observation_info
|
||||
and conversation_instance.chat_observer
|
||||
):
|
||||
last_user_msg = other_new_messages_during_planning[-1]
|
||||
last_user_msg_text = last_user_msg.get("processed_plain_text", "用户发了新消息")
|
||||
sender_name_for_event = getattr(conversation_instance.observation_info, "sender_name", "对方")
|
||||
event_desc = f"用户【{sender_name_for_event}】发送了新消息: '{last_user_msg_text[:30]}...'"
|
||||
await conversation_instance.emotion_updater.update_emotion_based_on_context(
|
||||
conversation_info=conversation_instance.conversation_info,
|
||||
observation_info=conversation_instance.observation_info,
|
||||
chat_observer_for_history=conversation_instance.chat_observer,
|
||||
event_description=event_desc,
|
||||
)
|
||||
|
||||
should_interrupt: bool = False
|
||||
interrupt_reason: str = ""
|
||||
if action in ["wait", "listening"] and new_msg_count > 0:
|
||||
should_interrupt = True
|
||||
interrupt_reason = f"规划 {action} 期间收到 {new_msg_count} 条新消息"
|
||||
elif other_new_msg_count > 2: # Threshold for other actions
|
||||
should_interrupt = True
|
||||
interrupt_reason = f"规划 {action} 期间收到 {other_new_msg_count} 条来自他人的新消息"
|
||||
|
||||
if should_interrupt:
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] (Loop) 中断 '{action}',原因: {interrupt_reason}。重新规划..."
|
||||
)
|
||||
cancel_record = {
|
||||
"action": action,
|
||||
"plan_reason": reason,
|
||||
"status": "cancelled_due_to_new_messages",
|
||||
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"final_reason": interrupt_reason,
|
||||
}
|
||||
if conversation_instance.conversation_info:
|
||||
if (
|
||||
not hasattr(conversation_instance.conversation_info, "done_action")
|
||||
or conversation_instance.conversation_info.done_action is None
|
||||
):
|
||||
conversation_instance.conversation_info.done_action = []
|
||||
conversation_instance.conversation_info.done_action.append(cancel_record)
|
||||
conversation_instance.conversation_info.last_successful_reply_action = None
|
||||
conversation_instance.state = ConversationState.ANALYZING
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 执行动作 (调用 actions 模块的函数)
|
||||
logger.debug(
|
||||
f"[私聊][{conversation_instance.private_name}] (Loop) 未中断,调用 actions.handle_action 执行动作 '{action}'..."
|
||||
)
|
||||
if conversation_instance.conversation_info:
|
||||
conversation_instance.conversation_info.other_new_messages_during_planning_count = other_new_msg_count
|
||||
|
||||
await actions.handle_action(
|
||||
conversation_instance,
|
||||
action,
|
||||
reason,
|
||||
conversation_instance.observation_info,
|
||||
conversation_instance.conversation_info,
|
||||
)
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) actions.handle_action 完成。")
|
||||
|
||||
# 检查是否需要反思
|
||||
last_action_record = {}
|
||||
if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action:
|
||||
last_action_record = conversation_instance.conversation_info.done_action[-1]
|
||||
if (
|
||||
last_action_record.get("action") == "send_new_message"
|
||||
and last_action_record.get("status") == "done_no_reply"
|
||||
):
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 检测到需反思,设置标志。")
|
||||
force_reflect_and_act = True
|
||||
|
||||
# 检查结束条件
|
||||
goal_ended: bool = False
|
||||
if (
|
||||
conversation_instance.conversation_info
|
||||
and hasattr(conversation_instance.conversation_info, "goal_list")
|
||||
and conversation_instance.conversation_info.goal_list
|
||||
):
|
||||
last_goal_item = conversation_instance.conversation_info.goal_list[-1]
|
||||
current_goal = (
|
||||
last_goal_item.get("goal")
|
||||
if isinstance(last_goal_item, dict)
|
||||
else (last_goal_item if isinstance(last_goal_item, str) else None)
|
||||
)
|
||||
if current_goal == "结束对话":
|
||||
goal_ended = True
|
||||
|
||||
last_action_record_for_end_check = {}
|
||||
if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action:
|
||||
last_action_record_for_end_check = conversation_instance.conversation_info.done_action[-1]
|
||||
action_ended: bool = (
|
||||
last_action_record_for_end_check.get("action") in ["end_conversation", "say_goodbye"]
|
||||
and last_action_record_for_end_check.get("status") == "done"
|
||||
)
|
||||
|
||||
if goal_ended or action_ended:
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 检测到结束条件,停止循环。")
|
||||
await conversation_instance.stop() # 调用 Conversation 的 stop
|
||||
continue # 虽然会 break,但 continue 更明确
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 主循环任务被取消。")
|
||||
await conversation_instance.stop() # 调用 Conversation 的 stop
|
||||
break
|
||||
except Exception as loop_err:
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 主循环出错: {loop_err}")
|
||||
logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) {traceback.format_exc()}")
|
||||
conversation_instance.state = ConversationState.ERROR
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 控制循环频率
|
||||
loop_duration = time.time() - loop_iter_start_time
|
||||
min_loop_interval = 0.1
|
||||
logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) 循环迭代耗时: {loop_duration:.3f} 秒。")
|
||||
if loop_duration < min_loop_interval:
|
||||
await asyncio.sleep(min_loop_interval - loop_duration)
|
||||
|
||||
logger.info(
|
||||
f"[私聊][{conversation_instance.private_name}] (Loop) PFC 循环已退出 for stream_id: {conversation_instance.stream_id}"
|
||||
)
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import time
|
||||
import traceback
|
||||
from dateutil import tz
|
||||
from typing import List, Optional, Dict, Any, Set
|
||||
|
||||
from maim_message import UserInfo
|
||||
from src.common.logger import get_module_logger
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.config.config import global_config
|
||||
|
||||
# 确保导入路径正确
|
||||
from .chat_observer import ChatObserver
|
||||
|
|
@ -12,6 +13,8 @@ from .chat_states import NotificationHandler, NotificationType, Notification
|
|||
|
||||
logger = get_module_logger("observation_info")
|
||||
|
||||
TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else "Asia/Shanghai") # 使用配置的时区,提供默认值
|
||||
|
||||
|
||||
class ObservationInfoHandler(NotificationHandler):
|
||||
"""ObservationInfo的通知处理器"""
|
||||
|
|
@ -111,6 +114,11 @@ class ObservationInfo:
|
|||
"""初始化 ObservationInfo"""
|
||||
self.private_name: str = private_name
|
||||
|
||||
# 新增:发信人信息
|
||||
self.sender_name: Optional[str] = None
|
||||
self.sender_user_id: Optional[str] = None # 存储为字符串
|
||||
self.sender_platform: Optional[str] = None
|
||||
|
||||
# 聊天记录相关
|
||||
self.chat_history: List[Dict[str, Any]] = [] # 存储已处理的消息历史
|
||||
self.chat_history_str: str = "还没有聊天记录。" # 用于生成 Prompt 的历史记录字符串
|
||||
|
|
@ -139,6 +147,9 @@ class ObservationInfo:
|
|||
self.is_typing: bool = False # 是否正在输入 (未来可能用到)
|
||||
self.changed: bool = False # 状态是否有变化 (用于优化)
|
||||
|
||||
# 用于存储格式化的当前时间
|
||||
self.current_time_str: Optional[str] = None
|
||||
|
||||
# 关联对象
|
||||
self.chat_observer: Optional[ChatObserver] = None
|
||||
self.handler: Optional[ObservationInfoHandler] = ObservationInfoHandler(self, self.private_name)
|
||||
|
|
@ -216,12 +227,37 @@ class ObservationInfo:
|
|||
logger.warning(f"[私聊][{self.private_name}] 收到的消息缺少 time 或 message_id: {message}")
|
||||
return
|
||||
|
||||
# --- 新增/修改:提取并存储发信人详细信息 ---
|
||||
current_message_sender_id: Optional[str] = None
|
||||
if user_info:
|
||||
try:
|
||||
self.sender_user_id = str(user_info.user_id) # 确保是字符串
|
||||
self.sender_name = user_info.user_nickname # 或者 user_info.card 如果私聊时card更准
|
||||
self.sender_platform = user_info.platform
|
||||
current_message_sender_id = self.sender_user_id # 用于后续逻辑
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 更新发信人信息: ID={self.sender_user_id}, Name={self.sender_name}, Platform={self.sender_platform}"
|
||||
)
|
||||
except AttributeError as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 从 UserInfo 对象提取信息时出错: {e}, UserInfo: {user_info}")
|
||||
# 如果提取失败,将这些新字段设为 None,避免使用旧数据
|
||||
self.sender_user_id = None
|
||||
self.sender_name = None
|
||||
self.sender_platform = None
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}] 处理消息更新时缺少有效的 UserInfo, message_id: {message_id}")
|
||||
# 如果没有 UserInfo,也将这些新字段设为 None
|
||||
self.sender_user_id = None
|
||||
self.sender_name = None
|
||||
self.sender_platform = None
|
||||
# --- 新增/修改结束 ---
|
||||
|
||||
# 更新最后消息时间(所有消息)
|
||||
if message_time > (self.last_message_time or 0):
|
||||
self.last_message_time = message_time
|
||||
self.last_message_id = message_id
|
||||
self.last_message_content = processed_text
|
||||
self.last_message_sender = sender_id_str
|
||||
self.last_message_sender = current_message_sender_id # 使用新获取的 current_message_sender_id
|
||||
|
||||
# 更新说话者特定时间
|
||||
if sender_id_str:
|
||||
|
|
@ -261,7 +297,7 @@ class ObservationInfo:
|
|||
|
||||
if new_count < original_count:
|
||||
self.new_messages_count = new_count
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 移除了未处理的消息 (ID: {message_id_to_delete}), 当前未处理数: {self.new_messages_count}"
|
||||
)
|
||||
self.update_changed()
|
||||
|
|
@ -330,7 +366,7 @@ class ObservationInfo:
|
|||
self.chat_history = self.chat_history[-max_history_len:]
|
||||
|
||||
# 更新历史记录字符串 (仅使用最近一部分生成,提高效率)
|
||||
history_slice_for_str = self.chat_history[-20:] # 例如最近 20 条
|
||||
history_slice_for_str = self.chat_history[-30:] # 例如最近 20 条
|
||||
try:
|
||||
self.chat_history_str = await build_readable_messages(
|
||||
history_slice_for_str,
|
||||
|
|
@ -348,7 +384,7 @@ class ObservationInfo:
|
|||
self.new_messages_count = len(self.unprocessed_messages)
|
||||
self.chat_history_count = len(self.chat_history)
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 已清理 {cleared_count} 条消息 (IDs: {message_ids_to_clear}),剩余未处理 {self.new_messages_count} 条,当前历史记录 {self.chat_history_count} 条。"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ class GoalAnalyzer:
|
|||
read_mark=0.0,
|
||||
)
|
||||
chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
|
||||
|
||||
else:
|
||||
chat_history_text += "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n"
|
||||
# await observation_info.clear_unprocessed_messages()
|
||||
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
|
|
@ -281,65 +282,3 @@ class GoalAnalyzer:
|
|||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]分析对话状态时出错: {str(e)}")
|
||||
return False, False, f"分析出错: {str(e)}"
|
||||
|
||||
|
||||
# 先注释掉,万一以后出问题了还能开回来(((
|
||||
# class DirectMessageSender:
|
||||
# """直接发送消息到平台的发送器"""
|
||||
|
||||
# def __init__(self, private_name: str):
|
||||
# self.logger = get_module_logger("direct_sender")
|
||||
# self.storage = MessageStorage()
|
||||
# self.private_name = private_name
|
||||
|
||||
# async def send_via_ws(self, message: MessageSending) -> None:
|
||||
# try:
|
||||
# await global_api.send_message(message)
|
||||
# except Exception as e:
|
||||
# raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
|
||||
# async def send_message(
|
||||
# self,
|
||||
# chat_stream: ChatStream,
|
||||
# content: str,
|
||||
# reply_to_message: Optional[Message] = None,
|
||||
# ) -> None:
|
||||
# """直接发送消息到平台
|
||||
|
||||
# Args:
|
||||
# chat_stream: 聊天流
|
||||
# content: 消息内容
|
||||
# reply_to_message: 要回复的消息
|
||||
# """
|
||||
# # 构建消息对象
|
||||
# message_segment = Seg(type="text", data=content)
|
||||
# bot_user_info = UserInfo(
|
||||
# user_id=global_config.BOT_QQ,
|
||||
# user_nickname=global_config.BOT_NICKNAME,
|
||||
# platform=chat_stream.platform,
|
||||
# )
|
||||
|
||||
# message = MessageSending(
|
||||
# message_id=f"dm{round(time.time(), 2)}",
|
||||
# chat_stream=chat_stream,
|
||||
# bot_user_info=bot_user_info,
|
||||
# sender_info=reply_to_message.message_info.user_info if reply_to_message else None,
|
||||
# message_segment=message_segment,
|
||||
# reply=reply_to_message,
|
||||
# is_head=True,
|
||||
# is_emoji=False,
|
||||
# thinking_start_time=time.time(),
|
||||
# )
|
||||
|
||||
# # 处理消息
|
||||
# await message.process()
|
||||
|
||||
# _message_json = message.to_dict()
|
||||
|
||||
# # 发送消息
|
||||
# try:
|
||||
# await self.send_via_ws(message)
|
||||
# await self.storage.store_message(message, chat_stream)
|
||||
# logger.success(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
|
||||
# except Exception as e:
|
||||
# logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
from typing import List, Dict, Any
|
||||
|
||||
from src.plugins.PFC.chat_observer import ChatObserver
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.models.utils_model import LLMRequest
|
||||
from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.plugins.PFC.observation_info import ObservationInfo
|
||||
from src.plugins.PFC.conversation_info import ConversationInfo
|
||||
from src.config.config import global_config # 导入全局配置
|
||||
|
||||
logger = get_logger("pfc_emotion")
|
||||
|
||||
|
||||
class PfcEmotionUpdater:
|
||||
def __init__(self, private_name: str, bot_name: str):
|
||||
"""
|
||||
初始化情绪更新器。
|
||||
"""
|
||||
self.private_name = private_name
|
||||
self.bot_name = bot_name
|
||||
self.mood_mng = MoodManager.get_instance() # 获取 MoodManager 单例
|
||||
|
||||
# LLM 实例 (根据 global_config.llm_summary 配置)
|
||||
llm_config_summary = getattr(global_config, "llm_summary", None)
|
||||
if llm_config_summary and isinstance(llm_config_summary, dict):
|
||||
logger.debug(f"[私聊][{self.private_name}] 使用 llm_summary 配置初始化情绪判断LLM。")
|
||||
self.llm = LLMRequest(
|
||||
model=llm_config_summary,
|
||||
temperature=llm_config_summary.get(
|
||||
"temperature", 0.5
|
||||
), # temperature 来自其自身配置或默认0.7,这里用0.5
|
||||
max_tokens=llm_config_summary.get("max_tokens", 256), # 情绪词输出不需要很多token
|
||||
request_type="pfc_emotion_evaluation",
|
||||
)
|
||||
else:
|
||||
logger.error(f"[私聊][{self.private_name}] 未找到 llm_summary 配置或配置无效!情绪判断功能将受限。")
|
||||
self.llm = None # LLM 未初始化
|
||||
|
||||
self.EMOTION_UPDATE_INTENSITY = getattr(global_config, "pfc_emotion_update_intensity", 0.6)
|
||||
self.EMOTION_HISTORY_COUNT = getattr(global_config, "pfc_emotion_history_count", 5)
|
||||
|
||||
async def update_emotion_based_on_context(
|
||||
self,
|
||||
conversation_info: ConversationInfo,
|
||||
observation_info: ObservationInfo,
|
||||
chat_observer_for_history: ChatObserver, # ChatObserver 实例
|
||||
event_description: str,
|
||||
) -> None:
|
||||
if not self.llm:
|
||||
logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行情绪更新。")
|
||||
# 即使LLM失败,也应该更新conversation_info中的情绪文本为MoodManager的当前状态
|
||||
if conversation_info and self.mood_mng:
|
||||
conversation_info.current_emotion_text = self.mood_mng.get_prompt()
|
||||
return
|
||||
|
||||
if not self.mood_mng or not conversation_info or not observation_info:
|
||||
logger.debug(f"[私聊][{self.private_name}] 情绪更新:缺少必要管理器或信息。")
|
||||
return
|
||||
|
||||
recent_messages_for_emotion: List[Dict[str, Any]] = []
|
||||
if chat_observer_for_history:
|
||||
recent_messages_for_emotion = chat_observer_for_history.get_cached_messages(
|
||||
limit=self.EMOTION_HISTORY_COUNT
|
||||
)
|
||||
elif observation_info.chat_history:
|
||||
recent_messages_for_emotion = observation_info.chat_history[-self.EMOTION_HISTORY_COUNT :]
|
||||
|
||||
readable_recent_history = await build_readable_messages(
|
||||
recent_messages_for_emotion, replace_bot_name=True, merge_messages=True, timestamp_mode="none"
|
||||
)
|
||||
|
||||
current_mood_text_from_manager = self.mood_mng.current_mood.text # 从 MoodManager 获取当前情绪文本
|
||||
sender_name_for_prompt = getattr(observation_info, "sender_name", "对方")
|
||||
if not sender_name_for_prompt:
|
||||
sender_name_for_prompt = "对方"
|
||||
relationship_text_for_prompt = getattr(
|
||||
conversation_info, "relationship_text", "关系一般。"
|
||||
) # 从 ConversationInfo 获取关系文本
|
||||
|
||||
emotion_prompt = f"""你是{self.bot_name}。你现在的心情是【{current_mood_text_from_manager}】。
|
||||
你正在和用户【{sender_name_for_prompt}】私聊,你们的关系是:【{relationship_text_for_prompt}】。
|
||||
最近发生的事件是:【{event_description}】
|
||||
最近的对话摘要:
|
||||
---
|
||||
{readable_recent_history}
|
||||
---
|
||||
基于以上所有信息,你认为你现在最主要的情绪是什么?请从以下情绪词中选择一个(必须是列表中的一个):
|
||||
[开心, 害羞, 愤怒, 恐惧, 悲伤, 厌恶, 惊讶, 困惑, 平静]
|
||||
请只输出一个最符合的情绪词。例如: 开心
|
||||
如果难以判断或当前情绪依然合适,请输出: 无变化
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"[私聊][{self.private_name}] 情绪判断Prompt:\n{emotion_prompt}")
|
||||
content, _ = await self.llm.generate_response_async(emotion_prompt)
|
||||
detected_emotion_word = content.strip().replace('"', "").replace("'", "")
|
||||
logger.debug(f"[私聊][{self.private_name}] 情绪判断LLM原始返回: '{detected_emotion_word}'")
|
||||
|
||||
if (
|
||||
detected_emotion_word
|
||||
and detected_emotion_word != "无变化"
|
||||
and detected_emotion_word in self.mood_mng.emotion_map
|
||||
):
|
||||
self.mood_mng.update_mood_from_emotion(detected_emotion_word, intensity=self.EMOTION_UPDATE_INTENSITY)
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 基于事件 '{event_description}',情绪已更新为倾向于 '{detected_emotion_word}'。当前心情: {self.mood_mng.current_mood.text}"
|
||||
)
|
||||
elif detected_emotion_word == "无变化":
|
||||
logger.debug(f"[私聊][{self.private_name}] 基于事件 '{event_description}',LLM判断情绪无显著变化。")
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}] LLM返回了未知的情绪词 '{detected_emotion_word}' 或未返回有效词,情绪未主动更新。"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 情绪判断LLM调用或处理失败: {e}")
|
||||
|
||||
# 无论LLM判断如何,都更新conversation_info中的情绪文本以供Prompt使用
|
||||
if conversation_info and self.mood_mng: # 确保conversation_info有效
|
||||
conversation_info.current_emotion_text = self.mood_mng.get_prompt()
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
import time
|
||||
import asyncio # 引入 asyncio
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import Dict, Optional
|
||||
|
||||
from src.common.logger import get_module_logger
|
||||
from .conversation import Conversation
|
||||
from .conversation_initializer import initialize_core_components
|
||||
|
||||
# >>> 新增导入 <<<
|
||||
from .pfc_types import ConversationState # 导入 ConversationState
|
||||
|
||||
logger = get_module_logger("pfc_manager")
|
||||
|
||||
|
|
@ -12,16 +16,12 @@ logger = get_module_logger("pfc_manager")
|
|||
class PFCManager:
|
||||
"""PFC对话管理器,负责管理所有对话实例"""
|
||||
|
||||
# 单例模式
|
||||
_instance = None
|
||||
|
||||
# 会话实例管理
|
||||
_instances: Dict[str, Conversation] = {}
|
||||
_initializing: Dict[str, bool] = {} # 用于防止并发初始化同一个 stream_id
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "PFCManager":
|
||||
"""获取管理器单例"""
|
||||
if cls._instance is None:
|
||||
cls._instance = PFCManager()
|
||||
return cls._instance
|
||||
|
|
@ -29,115 +29,106 @@ class PFCManager:
|
|||
async def get_or_create_conversation(self, stream_id: str, private_name: str) -> Optional[Conversation]:
|
||||
"""获取或创建对话实例,并确保其启动"""
|
||||
|
||||
# 检查是否正在初始化 (防止并发问题)
|
||||
if self._initializing.get(stream_id, False):
|
||||
logger.debug(f"[私聊][{private_name}] 会话实例正在初始化中,请稍候: {stream_id}")
|
||||
# 可以选择等待一小段时间或直接返回 None
|
||||
await asyncio.sleep(0.5) # 短暂等待,让初始化有机会完成
|
||||
# 再次检查实例是否存在
|
||||
await asyncio.sleep(0.5)
|
||||
if stream_id in self._instances and self._instances[stream_id]._initialized:
|
||||
logger.debug(f"[私聊][{private_name}] 初始化已完成,返回现有实例: {stream_id}")
|
||||
return self._instances[stream_id]
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] 等待后实例仍未初始化完成或不存在。")
|
||||
return None # 避免返回未完成的实例
|
||||
return None
|
||||
|
||||
# 检查是否已有活动实例
|
||||
if stream_id in self._instances:
|
||||
instance = self._instances[stream_id]
|
||||
# 检查忽略状态
|
||||
if (
|
||||
hasattr(instance, "ignore_until_timestamp")
|
||||
and instance.ignore_until_timestamp
|
||||
and time.time() < instance.ignore_until_timestamp
|
||||
):
|
||||
logger.debug(f"[私聊][{private_name}] 会话实例当前处于忽略状态: {stream_id}")
|
||||
return None # 处于忽略状态,不返回实例
|
||||
return None
|
||||
|
||||
# 检查是否已初始化且应继续运行
|
||||
if instance._initialized and instance.should_continue:
|
||||
logger.debug(f"[私聊][{private_name}] 使用现有活动会话实例: {stream_id}")
|
||||
return instance
|
||||
else:
|
||||
# 如果实例存在但未初始化或不应继续,清理旧实例
|
||||
logger.warning(f"[私聊][{private_name}] 发现无效或已停止的旧实例,清理并重新创建: {stream_id}")
|
||||
await self._cleanup_conversation(instance)
|
||||
# 从字典中移除,确保下面能创建新的
|
||||
if stream_id in self._instances:
|
||||
del self._instances[stream_id]
|
||||
if stream_id in self._initializing:
|
||||
if stream_id in self._initializing: # 确保也从这里移除
|
||||
del self._initializing[stream_id]
|
||||
|
||||
# --- 创建并初始化新实例 ---
|
||||
conversation_instance: Optional[Conversation] = None
|
||||
try:
|
||||
logger.info(f"[私聊][{private_name}] 创建新的对话实例: {stream_id}")
|
||||
self._initializing[stream_id] = True # 标记开始初始化
|
||||
self._initializing[stream_id] = True
|
||||
|
||||
# 创建实例
|
||||
conversation_instance = Conversation(stream_id, private_name)
|
||||
self._instances[stream_id] = conversation_instance # 立即存入字典
|
||||
self._instances[stream_id] = conversation_instance
|
||||
|
||||
# **启动实例初始化**
|
||||
# _initialize_conversation 会调用 conversation._initialize()
|
||||
await self._initialize_conversation(conversation_instance)
|
||||
# 调用初始化包装器
|
||||
await self._initialize_conversation_wrapper(conversation_instance)
|
||||
|
||||
# --- 关键修复:在初始化成功后调用 start() ---
|
||||
# 检查初始化结果并启动
|
||||
if conversation_instance._initialized and conversation_instance.should_continue:
|
||||
logger.info(f"[私聊][{private_name}] 初始化成功,调用 conversation.start() 启动主循环...")
|
||||
await conversation_instance.start() # 确保调用 start 方法
|
||||
await conversation_instance.start() # start 方法内部会创建 loop 任务
|
||||
else:
|
||||
# 如果 _initialize_conversation 内部初始化失败
|
||||
logger.error(f"[私聊][{private_name}] 初始化未成功完成,无法启动实例 {stream_id}。")
|
||||
# 清理可能部分创建的实例
|
||||
await self._cleanup_conversation(conversation_instance)
|
||||
if stream_id in self._instances:
|
||||
if stream_id in self._instances: # 再次检查以防万一
|
||||
del self._instances[stream_id]
|
||||
conversation_instance = None # 返回 None 表示失败
|
||||
conversation_instance = None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{private_name}] 创建或启动会话实例时发生严重错误: {stream_id}, 错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 确保清理
|
||||
if conversation_instance:
|
||||
await self._cleanup_conversation(conversation_instance)
|
||||
if stream_id in self._instances:
|
||||
del self._instances[stream_id]
|
||||
conversation_instance = None # 返回 None
|
||||
|
||||
conversation_instance = None
|
||||
finally:
|
||||
# 确保初始化标记被清除
|
||||
if stream_id in self._initializing:
|
||||
self._initializing[stream_id] = False
|
||||
if stream_id in self._initializing: # 确保在 finally 中也检查
|
||||
self._initializing[stream_id] = False # 清除初始化标记
|
||||
|
||||
return conversation_instance
|
||||
|
||||
async def _initialize_conversation(self, conversation: Conversation):
|
||||
"""(内部方法) 初始化会话实例的核心逻辑"""
|
||||
async def _initialize_conversation_wrapper(self, conversation: Conversation):
|
||||
"""
|
||||
(内部方法) 初始化会话实例的核心逻辑包装器。
|
||||
"""
|
||||
stream_id = conversation.stream_id
|
||||
private_name = conversation.private_name
|
||||
try:
|
||||
logger.info(f"[私聊][{private_name}] 管理器开始调用 conversation._initialize(): {stream_id}")
|
||||
await conversation._initialize() # 调用实例自身的初始化方法
|
||||
# 注意:初始化成功与否由 conversation._initialized 和 conversation.should_continue 标志决定
|
||||
if conversation._initialized:
|
||||
logger.info(f"[私聊][{private_name}] Manager 开始调用 initialize_core_components(): {stream_id}")
|
||||
await initialize_core_components(conversation)
|
||||
|
||||
# 检查初始化函数执行后的状态
|
||||
if conversation.state != ConversationState.INIT and conversation.state != ConversationState.ERROR:
|
||||
conversation._initialized = True
|
||||
conversation.should_continue = True
|
||||
logger.info(
|
||||
f"[私聊][{private_name}] conversation._initialize() 调用完成,实例标记为已初始化: {stream_id}"
|
||||
f"[私聊][{private_name}] initialize_core_components() 调用完成,实例标记为已初始化且可继续: {stream_id}"
|
||||
)
|
||||
else:
|
||||
conversation._initialized = False
|
||||
conversation.should_continue = False
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] conversation._initialize() 调用完成,但实例未成功标记为已初始化: {stream_id}"
|
||||
f"[私聊][{private_name}] initialize_core_components() 调用完成,但实例状态为 {conversation.state.name},标记为未初始化或不可继续: {stream_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# _initialize 内部应该处理自己的异常,但这里也捕获以防万一
|
||||
logger.error(
|
||||
f"[私聊][{private_name}] 调用 conversation._initialize() 时发生未捕获错误: {stream_id}, 错误: {e}"
|
||||
f"[私聊][{private_name}] 调用 initialize_core_components() 时发生未捕获错误: {stream_id}, 错误: {e}"
|
||||
)
|
||||
logger.error(traceback.format_exc())
|
||||
# 确保实例状态反映失败
|
||||
conversation._initialized = False
|
||||
conversation.should_continue = False
|
||||
# >>> 修改:在捕获到异常时设置 ERROR 状态 <<<
|
||||
conversation.state = ConversationState.ERROR
|
||||
|
||||
async def _cleanup_conversation(self, conversation: Conversation):
|
||||
"""清理会话实例的资源"""
|
||||
|
|
@ -147,17 +138,10 @@ class PFCManager:
|
|||
private_name = conversation.private_name
|
||||
logger.info(f"[私聊][{private_name}] 开始清理会话实例资源: {stream_id}")
|
||||
try:
|
||||
# 调用 conversation 的 stop 方法来停止其内部组件
|
||||
if hasattr(conversation, "stop") and callable(conversation.stop):
|
||||
await conversation.stop() # stop 方法应处理内部组件的停止
|
||||
await conversation.stop()
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] Conversation 对象缺少 stop 方法,可能无法完全清理资源。")
|
||||
# 尝试手动停止已知组件 (作为后备)
|
||||
if hasattr(conversation, "idle_conversation_starter") and conversation.idle_conversation_starter:
|
||||
conversation.idle_conversation_starter.stop()
|
||||
if hasattr(conversation, "observation_info") and conversation.observation_info:
|
||||
conversation.observation_info.unbind_from_chat_observer()
|
||||
# ChatObserver 是单例,不在此处停止
|
||||
logger.warning(f"[私聊][{private_name}] Conversation 对象缺少 stop 方法。")
|
||||
|
||||
logger.info(f"[私聊][{private_name}] 会话实例 {stream_id} 资源已清理")
|
||||
except Exception as e:
|
||||
|
|
@ -168,15 +152,14 @@ class PFCManager:
|
|||
"""获取已存在的会话实例 (只读)"""
|
||||
instance = self._instances.get(stream_id)
|
||||
if instance and instance._initialized and instance.should_continue:
|
||||
# 检查忽略状态
|
||||
if (
|
||||
hasattr(instance, "ignore_until_timestamp")
|
||||
and instance.ignore_until_timestamp
|
||||
and time.time() < instance.ignore_until_timestamp
|
||||
):
|
||||
return None # 忽略期间不返回
|
||||
return None
|
||||
return instance
|
||||
return None # 不存在或无效则返回 None
|
||||
return None
|
||||
|
||||
async def remove_conversation(self, stream_id: str):
|
||||
"""移除并清理会话实例"""
|
||||
|
|
@ -184,11 +167,9 @@ class PFCManager:
|
|||
instance_to_remove = self._instances[stream_id]
|
||||
logger.info(f"[管理器] 准备移除并清理会话实例: {stream_id}")
|
||||
try:
|
||||
# 先从字典中移除引用,防止新的请求获取到正在清理的实例
|
||||
del self._instances[stream_id]
|
||||
if stream_id in self._initializing:
|
||||
del self._initializing[stream_id]
|
||||
# 清理资源
|
||||
await self._cleanup_conversation(instance_to_remove)
|
||||
logger.info(f"[管理器] 会话实例 {stream_id} 已成功移除并清理")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
import traceback
|
||||
|
||||
from maim_message import UserInfo
|
||||
from src.config.config import global_config
|
||||
from src.common.logger_manager import get_logger
|
||||
from ..chat.chat_stream import chat_manager
|
||||
from typing import Optional, Dict, Any
|
||||
from .pfc_manager import PFCManager
|
||||
from src.plugins.chat.message import MessageRecv
|
||||
from src.plugins.storage.storage import MessageStorage
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
logger = get_logger("pfc_processor")
|
||||
|
||||
|
||||
async def _handle_error(error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
|
||||
"""统一的错误处理函数
|
||||
|
||||
Args:
|
||||
error: 捕获到的异常
|
||||
context: 错误发生的上下文描述
|
||||
message: 可选的消息对象,用于记录相关消息内容
|
||||
"""
|
||||
logger.error(f"{context}: {error}")
|
||||
logger.error(traceback.format_exc())
|
||||
if message and hasattr(message, "raw_message"):
|
||||
logger.error(f"相关消息原始内容: {message.raw_message}")
|
||||
|
||||
|
||||
class PFCProcessor:
|
||||
"""PFC 处理器,负责处理接收到的信息并计数"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化 PFC 处理器,创建消息存储实例"""
|
||||
self.storage = MessageStorage()
|
||||
self.pfc_manager = PFCManager.get_instance()
|
||||
|
||||
async def process_message(self, message_data: Dict[str, Any]) -> None:
|
||||
"""处理接收到的原始消息数据
|
||||
|
||||
主要流程:
|
||||
1. 消息解析与初始化
|
||||
2. 过滤检查
|
||||
3. 消息存储
|
||||
4. 创建 PFC 流
|
||||
5. 日志记录
|
||||
|
||||
Args:
|
||||
message_data: 原始消息字符串
|
||||
"""
|
||||
message = None
|
||||
try:
|
||||
# 1. 消息解析与初始化
|
||||
message = MessageRecv(message_data)
|
||||
groupinfo = message.message_info.group_info
|
||||
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,
|
||||
group_info=groupinfo,
|
||||
)
|
||||
message.update_chat_stream(chat)
|
||||
|
||||
# 2. 过滤检查
|
||||
# 处理消息
|
||||
await message.process()
|
||||
# 过滤词/正则表达式过滤
|
||||
if self._check_ban_words(message.processed_plain_text, userinfo) or self._check_ban_regex(
|
||||
message.raw_message, userinfo
|
||||
):
|
||||
return
|
||||
|
||||
# 3. 消息存储
|
||||
await self.storage.store_message(message, chat)
|
||||
logger.trace(f"存储成功: {message.processed_plain_text}")
|
||||
|
||||
# 4. 创建 PFC 聊天流
|
||||
await self._create_pfc_chat(message)
|
||||
|
||||
# 5. 日志记录
|
||||
# 将时间戳转换为datetime对象
|
||||
current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S")
|
||||
logger.info(
|
||||
f"[{current_time}][私聊]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await _handle_error(e, "消息处理失败", message)
|
||||
|
||||
async def _create_pfc_chat(self, message: MessageRecv):
|
||||
try:
|
||||
chat_id = str(message.chat_stream.stream_id)
|
||||
private_name = str(message.message_info.user_info.user_nickname)
|
||||
|
||||
if global_config.enable_pfc_chatting:
|
||||
await self.pfc_manager.get_or_create_conversation(chat_id, private_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建PFC聊天失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _check_ban_words(text: str, userinfo: UserInfo) -> bool:
|
||||
"""检查消息中是否包含过滤词"""
|
||||
for word in global_config.ban_words:
|
||||
if word in text:
|
||||
logger.info(f"[私聊]{userinfo.user_nickname}:{text}")
|
||||
logger.info(f"[过滤词识别]消息中含有{word},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _check_ban_regex(text: str, userinfo: UserInfo) -> bool:
|
||||
"""检查消息是否匹配过滤正则表达式"""
|
||||
for pattern in global_config.ban_msgs_regex:
|
||||
if pattern.search(text):
|
||||
logger.info(f"[私聊]{userinfo.user_nickname}:{text}")
|
||||
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||
return True
|
||||
return False
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
from typing import List, Dict, Any
|
||||
from src.plugins.PFC.chat_observer import ChatObserver
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.models.utils_model import LLMRequest
|
||||
from src.plugins.person_info.person_info import person_info_manager
|
||||
from src.plugins.person_info.relationship_manager import (
|
||||
relationship_manager,
|
||||
) # 主要用其 ensure_float 和 build_relationship_info
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.plugins.PFC.observation_info import ObservationInfo
|
||||
from src.plugins.PFC.conversation_info import ConversationInfo
|
||||
from src.plugins.PFC.pfc_utils import get_items_from_json, adjust_relationship_value_nonlinear
|
||||
from src.config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config)
|
||||
|
||||
|
||||
logger = get_logger("pfc_relationship")
|
||||
|
||||
|
||||
class PfcRelationshipUpdater:
|
||||
def __init__(self, private_name: str, bot_name: str):
|
||||
"""
|
||||
初始化关系更新器。
|
||||
|
||||
Args:
|
||||
private_name (str): 当前私聊对象的名称 (用于日志)。
|
||||
bot_name (str): 机器人自己的名称。
|
||||
"""
|
||||
self.private_name = private_name
|
||||
self.bot_name = bot_name
|
||||
self.person_info_mng = person_info_manager
|
||||
self.relationship_mng = relationship_manager # 复用其实例方法
|
||||
|
||||
# LLM 实例 (为关系评估创建一个新的)
|
||||
# 尝试读取 llm_PFC_relationship_eval 配置,如果不存在则回退
|
||||
llm_config_rel_eval = getattr(global_config, "llm_PFC_relationship_eval", None)
|
||||
if llm_config_rel_eval and isinstance(llm_config_rel_eval, dict):
|
||||
logger.info(f"[私聊][{self.private_name}] 使用 llm_PFC_relationship_eval 配置初始化关系评估LLM。")
|
||||
self.llm = LLMRequest(
|
||||
model=llm_config_rel_eval,
|
||||
temperature=llm_config_rel_eval.get("temp", 0.5), # 判断任务通常用较低温度
|
||||
max_tokens=llm_config_rel_eval.get("max_tokens", 512),
|
||||
request_type="pfc_relationship_evaluation",
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}] 未找到 llm_PFC_relationship_eval 配置或配置无效,将回退使用 llm_PFC_action_planner 的配置。"
|
||||
)
|
||||
llm_config_action_planner = getattr(global_config, "llm_PFC_action_planner", None)
|
||||
if llm_config_action_planner and isinstance(llm_config_action_planner, dict):
|
||||
self.llm = LLMRequest(
|
||||
model=llm_config_action_planner, # 使用 action_planner 的模型配置
|
||||
temperature=llm_config_action_planner.get("temp", 0.5), # 但温度可以尝试低一些
|
||||
max_tokens=llm_config_action_planner.get("max_tokens", 512),
|
||||
request_type="pfc_relationship_evaluation_fallback",
|
||||
)
|
||||
else: # 极端情况,连 action_planner 的配置都没有
|
||||
logger.error(f"[私聊][{self.private_name}] 无法找到任何有效的LLM配置用于关系评估!关系更新功能将受限。")
|
||||
self.llm = None # LLM 未初始化
|
||||
|
||||
# 从 global_config 读取参数,若无则使用默认值
|
||||
self.REL_INCREMENTAL_INTERVAL = getattr(global_config, "pfc_relationship_incremental_interval", 10)
|
||||
self.REL_INCREMENTAL_MSG_COUNT = getattr(global_config, "pfc_relationship_incremental_msg_count", 10)
|
||||
self.REL_INCREMENTAL_DEFAULT_CHANGE = getattr(global_config, "pfc_relationship_incremental_default_change", 1.0)
|
||||
self.REL_INCREMENTAL_MAX_CHANGE = getattr(global_config, "pfc_relationship_incremental_max_change", 5.0)
|
||||
|
||||
self.REL_FINAL_MSG_COUNT = getattr(global_config, "pfc_relationship_final_msg_count", 30)
|
||||
self.REL_FINAL_DEFAULT_CHANGE = getattr(global_config, "pfc_relationship_final_default_change", 5.0)
|
||||
self.REL_FINAL_MAX_CHANGE = getattr(global_config, "pfc_relationship_final_max_change", 50.0)
|
||||
|
||||
async def update_relationship_incremental(
|
||||
self,
|
||||
conversation_info: ConversationInfo,
|
||||
observation_info: ObservationInfo,
|
||||
chat_observer_for_history: ChatObserver, # ChatObserver 实例
|
||||
) -> None:
|
||||
if not self.llm:
|
||||
logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行增量关系更新。")
|
||||
return
|
||||
if not conversation_info or not conversation_info.person_id or not observation_info:
|
||||
logger.debug(f"[私聊][{self.private_name}] 增量关系更新:缺少必要信息。")
|
||||
return
|
||||
|
||||
if not (
|
||||
conversation_info.current_instance_message_count % self.REL_INCREMENTAL_INTERVAL == 0
|
||||
and conversation_info.current_instance_message_count > 0
|
||||
):
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] 达到增量关系更新阈值 ({conversation_info.current_instance_message_count}条消息),开始评估..."
|
||||
)
|
||||
|
||||
messages_for_eval: List[Dict[str, Any]] = []
|
||||
if chat_observer_for_history:
|
||||
messages_for_eval = chat_observer_for_history.get_cached_messages(limit=self.REL_INCREMENTAL_MSG_COUNT)
|
||||
elif observation_info.chat_history:
|
||||
messages_for_eval = observation_info.chat_history[-self.REL_INCREMENTAL_MSG_COUNT :]
|
||||
|
||||
if not messages_for_eval:
|
||||
logger.warning(f"[私聊][{self.private_name}] 增量关系更新:没有足够的消息进行评估。")
|
||||
return
|
||||
|
||||
readable_history_for_llm = await build_readable_messages(
|
||||
messages_for_eval, replace_bot_name=True, merge_messages=False, timestamp_mode="relative"
|
||||
)
|
||||
|
||||
current_relationship_value = await self.person_info_mng.get_value(
|
||||
conversation_info.person_id, "relationship_value"
|
||||
)
|
||||
current_relationship_value = self.relationship_mng.ensure_float(
|
||||
current_relationship_value, conversation_info.person_id
|
||||
)
|
||||
|
||||
relationship_prompt = f"""你是{self.bot_name}。你正在与{self.private_name}私聊。
|
||||
你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越代表关系越好)。
|
||||
以下是你们最近的对话内容:
|
||||
---
|
||||
{readable_history_for_llm}
|
||||
---
|
||||
请基于以上对话,判断你与{self.private_name}的关系值应该如何“谨慎地”调整。
|
||||
请输出一个JSON对象,包含一个 "adjustment" 字段,其值为一个介于 -{self.REL_INCREMENTAL_MAX_CHANGE} 和 +{self.REL_INCREMENTAL_MAX_CHANGE} 之间的整数,代表关系值的变化。
|
||||
例如:{{ "adjustment": 3 }}。如果对话内容不明确或难以判断,请倾向于输出较小的调整值(如0, 1, -1)。"""
|
||||
|
||||
raw_adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE
|
||||
try:
|
||||
logger.debug(f"[私聊][{self.private_name}] 增量关系评估Prompt:\n{relationship_prompt}")
|
||||
content, _ = await self.llm.generate_response_async(relationship_prompt)
|
||||
logger.debug(f"[私聊][{self.private_name}] 增量关系评估LLM原始返回: {content}")
|
||||
|
||||
success, result = get_items_from_json(
|
||||
content,
|
||||
self.private_name,
|
||||
"adjustment",
|
||||
default_values={"adjustment": self.REL_INCREMENTAL_DEFAULT_CHANGE},
|
||||
required_types={"adjustment": (int, float)},
|
||||
)
|
||||
raw_adjustment = result.get("adjustment", self.REL_INCREMENTAL_DEFAULT_CHANGE)
|
||||
if not isinstance(raw_adjustment, (int, float)):
|
||||
raw_adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE
|
||||
else:
|
||||
raw_adjustment_val = float(raw_adjustment)
|
||||
raw_adjustment_val = max(
|
||||
-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 增量关系评估LLM调用或解析失败: {e}")
|
||||
|
||||
adjustment_val = await adjust_relationship_value_nonlinear(current_relationship_value, raw_adjustment_val)
|
||||
|
||||
new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val))
|
||||
await self.person_info_mng.update_one_field(
|
||||
conversation_info.person_id, "relationship_value", new_relationship_value
|
||||
)
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] 增量关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},变为 {new_relationship_value:.2f}"
|
||||
)
|
||||
|
||||
if conversation_info.person_id:
|
||||
conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(
|
||||
conversation_info.person_id, is_id=True
|
||||
)
|
||||
|
||||
async def update_relationship_final(
|
||||
self,
|
||||
conversation_info: ConversationInfo,
|
||||
observation_info: ObservationInfo,
|
||||
chat_observer_for_history: ChatObserver,
|
||||
) -> None:
|
||||
if not self.llm:
|
||||
logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行最终关系更新。")
|
||||
return
|
||||
if not conversation_info or not conversation_info.person_id or not observation_info:
|
||||
logger.debug(f"[私聊][{self.private_name}] 最终关系更新:缺少必要信息。")
|
||||
return
|
||||
|
||||
logger.info(f"[私聊][{self.private_name}] 私聊结束,开始最终关系评估...")
|
||||
|
||||
messages_for_eval: List[Dict[str, Any]] = []
|
||||
if chat_observer_for_history:
|
||||
messages_for_eval = chat_observer_for_history.get_cached_messages(limit=self.REL_FINAL_MSG_COUNT)
|
||||
elif observation_info.chat_history:
|
||||
messages_for_eval = observation_info.chat_history[-self.REL_FINAL_MSG_COUNT :]
|
||||
|
||||
if not messages_for_eval:
|
||||
logger.warning(f"[私聊][{self.private_name}] 最终关系更新:没有足够的消息进行评估。")
|
||||
return
|
||||
|
||||
readable_history_for_llm = await build_readable_messages(
|
||||
messages_for_eval, replace_bot_name=True, merge_messages=False, timestamp_mode="relative"
|
||||
)
|
||||
|
||||
current_relationship_value = await self.person_info_mng.get_value(
|
||||
conversation_info.person_id, "relationship_value"
|
||||
)
|
||||
current_relationship_value = self.relationship_mng.ensure_float(
|
||||
current_relationship_value, conversation_info.person_id
|
||||
)
|
||||
|
||||
relationship_prompt = f"""你是{self.bot_name}。你与{self.private_name}的私聊刚刚结束。
|
||||
你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越好)。
|
||||
以下是你们本次私聊最后部分的对话内容:
|
||||
---
|
||||
{readable_history_for_llm}
|
||||
---
|
||||
请基于以上对话的整体情况,判断你与【{self.private_name}】的关系值应该如何进行一次总结性的调整。
|
||||
请输出一个JSON对象,包含一个 "final_adjustment" 字段,其值为一个整数,代表关系值的变化量(例如,可以是 -{self.REL_FINAL_MAX_CHANGE} 到 +{self.REL_FINAL_MAX_CHANGE} 之间的一个值)。
|
||||
请大胆评估,但也要合理。"""
|
||||
|
||||
raw_adjustment_val = self.REL_FINAL_DEFAULT_CHANGE
|
||||
try:
|
||||
logger.debug(f"[私聊][{self.private_name}] 最终关系评估Prompt:\n{relationship_prompt}")
|
||||
content, _ = await self.llm.generate_response_async(relationship_prompt)
|
||||
logger.debug(f"[私聊][{self.private_name}] 最终关系评估LLM原始返回: {content}")
|
||||
|
||||
success, result = get_items_from_json(
|
||||
content,
|
||||
self.private_name,
|
||||
"final_adjustment",
|
||||
default_values={"final_adjustment": self.REL_FINAL_DEFAULT_CHANGE},
|
||||
required_types={"final_adjustment": (int, float)},
|
||||
)
|
||||
raw_adjustment = result.get("final_adjustment", self.REL_FINAL_DEFAULT_CHANGE)
|
||||
if not isinstance(raw_adjustment, (int, float)):
|
||||
raw_adjustment_val = self.REL_FINAL_DEFAULT_CHANGE
|
||||
else:
|
||||
raw_adjustment_val = float(raw_adjustment)
|
||||
raw_adjustment_val = max(
|
||||
-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 最终关系评估LLM调用或解析失败: {e}")
|
||||
|
||||
adjustment_val = await adjust_relationship_value_nonlinear(current_relationship_value, raw_adjustment_val)
|
||||
|
||||
new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val))
|
||||
await self.person_info_mng.update_one_field(
|
||||
conversation_info.person_id, "relationship_value", new_relationship_value
|
||||
)
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] 最终关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}"
|
||||
)
|
||||
|
||||
if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨
|
||||
conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(
|
||||
conversation_info.person_id, is_id=True
|
||||
)
|
||||
|
||||
|
||||
class PfcRepationshipTranslator:
|
||||
"""直接完整导入群聊的relationship_manager.py可能不可取
|
||||
因为对于PFC的planner来说
|
||||
其暗示了选择回复
|
||||
所以新建代码文件来适配PFC的决策层面"""
|
||||
|
||||
def __init__(self, private_name: str):
|
||||
self.private_name = private_name
|
||||
|
||||
async def translate_relationship_value_to_text(self, relationship_value: float) -> str:
|
||||
"""
|
||||
将数值型的关系值转换为PFC私聊场景下简洁的关系描述文本。
|
||||
"""
|
||||
level_num = self._calculate_relationship_level_num(relationship_value, self.private_name)
|
||||
|
||||
relationship_descriptions = [
|
||||
"厌恶", # level_num 0
|
||||
"冷漠", # level_num 1
|
||||
"初识", # level_num 2
|
||||
"友好", # level_num 3
|
||||
"喜欢", # level_num 4
|
||||
"暧昧", # level_num 5
|
||||
]
|
||||
|
||||
if 0 <= level_num < len(relationship_descriptions):
|
||||
description = relationship_descriptions[level_num]
|
||||
else:
|
||||
description = "普通" # 默认或错误情况
|
||||
logger.warning(f"[私聊][{self.private_name}] 计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'")
|
||||
|
||||
return f"你们的关系是:{description}。"
|
||||
|
||||
@staticmethod
|
||||
def _calculate_relationship_level_num(relationship_value: float, private_name: str) -> int:
|
||||
"""
|
||||
根据关系值计算关系等级编号 (0-5)。
|
||||
这里的阈值应与 relationship_manager.py 中的保持一致
|
||||
"""
|
||||
if not isinstance(relationship_value, (int, float)):
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] 传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。"
|
||||
)
|
||||
relationship_value = 0.0
|
||||
|
||||
if -1000 <= relationship_value < -227:
|
||||
level_num = 0 # 厌恶
|
||||
elif -227 <= relationship_value < -73:
|
||||
level_num = 1 # 冷漠
|
||||
elif -73 <= relationship_value < 227:
|
||||
level_num = 2 # 普通/认识
|
||||
elif 227 <= relationship_value < 587:
|
||||
level_num = 3 # 友好
|
||||
elif 587 <= relationship_value < 900:
|
||||
level_num = 4 # 喜欢
|
||||
elif 900 <= relationship_value <= 1000:
|
||||
level_num = 5 # 暧昧
|
||||
else:
|
||||
# 超出范围的值处理
|
||||
if relationship_value > 1000:
|
||||
level_num = 5
|
||||
elif relationship_value < -1000:
|
||||
level_num = 0
|
||||
else: # 理论上不会到这里,除非前面的条件逻辑有误
|
||||
logger.warning(f"[私聊][{private_name}] 关系值 {relationship_value} 未落入任何预设范围,默认为普通。")
|
||||
level_num = 2
|
||||
return level_num
|
||||
|
|
@ -5,6 +5,9 @@ from typing import Dict, Any, Optional, Tuple, List, Union
|
|||
from src.common.logger_manager import get_logger # 确认 logger 的导入路径
|
||||
from src.plugins.memory_system.Hippocampus import HippocampusManager
|
||||
from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 确认 prompt_builder 的导入路径
|
||||
from src.plugins.chat.chat_stream import ChatStream
|
||||
from ..person_info.person_info import person_info_manager
|
||||
import math
|
||||
|
||||
logger = get_logger("pfc_utils")
|
||||
|
||||
|
|
@ -98,101 +101,241 @@ def get_items_from_json(
|
|||
Returns:
|
||||
Tuple[bool, Union[Dict[str, Any], List[Dict[str, Any]]]]: (是否成功, 提取的字段字典或字典列表)
|
||||
"""
|
||||
content = content.strip()
|
||||
result = {}
|
||||
cleaned_content = content.strip()
|
||||
result: Union[Dict[str, Any], List[Dict[str, Any]]] = {} # 初始化类型
|
||||
# 匹配 ```json ... ``` 或 ``` ... ```
|
||||
markdown_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", cleaned_content, re.IGNORECASE)
|
||||
if markdown_match:
|
||||
cleaned_content = markdown_match.group(1).strip()
|
||||
logger.debug(f"[私聊][{private_name}] 已去除 Markdown 标记,剩余内容: {cleaned_content[:100]}...")
|
||||
# --- 新增结束 ---
|
||||
|
||||
# 设置默认值
|
||||
default_result: Dict[str, Any] = {} # 用于单对象时的默认值
|
||||
if default_values:
|
||||
result.update(default_values)
|
||||
default_result.update(default_values)
|
||||
result = default_result.copy() # 先用默认值初始化
|
||||
|
||||
# 首先尝试解析为JSON数组
|
||||
if allow_array:
|
||||
try:
|
||||
# 尝试找到文本中的JSON数组
|
||||
array_pattern = r"\[[\s\S]*\]"
|
||||
array_match = re.search(array_pattern, content)
|
||||
if array_match:
|
||||
array_content = array_match.group()
|
||||
json_array = json.loads(array_content)
|
||||
# 尝试直接解析清理后的内容为列表
|
||||
json_array = json.loads(cleaned_content)
|
||||
|
||||
# 确认是数组类型
|
||||
if isinstance(json_array, list):
|
||||
# 验证数组中的每个项目是否包含所有必需字段
|
||||
valid_items = []
|
||||
for item in json_array:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if isinstance(json_array, list):
|
||||
valid_items_list: List[Dict[str, Any]] = []
|
||||
for item in json_array:
|
||||
if not isinstance(item, dict):
|
||||
logger.warning(f"[私聊][{private_name}] JSON数组中的元素不是字典: {item}")
|
||||
continue
|
||||
|
||||
# 检查是否有所有必需字段
|
||||
if all(field in item for field in items):
|
||||
# 验证字段类型
|
||||
if required_types:
|
||||
type_valid = True
|
||||
for field, expected_type in required_types.items():
|
||||
if field in item and not isinstance(item[field], expected_type):
|
||||
type_valid = False
|
||||
break
|
||||
current_item_result = default_result.copy() # 每个元素都用默认值初始化
|
||||
valid_item = True
|
||||
|
||||
if not type_valid:
|
||||
continue
|
||||
# 提取并验证字段
|
||||
for field in items:
|
||||
if field in item:
|
||||
current_item_result[field] = item[field]
|
||||
elif field not in default_result: # 如果字段不存在且没有默认值
|
||||
logger.warning(f"[私聊][{private_name}] JSON数组元素缺少必要字段 '{field}': {item}")
|
||||
valid_item = False
|
||||
break # 这个元素无效
|
||||
|
||||
# 验证字符串字段不为空
|
||||
string_valid = True
|
||||
for field in items:
|
||||
if isinstance(item[field], str) and not item[field].strip():
|
||||
string_valid = False
|
||||
break
|
||||
if not valid_item:
|
||||
continue
|
||||
|
||||
if not string_valid:
|
||||
continue
|
||||
# 验证类型
|
||||
if required_types:
|
||||
for field, expected_type in required_types.items():
|
||||
# 检查 current_item_result 中是否存在该字段 (可能来自 item 或 default_values)
|
||||
if field in current_item_result and not isinstance(
|
||||
current_item_result[field], expected_type
|
||||
):
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] JSON数组元素字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_item_result[field]).__name__}): {item}"
|
||||
)
|
||||
valid_item = False
|
||||
break
|
||||
|
||||
valid_items.append(item)
|
||||
if not valid_item:
|
||||
continue
|
||||
|
||||
# 验证字符串不为空 (只检查 items 中要求的字段)
|
||||
for field in items:
|
||||
if (
|
||||
field in current_item_result
|
||||
and isinstance(current_item_result[field], str)
|
||||
and not current_item_result[field].strip()
|
||||
):
|
||||
logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 不能为空字符串: {item}")
|
||||
valid_item = False
|
||||
break
|
||||
|
||||
if valid_item:
|
||||
valid_items_list.append(current_item_result) # 只添加完全有效的项
|
||||
|
||||
if valid_items_list: # 只有当列表不为空时才认为是成功
|
||||
logger.debug(f"[私聊][{private_name}] 成功解析JSON数组,包含 {len(valid_items_list)} 个有效项目。")
|
||||
return True, valid_items_list
|
||||
else:
|
||||
# 如果列表为空(可能所有项都无效),则继续尝试解析为单个对象
|
||||
logger.debug(f"[私聊][{private_name}] 解析为JSON数组,但未找到有效项目,尝试解析单个JSON对象。")
|
||||
# result 重置回单个对象的默认值
|
||||
result = default_result.copy()
|
||||
|
||||
if valid_items:
|
||||
return True, valid_items
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(f"[私聊][{private_name}]JSON数组解析失败,尝试解析单个JSON对象")
|
||||
logger.debug(f"[私聊][{private_name}] JSON数组直接解析失败,尝试解析单个JSON对象")
|
||||
# result 重置回单个对象的默认值
|
||||
result = default_result.copy()
|
||||
except Exception as e:
|
||||
logger.debug(f"[私聊][{private_name}]尝试解析JSON数组时出错: {str(e)}")
|
||||
logger.error(f"[私聊][{private_name}] 尝试解析JSON数组时发生未知错误: {str(e)}")
|
||||
# result 重置回单个对象的默认值
|
||||
result = default_result.copy()
|
||||
|
||||
# 尝试解析JSON对象
|
||||
# 尝试解析为单个JSON对象
|
||||
try:
|
||||
json_data = json.loads(content)
|
||||
# 尝试直接解析清理后的内容
|
||||
json_data = json.loads(cleaned_content)
|
||||
if not isinstance(json_data, dict):
|
||||
logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}")
|
||||
return False, default_result # 返回失败和默认值
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 如果直接解析失败,尝试查找和提取JSON部分
|
||||
json_pattern = r"\{[^{}]*\}"
|
||||
json_match = re.search(json_pattern, content)
|
||||
# 如果直接解析失败,尝试用正则表达式查找 JSON 对象部分 (作为后备)
|
||||
# 这个正则比较简单,可能无法处理嵌套或复杂的 JSON
|
||||
json_pattern = r"\{[\s\S]*?\}" # 使用非贪婪匹配
|
||||
json_match = re.search(json_pattern, cleaned_content)
|
||||
if json_match:
|
||||
try:
|
||||
json_data = json.loads(json_match.group())
|
||||
potential_json_str = json_match.group()
|
||||
json_data = json.loads(potential_json_str)
|
||||
if not isinstance(json_data, dict):
|
||||
logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}")
|
||||
return False, default_result
|
||||
logger.debug(f"[私聊][{private_name}] 通过正则提取并成功解析JSON对象。")
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"[私聊][{private_name}]提取的JSON内容解析失败")
|
||||
return False, result
|
||||
logger.error(f"[私聊][{private_name}] 正则提取的部分 '{potential_json_str[:100]}...' 无法解析为JSON。")
|
||||
return False, default_result
|
||||
else:
|
||||
logger.error(f"[私聊][{private_name}]无法在返回内容中找到有效的JSON")
|
||||
return False, result
|
||||
logger.error(
|
||||
f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}..."
|
||||
)
|
||||
return False, default_result
|
||||
|
||||
# 提取字段
|
||||
# 提取并验证字段 (适用于单个JSON对象)
|
||||
# 确保 result 是字典类型用于更新
|
||||
if not isinstance(result, dict):
|
||||
result = default_result.copy() # 如果之前是列表,重置为字典
|
||||
|
||||
valid_single_object = True
|
||||
for item in items:
|
||||
if item in json_data:
|
||||
result[item] = json_data[item]
|
||||
elif item not in default_result: # 如果字段不存在且没有默认值
|
||||
logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item}'。JSON内容: {json_data}")
|
||||
valid_single_object = False
|
||||
break # 这个对象无效
|
||||
|
||||
# 验证必需字段
|
||||
if not all(item in result for item in items):
|
||||
logger.error(f"[私聊][{private_name}]JSON缺少必要字段,实际内容: {json_data}")
|
||||
return False, result
|
||||
if not valid_single_object:
|
||||
return False, default_result
|
||||
|
||||
# 验证字段类型
|
||||
# 验证类型
|
||||
if required_types:
|
||||
for field, expected_type in required_types.items():
|
||||
if field in result and not isinstance(result[field], expected_type):
|
||||
logger.error(f"[私聊][{private_name}]{field} 必须是 {expected_type.__name__} 类型")
|
||||
return False, result
|
||||
logger.error(
|
||||
f"[私聊][{private_name}] JSON对象字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(result[field]).__name__})"
|
||||
)
|
||||
valid_single_object = False
|
||||
break
|
||||
|
||||
# 验证字符串字段不为空
|
||||
if not valid_single_object:
|
||||
return False, default_result
|
||||
|
||||
# 验证字符串不为空 (只检查 items 中要求的字段)
|
||||
for field in items:
|
||||
if isinstance(result[field], str) and not result[field].strip():
|
||||
logger.error(f"[私聊][{private_name}]{field} 不能为空")
|
||||
return False, result
|
||||
if field in result and isinstance(result[field], str) and not result[field].strip():
|
||||
logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 不能为空字符串")
|
||||
valid_single_object = False
|
||||
break
|
||||
|
||||
return True, result
|
||||
if valid_single_object:
|
||||
logger.debug(f"[私聊][{private_name}] 成功解析并验证了单个JSON对象。")
|
||||
return True, result # 返回提取并验证后的字典
|
||||
else:
|
||||
return False, default_result # 验证失败
|
||||
|
||||
|
||||
async def get_person_id(private_name: str, chat_stream: ChatStream):
|
||||
private_user_id_str: Optional[str] = None
|
||||
private_platform_str: Optional[str] = None
|
||||
private_nickname_str = private_name
|
||||
|
||||
if chat_stream.user_info:
|
||||
private_user_id_str = str(chat_stream.user_info.user_id)
|
||||
private_platform_str = chat_stream.user_info.platform
|
||||
logger.debug(
|
||||
f"[私聊][{private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}"
|
||||
)
|
||||
elif chat_stream.group_info is None and private_name:
|
||||
pass
|
||||
|
||||
if private_user_id_str and private_platform_str:
|
||||
try:
|
||||
private_user_id_int = int(private_user_id_str)
|
||||
# person_id = person_info_manager.get_person_id( # get_person_id 可能只查询,不创建
|
||||
# private_platform_str,
|
||||
# private_user_id_int
|
||||
# )
|
||||
# 使用 get_or_create_person 确保用户存在
|
||||
person_id = await person_info_manager.get_or_create_person(
|
||||
platform=private_platform_str,
|
||||
user_id=private_user_id_int,
|
||||
nickname=private_name, # 使用传入的 private_name 作为昵称
|
||||
)
|
||||
if person_id is None: # 如果 get_or_create_person 返回 None,说明创建失败
|
||||
logger.error(f"[私聊][{private_name}] get_or_create_person 未能获取或创建 person_id。")
|
||||
return None # 返回 None 表示失败
|
||||
|
||||
return person_id, private_platform_str, private_user_id_str # 返回获取或创建的 person_id
|
||||
except ValueError:
|
||||
logger.error(f"[私聊][{private_name}] 无法将 private_user_id_str ('{private_user_id_str}') 转换为整数。")
|
||||
return None # 返回 None 表示失败
|
||||
except Exception as e_pid:
|
||||
logger.error(f"[私聊][{private_name}] 获取或创建 person_id 时出错: {e_pid}")
|
||||
return None # 返回 None 表示失败
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。"
|
||||
)
|
||||
return None # 返回 None 表示失败
|
||||
|
||||
|
||||
async def adjust_relationship_value_nonlinear(old_value: float, raw_adjustment: float) -> float:
|
||||
# 限制 old_value 范围
|
||||
old_value = max(-1000, min(1000, old_value))
|
||||
value = raw_adjustment
|
||||
|
||||
if old_value >= 0:
|
||||
if value >= 0:
|
||||
value = value * math.cos(math.pi * old_value / 2000)
|
||||
if old_value > 500:
|
||||
rdict = await person_info_manager.get_specific_value_list("relationship_value", lambda x: x > 700)
|
||||
high_value_count = len(rdict)
|
||||
if old_value > 700:
|
||||
value *= 3 / (high_value_count + 2)
|
||||
else:
|
||||
value *= 3 / (high_value_count + 3)
|
||||
elif value < 0:
|
||||
value = value * math.exp(old_value / 2000)
|
||||
else:
|
||||
value = 0
|
||||
else:
|
||||
if value >= 0:
|
||||
value = value * math.exp(old_value / 2000)
|
||||
elif value < 0:
|
||||
value = value * math.cos(math.pi * old_value / 2000)
|
||||
else:
|
||||
value = 0
|
||||
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -1,183 +1,90 @@
|
|||
import json
|
||||
from typing import Tuple, List, Dict, Any
|
||||
from src.common.logger import get_module_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from src.config.config import global_config # 为了获取 BOT_QQ
|
||||
from .chat_observer import ChatObserver
|
||||
from maim_message import UserInfo
|
||||
|
||||
logger = get_module_logger("reply_checker")
|
||||
|
||||
|
||||
class ReplyChecker:
|
||||
"""回复检查器"""
|
||||
"""回复检查器 - 新版:仅检查机器人自身发言的精确重复"""
|
||||
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
self.llm = LLMRequest(
|
||||
model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check"
|
||||
)
|
||||
# self.llm = LLMRequest(...) # <--- 移除 LLM 初始化
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.private_name = private_name
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
self.max_retries = 3 # 最大重试次数
|
||||
# self.max_retries = 3 # 这个 max_retries 属性在当前设计下不再由 checker 控制,而是由 conversation.py 控制
|
||||
self.bot_qq_str = str(global_config.BOT_QQ) # 获取机器人QQ号用于识别自身消息
|
||||
|
||||
async def check(
|
||||
self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str, retry_count: int = 0
|
||||
self,
|
||||
reply: str,
|
||||
goal: str,
|
||||
chat_history: List[Dict[str, Any]],
|
||||
chat_history_text: str,
|
||||
current_time_str: str,
|
||||
retry_count: int = 0,
|
||||
) -> Tuple[bool, str, bool]:
|
||||
"""检查生成的回复是否合适
|
||||
"""检查生成的回复是否与机器人之前的发言完全一致(长度大于4)
|
||||
|
||||
Args:
|
||||
reply: 生成的回复
|
||||
goal: 对话目标
|
||||
chat_history: 对话历史记录
|
||||
chat_history_text: 对话历史记录文本
|
||||
retry_count: 当前重试次数
|
||||
reply: 待检查的机器人回复内容
|
||||
goal: 当前对话目标 (新逻辑中未使用)
|
||||
chat_history: 对话历史记录 (包含用户和机器人的消息字典列表)
|
||||
chat_history_text: 对话历史记录的文本格式 (新逻辑中未使用)
|
||||
current_time_str: 当前时间的字符串格式 (新逻辑中未使用)
|
||||
retry_count: 当前重试次数 (新逻辑中未使用)
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
|
||||
对于重复消息: (False, "机器人尝试发送重复消息", False)
|
||||
对于非重复消息: (True, "消息内容未与机器人历史发言重复。", False)
|
||||
"""
|
||||
# 不再从 observer 获取,直接使用传入的 chat_history
|
||||
# messages = self.chat_observer.get_cached_messages(limit=20)
|
||||
if not self.bot_qq_str:
|
||||
logger.error(
|
||||
f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查{global_config.BOT_NICKNAME}自身消息。"
|
||||
)
|
||||
return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过
|
||||
|
||||
if len(reply) <= 4:
|
||||
return True, "消息长度小于等于4字符,跳过重复检查。", False
|
||||
|
||||
try:
|
||||
# 筛选出最近由 Bot 自己发送的消息
|
||||
bot_messages = []
|
||||
for msg in reversed(chat_history):
|
||||
user_info = UserInfo.from_dict(msg.get("user_info", {}))
|
||||
if str(user_info.user_id) == str(global_config.BOT_QQ): # 确保比较的是字符串
|
||||
bot_messages.append(msg.get("processed_plain_text", ""))
|
||||
if len(bot_messages) >= 2: # 只和最近的两条比较
|
||||
break
|
||||
# 进行比较
|
||||
if bot_messages:
|
||||
# 可以用简单比较,或者更复杂的相似度库 (如 difflib)
|
||||
# 简单比较:是否完全相同
|
||||
if reply == bot_messages[0]: # 和最近一条完全一样
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
"被逻辑检查拒绝:回复内容与你上一条发言完全相同,可以选择深入话题或寻找其它话题或等待",
|
||||
True,
|
||||
) # 不合适,需要返回至决策层
|
||||
# 2. 相似度检查 (如果精确匹配未通过)
|
||||
import difflib # 导入 difflib 库
|
||||
match_found = False # <--- 用于调试
|
||||
for i, msg_dict in enumerate(chat_history): # <--- 添加索引用于日志
|
||||
if not isinstance(msg_dict, dict):
|
||||
continue
|
||||
|
||||
# 计算编辑距离相似度,ratio() 返回 0 到 1 之间的浮点数
|
||||
similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio()
|
||||
logger.debug(f"[私聊][{self.private_name}]ReplyChecker - 相似度: {similarity_ratio:.2f}")
|
||||
user_info_data = msg_dict.get("user_info")
|
||||
if not isinstance(user_info_data, dict):
|
||||
continue
|
||||
|
||||
# 设置一个相似度阈值
|
||||
similarity_threshold = 0.9
|
||||
if similarity_ratio > similarity_threshold:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
f"被逻辑检查拒绝:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),可以选择深入话题或寻找其它话题或等待。",
|
||||
True,
|
||||
sender_id = str(user_info_data.get("user_id"))
|
||||
|
||||
if sender_id == self.bot_qq_str:
|
||||
historical_message_text = msg_dict.get("processed_plain_text", "")
|
||||
# <--- 新增详细对比日志 --- START --->
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] ReplyChecker: 历史记录 #{i} ({global_config.BOT_NICKNAME}): '{historical_message_text}' (长度 {len(historical_message_text)})"
|
||||
)
|
||||
if reply == historical_message_text:
|
||||
logger.warning(f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!")
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}] ReplyChecker 检测到{global_config.BOT_NICKNAME}自身重复消息: '{reply}'"
|
||||
)
|
||||
match_found = True # <--- 标记找到
|
||||
return (False, "机器人尝试发送重复消息", False)
|
||||
# <--- 新增详细对比日志 --- END --->
|
||||
|
||||
if not match_found: # <--- 根据标记判断
|
||||
logger.debug(f"[私聊][{self.private_name}] ReplyChecker: 未找到重复。") # <--- 新增日志
|
||||
return (True, "消息内容未与机器人历史发言重复。", False)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
logger.error(f"[私聊][{self.private_name}]检查回复时出错: 类型={type(e)}, 值={e}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") # 打印详细的回溯信息
|
||||
|
||||
prompt = f"""你是一个聊天逻辑检查器,请检查以下回复或消息是否合适:
|
||||
|
||||
当前对话目标:{goal}
|
||||
最新的对话记录:
|
||||
{chat_history_text}
|
||||
|
||||
待检查的消息:
|
||||
{reply}
|
||||
|
||||
请结合聊天记录检查以下几点:
|
||||
1. 这条消息是否依然符合当前对话目标和实现方式
|
||||
2. 这条消息是否与最新的对话记录保持一致性
|
||||
3. 是否存在重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义)
|
||||
4. 这条消息是否包含违规内容(例如血腥暴力,政治敏感等)
|
||||
5. 这条消息是否以发送者的角度发言(不要让发送者自己回复自己的消息)
|
||||
6. 这条消息是否通俗易懂
|
||||
7. 这条消息是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断)
|
||||
8. 这条消息是否使用了完全没必要的修辞
|
||||
9. 这条消息是否逻辑通顺
|
||||
10. 这条消息是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况)
|
||||
11. 在连续多次发送消息的情况下,这条消息是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠)
|
||||
|
||||
请以JSON格式输出,包含以下字段:
|
||||
1. suitable: 是否合适 (true/false)
|
||||
2. reason: 原因说明
|
||||
3. need_replan: 是否需要重新决策 (true/false),当你认为此时已经不适合发消息,需要规划其它行动时,设为true
|
||||
|
||||
输出格式示例:
|
||||
{{
|
||||
"suitable": true,
|
||||
"reason": "回复符合要求,虽然有可能略微偏离目标,但是整体内容流畅得体",
|
||||
"need_replan": false
|
||||
}}
|
||||
|
||||
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
|
||||
|
||||
try:
|
||||
content, _ = await self.llm.generate_response_async(prompt)
|
||||
logger.debug(f"[私聊][{self.private_name}]检查回复的原始返回: {content}")
|
||||
|
||||
# 清理内容,尝试提取JSON部分
|
||||
content = content.strip()
|
||||
try:
|
||||
# 尝试直接解析
|
||||
result = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
# 如果直接解析失败,尝试查找和提取JSON部分
|
||||
import re
|
||||
|
||||
json_pattern = r"\{[^{}]*\}"
|
||||
json_match = re.search(json_pattern, content)
|
||||
if json_match:
|
||||
try:
|
||||
result = json.loads(json_match.group())
|
||||
except json.JSONDecodeError:
|
||||
# 如果JSON解析失败,尝试从文本中提取结果
|
||||
is_suitable = "不合适" not in content.lower() and "违规" not in content.lower()
|
||||
reason = content[:100] if content else "无法解析响应"
|
||||
need_replan = "重新规划" in content.lower() or "目标不适合" in content.lower()
|
||||
return is_suitable, reason, need_replan
|
||||
else:
|
||||
# 如果找不到JSON,从文本中判断
|
||||
is_suitable = "不合适" not in content.lower() and "违规" not in content.lower()
|
||||
reason = content[:100] if content else "无法解析响应"
|
||||
need_replan = "重新规划" in content.lower() or "目标不适合" in content.lower()
|
||||
return is_suitable, reason, need_replan
|
||||
|
||||
# 验证JSON字段
|
||||
suitable = result.get("suitable", None)
|
||||
reason = result.get("reason", "未提供原因")
|
||||
need_replan = result.get("need_replan", False)
|
||||
|
||||
# 如果suitable字段是字符串,转换为布尔值
|
||||
if isinstance(suitable, str):
|
||||
suitable = suitable.lower() == "true"
|
||||
|
||||
# 如果suitable字段不存在或不是布尔值,从reason中判断
|
||||
if suitable is None:
|
||||
suitable = "不合适" not in reason.lower() and "违规" not in reason.lower()
|
||||
|
||||
# 如果不合适且未达到最大重试次数,返回需要重试
|
||||
if not suitable and retry_count < self.max_retries:
|
||||
return False, reason, False
|
||||
|
||||
# 如果不合适且已达到最大重试次数,返回需要重新规划
|
||||
if not suitable and retry_count >= self.max_retries:
|
||||
return False, f"多次重试后仍不合适: {reason}", True
|
||||
|
||||
return suitable, reason, need_replan
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]检查回复时出错: {e}")
|
||||
# 如果出错且已达到最大重试次数,建议重新规划
|
||||
if retry_count >= self.max_retries:
|
||||
return False, "多次检查失败,建议重新规划", True
|
||||
return False, f"检查过程出错,建议重试: {str(e)}", False
|
||||
logger.error(f"[私聊][{self.private_name}] ReplyChecker 检查重复时出错: 类型={type(e)}, 值={e}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
|
||||
# 发生未知错误时,为安全起见,默认通过,并记录原因
|
||||
return (True, f"检查重复时发生内部错误: {str(e)}", False)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import random
|
||||
from .pfc_utils import retrieve_contextual_info
|
||||
|
||||
# 可能用于旧知识库提取主题 (如果需要回退到旧方法)
|
||||
# import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba
|
||||
# import re # 正则表达式库,通常 Python 自带
|
||||
from typing import Tuple, List, Dict, Any
|
||||
|
||||
# from src.common.logger import get_module_logger
|
||||
from src.common.logger_manager import get_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
|
|
@ -18,10 +13,45 @@ from src.plugins.utils.chat_message_builder import build_readable_messages
|
|||
|
||||
logger = get_logger("reply_generator")
|
||||
|
||||
PROMPT_GER_VARIATIONS = [
|
||||
("不用输出或提及提及对方的网名或绰号", 0.50),
|
||||
("如果当前对话比较轻松,可以尝试用轻松幽默或者略带调侃的语气回应,但要注意分寸", 0.8),
|
||||
("避免使用过于正式或书面化的词语,多用生活化的口语表达", 0.8),
|
||||
("如果对方的发言比较跳跃或难以理解,可以尝试用猜测或确认的语气回应", 0.8),
|
||||
("如果感觉对话有点干巴,可以尝试引入一些轻松的相关小话题或者自己的小想法,但不要偏离太远", 0.8),
|
||||
("注意观察对方的情绪(如果能从文字中判断),并作出适当的回应,比如安慰、鼓励或表示理解", 0.8),
|
||||
("", 0.10),
|
||||
]
|
||||
|
||||
REPLY_STYLE1_VARIATIONS = [
|
||||
("整体风格可以平和、简短", 0.3),
|
||||
("回复可以非常简洁,有时甚至用单个词、短语或者一个反问就能表达清楚", 0.10),
|
||||
("尝试使用更自然的口语连接词,例如:然后/所以呢/不过嘛/倒是", 0.05),
|
||||
("在表达观点时,可以说得主观一些,例如:我觉得.../我个人感觉.../要我说...", 0.10),
|
||||
("**请省略主语,简短**", 0.4),
|
||||
("回复得认真一些", 0.05),
|
||||
]
|
||||
|
||||
REPLY_STYLE2_VARIATIONS = [
|
||||
("结尾可以使用语气助词,例如:呀/噢/诶/哈/啦,让语气更生动", 0.10),
|
||||
("不要输出任何语气词", 0.10),
|
||||
("在适当的时候,可以用一些感叹词来表达情绪或态度,例如:哇/啊?/啧啧/哎呀", 0.05),
|
||||
("可以模糊化表达,例如:'我记得...'", 0.10),
|
||||
("对于一些无聊或者不想深入的话题,可以敷衍一下,例如:/哦这样啊/还行吧/随便啦", 0.10),
|
||||
("尽量用简单句和短句", 0.25),
|
||||
("不要输出任何标点符号,简短", 0.30),
|
||||
]
|
||||
|
||||
# --- 定义 Prompt 模板 ---
|
||||
|
||||
# Prompt for direct_reply (首次回复)
|
||||
PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复:
|
||||
PROMPT_DIRECT_REPLY = """
|
||||
当前时间:{current_time_str}
|
||||
{persona_text}。
|
||||
你正在和{sender_name}在QQ上私聊。
|
||||
你与对方的关系是:{relationship_text}
|
||||
你现在的心情是:{current_emotion_text}
|
||||
请根据以下信息生成一条回复:
|
||||
|
||||
当前对话目标:{goals_str}
|
||||
|
||||
|
|
@ -42,17 +72,23 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请
|
|||
2. 符合你的性格特征和身份细节
|
||||
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
|
||||
4. 可以适当利用相关知识和回忆,但**不要生硬引用**,若无必要,也可以不利用
|
||||
5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容
|
||||
5. 自然、得体,结合聊天记录逻辑合理,没有重复表达同质内容,也没有复读你之前的发言
|
||||
|
||||
请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
|
||||
可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。
|
||||
请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
|
||||
可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,{reply_style1},不要刻意突出自身学科背景,不要说你说过的话,{reply_style2}。
|
||||
{prompt_ger},请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
|
||||
不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。
|
||||
|
||||
请直接输出回复内容,不需要任何额外格式。"""
|
||||
|
||||
# Prompt for send_new_message (追问/补充)
|
||||
PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息:
|
||||
PROMPT_SEND_NEW_MESSAGE = """
|
||||
当前时间:{current_time_str}
|
||||
{persona_text}。
|
||||
你正在和{sender_name}在QQ上私聊,**并且刚刚你已经发送了一条或多条消息**
|
||||
你与对方的关系是:{relationship_text}
|
||||
你现在的心情是:{current_emotion_text}
|
||||
现在请根据以下信息判断你是否要继续发一条新消息,当然,如果你决定继续发消息不合适,也可以不发:
|
||||
|
||||
当前对话目标:{goals_str}
|
||||
|
||||
|
|
@ -67,22 +103,35 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊
|
|||
|
||||
{last_rejection_info}
|
||||
|
||||
请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该:
|
||||
{spam_warning_info}
|
||||
|
||||
请根据上述信息,判断你是否要继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。如果你觉得要发送,该消息应该:
|
||||
1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
|
||||
2. 符合你的性格特征和身份细节
|
||||
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
|
||||
4. 可以适当利用相关知识和回忆,但**不要生硬引用**,若无必要,也可以不利用
|
||||
5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容
|
||||
5. 跟之前你发的消息自然的衔接,逻辑合理,没有重复表达同质内容或部分重叠内容,也没有复读你之前的发言
|
||||
|
||||
请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
|
||||
这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。
|
||||
请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出消息内容。
|
||||
不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。
|
||||
这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,{reply_style1},不要刻意突出自身学科背景,不要说你说过的话,{reply_style2}。
|
||||
{prompt_ger}。
|
||||
如果你决定继续发消息不合适,也可以不发送。
|
||||
|
||||
请直接输出回复内容,不需要任何额外格式。"""
|
||||
请严格按照以下JSON格式输出你的选择和消息内容,不要包含任何其他说明或非JSON文本:
|
||||
{{
|
||||
"send": "yes/no",
|
||||
"txt": "如果选择发送,这里是具体的消息文本。如果选择不发送,这里也填写 'no'。"
|
||||
}}
|
||||
"""
|
||||
|
||||
# Prompt for say_goodbye (告别语生成)
|
||||
PROMPT_FAREWELL = """{persona_text}。你在参与一场 QQ 私聊,现在对话似乎已经结束,你决定再发一条最后的消息来圆满结束。
|
||||
PROMPT_FAREWELL = """
|
||||
当前时间:{current_time_str}
|
||||
{persona_text}。
|
||||
你正在和{sender_name}私聊,在QQ上私聊,现在你们的对话似乎已经结束。
|
||||
你与对方的关系是:{relationship_text}
|
||||
你现在的心情是:{current_emotion_text}
|
||||
现在你决定再发一条最后的消息来圆满结束。
|
||||
|
||||
最近的聊天记录:
|
||||
{chat_history_text}
|
||||
|
|
@ -107,7 +156,7 @@ class ReplyGenerator:
|
|||
self.llm = LLMRequest(
|
||||
model=global_config.llm_PFC_chat,
|
||||
temperature=global_config.llm_PFC_chat["temp"],
|
||||
max_tokens=300,
|
||||
max_tokens=300, # 对于JSON输出,这个可能需要适当调整,但一般回复短,JSON结构也简单
|
||||
request_type="reply_generation",
|
||||
)
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
|
|
@ -125,20 +174,32 @@ class ReplyGenerator:
|
|||
Args:
|
||||
observation_info: 观察信息
|
||||
conversation_info: 对话信息
|
||||
action_type: 当前执行的动作类型 ('direct_reply' 或 'send_new_message')
|
||||
action_type: 当前执行的动作类型 ('direct_reply', 'send_new_message', 'say_goodbye')
|
||||
|
||||
Returns:
|
||||
str: 生成的回复
|
||||
str: 生成的回复。
|
||||
对于 'direct_reply' 和 'say_goodbye',返回纯文本回复。
|
||||
对于 'send_new_message',返回包含决策和文本的JSON字符串。
|
||||
"""
|
||||
# 构建提示词
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}"
|
||||
)
|
||||
|
||||
# --- 构建通用 Prompt 参数 ---
|
||||
# (这部分逻辑基本不变)
|
||||
chosen_prompt_ger = random.choices(
|
||||
[style[0] for style in PROMPT_GER_VARIATIONS], weights=[style[1] for style in PROMPT_GER_VARIATIONS], k=1
|
||||
)[0]
|
||||
chosen_reply_style1 = random.choices(
|
||||
[style[0] for style in REPLY_STYLE1_VARIATIONS],
|
||||
weights=[style[1] for style in REPLY_STYLE1_VARIATIONS],
|
||||
k=1,
|
||||
)[0]
|
||||
chosen_reply_style2 = random.choices(
|
||||
[style[0] for style in REPLY_STYLE2_VARIATIONS],
|
||||
weights=[style[1] for style in REPLY_STYLE2_VARIATIONS],
|
||||
k=1,
|
||||
)[0]
|
||||
|
||||
# 构建对话目标 (goals_str)
|
||||
# --- 构建通用 Prompt 参数 ---
|
||||
goals_str = ""
|
||||
if conversation_info.goal_list:
|
||||
for goal_reason in conversation_info.goal_list:
|
||||
|
|
@ -153,9 +214,8 @@ class ReplyGenerator:
|
|||
reasoning = str(reasoning) if reasoning is not None else "没有明确原因"
|
||||
goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n"
|
||||
else:
|
||||
goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况
|
||||
goals_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:
|
||||
new_messages_list = observation_info.unprocessed_messages
|
||||
|
|
@ -169,11 +229,18 @@ class ReplyGenerator:
|
|||
chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
|
||||
elif not chat_history_text:
|
||||
chat_history_text = "还没有聊天记录。"
|
||||
else:
|
||||
chat_history_text += "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n"
|
||||
|
||||
sender_name_str = getattr(observation_info, "sender_name", "对方")
|
||||
if not sender_name_str:
|
||||
sender_name_str = "对方"
|
||||
|
||||
relationship_text_str = getattr(conversation_info, "relationship_text", "你们还不熟悉。")
|
||||
current_emotion_text_str = getattr(conversation_info, "current_emotion_text", "心情平静。")
|
||||
|
||||
# 构建 Persona 文本 (persona_text)
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text
|
||||
# 调用共享函数进行检索
|
||||
retrieval_context = chat_history_text
|
||||
retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info(
|
||||
retrieval_context, self.private_name
|
||||
)
|
||||
|
|
@ -181,54 +248,111 @@ class ReplyGenerator:
|
|||
f"[私聊][{self.private_name}] (ReplyGenerator) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str else '无'}"
|
||||
)
|
||||
|
||||
# --- 修改:构建上次回复失败原因和内容提示 ---
|
||||
last_rejection_info_str = ""
|
||||
# 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None
|
||||
last_reason = getattr(conversation_info, "last_reply_rejection_reason", None)
|
||||
last_content = getattr(conversation_info, "last_rejected_reply_content", None)
|
||||
|
||||
if last_reason and last_content:
|
||||
last_rejection_info_str = (
|
||||
f"\n------\n"
|
||||
f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n"
|
||||
f"上次试图发送的消息内容: “{last_content}”\n" # <-- 显示上次内容
|
||||
f"失败原因: “{last_reason}”\n"
|
||||
f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n"
|
||||
f"------\n"
|
||||
)
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}]检测到上次回复失败信息,将加入 Prompt:\n"
|
||||
f" 内容: {last_content}\n"
|
||||
f" 原因: {last_reason}"
|
||||
)
|
||||
if last_reason == "机器人尝试发送重复消息": # 这是我们从 ReplyChecker 设置的特定原因
|
||||
last_rejection_info_str = (
|
||||
f"\n------\n"
|
||||
f"【重要提示:你上一次尝试发送的消息 “{last_content}” 与你更早之前发送过的某条消息完全相同。这属于复读行为,请避免。】\n"
|
||||
f"请根据此提示调整你的新回复,确保内容新颖,不要重复你已经说过的话。\n"
|
||||
f"------\n"
|
||||
)
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] (ReplyGenerator) 检测到自身复读,将加入特定警告到 Prompt:\n"
|
||||
f" 内容: {last_content}"
|
||||
)
|
||||
else: # 其他类型的拒绝原因,保持原有格式
|
||||
last_rejection_info_str = (
|
||||
f"\n------\n"
|
||||
f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n"
|
||||
f"上次试图发送的消息内容: “{last_content}”\n"
|
||||
f"失败原因: “{last_reason}”\n"
|
||||
f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n"
|
||||
f"------\n"
|
||||
)
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] (ReplyGenerator) 检测到上次回复失败信息,将加入 Prompt:\n"
|
||||
f" 内容: {last_content}\n"
|
||||
f" 原因: {last_reason}"
|
||||
)
|
||||
|
||||
# 新增:构建刷屏警告信息 for PROMPT_SEND_NEW_MESSAGE
|
||||
spam_warning_message = ""
|
||||
if action_type == "send_new_message": # 只在 send_new_message 时构建刷屏警告
|
||||
if conversation_info.my_message_count > 5:
|
||||
spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎考虑是否继续发送!以免刷屏对造成对方困扰!**"
|
||||
elif conversation_info.my_message_count > 2:
|
||||
spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免连续发送,以免给对方造成困扰。**"
|
||||
if spam_warning_message:
|
||||
spam_warning_message = f"\n{spam_warning_message}\n"
|
||||
|
||||
# --- 选择 Prompt ---
|
||||
if action_type == "send_new_message":
|
||||
prompt_template = PROMPT_SEND_NEW_MESSAGE
|
||||
logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问生成)")
|
||||
elif action_type == "say_goodbye": # 处理告别动作
|
||||
logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问/补充生成, 期望JSON输出)")
|
||||
elif action_type == "say_goodbye":
|
||||
prompt_template = PROMPT_FAREWELL
|
||||
logger.info(f"[私聊][{self.private_name}]使用 PROMPT_FAREWELL (告别语生成)")
|
||||
else: # 默认使用 direct_reply 的 prompt (包括 'direct_reply' 或其他未明确处理的类型)
|
||||
else:
|
||||
prompt_template = PROMPT_DIRECT_REPLY
|
||||
logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)")
|
||||
|
||||
# --- 格式化最终的 Prompt ---
|
||||
try: # <--- 增加 try-except 块处理可能的 format 错误
|
||||
prompt = prompt_template.format(
|
||||
persona_text=persona_text,
|
||||
goals_str=goals_str,
|
||||
chat_history_text=chat_history_text,
|
||||
retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。",
|
||||
retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。",
|
||||
last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因
|
||||
)
|
||||
try:
|
||||
current_time_value = "获取时间失败"
|
||||
if observation_info and hasattr(observation_info, "current_time_str") and observation_info.current_time_str:
|
||||
current_time_value = observation_info.current_time_str
|
||||
|
||||
base_format_params = {
|
||||
"persona_text": persona_text,
|
||||
"goals_str": goals_str,
|
||||
"chat_history_text": chat_history_text,
|
||||
"retrieved_memory_str": retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 确保已定义
|
||||
"retrieved_knowledge_str": retrieved_knowledge_str
|
||||
if retrieved_knowledge_str
|
||||
else "无相关知识。", # 确保已定义
|
||||
"last_rejection_info": last_rejection_info_str,
|
||||
"current_time_str": current_time_value,
|
||||
"sender_name": sender_name_str,
|
||||
"relationship_text": relationship_text_str,
|
||||
"current_emotion_text": current_emotion_text_str,
|
||||
"reply_style1": chosen_reply_style1,
|
||||
"reply_style2": chosen_reply_style2,
|
||||
"prompt_ger": chosen_prompt_ger,
|
||||
}
|
||||
|
||||
if action_type == "send_new_message":
|
||||
current_format_params = base_format_params.copy()
|
||||
current_format_params["spam_warning_info"] = spam_warning_message
|
||||
prompt = prompt_template.format(**current_format_params)
|
||||
elif action_type == "say_goodbye":
|
||||
farewell_params = {
|
||||
k: v
|
||||
for k, v in base_format_params.items()
|
||||
if k
|
||||
in [
|
||||
"persona_text",
|
||||
"chat_history_text",
|
||||
"current_time_str",
|
||||
"sender_name",
|
||||
"relationship_text",
|
||||
"current_emotion_text",
|
||||
]
|
||||
}
|
||||
|
||||
prompt = prompt_template.format(**farewell_params)
|
||||
else: # direct_reply
|
||||
current_format_params = base_format_params.copy()
|
||||
prompt = prompt_template.format(**current_format_params)
|
||||
|
||||
except KeyError as e:
|
||||
logger.error(
|
||||
f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。"
|
||||
)
|
||||
# 返回错误信息或默认回复
|
||||
return "抱歉,准备回复时出了点问题,请检查一下我的代码..."
|
||||
return "抱歉,准备回复时出了点问题,请检查一下我的代码..." # 对于JSON期望的场景,这里可能也需要返回一个固定的错误JSON
|
||||
except Exception as fmt_err:
|
||||
logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}")
|
||||
return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..."
|
||||
|
|
@ -237,19 +361,20 @@ class ReplyGenerator:
|
|||
logger.debug(f"[私聊][{self.private_name}]发送到LLM的生成提示词:\n------\n{prompt}\n------")
|
||||
try:
|
||||
content, _ = await self.llm.generate_response_async(prompt)
|
||||
logger.debug(f"[私聊][{self.private_name}]生成的回复: {content}")
|
||||
# 移除旧的检查新消息逻辑,这应该由 conversation 控制流处理
|
||||
# 对于 PROMPT_SEND_NEW_MESSAGE,我们期望 content 是一个 JSON 字符串
|
||||
# 对于其他 prompts,content 是纯文本回复
|
||||
# 该方法现在直接返回 LLM 的原始输出,由调用者 (conversation._handle_action) 负责解析
|
||||
logger.debug(f"[私聊][{self.private_name}]LLM原始生成内容: {content}")
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]生成回复时出错: {e}")
|
||||
return "抱歉,我现在有点混乱,让我重新思考一下..."
|
||||
|
||||
# check_reply 方法保持不变
|
||||
async def check_reply(
|
||||
self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0
|
||||
) -> Tuple[bool, str, bool]:
|
||||
"""检查回复是否合适
|
||||
(此方法逻辑保持不变)
|
||||
"""
|
||||
return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count)
|
||||
# 根据 action_type 返回不同的错误指示
|
||||
if action_type == "send_new_message":
|
||||
# 返回一个表示错误的JSON,让调用方知道出错了但仍能解析
|
||||
return """{{
|
||||
"send": "no",
|
||||
"txt": "LLM生成回复时出错"
|
||||
}}""".strip()
|
||||
else:
|
||||
return "抱歉,我现在有点混乱,让我重新思考一下..."
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from src.common.logger import get_module_logger
|
||||
from .chat_observer import ChatObserver
|
||||
from .conversation_info import ConversationInfo
|
||||
|
||||
# from src.individuality.individuality import Individuality # 不再需要
|
||||
from ...config.config import global_config
|
||||
import time
|
||||
import asyncio
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ from typing import Dict, Any
|
|||
from ..moods.moods import MoodManager # 导入情绪管理器
|
||||
from ...config.config import global_config
|
||||
from .message import MessageRecv
|
||||
from ..PFC.pfc_manager import PFCManager
|
||||
from .chat_stream import chat_manager
|
||||
from .only_message_process import MessageProcessor
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from ..heartFC_chat.heartflow_processor import HeartFCProcessor
|
||||
from ..PFC.pfc_processor import PFCProcessor
|
||||
from ..utils.prompt_builder import Prompt, global_prompt_manager
|
||||
import traceback
|
||||
|
||||
|
|
@ -25,10 +23,7 @@ class ChatBot:
|
|||
self._started = False
|
||||
self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例
|
||||
self.heartflow_processor = HeartFCProcessor() # 新增
|
||||
|
||||
# 创建初始化PFC管理器的任务,会在_ensure_started时执行
|
||||
self.only_process_chat = MessageProcessor()
|
||||
self.pfc_manager = PFCManager.get_instance()
|
||||
self.pfc_processor = PFCProcessor()
|
||||
|
||||
async def _ensure_started(self):
|
||||
"""确保所有任务已启动"""
|
||||
|
|
@ -37,17 +32,6 @@ class ChatBot:
|
|||
|
||||
self._started = True
|
||||
|
||||
async def _create_pfc_chat(self, message: MessageRecv):
|
||||
try:
|
||||
chat_id = str(message.chat_stream.stream_id)
|
||||
private_name = str(message.message_info.user_info.user_nickname)
|
||||
|
||||
if global_config.enable_pfc_chatting:
|
||||
await self.pfc_manager.get_or_create_conversation(chat_id, private_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建PFC聊天失败: {e}")
|
||||
|
||||
async def message_process(self, message_data: Dict[str, Any]) -> None:
|
||||
"""处理转化后的统一格式消息
|
||||
这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中
|
||||
|
|
@ -118,18 +102,7 @@ class ChatBot:
|
|||
# 是否进入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,
|
||||
group_info=groupinfo,
|
||||
)
|
||||
message.update_chat_stream(chat)
|
||||
await self.only_process_chat.process_message(message)
|
||||
await self._create_pfc_chat(message)
|
||||
await self.pfc_processor.process_message(message_data)
|
||||
# 禁止PFC,进入普通的心流消息处理逻辑
|
||||
else:
|
||||
logger.trace("进入普通心流私聊处理")
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.chat.message import MessageRecv
|
||||
from src.plugins.storage.storage import MessageStorage
|
||||
from src.config.config import global_config
|
||||
from datetime import datetime
|
||||
|
||||
logger = get_logger("pfc")
|
||||
|
||||
|
||||
class MessageProcessor:
|
||||
"""消息处理器,负责处理接收到的消息并存储"""
|
||||
|
||||
def __init__(self):
|
||||
self.storage = MessageStorage()
|
||||
|
||||
@staticmethod
|
||||
def _check_ban_words(text: str, chat, userinfo) -> bool:
|
||||
"""检查消息中是否包含过滤词"""
|
||||
for word in global_config.ban_words:
|
||||
if word in text:
|
||||
logger.info(
|
||||
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
|
||||
)
|
||||
logger.info(f"[过滤词识别]消息中含有{word},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _check_ban_regex(text: str, chat, userinfo) -> bool:
|
||||
"""检查消息是否匹配过滤正则表达式"""
|
||||
for pattern in global_config.ban_msgs_regex:
|
||||
if pattern.search(text):
|
||||
logger.info(
|
||||
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
|
||||
)
|
||||
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def process_message(self, message: MessageRecv) -> None:
|
||||
"""处理消息并存储
|
||||
|
||||
Args:
|
||||
message: 消息对象
|
||||
"""
|
||||
userinfo = message.message_info.user_info
|
||||
chat = message.chat_stream
|
||||
|
||||
# 处理消息
|
||||
await message.process()
|
||||
|
||||
# 过滤词/正则表达式过滤
|
||||
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
|
||||
message.raw_message, chat, userinfo
|
||||
):
|
||||
return
|
||||
|
||||
# 存储消息
|
||||
await self.storage.store_message(message, chat)
|
||||
|
||||
# 打印消息信息
|
||||
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
|
||||
# 将时间戳转换为datetime对象
|
||||
current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S")
|
||||
logger.info(
|
||||
f"[{current_time}][{mes_name}]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}"
|
||||
)
|
||||
|
|
@ -28,6 +28,12 @@ from rich.progress import (
|
|||
|
||||
install(extra_lines=3)
|
||||
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
|
||||
EMBEDDING_DATA_DIR = (
|
||||
os.path.join(ROOT_PATH, "data", "embedding")
|
||||
if global_config["persistence"]["embedding_data_dir"] is None
|
||||
else os.path.join(ROOT_PATH, global_config["persistence"]["embedding_data_dir"])
|
||||
)
|
||||
EMBEDDING_DATA_DIR_STR = str(EMBEDDING_DATA_DIR).replace("\\", "/")
|
||||
TOTAL_EMBEDDING_TIMES = 3 # 统计嵌入次数
|
||||
|
||||
# 嵌入模型测试字符串,测试模型一致性,来自开发群的聊天记录
|
||||
|
|
@ -288,17 +294,17 @@ class EmbeddingManager:
|
|||
self.paragraphs_embedding_store = EmbeddingStore(
|
||||
llm_client,
|
||||
PG_NAMESPACE,
|
||||
global_config["persistence"]["embedding_data_dir"],
|
||||
EMBEDDING_DATA_DIR_STR,
|
||||
)
|
||||
self.entities_embedding_store = EmbeddingStore(
|
||||
llm_client,
|
||||
ENT_NAMESPACE,
|
||||
global_config["persistence"]["embedding_data_dir"],
|
||||
EMBEDDING_DATA_DIR_STR,
|
||||
)
|
||||
self.relation_embedding_store = EmbeddingStore(
|
||||
llm_client,
|
||||
REL_NAMESPACE,
|
||||
global_config["persistence"]["embedding_data_dir"],
|
||||
EMBEDDING_DATA_DIR_STR,
|
||||
)
|
||||
self.stored_pg_hashes = set()
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ from .lpmmconfig import (
|
|||
|
||||
from .global_logger import logger
|
||||
|
||||
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
|
||||
KG_DIR = (
|
||||
os.path.join(ROOT_PATH, "data/rag")
|
||||
if global_config["persistence"]["rag_data_dir"] is None
|
||||
else os.path.join(ROOT_PATH, global_config["persistence"]["rag_data_dir"])
|
||||
)
|
||||
KG_DIR_STR = str(KG_DIR).replace("\\", "/")
|
||||
|
||||
|
||||
class KGManager:
|
||||
def __init__(self):
|
||||
|
|
@ -43,7 +51,7 @@ class KGManager:
|
|||
self.graph = di_graph.DiGraph()
|
||||
|
||||
# 持久化相关
|
||||
self.dir_path = global_config["persistence"]["rag_data_dir"]
|
||||
self.dir_path = KG_DIR_STR
|
||||
self.graph_data_path = self.dir_path + "/" + RAG_GRAPH_NAMESPACE + ".graphml"
|
||||
self.ent_cnt_data_path = self.dir_path + "/" + RAG_ENT_CNT_NAMESPACE + ".parquet"
|
||||
self.pg_hash_file_path = self.dir_path + "/" + RAG_PG_HASH_NAMESPACE + ".json"
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ import platform
|
|||
import os
|
||||
import json
|
||||
import threading
|
||||
from src.common.logger import get_module_logger, LogConfig, REMOTE_STYLE_CONFIG
|
||||
import subprocess
|
||||
|
||||
# from loguru import logger
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.config.config import global_config
|
||||
|
||||
|
||||
remote_log_config = LogConfig(
|
||||
console_format=REMOTE_STYLE_CONFIG["console_format"],
|
||||
file_format=REMOTE_STYLE_CONFIG["file_format"],
|
||||
)
|
||||
logger = get_module_logger("remote", config=remote_log_config)
|
||||
logger = get_logger("remote")
|
||||
|
||||
# --- 使用向上导航的方式定义路径 ---
|
||||
|
||||
|
|
@ -82,9 +80,76 @@ def get_unique_id():
|
|||
|
||||
# 生成客户端唯一ID
|
||||
def generate_unique_id():
|
||||
# 结合主机名、系统信息和随机UUID生成唯一ID
|
||||
# 基于机器码生成唯一ID,同一台机器上生成的UUID是固定的,只要机器码不变
|
||||
import hashlib
|
||||
|
||||
system_info = platform.system()
|
||||
unique_id = f"{system_info}-{uuid.uuid4()}"
|
||||
machine_code = None
|
||||
|
||||
try:
|
||||
if system_info == "Windows":
|
||||
# 使用wmic命令获取主机UUID(更稳定)
|
||||
result = subprocess.check_output(
|
||||
"wmic csproduct get uuid", shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
|
||||
)
|
||||
lines = result.decode(errors="ignore").splitlines()
|
||||
# 过滤掉空行和表头,只取有效UUID
|
||||
uuids = [line.strip() for line in lines if line.strip() and line.strip().lower() != "uuid"]
|
||||
if uuids:
|
||||
uuid_val = uuids[0]
|
||||
# logger.debug(f"主机UUID: {uuid_val}")
|
||||
# 增加无效值判断
|
||||
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
|
||||
machine_code = uuid_val
|
||||
elif system_info == "Linux":
|
||||
# 优先读取 /etc/machine-id,其次 /var/lib/dbus/machine-id,取第一个非空且内容有效的
|
||||
for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
code = f.read().strip()
|
||||
# 只要内容非空且不是全0
|
||||
if code and set(code) != {"0"}:
|
||||
machine_code = code
|
||||
break
|
||||
elif system_info == "Darwin":
|
||||
# macOS: 使用IOPlatformUUID
|
||||
result = subprocess.check_output(
|
||||
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", shell=True
|
||||
)
|
||||
uuid_line = result.decode(errors="ignore")
|
||||
# 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx"
|
||||
import re
|
||||
|
||||
m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line)
|
||||
if m:
|
||||
uuid_val = m.group(1)
|
||||
logger.debug(f"IOPlatformUUID: {uuid_val}")
|
||||
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
|
||||
machine_code = uuid_val
|
||||
except Exception as e:
|
||||
logger.debug(f"获取机器码失败: {e}")
|
||||
|
||||
# 如果主板序列号无效,尝试用MAC地址
|
||||
if not machine_code:
|
||||
try:
|
||||
mac = uuid.getnode()
|
||||
if (mac >> 40) % 2 == 0: # 不是本地伪造MAC
|
||||
machine_code = str(mac)
|
||||
except Exception as e:
|
||||
logger.debug(f"获取MAC地址失败: {e}")
|
||||
|
||||
def md5_to_uuid(md5hex):
|
||||
# 将32位md5字符串格式化为8-4-4-4-12的UUID格式
|
||||
return f"{md5hex[0:8]}-{md5hex[8:12]}-{md5hex[12:16]}-{md5hex[16:20]}-{md5hex[20:32]}"
|
||||
|
||||
if machine_code:
|
||||
# print(f"machine_code={machine_code!r}") # 可用于调试
|
||||
md5 = hashlib.md5(machine_code.encode("utf-8")).hexdigest()
|
||||
uuid_str = md5_to_uuid(md5)
|
||||
else:
|
||||
uuid_str = str(uuid.uuid4())
|
||||
|
||||
unique_id = f"{system_info}-{uuid_str}"
|
||||
return unique_id
|
||||
|
||||
|
||||
|
|
@ -175,3 +240,9 @@ def main():
|
|||
|
||||
return heartbeat_thread # 返回线程对象,便于外部控制
|
||||
return None
|
||||
|
||||
|
||||
# --- 测试用例 ---
|
||||
if __name__ == "__main__":
|
||||
print("测试唯一ID生成:")
|
||||
print("唯一ID:", get_unique_id())
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ nonebot-qq="http://127.0.0.1:18002/api/message"
|
|||
[chat] #麦麦的聊天通用设置
|
||||
allow_focus_mode = true # 是否允许专注聊天状态
|
||||
# 是否启用heart_flowC(HFC)模式
|
||||
# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token
|
||||
# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token
|
||||
base_normal_chat_num = 8 # 最多允许多少个群进行普通聊天
|
||||
base_focused_chat_num = 5 # 最多允许多少个群进行专注聊天
|
||||
allow_remove_duplicates = true # 是否开启心流去重(如果发现心流截断问题严重可尝试关闭)
|
||||
|
|
@ -201,9 +201,10 @@ enable_friend_chat = false # 是否启用好友聊天
|
|||
talk_allowed_private = [] # 可以回复消息的QQ号
|
||||
pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
|
||||
api_polling_max_retries = 3
|
||||
enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器
|
||||
|
||||
[idle_conversation]
|
||||
enable_idle_conversation = true
|
||||
enable_idle_conversation = false # 是否启用 pfc 主动发言
|
||||
idle_check_interval = 10 # 检查间隔,10分钟检查一次
|
||||
min_idle_time = 7200 # 最短无活动时间,2小时 (7200秒)
|
||||
max_idle_time = 18000 # 最长无活动时间,5小时 (18000秒)
|
||||
|
|
@ -303,10 +304,11 @@ temp = 0.3
|
|||
pri_in = 2
|
||||
pri_out = 8
|
||||
|
||||
#PFC检查模型
|
||||
[model.llm_PFC_reply_checker]
|
||||
name = "Pro/deepseek-ai/DeepSeek-V3"
|
||||
# PFC 关系评估LLM
|
||||
[model.llm_PFC_relationship_eval]
|
||||
name = "Pro/deepseek-ai/DeepSeek-V3" # 或者其他你认为适合判断任务的模型
|
||||
provider = "SILICONFLOW"
|
||||
temp = 0.4
|
||||
pri_in = 2
|
||||
pri_out = 8
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue