summary_service.py 13 KB

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