| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- """
- 上下文管理服务(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:
- """
- 获取指定轮次的上下文
-
- 逻辑:
- - 如果 current_round <= 15: 返回前N轮所有消息
- - 如果 current_round > 15:
- * 最近15轮消息 (round_number in [current_round-15, current_round-1])
- * 所有历史总结 (1 到 current_round-15)
-
- Args:
- user_id: 用户ID
- current_round: 当前轮次
-
- Returns:
- {
- "summary": str | None, # 最新的一个总结(已包含所有历史信息)
- "recent_messages": List[Dict], # 最近消息
- "total_tokens": int # 总token数
- }
- """
-
- try:
- # 🔥 确保Redis连接可用
- await self._ensure_redis()
-
- if current_round <= 15:
- # 前15轮:直接返回所有历史消息
- 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:
- # 超过15轮:最新总结 + 最近15轮
- # 🔥 修复:查找 end_round < current_round 的最新总结
- # 因为总结是累积的,最新的总结包含了所有历史信息
- summary_end_round = current_round - 1
-
- logger.info(f"🔍 查询总结: user={user_id}, current_round={current_round}, summary_end_round={summary_end_round}")
-
- # 1. 获取最新的一个总结(已包含所有历史信息)
- summary = await self._get_latest_summary(user_id, summary_end_round)
-
- # 2. 获取最近15轮消息
- recent_messages = await self._get_messages_from_cache_or_db(
- user_id,
- start_round=current_round - 15,
- 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]:
- """
- 获取最新的一个总结(最新总结已包含所有历史信息)
-
- Args:
- user_id: 用户ID
- up_to_round: 总结到的轮次(包含)
-
- Returns:
- 最新的总结文本,如果没有则返回None
- """
-
- try:
- # 使用 ORM 查询最新总结
- from database import db_manager
- from models_db import ConversationSummary
- from sqlalchemy import func
-
- logger.debug(f"🔍 从MySQL查询最新总结: user={user_id}, up_to={up_to_round}")
-
- async with db_manager.get_session() as session:
- # 先查询所有总结,用于调试
- all_summaries_query = await session.execute(
- select(ConversationSummary)
- .where(ConversationSummary.user_id == user_id)
- .order_by(ConversationSummary.end_round.desc())
- .limit(10)
- )
- all_summaries = all_summaries_query.scalars().all()
-
- if all_summaries:
- logger.info(f"📊 数据库中的总结: user={user_id}, 总数={len(all_summaries)}, 列表={[(s.start_round, s.end_round) for s in all_summaries]}")
- else:
- logger.warning(f"⚠️ 数据库中没有任何总结: user={user_id}")
-
- # 查询最新的一个总结
- 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 = result_query.scalar_one_or_none()
-
- if summary:
- logger.info(f"✅ 获取最新总结: user={user_id}, end_round={summary.end_round}")
- return summary.summary_text
-
- logger.warning(f"⚠️ 未找到总结: user={user_id}, up_to={up_to_round}, 数据库中总结数={len(all_summaries)}")
- 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()
|