Files
Mutual_Fund/service/memory/facade.py
T

183 lines
7.0 KiB
Python

"""MemoryService Facade:客服 Agent 的唯一记忆调用入口。"""
from __future__ import annotations
from contextlib import asynccontextmanager
from typing import Any
from config.database.mysql import get_session_factory
from tool.confidence_rank import FinalConfidenceRankTool
from .archive import ConversationArchiver
from .customer_relation import CustomerRelationMemory
from .customer_product import CustomerProductMemory
from .context_builder import build_customer_memory_context
from .long_term import LongTermMemoryService
from .interest_topic import InterestTopicTracker
from .profile import CustomerProfileMemory
from .schemas import CustomerMemoryContext, MemoryUnitDTO, ShortTermMessage
from .short_term import ShortTermMemory
from .work_order import WorkOrderMemory
class MemoryService:
"""协调短期、中期和长期记忆,屏蔽底层数据库细节。"""
def __init__(
self,
*,
session_factory=None,
short_term: ShortTermMemory | None = None,
profile: CustomerProfileMemory | None = None,
work_orders: WorkOrderMemory | None = None,
relations: CustomerRelationMemory | None = None,
products: CustomerProductMemory | None = None,
long_term: LongTermMemoryService | None = None,
archiver: ConversationArchiver | None = None,
rank_tool: FinalConfidenceRankTool | None = None,
interest_tracker: InterestTopicTracker | None = None,
):
self.session_factory = session_factory or get_session_factory()
self.short_term = short_term or ShortTermMemory()
self.profile = profile or CustomerProfileMemory(redis=self.short_term.redis)
self.work_orders = work_orders or WorkOrderMemory()
self.relations = relations or CustomerRelationMemory()
self.products = products or CustomerProductMemory()
self.long_term = long_term or LongTermMemoryService()
self.archiver = archiver or ConversationArchiver(short_term=self.short_term)
self.rank_tool = rank_tool or FinalConfidenceRankTool()
self.interest_tracker = interest_tracker or InterestTopicTracker(self.short_term.redis)
@asynccontextmanager
async def _db(self):
"""按请求获取并释放 MySQL 会话。"""
async with self.session_factory() as session:
yield session
async def recall(
self,
*,
customer_id: int,
session_id: str,
query: str | None = None,
limit: int = 10,
) -> CustomerMemoryContext:
"""召回并组装客服 Agent 当前请求的全部可用记忆。"""
if limit < 0:
raise ValueError("limit 必须是非负整数")
warnings: list[str] = []
short_term_messages = await self.short_term.load_messages(session_id)
warnings.extend(self.short_term.last_warnings)
async with self._db() as db:
profile, profile_warnings = await self.profile.get(db, customer_id)
warnings.extend(profile_warnings)
try:
work_orders = await self.work_orders.list(db, customer_id)
except Exception as exc:
work_orders = []
warnings.append(f"work_order_recall_failed:{type(exc).__name__}")
try:
customer_relations = await self.relations.list(db, customer_id)
except Exception as exc:
customer_relations = []
warnings.append(f"customer_relation_recall_failed:{type(exc).__name__}")
try:
customer_products = await self.products.list(db, customer_id)
except Exception as exc:
customer_products = []
warnings.append(f"customer_product_recall_failed:{type(exc).__name__}")
try:
memories, memory_warnings = await self.long_term.recall(
db, customer_id, limit=max(limit, 10)
)
warnings.extend(memory_warnings)
except Exception as exc:
memories = []
warnings.append(f"long_term_recall_failed:{type(exc).__name__}")
ranked = self.rank_tool.rank(
[memory.model_dump(mode="json") for memory in memories],
top_k=limit,
)
ranked_memories = [MemoryUnitDTO.model_validate(item) for item in ranked]
return build_customer_memory_context(
customer_id=customer_id,
session_id=session_id,
short_term_messages=short_term_messages,
customer_profile=profile,
work_orders=work_orders,
customer_relations=customer_relations,
customer_products=customer_products,
long_term_memories=ranked_memories,
warnings=warnings,
)
async def append_message(
self,
*,
customer_id: int,
session_id: str,
message: ShortTermMessage,
) -> list[str]:
"""写入短期消息,并统一返回降级 warnings。"""
await self.short_term.append_message(
session_id,
message.role,
message.content,
message_id=message.message_id,
agent_run_id=message.agent_run_id,
tool_calls=message.tool_calls,
)
return list(self.short_term.last_warnings)
async def save_memory(
self,
*,
customer_id: int,
memory: MemoryUnitDTO,
) -> tuple[MemoryUnitDTO, list[str]]:
"""保存长期记忆,并返回记忆结果和底层 warnings。"""
if memory.customer_id != customer_id:
raise ValueError("memory.customer_id 与当前客户不一致")
async with self._db() as db:
return await self.long_term.save(db, memory)
async def record_interest_signal(self, *, customer_id: int, tag: str) -> tuple[int, bool]:
"""记录一次重复兴趣主题信号,达到阈值后允许写入长期记忆。"""
return await self.interest_tracker.record(customer_id, tag)
async def close_session(
self,
*,
customer_id: int,
session_id: str,
agent_run_id: str | None = None,
) -> list[str]:
"""归档并关闭客户会话;归档失败时保留 Redis 消息。"""
try:
async with self._db() as db:
await self.archiver.archive_session(
db,
session_id=session_id,
user_id=customer_id,
customer_id=customer_id,
agent_type="customer",
agent_run_id=agent_run_id,
)
return []
except Exception as exc:
return [f"session_close_failed:{type(exc).__name__}"]
def normalize_warnings(value: Any) -> list[str]:
"""将底层异常或组件返回的提示统一为字符串列表。"""
if value is None:
return []
if isinstance(value, str):
return [value]
return [str(item) for item in value]
__all__ = ["MemoryService", "normalize_warnings"]