context_service.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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. 获取指定轮次的上下文 - v0.3
  48. 逻辑:
  49. - 如果 current_round <= 20: 返回前N轮所有消息
  50. - 如果 current_round > 20:
  51. * 最近20轮消息 (round_number in [current_round-20, current_round-1])
  52. * 历史总结(从 conversation_summaries 表 + JSON memory)
  53. Args:
  54. user_id: 用户ID
  55. current_round: 当前轮次
  56. Returns:
  57. {
  58. "summary": str | None, # 历史总结(数据库 + JSON memory)
  59. "recent_messages": List[Dict], # 最近20轮消息
  60. "total_tokens": int # 总token数
  61. }
  62. """
  63. try:
  64. # 🔥 确保Redis连接可用
  65. await self._ensure_redis()
  66. if current_round <= 20:
  67. # 前20轮:直接返回所有历史消息
  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. # 超过20轮:最新总结 + 最近20轮
  81. summary_end_round = current_round - 1
  82. logger.info(f"🔍 查询总结: user={user_id}, current_round={current_round}, summary_end_round={summary_end_round}")
  83. # 1. 获取最新的总结(数据库 + JSON memory)
  84. summary = await self._get_latest_summary(user_id, summary_end_round)
  85. # 2. 获取最近20轮消息
  86. recent_messages = await self._get_messages_from_cache_or_db(
  87. user_id,
  88. start_round=current_round - 20,
  89. end_round=current_round
  90. )
  91. logger.info(
  92. f"[Context] Round {current_round}: "
  93. f"总结={'有' if summary else '无'}, 消息={len(recent_messages)}条"
  94. )
  95. return {
  96. "summary": summary,
  97. "recent_messages": recent_messages,
  98. "total_tokens": self._count_tokens(summary=summary, messages=recent_messages)
  99. }
  100. except Exception as e:
  101. logger.error(f"❌ 获取上下文失败: user={user_id}, round={current_round}, error={e}")
  102. # 降级方案:返回空上下文
  103. return {
  104. "summary": None,
  105. "recent_messages": [],
  106. "total_tokens": 0
  107. }
  108. async def _get_messages_from_cache_or_db(
  109. self,
  110. user_id: str,
  111. start_round: int,
  112. end_round: int
  113. ) -> List[Dict]:
  114. """
  115. 优先从Redis获取,未命中则从MySQL
  116. Args:
  117. user_id: 用户ID
  118. start_round: 起始轮次
  119. end_round: 结束轮次(不包含)
  120. Returns:
  121. 消息列表
  122. """
  123. try:
  124. # 1. 如果请求的是最近15轮以内,尝试从Redis获取
  125. messages_needed = end_round - start_round
  126. if messages_needed <= 15:
  127. cached_messages = await self._get_from_redis(user_id, start_round, end_round)
  128. if cached_messages:
  129. logger.debug(f"✅ 从Redis命中: user={user_id}, rounds={start_round}-{end_round}")
  130. return cached_messages
  131. # 2. 从MySQL查询
  132. logger.debug(f"🔍 从MySQL查询: user={user_id}, rounds={start_round}-{end_round}")
  133. # 使用 ORM 查询消息
  134. from database import db_manager
  135. from models_db import Message
  136. async with db_manager.get_session() as session:
  137. result_query = await session.execute(
  138. select(Message)
  139. .where(Message.user_id == user_id)
  140. .where(Message.round_number >= start_round)
  141. .where(Message.round_number < end_round)
  142. .order_by(Message.round_number.asc(), Message.id.asc())
  143. )
  144. messages = result_query.scalars().all()
  145. result = [
  146. {
  147. "role": msg.role,
  148. "content": msg.content,
  149. "round_number": msg.round_number,
  150. "emotion": msg.emotion,
  151. "created_at": msg.created_at.isoformat() if msg.created_at else None,
  152. }
  153. for msg in messages
  154. ]
  155. # 3. 如果是最近的消息,回填Redis
  156. # (这里简化处理,实际应判断end_round是否接近当前轮次)
  157. if messages_needed <= 15 and len(result) > 0:
  158. asyncio.create_task(self._cache_recent_messages(user_id, result))
  159. return result
  160. except Exception as e:
  161. logger.error(f"❌ 获取消息失败: user={user_id}, error={e}")
  162. return []
  163. async def _get_from_redis(
  164. self,
  165. user_id: str,
  166. start_round: int,
  167. end_round: int
  168. ) -> Optional[List[Dict]]:
  169. """
  170. 从Redis获取消息
  171. Args:
  172. user_id: 用户ID
  173. start_round: 起始轮次
  174. end_round: 结束轮次(不包含)
  175. Returns:
  176. 消息列表或None
  177. """
  178. try:
  179. cache_key = f"user:{user_id}:recent_messages"
  180. # 获取所有缓存消息(最多30条)
  181. cached_data = await self.redis.lrange(cache_key, 0, -1)
  182. if not cached_data:
  183. return None
  184. # 解析并过滤
  185. messages = []
  186. for data in reversed(cached_data): # Redis是LPUSH,需要反转
  187. try:
  188. msg = json.loads(data)
  189. round_num = msg.get('round_number', 0)
  190. if start_round <= round_num < end_round:
  191. messages.append(msg)
  192. except:
  193. continue
  194. # 按轮次排序
  195. messages.sort(key=lambda x: x.get('round_number', 0))
  196. # 检查是否完整覆盖请求范围
  197. if len(messages) > 0:
  198. rounds_in_cache = set(msg['round_number'] for msg in messages)
  199. rounds_needed = set(range(start_round, end_round))
  200. # 如果缓存不完整,返回None触发MySQL查询
  201. if not rounds_needed.issubset(rounds_in_cache):
  202. return None
  203. return messages if messages else None
  204. except Exception as e:
  205. logger.warning(f"⚠️ Redis获取失败: user={user_id}, error={e}")
  206. return None
  207. async def _cache_recent_messages(self, user_id: str, messages: List[Dict]):
  208. """缓存最近消息到Redis(异步)"""
  209. try:
  210. # 这里简化处理,实际应该更智能地合并现有缓存
  211. pass
  212. except Exception as e:
  213. logger.warning(f"⚠️ 缓存消息失败: {e}")
  214. async def _get_latest_summary(
  215. self,
  216. user_id: str,
  217. up_to_round: int
  218. ) -> Optional[str]:
  219. """
  220. 获取最新的总结(v0.3:数据库总结 + JSON memory摘要)
  221. Args:
  222. user_id: 用户ID
  223. up_to_round: 总结到的轮次(包含)
  224. Returns:
  225. 合并后的总结文本,如果没有则返回None
  226. """
  227. try:
  228. # 1. 从数据库获取最新总结
  229. from database import db_manager
  230. from models_db import ConversationSummary
  231. logger.debug(f"🔍 从MySQL查询最新总结: user={user_id}, up_to={up_to_round}")
  232. db_summary = None
  233. async with db_manager.get_session() as session:
  234. result_query = await session.execute(
  235. select(ConversationSummary)
  236. .where(ConversationSummary.user_id == user_id)
  237. .where(ConversationSummary.end_round <= up_to_round)
  238. .order_by(ConversationSummary.end_round.desc())
  239. .limit(1)
  240. )
  241. summary_record = result_query.scalar_one_or_none()
  242. if summary_record:
  243. db_summary = summary_record.summary_text
  244. logger.info(f"✅ 获取数据库总结: user={user_id}, end_round={summary_record.end_round}")
  245. # 2. 从 JSON 文件获取记忆摘要
  246. json_summary = None
  247. try:
  248. import sys
  249. from pathlib import Path
  250. project_root = Path(__file__).parent.parent.parent
  251. sys.path.insert(0, str(project_root))
  252. from memory_summaries.memory_service import load_memory
  253. memory = load_memory(user_id)
  254. json_summary = memory.get("meta.memory_summary", "")
  255. if isinstance(json_summary, str):
  256. json_summary = json_summary.strip()
  257. if json_summary and json_summary != "(none)":
  258. logger.debug(f"✅ 获取JSON记忆摘要: user={user_id}")
  259. else:
  260. json_summary = None
  261. except Exception as e:
  262. logger.debug(f"加载JSON记忆失败: user={user_id}, error={e}")
  263. json_summary = None
  264. # 3. 合并总结(优先使用数据库总结,JSON摘要作为补充)
  265. if db_summary and json_summary:
  266. # 合并两者
  267. combined = f"{db_summary}\n\n[JSON Memory Summary]\n{json_summary}"
  268. return combined
  269. elif db_summary:
  270. return db_summary
  271. elif json_summary:
  272. return json_summary
  273. else:
  274. logger.warning(f"⚠️ 未找到总结: user={user_id}, up_to={up_to_round}")
  275. return None
  276. except Exception as e:
  277. logger.error(f"❌ 获取总结失败: user={user_id}, error={e}")
  278. return None
  279. def _count_tokens(
  280. self,
  281. summary: Optional[str] = None,
  282. messages: Optional[List[Dict]] = None
  283. ) -> int:
  284. """
  285. 估算token数量(简单实现)
  286. Args:
  287. summary: 总结文本(单个)
  288. messages: 消息列表
  289. Returns:
  290. 估算的token数
  291. """
  292. total = 0
  293. if summary:
  294. # 中文:1字约等于1token,英文:1词约等于1token
  295. total += len(summary) // 2
  296. if messages:
  297. for msg in messages:
  298. content = msg.get('content', '')
  299. total += len(content) // 2
  300. return total
  301. # 为了避免循环导入,延迟导入
  302. import asyncio
  303. # 全局上下文服务实例
  304. context_service = ContextService()