04_note_tool_integration.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. """
  2. NoteTool 与 ContextBuilder 集成示例
  3. 展示如何将 NoteTool 与 ContextBuilder 集成,实现:
  4. 1. 长期项目追踪
  5. 2. 笔记检索与上下文注入
  6. 3. 基于历史笔记的连贯建议
  7. """
  8. from dotenv import load_dotenv
  9. load_dotenv()
  10. from hello_agents import SimpleAgent, HelloAgentsLLM
  11. from hello_agents.context import ContextBuilder, ContextConfig, ContextPacket
  12. from hello_agents.tools import MemoryTool, RAGTool, NoteTool
  13. from hello_agents.core.message import Message
  14. from datetime import datetime
  15. from typing import List, Dict
  16. class ProjectAssistant(SimpleAgent):
  17. """长期项目助手,集成 NoteTool 和 ContextBuilder"""
  18. def __init__(self, name: str, project_name: str, **kwargs):
  19. # 配置 LLM
  20. from hello_agents.core.llm import HelloAgentsLLM
  21. llm = HelloAgentsLLM()
  22. super().__init__(name=name, llm=llm, **kwargs)
  23. self.project_name = project_name
  24. # 初始化工具
  25. # self.memory_tool = MemoryTool(user_id=project_name)
  26. # self.rag_tool = RAGTool(knowledge_base_path=f"./{project_name}_kb")
  27. self.note_tool = NoteTool(workspace=f"./{project_name}_notes")
  28. # 初始化上下文构建器
  29. self.context_builder = ContextBuilder(
  30. # memory_tool=self.memory_tool,
  31. # rag_tool=self.rag_tool,
  32. config=ContextConfig(max_tokens=4000)
  33. )
  34. self.conversation_history = []
  35. def run(self, user_input: str, note_as_action: bool = False) -> str:
  36. """运行助手,自动集成笔记"""
  37. # 1. 从 NoteTool 检索相关笔记
  38. relevant_notes = self._retrieve_relevant_notes(user_input)
  39. # 2. 将笔记转换为 ContextPacket
  40. note_packets = self._notes_to_packets(relevant_notes)
  41. # 3. 构建优化的上下文
  42. optimized_context = self.context_builder.build(
  43. user_query=user_input,
  44. conversation_history=self.conversation_history,
  45. system_instructions=self._build_system_instructions(),
  46. additional_packets=note_packets
  47. )
  48. # 4. 调用 LLM (以 messages 数组形式传入)
  49. messages = [
  50. {"role": "system", "content": optimized_context},
  51. {"role": "user", "content": user_input}
  52. ]
  53. response = self.llm.invoke(messages)
  54. # 5. 如果需要,将交互记录为笔记
  55. if note_as_action:
  56. self._save_as_note(user_input, response)
  57. # 6. 更新对话历史
  58. self._update_history(user_input, response)
  59. return response
  60. def _retrieve_relevant_notes(self, query: str, limit: int = 3) -> List[Dict]:
  61. """检索相关笔记"""
  62. try:
  63. # 优先检索 blocker 和 action 类型的笔记
  64. blockers_raw = self.note_tool.run({
  65. "action": "list",
  66. "note_type": "blocker",
  67. "limit": 2
  68. })
  69. # 通用搜索
  70. search_results_raw = self.note_tool.run({
  71. "action": "search",
  72. "query": query,
  73. "limit": limit
  74. })
  75. blockers = self._ensure_list_of_dicts(blockers_raw)
  76. search_results = self._ensure_list_of_dicts(search_results_raw)
  77. # 合并并去重
  78. all_notes = {}
  79. for note in blockers + search_results:
  80. if not isinstance(note, dict):
  81. continue
  82. note_id = (
  83. note.get("note_id")
  84. or note.get("id")
  85. or note.get("uuid")
  86. or note.get("title")
  87. or str(hash(str(note)))
  88. )
  89. all_notes[note_id] = note
  90. return list(all_notes.values())[:limit]
  91. except Exception as e:
  92. print(f"[WARNING] 笔记检索失败: {e}")
  93. return []
  94. def _ensure_list_of_dicts(self, data) -> List[Dict]:
  95. """将 NoteTool 返回规范化为字典列表"""
  96. import json
  97. if data is None:
  98. return []
  99. if isinstance(data, str):
  100. try:
  101. data = json.loads(data)
  102. except Exception:
  103. return []
  104. if isinstance(data, dict):
  105. # 兼容 {"items": [...]} 或单条记录
  106. if "items" in data and isinstance(data["items"], list):
  107. return [item for item in data["items"] if isinstance(item, dict)]
  108. return [data]
  109. if isinstance(data, list):
  110. return [item for item in data if isinstance(item, dict)]
  111. return []
  112. def _notes_to_packets(self, notes: List[Dict]) -> List[ContextPacket]:
  113. """将笔记转换为上下文包"""
  114. packets = []
  115. for note in notes:
  116. title = note.get("title", "")
  117. body = note.get("content", "")
  118. content = f"[笔记:{title}]\n{body}"
  119. # 安全解析时间戳
  120. ts = None
  121. for key in ("updated_at", "updatedAt", "time", "timestamp"):
  122. if key in note:
  123. ts = note.get(key)
  124. break
  125. parsed_ts = None
  126. if isinstance(ts, (int, float)):
  127. try:
  128. parsed_ts = datetime.fromtimestamp(ts)
  129. except Exception:
  130. parsed_ts = None
  131. elif isinstance(ts, str):
  132. try:
  133. parsed_ts = datetime.fromisoformat(ts)
  134. except Exception:
  135. parsed_ts = None
  136. if parsed_ts is None:
  137. parsed_ts = datetime.now()
  138. note_type = note.get("type") or note.get("note_type") or "note"
  139. note_id = (
  140. note.get("note_id")
  141. or note.get("id")
  142. or note.get("uuid")
  143. or title
  144. or str(hash(str(note)))
  145. )
  146. packets.append(ContextPacket(
  147. content=content,
  148. timestamp=parsed_ts,
  149. token_count=len(content) // 4, # 简单估算
  150. relevance_score=0.75, # 笔记具有较高相关性
  151. metadata={
  152. "type": "note",
  153. "note_type": note_type,
  154. "note_id": note_id
  155. }
  156. ))
  157. return packets
  158. def _save_as_note(self, user_input: str, response: str):
  159. """将交互保存为笔记"""
  160. try:
  161. # 判断应该保存为什么类型的笔记
  162. if "问题" in user_input or "阻塞" in user_input:
  163. note_type = "blocker"
  164. elif "计划" in user_input or "下一步" in user_input:
  165. note_type = "action"
  166. else:
  167. note_type = "conclusion"
  168. self.note_tool.run({
  169. "action": "create",
  170. "title": f"{user_input[:30]}...",
  171. "content": f"## 问题\n{user_input}\n\n## 分析\n{response}",
  172. "note_type": note_type,
  173. "tags": [self.project_name, "auto_generated"]
  174. })
  175. except Exception as e:
  176. print(f"[WARNING] 保存笔记失败: {e}")
  177. def _build_system_instructions(self) -> str:
  178. """构建系统指令"""
  179. return f"""你是 {self.project_name} 项目的长期助手。
  180. 你的职责:
  181. 1. 基于历史笔记提供连贯的建议
  182. 2. 追踪项目进展和待解决问题
  183. 3. 在回答时引用相关的历史笔记
  184. 4. 提供具体、可操作的下一步建议
  185. 注意:
  186. - 优先关注标记为 blocker 的问题
  187. - 在建议中说明依据来源(笔记、记忆或知识库)
  188. - 保持对项目整体进度的认识"""
  189. def _update_history(self, user_input: str, response: str):
  190. """更新对话历史"""
  191. self.conversation_history.append(
  192. Message(content=user_input, role="user", timestamp=datetime.now())
  193. )
  194. self.conversation_history.append(
  195. Message(content=response, role="assistant", timestamp=datetime.now())
  196. )
  197. # 限制历史长度
  198. if len(self.conversation_history) > 10:
  199. self.conversation_history = self.conversation_history[-10:]
  200. def main():
  201. print("=" * 80)
  202. print("NoteTool 与 ContextBuilder 集成示例")
  203. print("=" * 80 + "\n")
  204. # 使用示例
  205. assistant = ProjectAssistant(
  206. name="项目助手",
  207. project_name="data_pipeline_refactoring"
  208. )
  209. # 第一次交互:记录项目状态
  210. print("第一次交互:记录项目状态")
  211. response = assistant.run(
  212. "我们已经完成了数据模型层的重构,测试覆盖率达到85%。下一步计划重构业务逻辑层。",
  213. note_as_action=True
  214. )
  215. print(f"助手回答: {response}\n")
  216. # 第二次交互:提出问题
  217. print("第二次交互:提出问题")
  218. response = assistant.run(
  219. "在重构业务逻辑层时,我遇到了依赖版本冲突的问题,该如何解决?"
  220. )
  221. print(f"助手回答: {response}\n")
  222. # 查看笔记摘要
  223. print("查看笔记摘要:")
  224. summary = assistant.note_tool.run({"action": "summary"})
  225. import json
  226. print(json.dumps(summary, indent=2, ensure_ascii=False).replace("\\n", "\n"))
  227. print("\n" + "=" * 80)
  228. print("演示完成!")
  229. print("=" * 80)
  230. if __name__ == "__main__":
  231. main()