summary_service.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. """
  2. 对话总结服务
  3. """
  4. import uuid
  5. from typing import List, Dict
  6. import logging
  7. logger = logging.getLogger(__name__)
  8. class SummaryService:
  9. """对话总结服务"""
  10. def __init__(self, db_manager):
  11. """
  12. 初始化总结服务
  13. Args:
  14. db_manager: 数据库管理器实例
  15. """
  16. self.db = db_manager
  17. self.redis = None # 🔥 延迟获取Redis
  18. async def _ensure_redis(self):
  19. """确保Redis连接可用"""
  20. if not self.redis and self.db:
  21. self.redis = await self.db.get_redis()
  22. async def generate_summary(
  23. self,
  24. user_id: str,
  25. start_round: int,
  26. end_round: int
  27. ):
  28. """
  29. 生成对话总结(异步执行)
  30. 逻辑:
  31. - 如果是第一次总结(start_round=1):只总结当前15轮消息
  32. - 如果是后续总结:获取之前的总结 + 当前15轮消息,进行累积总结
  33. Args:
  34. user_id: 用户ID
  35. start_round: 起始轮次
  36. end_round: 结束轮次
  37. """
  38. try:
  39. # 🔥 确保Redis连接可用
  40. await self._ensure_redis()
  41. logger.info(f"🔄 开始生成总结: user={user_id}, rounds={start_round}-{end_round}")
  42. # 0. 参数验证
  43. if end_round <= 0:
  44. logger.warning(f"⚠️ 无效的总结范围: user={user_id}, start_round={start_round}, end_round={end_round}")
  45. return
  46. if start_round > end_round:
  47. logger.warning(f"⚠️ 总结范围错误: user={user_id}, start_round={start_round}, end_round={end_round}")
  48. return
  49. # 1. 检查是否已存在总结
  50. existing = await self.db.execute_one(
  51. """
  52. SELECT id FROM conversation_summaries
  53. WHERE user_id = %(user_id)s
  54. AND start_round = %(start_round)s
  55. AND end_round = %(end_round)s
  56. """,
  57. {"user_id": user_id, "start_round": start_round, "end_round": end_round}
  58. )
  59. if existing:
  60. logger.info(f"⏭️ 总结已存在,跳过: user={user_id}, rounds={start_round}-{end_round}")
  61. return
  62. # 2. 获取之前的总结(如果有)
  63. previous_summaries = []
  64. if start_round > 1:
  65. previous_summaries_result = await self.db.execute_query(
  66. """
  67. SELECT summary_text, end_round
  68. FROM conversation_summaries
  69. WHERE user_id = %(user_id)s AND end_round < %(start_round)s
  70. ORDER BY end_round ASC
  71. """,
  72. {"user_id": user_id, "start_round": start_round}
  73. )
  74. previous_summaries = [s['summary_text'] for s in previous_summaries_result]
  75. logger.info(f"📚 获取到 {len(previous_summaries)} 个历史总结")
  76. # 3. 获取待总结的消息
  77. logger.info(f"🔍 [总结查询] user={user_id}, start_round={start_round}, end_round={end_round}")
  78. messages = await self.db.execute_query(
  79. """
  80. SELECT role, content, round_number
  81. FROM messages
  82. WHERE user_id = %(user_id)s
  83. AND round_number >= %(start_round)s
  84. AND round_number <= %(end_round)s
  85. ORDER BY round_number ASC, id ASC
  86. """,
  87. {"user_id": user_id, "start_round": start_round, "end_round": end_round}
  88. )
  89. logger.info(f"📊 [总结查询结果] user={user_id}, 查询到 {len(messages)} 条消息")
  90. # 如果查询不到消息,检查数据库中实际有哪些轮次
  91. if not messages:
  92. # 诊断:查询该用户的所有消息轮次
  93. all_rounds = await self.db.execute_query(
  94. """
  95. SELECT DISTINCT round_number, COUNT(*) as count
  96. FROM messages
  97. WHERE user_id = %(user_id)s
  98. GROUP BY round_number
  99. ORDER BY round_number ASC
  100. """,
  101. {"user_id": user_id}
  102. )
  103. # 过滤掉 round_number = 0 的记录(可能是历史错误数据)
  104. valid_rounds = [r['round_number'] for r in all_rounds if r['round_number'] > 0] if all_rounds else []
  105. logger.warning(
  106. f"⚠️ 没有消息可总结: user={user_id}, "
  107. f"查询范围={start_round}-{end_round}, "
  108. f"数据库中实际轮次={valid_rounds if valid_rounds else '无消息'}, "
  109. f"总记录数={len(all_rounds) if all_rounds else 0}"
  110. )
  111. return
  112. # 记录查询到的消息轮次范围
  113. if messages:
  114. actual_start = min(m['round_number'] for m in messages)
  115. actual_end = max(m['round_number'] for m in messages)
  116. logger.info(f"✅ [总结查询] 实际消息轮次范围: {actual_start}-{actual_end}, 共 {len(messages)} 条")
  117. # 4. 调用LLM生成总结(包含之前的总结)
  118. summary_text = await self._generate_summary_with_llm(messages, previous_summaries)
  119. if not summary_text:
  120. logger.warning(f"⚠️ 总结生成失败: user={user_id}")
  121. return
  122. # 4. 保存总结到MySQL
  123. summary_id = str(uuid.uuid4())
  124. query = """
  125. INSERT INTO conversation_summaries
  126. (summary_id, user_id, start_round, end_round, summary_text,
  127. summary_type, message_count, token_count)
  128. VALUES (%(summary_id)s, %(user_id)s, %(start_round)s, %(end_round)s,
  129. %(summary_text)s, %(summary_type)s, %(message_count)s, %(token_count)s)
  130. """
  131. await self.db.execute_update(query, {
  132. "summary_id": summary_id,
  133. "user_id": user_id,
  134. "start_round": start_round,
  135. "end_round": end_round,
  136. "summary_text": summary_text,
  137. "summary_type": "auto",
  138. "message_count": len(messages),
  139. "token_count": len(summary_text) // 2
  140. })
  141. logger.info(f"✅ 总结保存成功: user={user_id}, messages={len(messages)}")
  142. # 5. 更新Redis缓存
  143. await self._update_summary_cache(user_id, summary_text)
  144. # 6. 更新元数据
  145. meta_key = f"user:{user_id}:meta"
  146. await self.redis.hset(meta_key, 'last_summary_round', end_round)
  147. await self.redis.expire(meta_key, 86400)
  148. logger.info(f"🎉 总结完成: user={user_id}, rounds={start_round}-{end_round}")
  149. except Exception as e:
  150. logger.error(f"❌ 生成总结失败: user={user_id}, error={e}")
  151. import traceback
  152. traceback.print_exc()
  153. async def _generate_summary_with_llm(
  154. self,
  155. messages: List[Dict],
  156. previous_summaries: List[str] = None
  157. ) -> str:
  158. """
  159. 调用LLM生成总结
  160. 总结内容聚焦于:
  161. 1. 用户个人信息(姓名、年龄、家庭住址等)- 用于后续精准回答用户问题
  162. 2. 用户当前英语水平评估 - 用于逐步引导用户提高英语水平
  163. Args:
  164. messages: 消息列表
  165. previous_summaries: 之前的总结列表(用于累积总结)
  166. Returns:
  167. 总结文本(包含用户信息和英语水平评估)
  168. """
  169. try:
  170. # 构建对话内容
  171. conversation_lines = []
  172. for msg in messages:
  173. role = msg['role']
  174. content = msg['content']
  175. round_num = msg.get('round_number', '')
  176. role_label = "用户" if role == "user" else "助手"
  177. conversation_lines.append(f"[第{round_num}轮] {role_label}: {content}")
  178. conversation = "\n".join(conversation_lines)
  179. # 构建总结Prompt
  180. if previous_summaries:
  181. # 有历史总结:进行累积总结
  182. previous_summaries_text = "\n\n".join([
  183. f"【历史总结{i+1}】\n{summary}"
  184. for i, summary in enumerate(previous_summaries)
  185. ])
  186. prompt = f"""请基于以下历史总结和新的对话内容,生成一个更新的累积总结。
  187. **历史总结:**
  188. {previous_summaries_text}
  189. **新的对话内容(第{messages[0].get('round_number', 1)}-{messages[-1].get('round_number', 15)}轮):**
  190. {conversation}
  191. 请整合历史总结和新对话内容,更新以下两类关键信息:
  192. **第一类:用户个人信息**
  193. 请提取并整理用户在对话中透露的个人信息,包括但不限于:
  194. - 姓名、昵称
  195. - 年龄
  196. - 家庭住址、所在城市
  197. - 职业、工作
  198. - 兴趣爱好
  199. - 家庭成员信息
  200. - 其他个人背景信息
  201. 如果某些信息未在对话中提及,请标注"未提及"。
  202. **第二类:用户英语水平评估**
  203. 请评估用户当前的英语水平,包括:
  204. - 词汇量水平(基础/中级/高级)
  205. - 语法掌握程度(能使用哪些时态、句型)
  206. - 口语流利度(是否流畅、是否有较多停顿)
  207. - 发音准确性(是否有明显发音问题)
  208. - 表达能力(能否清晰表达想法)
  209. - 当前薄弱环节(需要重点提升的方面)
  210. 请按照以下格式输出总结(每部分100-150字):
  211. **用户个人信息:**
  212. [整合后的用户个人信息,未提及的标注"未提及"]
  213. **英语水平评估:**
  214. [对用户当前英语水平的详细评估]"""
  215. else:
  216. # 第一次总结:只总结当前对话
  217. prompt = f"""请从以下英语口语练习对话中提炼两类关键信息:
  218. **第一类:用户个人信息**
  219. 请提取并整理用户在对话中透露的个人信息,包括但不限于:
  220. - 姓名、昵称
  221. - 年龄
  222. - 家庭住址、所在城市
  223. - 职业、工作
  224. - 兴趣爱好
  225. - 家庭成员信息
  226. - 其他个人背景信息
  227. 如果某些信息未在对话中提及,请标注"未提及"。
  228. **第二类:用户英语水平评估**
  229. 请评估用户当前的英语水平,包括:
  230. - 词汇量水平(基础/中级/高级)
  231. - 语法掌握程度(能使用哪些时态、句型)
  232. - 口语流利度(是否流畅、是否有较多停顿)
  233. - 发音准确性(是否有明显发音问题)
  234. - 表达能力(能否清晰表达想法)
  235. - 当前薄弱环节(需要重点提升的方面)
  236. 对话内容:
  237. {conversation}
  238. 请按照以下格式输出总结(每部分100-150字):
  239. **用户个人信息:**
  240. [提取的用户个人信息,未提及的标注"未提及"]
  241. **英语水平评估:**
  242. [对用户当前英语水平的详细评估]"""
  243. # 调用AI服务生成总结
  244. from ai_service import AIService
  245. ai_service = AIService()
  246. # 使用非流式调用
  247. summary = await ai_service.generate_summary(prompt)
  248. return summary.strip()
  249. except Exception as e:
  250. logger.error(f"❌ LLM总结生成失败: {e}")
  251. # 降级方案:简单拼接
  252. return f"**用户个人信息:**\n未提及\n\n**英语水平评估:**\n对话总结(第{messages[0].get('round_number', 1)}-{messages[-1].get('round_number', 15)}轮):共{len(messages)}条消息的英语口语练习对话。"
  253. async def _update_summary_cache(self, user_id: str, summary_text: str):
  254. """
  255. 更新Redis中的总结缓存
  256. Args:
  257. user_id: 用户ID
  258. summary_text: 总结文本
  259. """
  260. try:
  261. cache_key = f"user:{user_id}:latest_summary"
  262. await self.redis.setex(cache_key, 86400, summary_text)
  263. logger.debug(f"✅ 总结已缓存: user={user_id}")
  264. except Exception as e:
  265. logger.warning(f"⚠️ 缓存总结失败: user={user_id}, error={e}")
  266. async def get_latest_summary(self, user_id: str) -> str:
  267. """
  268. 获取用户最新的总结
  269. Args:
  270. user_id: 用户ID
  271. Returns:
  272. 总结文本或空字符串
  273. """
  274. try:
  275. # 先查Redis
  276. cache_key = f"user:{user_id}:latest_summary"
  277. cached = await self.redis.get(cache_key)
  278. if cached:
  279. return cached
  280. # 再查MySQL
  281. result = await self.db.execute_one(
  282. """
  283. SELECT summary_text
  284. FROM conversation_summaries
  285. WHERE user_id = %(user_id)s
  286. ORDER BY end_round DESC
  287. LIMIT 1
  288. """,
  289. {"user_id": user_id}
  290. )
  291. if result:
  292. summary_text = result['summary_text']
  293. # 回填缓存
  294. await self.redis.setex(cache_key, 86400, summary_text)
  295. return summary_text
  296. return ""
  297. except Exception as e:
  298. logger.error(f"❌ 获取最新总结失败: user={user_id}, error={e}")
  299. return ""