context_service.py 12 KB

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