context_service.py 12 KB

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