context_service.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. """
  2. 上下文管理服务(SQLAlchemy ORM 版本)
  3. """
  4. import json
  5. import asyncio
  6. from typing import Awaitable, Callable, List, Dict, Optional
  7. import logging
  8. import redis.asyncio as aioredis
  9. from sqlalchemy import select
  10. logger = logging.getLogger(__name__)
  11. class ContextService:
  12. """上下文管理服务"""
  13. def __init__(self, db_manager=None):
  14. """
  15. 初始化上下文服务
  16. Args:
  17. db_manager: 数据库管理器实例(可选,用于延迟初始化)
  18. """
  19. self.db = db_manager
  20. self.redis = None
  21. self._redis_provider: Optional[Callable[[], Awaitable[aioredis.Redis]]] = None
  22. if db_manager:
  23. # 🔥 延迟获取Redis(在首次使用时)
  24. self._redis_provider = db_manager.get_redis
  25. async def set_db_manager(self, db_manager):
  26. """设置数据库管理器(用于延迟初始化)"""
  27. self.db = db_manager
  28. self._redis_provider = db_manager.get_redis
  29. self.redis = None
  30. async def _ensure_redis(self):
  31. """确保Redis连接可用"""
  32. if not self._redis_provider:
  33. if not self.db:
  34. raise RuntimeError("Redis 未初始化,缺少数据库管理器")
  35. self._redis_provider = self.db.get_redis
  36. redis_client = await self._redis_provider()
  37. if not redis_client:
  38. raise RuntimeError("Redis 客户端不可用")
  39. self.redis = redis_client
  40. return redis_client
  41. async def get_context_for_round(
  42. self,
  43. user_id: str,
  44. current_round: int
  45. ) -> Dict:
  46. """
  47. 获取指定轮次的上下文
  48. 逻辑:
  49. - 如果 current_round <= 15: 返回前N轮所有消息
  50. - 如果 current_round > 15:
  51. * 最近15轮消息 (round_number in [current_round-15, current_round-1])
  52. * 所有历史总结 (1 到 current_round-15)
  53. Args:
  54. user_id: 用户ID
  55. current_round: 当前轮次
  56. Returns:
  57. {
  58. "summary": str | None, # 最新的一个总结(已包含所有历史信息)
  59. "recent_messages": List[Dict], # 最近消息
  60. "total_tokens": int # 总token数
  61. }
  62. """
  63. try:
  64. # 🔥 确保Redis连接可用
  65. await self._ensure_redis()
  66. if current_round <= 15:
  67. # 前15轮:直接返回所有历史消息
  68. messages = await self._get_messages_from_cache_or_db(
  69. user_id,
  70. start_round=1,
  71. end_round=current_round
  72. )
  73. logger.info(f"[Context] Round {current_round}: {len(messages)} 条历史消息")
  74. return {
  75. "summary": None,
  76. "recent_messages": messages,
  77. "total_tokens": self._count_tokens(messages=messages)
  78. }
  79. else:
  80. # 超过15轮:最新总结 + 最近15轮
  81. # 🔥 修复:查找 end_round < current_round 的最新总结
  82. # 因为总结是累积的,最新的总结包含了所有历史信息
  83. summary_end_round = current_round - 1
  84. logger.info(f"🔍 查询总结: user={user_id}, current_round={current_round}, summary_end_round={summary_end_round}")
  85. # 1. 获取最新的一个总结(已包含所有历史信息)
  86. summary = await self._get_latest_summary(user_id, summary_end_round)
  87. # 2. 获取最近15轮消息
  88. recent_messages = await self._get_messages_from_cache_or_db(
  89. user_id,
  90. start_round=current_round - 15,
  91. end_round=current_round
  92. )
  93. logger.info(
  94. f"[Context] Round {current_round}: "
  95. f"总结={'有' if summary else '无'}, 消息={len(recent_messages)}条"
  96. )
  97. return {
  98. "summary": summary,
  99. "recent_messages": recent_messages,
  100. "total_tokens": self._count_tokens(summary=summary, messages=recent_messages)
  101. }
  102. except Exception as e:
  103. logger.error(f"❌ 获取上下文失败: user={user_id}, round={current_round}, error={e}")
  104. # 降级方案:返回空上下文
  105. return {
  106. "summary": None,
  107. "recent_messages": [],
  108. "total_tokens": 0
  109. }
  110. async def _get_messages_from_cache_or_db(
  111. self,
  112. user_id: str,
  113. start_round: int,
  114. end_round: int
  115. ) -> List[Dict]:
  116. """
  117. 优先从Redis获取,未命中则从MySQL
  118. Args:
  119. user_id: 用户ID
  120. start_round: 起始轮次
  121. end_round: 结束轮次(不包含)
  122. Returns:
  123. 消息列表
  124. """
  125. try:
  126. # 1. 如果请求的是最近15轮以内,尝试从Redis获取
  127. messages_needed = end_round - start_round
  128. if messages_needed <= 15:
  129. cached_messages = await self._get_from_redis(user_id, start_round, end_round)
  130. if cached_messages:
  131. logger.debug(f"✅ 从Redis命中: user={user_id}, rounds={start_round}-{end_round}")
  132. return cached_messages
  133. # 2. 从MySQL查询
  134. logger.debug(f"🔍 从MySQL查询: user={user_id}, rounds={start_round}-{end_round}")
  135. # 使用 ORM 查询消息
  136. from database import db_manager
  137. from models_db import Message
  138. async with db_manager.get_session() as session:
  139. result_query = await session.execute(
  140. select(Message)
  141. .where(Message.user_id == user_id)
  142. .where(Message.round_number >= start_round)
  143. .where(Message.round_number < end_round)
  144. .order_by(Message.round_number.asc(), Message.id.asc())
  145. )
  146. messages = result_query.scalars().all()
  147. result = [
  148. {
  149. "role": msg.role,
  150. "content": msg.content,
  151. "round_number": msg.round_number,
  152. "emotion": msg.emotion,
  153. "created_at": msg.created_at.isoformat() if msg.created_at else None,
  154. }
  155. for msg in messages
  156. ]
  157. # 3. 如果是最近的消息,回填Redis
  158. # (这里简化处理,实际应判断end_round是否接近当前轮次)
  159. if messages_needed <= 15 and len(result) > 0:
  160. asyncio.create_task(self._cache_recent_messages(user_id, result))
  161. return result
  162. except Exception as e:
  163. logger.error(f"❌ 获取消息失败: user={user_id}, error={e}")
  164. return []
  165. async def _get_from_redis(
  166. self,
  167. user_id: str,
  168. start_round: int,
  169. end_round: int
  170. ) -> Optional[List[Dict]]:
  171. """
  172. 从Redis获取消息
  173. Args:
  174. user_id: 用户ID
  175. start_round: 起始轮次
  176. end_round: 结束轮次(不包含)
  177. Returns:
  178. 消息列表或None
  179. """
  180. try:
  181. cache_key = f"user:{user_id}:recent_messages"
  182. # 获取所有缓存消息(最多30条)
  183. cached_data = await self.redis.lrange(cache_key, 0, -1)
  184. if not cached_data:
  185. return None
  186. # 解析并过滤
  187. messages = []
  188. for data in reversed(cached_data): # Redis是LPUSH,需要反转
  189. try:
  190. msg = json.loads(data)
  191. round_num = msg.get('round_number', 0)
  192. if start_round <= round_num < end_round:
  193. messages.append(msg)
  194. except:
  195. continue
  196. # 按轮次排序
  197. messages.sort(key=lambda x: x.get('round_number', 0))
  198. # 检查是否完整覆盖请求范围
  199. if len(messages) > 0:
  200. rounds_in_cache = set(msg['round_number'] for msg in messages)
  201. rounds_needed = set(range(start_round, end_round))
  202. # 如果缓存不完整,返回None触发MySQL查询
  203. if not rounds_needed.issubset(rounds_in_cache):
  204. return None
  205. return messages if messages else None
  206. except Exception as e:
  207. logger.warning(f"⚠️ Redis获取失败: user={user_id}, error={e}")
  208. return None
  209. async def _cache_recent_messages(self, user_id: str, messages: List[Dict]):
  210. """缓存最近消息到Redis(异步)"""
  211. try:
  212. # 这里简化处理,实际应该更智能地合并现有缓存
  213. pass
  214. except Exception as e:
  215. logger.warning(f"⚠️ 缓存消息失败: {e}")
  216. async def _get_latest_summary(
  217. self,
  218. user_id: str,
  219. up_to_round: int
  220. ) -> Optional[str]:
  221. """
  222. 获取最新的一个总结(最新总结已包含所有历史信息)
  223. Args:
  224. user_id: 用户ID
  225. up_to_round: 总结到的轮次(包含)
  226. Returns:
  227. 最新的总结文本,如果没有则返回None
  228. """
  229. try:
  230. # 使用 ORM 查询最新总结
  231. from database import db_manager
  232. from models_db import ConversationSummary
  233. from sqlalchemy import func
  234. logger.debug(f"🔍 从MySQL查询最新总结: user={user_id}, up_to={up_to_round}")
  235. async with db_manager.get_session() as session:
  236. # 先查询所有总结,用于调试
  237. all_summaries_query = await session.execute(
  238. select(ConversationSummary)
  239. .where(ConversationSummary.user_id == user_id)
  240. .order_by(ConversationSummary.end_round.desc())
  241. .limit(10)
  242. )
  243. all_summaries = all_summaries_query.scalars().all()
  244. if all_summaries:
  245. logger.info(f"📊 数据库中的总结: user={user_id}, 总数={len(all_summaries)}, 列表={[(s.start_round, s.end_round) for s in all_summaries]}")
  246. else:
  247. logger.warning(f"⚠️ 数据库中没有任何总结: user={user_id}")
  248. # 查询最新的一个总结
  249. result_query = await session.execute(
  250. select(ConversationSummary)
  251. .where(ConversationSummary.user_id == user_id)
  252. .where(ConversationSummary.end_round <= up_to_round)
  253. .order_by(ConversationSummary.end_round.desc())
  254. .limit(1)
  255. )
  256. summary = result_query.scalar_one_or_none()
  257. if summary:
  258. logger.info(f"✅ 获取最新总结: user={user_id}, end_round={summary.end_round}")
  259. return summary.summary_text
  260. logger.warning(f"⚠️ 未找到总结: user={user_id}, up_to={up_to_round}, 数据库中总结数={len(all_summaries)}")
  261. return None
  262. except Exception as e:
  263. logger.error(f"❌ 获取总结失败: user={user_id}, error={e}")
  264. return None
  265. def _count_tokens(
  266. self,
  267. summary: Optional[str] = None,
  268. messages: Optional[List[Dict]] = None
  269. ) -> int:
  270. """
  271. 估算token数量(简单实现)
  272. Args:
  273. summary: 总结文本(单个)
  274. messages: 消息列表
  275. Returns:
  276. 估算的token数
  277. """
  278. total = 0
  279. if summary:
  280. # 中文:1字约等于1token,英文:1词约等于1token
  281. total += len(summary) // 2
  282. if messages:
  283. for msg in messages:
  284. content = msg.get('content', '')
  285. total += len(content) // 2
  286. return total
  287. # 为了避免循环导入,延迟导入
  288. import asyncio
  289. # 全局上下文服务实例
  290. context_service = ContextService()