goal_tool.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. """
  2. Goal 工具 - 计划管理
  3. 提供 goal 工具供 LLM 管理执行计划。
  4. """
  5. from typing import Optional, List, TYPE_CHECKING
  6. from agent.tools import tool
  7. if TYPE_CHECKING:
  8. from .goal_models import GoalTree
  9. from .protocols import TraceStore
  10. # ===== LLM 可调用的 goal 工具 =====
  11. @tool(description="管理执行计划,添加/完成/放弃目标,切换焦点")
  12. async def goal(
  13. add: Optional[str] = None,
  14. reason: Optional[str] = None,
  15. after: Optional[str] = None,
  16. under: Optional[str] = None,
  17. done: Optional[str] = None,
  18. abandon: Optional[str] = None,
  19. focus: Optional[str] = None,
  20. context: Optional[dict] = None
  21. ) -> str:
  22. """
  23. 管理执行计划,添加/完成/放弃目标,切换焦点。
  24. Args:
  25. add: 添加目标(逗号分隔多个)
  26. reason: 创建理由(逗号分隔多个,与 add 一一对应)
  27. after: 在指定目标后面添加(同层级)
  28. under: 为指定目标添加子目标
  29. done: 完成当前目标,值为 summary
  30. abandon: 放弃当前目标,值为原因
  31. focus: 切换焦点到指定 ID
  32. context: 工具执行上下文(包含 store、trace_id、goal_tree)
  33. Returns:
  34. str: 更新后的计划状态文本
  35. """
  36. # GoalTree 从 context 获取,每个 agent 实例独立,不再依赖全局变量
  37. tree = context.get("goal_tree") if context else None
  38. if tree is None:
  39. return "错误:GoalTree 未初始化"
  40. # 从 context 获取 store 和 trace_id
  41. store = context.get("store") if context else None
  42. trace_id = context.get("trace_id") if context else None
  43. return await goal_tool(
  44. tree=tree,
  45. store=store,
  46. trace_id=trace_id,
  47. add=add,
  48. reason=reason,
  49. after=after,
  50. under=under,
  51. done=done,
  52. abandon=abandon,
  53. focus=focus
  54. )
  55. # ===== 核心逻辑函数 =====
  56. async def goal_tool(
  57. tree: "GoalTree",
  58. store: Optional["TraceStore"] = None,
  59. trace_id: Optional[str] = None,
  60. add: Optional[str] = None,
  61. reason: Optional[str] = None,
  62. after: Optional[str] = None,
  63. under: Optional[str] = None,
  64. done: Optional[str] = None,
  65. abandon: Optional[str] = None,
  66. focus: Optional[str] = None,
  67. ) -> str:
  68. """
  69. 管理执行计划。
  70. Args:
  71. tree: GoalTree 实例
  72. store: TraceStore 实例(用于推送事件)
  73. trace_id: 当前 Trace ID
  74. add: 添加目标(逗号分隔多个)
  75. reason: 创建理由(逗号分隔多个,与 add 一一对应)
  76. after: 在指定目标后面添加(同层级)
  77. under: 为指定目标添加子目标
  78. done: 完成当前目标,值为 summary
  79. abandon: 放弃当前目标,值为原因
  80. focus: 切换焦点到指定 ID
  81. Returns:
  82. 更新后的计划状态文本
  83. """
  84. changes = []
  85. # 1. 处理 done(完成当前目标)
  86. if done is not None:
  87. if not tree.current_id:
  88. return f"错误:没有当前目标可以完成。当前焦点为空,请先使用 focus 参数切换到要完成的目标。\n\n当前计划:\n{tree.to_prompt()}"
  89. # 完成当前目标
  90. # 如果同时指定了 focus,则不清空焦点(后面会切换到新目标)
  91. # 如果只有 done,则清空焦点
  92. clear_focus = (focus is None)
  93. goal = tree.complete(tree.current_id, done, clear_focus=clear_focus)
  94. display_id = tree._generate_display_id(goal)
  95. changes.append(f"已完成: {display_id}. {goal.description}")
  96. # 推送事件
  97. if store and trace_id:
  98. await store.update_goal(trace_id, goal.id, status="completed", summary=done)
  99. # 检查是否有级联完成的父目标(complete方法已经处理,这里只需要记录)
  100. if goal.parent_id:
  101. parent = tree.find(goal.parent_id)
  102. if parent and parent.status == "completed":
  103. parent_display_id = tree._generate_display_id(parent)
  104. changes.append(f"自动完成: {parent_display_id}. {parent.description}(所有子目标已完成)")
  105. # 2. 处理 focus(切换焦点到新目标)
  106. if focus is not None:
  107. goal = tree.find_by_display_id(focus)
  108. if not goal:
  109. return f"错误:找不到目标 {focus}\n\n当前计划:\n{tree.to_prompt()}"
  110. tree.focus(goal.id)
  111. display_id = tree._generate_display_id(goal)
  112. changes.append(f"切换焦点: {display_id}. {goal.description}")
  113. # 自动注入知识
  114. try:
  115. from agent.tools.builtin.knowledge import knowledge_search
  116. knowledge_result = await knowledge_search(
  117. query=goal.description,
  118. top_k=3,
  119. min_score=3,
  120. context=None
  121. )
  122. # 将知识保存到 goal 对象
  123. if knowledge_result.metadata and knowledge_result.metadata.get("items"):
  124. goal.knowledge = knowledge_result.metadata["items"]
  125. knowledge_count = len(goal.knowledge)
  126. changes.append(f"📚 已注入 {knowledge_count} 条相关知识")
  127. # 持久化到 store
  128. if store and trace_id:
  129. await store.update_goal_tree(trace_id, tree)
  130. else:
  131. goal.knowledge = []
  132. except Exception as e:
  133. # 知识注入失败不影响 focus 操作
  134. import logging
  135. logging.getLogger(__name__).warning(f"知识注入失败: {e}")
  136. goal.knowledge = []
  137. # 3. 处理 abandon(放弃当前目标)
  138. if abandon is not None:
  139. if not tree.current_id:
  140. return f"错误:没有当前目标可以放弃。当前焦点为空。\n\n当前计划:\n{tree.to_prompt()}"
  141. goal = tree.abandon(tree.current_id, abandon)
  142. display_id = tree._generate_display_id(goal)
  143. changes.append(f"已放弃: {display_id}. {goal.description}")
  144. # 推送事件
  145. if store and trace_id:
  146. await store.update_goal(trace_id, goal.id, status="abandoned", summary=abandon)
  147. # 4. 处理 add
  148. if add is not None:
  149. # 检查 after 和 under 互斥
  150. if after is not None and under is not None:
  151. return "错误:after 和 under 参数不能同时指定"
  152. descriptions = [d.strip() for d in add.split(",") if d.strip()]
  153. if descriptions:
  154. # 解析 reasons(与 descriptions 一一对应)
  155. reasons = None
  156. if reason:
  157. reasons = [r.strip() for r in reason.split(",")]
  158. # 如果 reasons 数量少于 descriptions,补空字符串
  159. while len(reasons) < len(descriptions):
  160. reasons.append("")
  161. # 确定添加位置
  162. if after is not None:
  163. # 在指定 goal 后面添加(同层级)
  164. target_goal = tree.find_by_display_id(after)
  165. if not target_goal:
  166. return f"错误:找不到目标 {after}\n\n当前计划:\n{tree.to_prompt()}"
  167. new_goals = tree.add_goals_after(target_goal.id, descriptions, reasons=reasons)
  168. changes.append(f"在 {tree._generate_display_id(target_goal)} 后面添加 {len(new_goals)} 个同级目标")
  169. elif under is not None:
  170. # 为指定 goal 添加子目标
  171. parent_goal = tree.find_by_display_id(under)
  172. if not parent_goal:
  173. return f"错误:找不到目标 {under}\n\n当前计划:\n{tree.to_prompt()}"
  174. new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_goal.id)
  175. changes.append(f"在 {tree._generate_display_id(parent_goal)} 下添加 {len(new_goals)} 个子目标")
  176. else:
  177. # 默认行为:添加到当前焦点下(如果有焦点),否则添加到顶层
  178. parent_id = tree.current_id
  179. new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_id)
  180. if parent_id:
  181. parent_display_id = tree._generate_display_id(tree.find(parent_id))
  182. changes.append(f"在 {parent_display_id} 下添加 {len(new_goals)} 个子目标")
  183. else:
  184. changes.append(f"添加 {len(new_goals)} 个顶层目标")
  185. # 推送事件
  186. if store and trace_id:
  187. for goal in new_goals:
  188. await store.add_goal(trace_id, goal)
  189. # 如果没有焦点且添加了目标,自动 focus 到第一个新目标
  190. if not tree.current_id and new_goals:
  191. tree.focus(new_goals[0].id)
  192. display_id = tree._generate_display_id(new_goals[0])
  193. changes.append(f"自动切换焦点: {display_id}")
  194. # 将完整内存树状态(含 current_id)同步到存储,
  195. # 因为 store.add_goal / update_goal 各自从磁盘加载,不包含 focus 等内存变更
  196. if store and trace_id and changes:
  197. await store.update_goal_tree(trace_id, tree)
  198. # 返回当前状态
  199. result = []
  200. if changes:
  201. result.append("## 更新")
  202. result.extend(f"- {c}" for c in changes)
  203. result.append("")
  204. result.append("## Current Plan")
  205. result.append(tree.to_prompt())
  206. return "\n".join(result)
  207. def create_goal_tool_schema() -> dict:
  208. """创建 goal 工具的 JSON Schema"""
  209. return {
  210. "name": "goal",
  211. "description": """管理执行计划。目标工具是灵活的支持系统,帮助你组织和追踪工作进度。
  212. 使用策略(按需选择):
  213. - 全局规划:先规划所有目标,再逐个执行
  214. - 渐进规划:走一步看一步,每次只创建下一个目标
  215. - 动态调整:行动中随时 abandon 不可行的目标,创建新目标
  216. 参数:
  217. - add: 添加目标(逗号分隔多个)
  218. - reason: 创建理由(逗号分隔,与 add 一一对应)
  219. - after: 在指定目标后面添加同级目标。使用目标 ID。
  220. - under: 为指定目标添加子目标。使用目标 ID。如已有子目标,追加到最后。
  221. - done: 完成当前目标,值为 summary(记录关键结论)
  222. - abandon: 放弃当前目标,值为原因
  223. - focus: 切换焦点到指定目标。使用目标 ID。
  224. 位置控制(优先使用 after):
  225. - 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层)
  226. - after="X": 在目标 X 后面添加兄弟节点(同层级)
  227. - under="X": 为目标 X 添加子目标
  228. - after 和 under 不能同时指定
  229. 执行顺序:
  230. - done → focus → abandon → add
  231. - 如果同时指定 done 和 focus,会先完成当前目标,再切换焦点到新目标
  232. 示例:
  233. - goal(add="分析代码, 实现功能, 测试") - 添加顶层目标
  234. - goal(add="设计接口, 实现代码", under="2") - 为目标2添加子目标
  235. - goal(add="编写文档", after="3") - 在目标3后面添加同级任务
  236. - goal(add="集成测试", after="2.2") - 在目标2.2后面添加同级任务
  237. - goal(done="发现用户模型在 models/user.py") - 完成当前目标
  238. - goal(done="已完成调研", focus="2") - 完成当前目标,切换到目标2
  239. - goal(abandon="方案A需要Redis,环境没有") - 放弃当前目标
  240. 注意:
  241. - 目标 ID 的格式为 "1", "2", "2.1", "2.2" 等,在计划视图中可以看到
  242. - reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 reason 将为空
  243. """,
  244. "parameters": {
  245. "type": "object",
  246. "properties": {
  247. "add": {
  248. "type": "string",
  249. "description": "添加目标(逗号分隔多个)"
  250. },
  251. "reason": {
  252. "type": "string",
  253. "description": "创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。"
  254. },
  255. "after": {
  256. "type": "string",
  257. "description": "在指定目标后面添加(同层级)。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  258. },
  259. "under": {
  260. "type": "string",
  261. "description": "为指定目标添加子目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  262. },
  263. "done": {
  264. "type": "string",
  265. "description": "完成当前目标,值为 summary"
  266. },
  267. "abandon": {
  268. "type": "string",
  269. "description": "放弃当前目标,值为原因"
  270. },
  271. "focus": {
  272. "type": "string",
  273. "description": "切换焦点到指定目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  274. }
  275. },
  276. "required": []
  277. }
  278. }