goal_tool.py 15 KB

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