| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- """
- 上下文管理服务(SQLAlchemy ORM 版本)
- """
- import json
- import asyncio
- from typing import Awaitable, Callable, List, Dict, Optional
- import logging
- import redis.asyncio as aioredis
- from sqlalchemy import select
- logger = logging.getLogger(__name__)
- class ContextService:
- """上下文管理服务"""
-
- def __init__(self, db_manager=None):
- """
- 初始化上下文服务
- Args:
- db_manager: 数据库管理器实例(可选,用于延迟初始化)
- """
- self.db = db_manager
- self.redis = None
- self._redis_provider: Optional[Callable[[], Awaitable[aioredis.Redis]]] = None
- if db_manager:
- # 🔥 延迟获取Redis(在首次使用时)
- self._redis_provider = db_manager.get_redis
- async def set_db_manager(self, db_manager):
- """设置数据库管理器(用于延迟初始化)"""
- self.db = db_manager
- self._redis_provider = db_manager.get_redis
- self.redis = None
-
- async def _ensure_redis(self):
- """确保Redis连接可用"""
- if not self._redis_provider:
- if not self.db:
- raise RuntimeError("Redis 未初始化,缺少数据库管理器")
- self._redis_provider = self.db.get_redis
- redis_client = await self._redis_provider()
- if not redis_client:
- raise RuntimeError("Redis 客户端不可用")
- self.redis = redis_client
- return redis_client
-
- async def get_context_for_round(
- self,
- user_id: str,
- current_round: int
- ) -> Dict:
- """
- 获取指定轮次的上下文 - v0.3
-
- 逻辑:
- - 如果 current_round <= 20: 返回前N轮所有消息
- - 如果 current_round > 20:
- * 最近20轮消息 (round_number in [current_round-20, current_round-1])
- * 历史总结(从 conversation_summaries 表 + JSON memory)
-
- Args:
- user_id: 用户ID
- current_round: 当前轮次
-
- Returns:
- {
- "summary": str | None, # 历史总结(数据库 + JSON memory)
- "recent_messages": List[Dict], # 最近20轮消息
- "total_tokens": int # 总token数
- }
- """
-
- try:
- # 🔥 确保Redis连接可用
- await self._ensure_redis()
-
- if current_round <= 20:
- # 前20轮:直接返回所有历史消息
- messages = await self._get_messages_from_cache_or_db(
- user_id,
- start_round=1,
- end_round=current_round
- )
-
- logger.info(f"[Context] Round {current_round}: {len(messages)} 条历史消息")
-
- return {
- "summary": None,
- "recent_messages": messages,
- "total_tokens": self._count_tokens(messages=messages)
- }
-
- else:
- # 超过20轮:最新总结 + 最近20轮
- summary_end_round = current_round - 1
-
- logger.info(f"🔍 查询总结: user={user_id}, current_round={current_round}, summary_end_round={summary_end_round}")
-
- # 1. 获取最新的总结(数据库 + JSON memory)
- summary = await self._get_latest_summary(user_id, summary_end_round)
-
- # 2. 获取最近20轮消息
- recent_messages = await self._get_messages_from_cache_or_db(
- user_id,
- start_round=current_round - 20,
- end_round=current_round
- )
-
- logger.info(
- f"[Context] Round {current_round}: "
- f"总结={'有' if summary else '无'}, 消息={len(recent_messages)}条"
- )
-
- return {
- "summary": summary,
- "recent_messages": recent_messages,
- "total_tokens": self._count_tokens(summary=summary, messages=recent_messages)
- }
-
- except Exception as e:
- logger.error(f"❌ 获取上下文失败: user={user_id}, round={current_round}, error={e}")
- # 降级方案:返回空上下文
- return {
- "summary": None,
- "recent_messages": [],
- "total_tokens": 0
- }
-
- async def _get_messages_from_cache_or_db(
- self,
- user_id: str,
- start_round: int,
- end_round: int
- ) -> List[Dict]:
- """
- 优先从Redis获取,未命中则从MySQL
-
- Args:
- user_id: 用户ID
- start_round: 起始轮次
- end_round: 结束轮次(不包含)
-
- Returns:
- 消息列表
- """
-
- try:
- # 1. 如果请求的是最近15轮以内,尝试从Redis获取
- messages_needed = end_round - start_round
-
- if messages_needed <= 15:
- cached_messages = await self._get_from_redis(user_id, start_round, end_round)
- if cached_messages:
- logger.debug(f"✅ 从Redis命中: user={user_id}, rounds={start_round}-{end_round}")
- return cached_messages
-
- # 2. 从MySQL查询
- logger.debug(f"🔍 从MySQL查询: user={user_id}, rounds={start_round}-{end_round}")
-
- # 使用 ORM 查询消息
- from database import db_manager
- from models_db import Message
-
- async with db_manager.get_session() as session:
- result_query = await session.execute(
- select(Message)
- .where(Message.user_id == user_id)
- .where(Message.round_number >= start_round)
- .where(Message.round_number < end_round)
- .order_by(Message.round_number.asc(), Message.id.asc())
- )
- messages = result_query.scalars().all()
- result = [
- {
- "role": msg.role,
- "content": msg.content,
- "round_number": msg.round_number,
- "emotion": msg.emotion,
- "created_at": msg.created_at.isoformat() if msg.created_at else None,
- }
- for msg in messages
- ]
-
- # 3. 如果是最近的消息,回填Redis
- # (这里简化处理,实际应判断end_round是否接近当前轮次)
- if messages_needed <= 15 and len(result) > 0:
- asyncio.create_task(self._cache_recent_messages(user_id, result))
-
- return result
-
- except Exception as e:
- logger.error(f"❌ 获取消息失败: user={user_id}, error={e}")
- return []
-
- async def _get_from_redis(
- self,
- user_id: str,
- start_round: int,
- end_round: int
- ) -> Optional[List[Dict]]:
- """
- 从Redis获取消息
-
- Args:
- user_id: 用户ID
- start_round: 起始轮次
- end_round: 结束轮次(不包含)
-
- Returns:
- 消息列表或None
- """
-
- try:
- cache_key = f"user:{user_id}:recent_messages"
-
- # 获取所有缓存消息(最多30条)
- cached_data = await self.redis.lrange(cache_key, 0, -1)
-
- if not cached_data:
- return None
-
- # 解析并过滤
- messages = []
- for data in reversed(cached_data): # Redis是LPUSH,需要反转
- try:
- msg = json.loads(data)
- round_num = msg.get('round_number', 0)
-
- if start_round <= round_num < end_round:
- messages.append(msg)
- except:
- continue
-
- # 按轮次排序
- messages.sort(key=lambda x: x.get('round_number', 0))
-
- # 检查是否完整覆盖请求范围
- if len(messages) > 0:
- rounds_in_cache = set(msg['round_number'] for msg in messages)
- rounds_needed = set(range(start_round, end_round))
-
- # 如果缓存不完整,返回None触发MySQL查询
- if not rounds_needed.issubset(rounds_in_cache):
- return None
-
- return messages if messages else None
-
- except Exception as e:
- logger.warning(f"⚠️ Redis获取失败: user={user_id}, error={e}")
- return None
-
- async def _cache_recent_messages(self, user_id: str, messages: List[Dict]):
- """缓存最近消息到Redis(异步)"""
- try:
- # 这里简化处理,实际应该更智能地合并现有缓存
- pass
- except Exception as e:
- logger.warning(f"⚠️ 缓存消息失败: {e}")
-
- async def _get_latest_summary(
- self,
- user_id: str,
- up_to_round: int
- ) -> Optional[str]:
- """
- 获取最新的总结(v0.3:数据库总结 + JSON memory摘要)
-
- Args:
- user_id: 用户ID
- up_to_round: 总结到的轮次(包含)
-
- Returns:
- 合并后的总结文本,如果没有则返回None
- """
-
- try:
- # 1. 从数据库获取最新总结
- from database import db_manager
- from models_db import ConversationSummary
-
- logger.debug(f"🔍 从MySQL查询最新总结: user={user_id}, up_to={up_to_round}")
-
- db_summary = None
- async with db_manager.get_session() as session:
- result_query = await session.execute(
- select(ConversationSummary)
- .where(ConversationSummary.user_id == user_id)
- .where(ConversationSummary.end_round <= up_to_round)
- .order_by(ConversationSummary.end_round.desc())
- .limit(1)
- )
- summary_record = result_query.scalar_one_or_none()
-
- if summary_record:
- db_summary = summary_record.summary_text
- logger.info(f"✅ 获取数据库总结: user={user_id}, end_round={summary_record.end_round}")
-
- # 2. 从 JSON 文件获取记忆摘要
- json_summary = None
- try:
- import sys
- from pathlib import Path
- project_root = Path(__file__).parent.parent.parent
- sys.path.insert(0, str(project_root))
- from memory_summaries.memory_service import load_memory
-
- memory = load_memory(user_id)
- json_summary = memory.get("meta.memory_summary", "")
- if isinstance(json_summary, str):
- json_summary = json_summary.strip()
- if json_summary and json_summary != "(none)":
- logger.debug(f"✅ 获取JSON记忆摘要: user={user_id}")
- else:
- json_summary = None
- except Exception as e:
- logger.debug(f"加载JSON记忆失败: user={user_id}, error={e}")
- json_summary = None
-
- # 3. 合并总结(优先使用数据库总结,JSON摘要作为补充)
- if db_summary and json_summary:
- # 合并两者
- combined = f"{db_summary}\n\n[JSON Memory Summary]\n{json_summary}"
- return combined
- elif db_summary:
- return db_summary
- elif json_summary:
- return json_summary
- else:
- logger.warning(f"⚠️ 未找到总结: user={user_id}, up_to={up_to_round}")
- return None
-
- except Exception as e:
- logger.error(f"❌ 获取总结失败: user={user_id}, error={e}")
- return None
-
- def _count_tokens(
- self,
- summary: Optional[str] = None,
- messages: Optional[List[Dict]] = None
- ) -> int:
- """
- 估算token数量(简单实现)
-
- Args:
- summary: 总结文本(单个)
- messages: 消息列表
-
- Returns:
- 估算的token数
- """
-
- total = 0
-
- if summary:
- # 中文:1字约等于1token,英文:1词约等于1token
- total += len(summary) // 2
-
- if messages:
- for msg in messages:
- content = msg.get('content', '')
- total += len(content) // 2
-
- return total
- # 为了避免循环导入,延迟导入
- import asyncio
- # 全局上下文服务实例
- context_service = ContextService()
|