mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-10 15:50:24 +08:00
296 lines
9.9 KiB
Python
296 lines
9.9 KiB
Python
"""对话记忆管理器"""
|
||
|
||
import asyncio
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional
|
||
|
||
from langchain_core.messages import BaseMessage, messages_from_dict, messages_to_dict
|
||
|
||
from app.application.messaging.chat import (
|
||
AgentChatPersistenceService,
|
||
AgentChatRecord,
|
||
AgentChatService,
|
||
get_configured_agent_chat_persistence,
|
||
get_configured_agent_chat_service,
|
||
)
|
||
from app.runtime.log import logger
|
||
from app.runtime.settings import get_runtime_setting
|
||
from app.schemas.agent import ConversationMemory
|
||
|
||
|
||
class MemoryManager:
|
||
"""
|
||
对话记忆管理器
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
chat: AgentChatService | None = None,
|
||
persistence: AgentChatPersistenceService | None = None,
|
||
) -> None:
|
||
"""创建记忆缓存,并保存组合根显式注入的会话能力。"""
|
||
self._chat = chat
|
||
self._persistence = persistence
|
||
# 内存中的会话记忆缓存
|
||
self.memory_cache: Dict[str, ConversationMemory] = {}
|
||
# 内存缓存清理任务
|
||
self.cleanup_task: Optional[asyncio.Task[None]] = None
|
||
|
||
def configure(
|
||
self,
|
||
chat: AgentChatService,
|
||
persistence: AgentChatPersistenceService,
|
||
) -> None:
|
||
"""在 Agent 启动前绑定唯一会话查询与写入服务。"""
|
||
self._chat = chat
|
||
self._persistence = persistence
|
||
|
||
def _chat_service(self) -> AgentChatService:
|
||
"""返回显式注入服务,兼容测试未装配时使用既有应用服务。"""
|
||
return self._chat or get_configured_agent_chat_service()
|
||
|
||
def _chat_persistence(self) -> AgentChatPersistenceService:
|
||
"""返回显式注入写服务,兼容测试未装配时使用既有应用服务。"""
|
||
return self._persistence or get_configured_agent_chat_persistence()
|
||
|
||
def initialize(self) -> None:
|
||
"""
|
||
初始化记忆管理器
|
||
"""
|
||
try:
|
||
if self.cleanup_task and not self.cleanup_task.done():
|
||
return
|
||
# 启动内存缓存清理任务(Redis通过TTL自动过期)
|
||
self.cleanup_task = asyncio.create_task(
|
||
self._cleanup_expired_memories()
|
||
)
|
||
logger.info("对话记忆管理器初始化完成")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Redis连接失败,将使用内存存储: {e}")
|
||
|
||
async def close(self) -> None:
|
||
"""
|
||
关闭记忆管理器
|
||
"""
|
||
if self.cleanup_task:
|
||
self.cleanup_task.cancel()
|
||
try:
|
||
await self.cleanup_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
self.cleanup_task = None
|
||
|
||
logger.info("对话记忆管理器已关闭")
|
||
|
||
@staticmethod
|
||
def _get_memory_key(session_id: str, user_id: Optional[str]) -> str:
|
||
"""
|
||
计算内存Key
|
||
"""
|
||
return f"{user_id}:{session_id}" if user_id else session_id
|
||
|
||
def get_memory(self, session_id: str, user_id: str) -> Optional[ConversationMemory]:
|
||
"""
|
||
获取内存中的记忆
|
||
"""
|
||
cache_key = self._get_memory_key(session_id, user_id)
|
||
return self.memory_cache.get(cache_key)
|
||
|
||
@staticmethod
|
||
def _chat_lookup_params(
|
||
session_id: str,
|
||
user_id: str,
|
||
) -> tuple[dict[str, str], ...]:
|
||
"""返回用户专属会话优先、旧无用户会话兜底的查询顺序。"""
|
||
return (
|
||
{"session_id": session_id, "user_id": user_id},
|
||
{"session_id": session_id},
|
||
)
|
||
|
||
def _restore_agent_messages(
|
||
self,
|
||
*,
|
||
session_id: str,
|
||
user_id: str,
|
||
chat: Optional[AgentChatRecord],
|
||
) -> List[BaseMessage]:
|
||
"""统一校验持久化快照、反序列化消息并回填内存缓存。"""
|
||
if not chat or not chat.agent_messages:
|
||
return []
|
||
try:
|
||
messages = messages_from_dict(chat.agent_messages)
|
||
except Exception as e:
|
||
logger.debug(f"恢复持久化Agent消息失败: {e}")
|
||
return []
|
||
memory = ConversationMemory(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
messages=messages,
|
||
)
|
||
self.save_memory(memory)
|
||
return memory.messages
|
||
|
||
def get_agent_messages(
|
||
self, session_id: str, user_id: str
|
||
) -> List[BaseMessage]:
|
||
"""
|
||
为Agent获取最近的消息。
|
||
|
||
优先使用内存缓存,缓存不存在时从数据库恢复上一轮持久化的原始 messages。
|
||
"""
|
||
memory = self.get_memory(session_id, user_id)
|
||
if memory:
|
||
return memory.messages
|
||
|
||
try:
|
||
service = self._chat_service()
|
||
chat = None
|
||
for lookup_params in self._chat_lookup_params(session_id, user_id):
|
||
chat = service.get_sync(**lookup_params)
|
||
if chat:
|
||
break
|
||
except Exception as e:
|
||
logger.debug(f"读取持久化Agent会话失败: {e}")
|
||
return []
|
||
return self._restore_agent_messages(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
chat=chat,
|
||
)
|
||
|
||
async def async_get_agent_messages(
|
||
self, session_id: str, user_id: str
|
||
) -> List[BaseMessage]:
|
||
"""异步恢复 Agent 消息,查询与会话应用服务保持同一异步端口。"""
|
||
memory = self.get_memory(session_id, user_id)
|
||
if memory:
|
||
return memory.messages
|
||
|
||
try:
|
||
service = self._chat_service()
|
||
chat = None
|
||
for lookup_params in self._chat_lookup_params(session_id, user_id):
|
||
chat = await service.get(**lookup_params)
|
||
if chat:
|
||
break
|
||
except Exception as e:
|
||
logger.debug(f"读取持久化Agent会话失败: {e}")
|
||
return []
|
||
return self._restore_agent_messages(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
chat=chat,
|
||
)
|
||
|
||
def _update_agent_messages(
|
||
self,
|
||
*,
|
||
session_id: str,
|
||
user_id: str,
|
||
messages: List[BaseMessage],
|
||
) -> None:
|
||
"""统一更新同步和异步写路径共享的内存消息状态。"""
|
||
memory = self.get_memory(session_id, user_id)
|
||
if not memory:
|
||
memory = ConversationMemory(session_id=session_id, user_id=user_id)
|
||
memory.messages = messages
|
||
memory.updated_at = datetime.now()
|
||
self.save_memory(memory)
|
||
|
||
def save_agent_messages(
|
||
self, session_id: str, user_id: str, messages: List[BaseMessage]
|
||
) -> None:
|
||
"""
|
||
保存Agent消息到内存缓存与持久化会话表。
|
||
"""
|
||
self._update_agent_messages(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
messages=messages,
|
||
)
|
||
try:
|
||
self._chat_service().save_agent_messages(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
messages=messages_to_dict(messages),
|
||
)
|
||
except Exception as e:
|
||
logger.debug(f"持久化Agent消息失败: {e}")
|
||
|
||
async def async_save_agent_messages(
|
||
self, session_id: str, user_id: str, messages: List[BaseMessage]
|
||
) -> None:
|
||
"""异步保存 Agent 消息,持久化写入经有界数据库 worker 承接。"""
|
||
self._update_agent_messages(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
messages=messages,
|
||
)
|
||
try:
|
||
persistence = self._chat_persistence()
|
||
await persistence.async_save_agent_messages(
|
||
session_id=session_id,
|
||
user_id=user_id,
|
||
messages=messages_to_dict(messages),
|
||
)
|
||
except Exception as e:
|
||
logger.debug(f"持久化Agent消息失败: {e}")
|
||
|
||
def save_memory(self, memory: ConversationMemory) -> None:
|
||
"""
|
||
保存记忆到内存缓存
|
||
|
||
注意:Redis中的记忆通过TTL机制自动过期,这里只更新内存缓存,Redis会在下次访问时自动过期
|
||
"""
|
||
cache_key = self._get_memory_key(memory.session_id, memory.user_id)
|
||
self.memory_cache[cache_key] = memory
|
||
|
||
def clear_memory(self, session_id: str, user_id: str) -> None:
|
||
"""
|
||
清空会话记忆
|
||
"""
|
||
cache_key = self._get_memory_key(session_id, user_id)
|
||
if cache_key in self.memory_cache:
|
||
del self.memory_cache[cache_key]
|
||
|
||
logger.info(f"会话记忆已清空: session_id={session_id}, user_id={user_id}")
|
||
|
||
async def _cleanup_expired_memories(self) -> None:
|
||
"""
|
||
清理内存中过期记忆的后台任务
|
||
|
||
注意:Redis中的记忆通过TTL机制自动过期,这里只清理内存缓存
|
||
"""
|
||
while True:
|
||
try:
|
||
# 每小时清理一次
|
||
await asyncio.sleep(3600)
|
||
|
||
current_time = datetime.now()
|
||
expired_sessions = []
|
||
|
||
# 只检查内存缓存中的过期记忆
|
||
# Redis中的记忆会通过TTL自动过期,无需手动处理
|
||
for cache_key, memory in self.memory_cache.items():
|
||
if (
|
||
current_time - memory.updated_at
|
||
).days > get_runtime_setting('LLM_MEMORY_RETENTION_DAYS'):
|
||
expired_sessions.append(cache_key)
|
||
|
||
# 只清理内存缓存,不删除Redis中的键(Redis会自动过期)
|
||
for cache_key in expired_sessions:
|
||
if cache_key in self.memory_cache:
|
||
del self.memory_cache[cache_key]
|
||
|
||
if expired_sessions:
|
||
logger.info(f"清理了{len(expired_sessions)}个过期内存会话记忆")
|
||
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"清理记忆时发生错误: {e}")
|
||
|
||
|
||
memory_manager = MemoryManager()
|