vibe_learning_agent.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. # agents/vibe_learning_agent.py
  2. """互动学习 Agent - 通过对话和测验巩固知识"""
  3. import json
  4. from datetime import datetime
  5. from typing import Dict, List
  6. from hello_agents import HelloAgentsLLM
  7. from hello_agents import SimpleAgent
  8. from specialist.quiz_generator import QuizGeneratorAgent
  9. from core.file_manager import FileManager
  10. from core.summary_manager import SummaryManager
  11. class VibeLearningAgent(SimpleAgent):
  12. """
  13. 互动学习专家
  14. 功能:
  15. - 支持两种模式:free(自由对话)和 quiz(结构化测验)
  16. - 根据学习计划生成问题
  17. - 评估用户回答并提供反馈
  18. - 动态调整问题难度
  19. - 生成会话总结
  20. """
  21. def __init__(self, llm: HelloAgentsLLM, file_manager: FileManager, streaming: bool = None):
  22. """
  23. 初始化 VibeLearningAgent
  24. Args:
  25. llm: HelloAgentsLLM 实例
  26. file_manager: FileManager 实例
  27. streaming: 是否启用流式输出(None = 自动检测)
  28. """
  29. system_prompt = """
  30. 你是专业的学习教练。
  31. 工作流程:
  32. 1. 读取学习计划(plan.md),了解知识体系
  33. 2. 根据模式(free/quiz)生成初始问题
  34. 3. 评估用户回答,给予反馈
  35. 4. 动态调整问题难度
  36. 5. 结束时生成会话总结
  37. 模式差异:
  38. - free: 开放性问题,鼓励讨论,引导思考
  39. - quiz: 结构化考察,固定问题,自动评分
  40. 反馈技巧:
  41. - 肯定正确的部分
  42. - 指出需要改进的地方
  43. - 提供额外的知识点链接
  44. - 鼓励继续探索
  45. """
  46. self.llm = llm
  47. self.file_manager = file_manager
  48. self.quiz_generator = QuizGeneratorAgent(llm)
  49. self.max_iterations = 10
  50. # 添加流式输出支持
  51. from utils.streaming import should_stream
  52. self.streaming = should_stream(streaming)
  53. # 使用父类初始化
  54. super().__init__("VibeLearningAgent", llm, system_prompt)
  55. def start_session(
  56. self, domain: str, mode: str = "free"
  57. ) -> str:
  58. """
  59. 开始互动学习会话(只生成第一个问题)
  60. Args:
  61. domain: 领域名称
  62. mode: 学习模式(free/quiz)
  63. Returns:
  64. 第一个问题
  65. """
  66. # 检查领域是否存在
  67. if not self.file_manager.domain_exists(domain):
  68. return f"❌ 领域 '{domain}' 不存在。请先使用 /create 创建学习计划。"
  69. # 读取学习计划
  70. try:
  71. plan = self.file_manager.read_plan(domain)
  72. except Exception as e:
  73. return f"❌ 读取学习计划失败:{e}"
  74. # 生成第一个问题
  75. question = self._generate_first_question(plan, mode)
  76. # 保存问题到会话历史(用于后续继续)
  77. self._save_session_start(domain, mode, question)
  78. return f"""💬 {mode.upper()} 模式学习会话开始
  79. {question}
  80. 💡 提示:输入你的回答开始对话
  81. """
  82. def _save_session_start(self, domain: str, mode: str, question: str) -> None:
  83. """
  84. 保存会话开始状态
  85. Args:
  86. domain: 领域名称
  87. mode: 模式
  88. question: 第一个问题
  89. """
  90. session_path = self.file_manager.BASE_DIR / domain / "sessions"
  91. session_path.mkdir(parents=True, exist_ok=True)
  92. # 创建临时会话文件
  93. temp_file = session_path / ".current_session.txt"
  94. temp_file.write_text(
  95. f"{mode}\n{datetime.now().strftime('%Y-%m-%d %H:%M')}\n{question}",
  96. encoding='utf-8'
  97. )
  98. def continue_session(self, domain: str, user_answer: str, mode: str) -> str:
  99. """
  100. 继续对话会话
  101. Args:
  102. domain: 领域名称
  103. user_answer: 用户回答
  104. mode: 模式
  105. Returns:
  106. 反馈和下一个问题
  107. """
  108. try:
  109. # 读取计划
  110. plan = self.file_manager.read_plan(domain)
  111. # 读取上一个问题(从临时文件)
  112. session_path = self.file_manager.BASE_DIR / domain / "sessions"
  113. temp_file = session_path / ".current_session.txt"
  114. if temp_file.exists():
  115. lines = temp_file.read_text(encoding='utf-8').strip().split('\n')
  116. last_question = lines[-1] if len(lines) > 0 else ""
  117. else:
  118. last_question = "请描述你对这个主题的理解。"
  119. # 生成反馈
  120. feedback = self._generate_feedback(last_question, user_answer, plan)
  121. # 生成下一个问题
  122. next_question = self._generate_next_question(plan, [last_question, user_answer], mode)
  123. # 更新临时文件
  124. temp_file.write_text(
  125. f"{mode}\n{datetime.now().strftime('%Y-%m-%d %H:%M')}\n{next_question}",
  126. encoding='utf-8'
  127. )
  128. # 返回反馈和下一个问题
  129. return f"""✅ {feedback}
  130. {next_question}
  131. 💡 输入你的回答,或输入"退出"结束会话
  132. """
  133. except Exception as e:
  134. # 发生错误时保存会话并返回
  135. return f"❌ 处理回答时发生错误:{e}\n\n会话已自动保存。"
  136. def _save_conversation_history(self, domain: str, mode: str, conversation: List[str], error: str = None) -> None:
  137. """
  138. 保存对话历史
  139. Args:
  140. domain: 领域名称
  141. mode: 模式
  142. conversation: 对话记录
  143. error: 错误信息(可选)
  144. """
  145. try:
  146. session_path = self.file_manager.BASE_DIR / domain / "sessions"
  147. timestamp = datetime.now().strftime("%Y-%m-%d %H-%M")
  148. content = f"# 学习会话 - {domain}\n"
  149. content += f"模式: {mode}\n"
  150. content += f"时间: {timestamp}\n\n"
  151. if conversation:
  152. content += "\n".join(conversation)
  153. if error:
  154. content += f"\n\n错误: {error}"
  155. # 保存会话
  156. self.file_manager.save_session(domain, content)
  157. except Exception:
  158. pass # 静默失败,避免干扰主流程
  159. def _generate_first_question(self, plan: str, mode: str) -> str:
  160. """
  161. 生成第一个问题
  162. Args:
  163. plan: 学习计划
  164. mode: 模式(free/quiz)
  165. Returns:
  166. 问题文本
  167. """
  168. if mode == "quiz":
  169. # quiz 模式:使用 QuizGenerator
  170. return self.quiz_generator.generate_question(plan, difficulty="easy")
  171. else:
  172. # free 模式:生成开放性问题
  173. user_prompt = f"""基于以下学习计划,生成一个开放性的问题,开始对话:
  174. {plan[:2000]}
  175. 问题应该:
  176. 1. 从基础概念开始
  177. 2. 引导用户思考和表达
  178. 3. 不要太难,建立信心
  179. 直接返回问题,不需要额外说明。
  180. """
  181. messages = [
  182. {
  183. "role": "system",
  184. "content": "你是一个专业的学习教练,擅长通过提问引导学习。",
  185. },
  186. {"role": "user", "content": user_prompt},
  187. ]
  188. try:
  189. if self.streaming:
  190. from utils.streaming import stream_response
  191. return stream_response(self.llm, messages)
  192. else:
  193. return self.llm.invoke(messages).strip()
  194. except Exception:
  195. return "请简单描述一下你对这个主题的理解,以及你最想学习的部分是什么?"
  196. def _generate_next_question(
  197. self, plan: str, history: List[str], mode: str
  198. ) -> str:
  199. """
  200. 生成下一个问题(根据历史对话调整)
  201. Args:
  202. plan: 学习计划
  203. history: 对话历史
  204. mode: 模式
  205. Returns:
  206. 问题文本
  207. """
  208. # 提取最后一个问题和回答(如果有)
  209. if len(history) < 3:
  210. return self._generate_first_question(plan, mode)
  211. if mode == "quiz":
  212. # quiz 模式:逐步增加难度
  213. difficulty = min(1.0, 0.3 + len(history) * 0.1)
  214. return self.quiz_generator.generate_question(plan, difficulty=difficulty)
  215. else:
  216. # free 模式:基于上下文生成问题
  217. recent_context = "\n".join(history[-5:])
  218. user_prompt = f"""基于以下对话历史,生成下一个问题:
  219. {recent_context}
  220. 要求:
  221. 1. 继续深入探讨
  222. 2. 根据用户之前的回答调整方向
  223. 3. 保持对话流畅性
  224. 直接返回问题,不需要额外说明。
  225. """
  226. messages = [
  227. {
  228. "role": "system",
  229. "content": "你是一个专业的学习教练,擅长通过对话引导深入学习。",
  230. },
  231. {"role": "user", "content": user_prompt},
  232. ]
  233. try:
  234. if self.streaming:
  235. from utils.streaming import stream_response
  236. return stream_response(self.llm, messages)
  237. else:
  238. return self.llm.invoke(messages).strip()
  239. except Exception:
  240. return "请继续分享你的想法,或者有什么具体的问题想讨论吗?"
  241. def _generate_feedback(
  242. self, question: str, answer: str, plan: str
  243. ) -> str:
  244. """
  245. 生成反馈
  246. Args:
  247. question: 问题
  248. answer: 用户回答
  249. plan: 学习计划
  250. Returns:
  251. 反馈文本
  252. """
  253. user_prompt = f"""问题:{question}
  254. 用户回答:{answer}
  255. 参考计划:{plan[:1000]}
  256. 生成友好的反馈(100字以内):
  257. 1. 肯定正确的部分
  258. 2. 指出需要改进的地方(温和地)
  259. 3. 提供一个额外的知识点或建议
  260. """
  261. messages = [
  262. {
  263. "role": "system",
  264. "content": "你是一个友善的学习教练,善于鼓励和引导。",
  265. },
  266. {"role": "user", "content": user_prompt},
  267. ]
  268. try:
  269. if self.streaming:
  270. from utils.streaming import stream_response
  271. return stream_response(self.llm, messages)
  272. else:
  273. return self.llm.invoke(messages).strip()
  274. except Exception:
  275. return "好的,谢谢你的回答。让我们继续深入探讨这个话题。"
  276. def _evaluate_answer(
  277. self, question: str, answer: str, plan: str
  278. ) -> Dict[str, any]:
  279. """
  280. 评估回答质量
  281. Args:
  282. question: 问题
  283. answer: 回答
  284. plan: 学习计划
  285. Returns:
  286. 评估结果(包含 score, mastery_level, suggested_next)
  287. """
  288. user_prompt = f"""评估以下回答的质量(0-1分):
  289. 问题:{question}
  290. 回答:{answer}
  291. 返回 JSON 格式:
  292. {{
  293. "score": 0.8,
  294. "mastery_level": "good/poor/medium",
  295. "suggested_next": "increase/maintain/decrease"
  296. }}
  297. 只返回 JSON,不需要其他内容。
  298. """
  299. messages = [
  300. {
  301. "role": "system",
  302. "content": "你是一个教育评估专家,擅长评估学生的学习质量。",
  303. },
  304. {"role": "user", "content": user_prompt},
  305. ]
  306. try:
  307. response = self.llm.invoke(messages).strip()
  308. # 尝试解析 JSON
  309. # 简化实现:使用规则提取
  310. return self._extract_evaluation(response)
  311. except Exception:
  312. # 降级:返回默认评估
  313. return {
  314. "score": 0.5,
  315. "mastery_level": "medium",
  316. "suggested_next": "maintain",
  317. }
  318. def _extract_evaluation(self, text: str) -> Dict[str, any]:
  319. """
  320. 从文本中提取评估结果(简化版)
  321. Args:
  322. text: LLM 响应文本
  323. Returns:
  324. 评估结果字典
  325. """
  326. # 简化实现:返回默认值
  327. # 在真实场景中,应该使用更健壮的 JSON 解析
  328. try:
  329. # 尝试直接解析
  330. return json.loads(text)
  331. except:
  332. # 失败则返回默认值
  333. return {
  334. "score": 0.5,
  335. "mastery_level": "medium",
  336. "suggested_next": "maintain",
  337. }
  338. def _summarize_session(self, conversation: List[str], domain: str) -> str:
  339. """
  340. 总结会话
  341. Args:
  342. conversation: 对话历史
  343. domain: 领域名称
  344. Returns:
  345. 会话总结
  346. """
  347. content = "\n".join(conversation)
  348. user_prompt = f"""总结以下学习会话(控制在200字以内):
  349. {content[:3000]}
  350. 包括:
  351. 1. 讨论的主题
  352. 2. 用户掌握良好的知识点
  353. 3. 需要复习的内容
  354. 4. 下次学习的建议
  355. 输出格式:
  356. ## 会话总结
  357. **讨论主题:** ...
  358. **掌握情况:**
  359. - ...
  360. **需要复习:**
  361. - ...
  362. **下一步建议:**
  363. - ...
  364. """
  365. messages = [
  366. {
  367. "role": "system",
  368. "content": "你是一个学习总结专家,擅长提炼关键信息。",
  369. },
  370. {"role": "user", "content": user_prompt},
  371. ]
  372. try:
  373. return self.llm.invoke(messages).strip()
  374. except Exception:
  375. return f"## 会话总结\n\n完成了 {domain} 领域的学习会话。\n继续加油!"