""" Goal 工具 - 计划管理 提供 goal 工具供 LLM 管理执行计划。 """ import logging from typing import Optional, List, TYPE_CHECKING from agent.tools import tool if TYPE_CHECKING: from .goal_models import GoalTree, Goal from .protocols import TraceStore logger = logging.getLogger(__name__) # ===== 知识注入 ===== async def inject_knowledge_for_goal( goal: "Goal", tree: "GoalTree", store: Optional["TraceStore"] = None, trace_id: Optional[str] = None, knowledge_config: Optional[dict] = None, ) -> Optional[str]: """ 为指定 goal 注入相关知识。 Args: goal: 目标对象 tree: GoalTree store: TraceStore(用于持久化) trace_id: Trace ID knowledge_config: 知识管理配置(KnowledgeConfig 对象) Returns: 注入结果描述(如 "📚 已注入 3 条相关知识"),无结果返回 None """ # 检查是否启用知识注入 if knowledge_config and not getattr(knowledge_config, 'enable_injection', True): logger.debug(f"[Knowledge Inject] 知识注入已禁用,跳过") return None try: from agent.tools.builtin.knowledge import knowledge_search logger.info(f"[Knowledge Inject] goal: {goal.id}, query: {goal.description[:80]}") # 从配置中获取搜索参数 search_types = None search_owner = None if knowledge_config: search_types = getattr(knowledge_config, 'default_search_types', None) search_owner = getattr(knowledge_config, 'default_search_owner', None) or None knowledge_result = await knowledge_search( query=goal.description, top_k=3, min_score=3, types=search_types, owner=search_owner, context=None ) logger.debug(f"[Knowledge Inject] result type: {type(knowledge_result)}, metadata: {getattr(knowledge_result, 'metadata', None)}") if knowledge_result.metadata and knowledge_result.metadata.get("items"): goal.knowledge = knowledge_result.metadata["items"] knowledge_count = len(goal.knowledge) logger.info(f"[Knowledge Inject] 注入 {knowledge_count} 条知识到 goal {goal.id}") if store and trace_id: await store.update_goal_tree(trace_id, tree) return f"📚 已注入 {knowledge_count} 条相关知识" else: goal.knowledge = [] logger.info(f"[Knowledge Inject] 未找到相关知识") return None except Exception as e: logger.warning(f"[Knowledge Inject] 知识注入失败: {e}") goal.knowledge = [] return None # ===== LLM 可调用的 goal 工具 ===== @tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"]) async def goal( add: Optional[str] = None, reason: Optional[str] = None, after: Optional[str] = None, under: Optional[str] = None, done: Optional[str] = None, abandon: Optional[str] = None, focus: Optional[str] = None, context: Optional[dict] = None ) -> str: """ 管理执行计划,添加/完成/放弃目标,切换焦点。 Args: add: 添加目标(逗号分隔多个) reason: 创建理由(逗号分隔多个,与 add 一一对应) after: 在指定目标后面添加(同层级) under: 为指定目标添加子目标 done: 完成当前目标,值为 summary abandon: 放弃当前目标,值为原因 focus: 切换焦点到指定 ID context: 工具执行上下文(包含 store、trace_id、goal_tree) Returns: str: 更新后的计划状态文本 """ # GoalTree 从 context 获取,每个 agent 实例独立,不再依赖全局变量 tree = context.get("goal_tree") if context else None if tree is None: return "错误:GoalTree 未初始化" # 从 context 获取 store、trace_id 和 knowledge_config store = context.get("store") if context else None trace_id = context.get("trace_id") if context else None knowledge_config = context.get("knowledge_config") if context else None return await goal_tool( tree=tree, store=store, trace_id=trace_id, add=add, reason=reason, after=after, under=under, done=done, abandon=abandon, focus=focus, knowledge_config=knowledge_config ) # ===== 核心逻辑函数 ===== async def goal_tool( tree: "GoalTree", store: Optional["TraceStore"] = None, trace_id: Optional[str] = None, add: Optional[str] = None, reason: Optional[str] = None, after: Optional[str] = None, under: Optional[str] = None, done: Optional[str] = None, abandon: Optional[str] = None, focus: Optional[str] = None, knowledge_config: Optional[object] = None, ) -> str: """ 管理执行计划。 Args: tree: GoalTree 实例 store: TraceStore 实例(用于推送事件) trace_id: 当前 Trace ID add: 添加目标(逗号分隔多个) reason: 创建理由(逗号分隔多个,与 add 一一对应) after: 在指定目标后面添加(同层级) under: 为指定目标添加子目标 done: 完成当前目标,值为 summary abandon: 放弃当前目标,值为原因 focus: 切换焦点到指定 ID knowledge_config: 知识管理配置(KnowledgeConfig 对象) Returns: 更新后的计划状态文本 """ changes = [] # 1. 处理 done(完成当前目标) if done is not None: if not tree.current_id: return f"错误:没有当前目标可以完成。当前焦点为空,请先使用 focus 参数切换到要完成的目标。\n\n当前计划:\n{tree.to_prompt()}" # 完成当前目标 # 如果同时指定了 focus,则不清空焦点(后面会切换到新目标) # 如果只有 done,则清空焦点 clear_focus = (focus is None) goal = tree.complete(tree.current_id, done, clear_focus=clear_focus) display_id = tree._generate_display_id(goal) changes.append(f"已完成: {display_id}. {goal.description}") # 推送事件 if store and trace_id: await store.update_goal(trace_id, goal.id, status="completed", summary=done) # 检查是否有级联完成的父目标(complete方法已经处理,这里只需要记录) if goal.parent_id: parent = tree.find(goal.parent_id) if parent and parent.status == "completed": parent_display_id = tree._generate_display_id(parent) changes.append(f"自动完成: {parent_display_id}. {parent.description}(所有子目标已完成)") # 2. 处理 focus(切换焦点到新目标) if focus is not None: goal = tree.find_by_display_id(focus) if not goal: return f"错误:找不到目标 {focus}\n\n当前计划:\n{tree.to_prompt()}" tree.focus(goal.id) display_id = tree._generate_display_id(goal) changes.append(f"切换焦点: {display_id}. {goal.description}") # 自动注入知识 inject_msg = await inject_knowledge_for_goal(goal, tree, store, trace_id, knowledge_config) if inject_msg: changes.append(inject_msg) # 3. 处理 abandon(放弃当前目标) if abandon is not None: if not tree.current_id: return f"错误:没有当前目标可以放弃。当前焦点为空。\n\n当前计划:\n{tree.to_prompt()}" goal = tree.abandon(tree.current_id, abandon) display_id = tree._generate_display_id(goal) changes.append(f"已放弃: {display_id}. {goal.description}") # 推送事件 if store and trace_id: await store.update_goal(trace_id, goal.id, status="abandoned", summary=abandon) # 4. 处理 add if add is not None: # 检查 after 和 under 互斥 if after is not None and under is not None: return "错误:after 和 under 参数不能同时指定" descriptions = [d.strip() for d in add.split(",") if d.strip()] if descriptions: # 解析 reasons(与 descriptions 一一对应) reasons = None if reason: reasons = [r.strip() for r in reason.split(",")] # 如果 reasons 数量少于 descriptions,补空字符串 while len(reasons) < len(descriptions): reasons.append("") # 确定添加位置 if after is not None: # 在指定 goal 后面添加(同层级) target_goal = tree.find_by_display_id(after) if not target_goal: return f"错误:找不到目标 {after}\n\n当前计划:\n{tree.to_prompt()}" new_goals = tree.add_goals_after(target_goal.id, descriptions, reasons=reasons) changes.append(f"在 {tree._generate_display_id(target_goal)} 后面添加 {len(new_goals)} 个同级目标") elif under is not None: # 为指定 goal 添加子目标 parent_goal = tree.find_by_display_id(under) if not parent_goal: return f"错误:找不到目标 {under}\n\n当前计划:\n{tree.to_prompt()}" new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_goal.id) changes.append(f"在 {tree._generate_display_id(parent_goal)} 下添加 {len(new_goals)} 个子目标") else: # 默认行为:添加到当前焦点下(如果有焦点),否则添加到顶层 parent_id = tree.current_id new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_id) if parent_id: parent_display_id = tree._generate_display_id(tree.find(parent_id)) changes.append(f"在 {parent_display_id} 下添加 {len(new_goals)} 个子目标") else: changes.append(f"添加 {len(new_goals)} 个顶层目标") # 推送事件 if store and trace_id: for goal in new_goals: await store.add_goal(trace_id, goal) # 将完整内存树状态(含 current_id)同步到存储, # 因为 store.add_goal / update_goal 各自从磁盘加载,不包含 focus 等内存变更 if store and trace_id and changes: await store.update_goal_tree(trace_id, tree) # 返回当前状态 result = [] if changes: result.append("## 更新") result.extend(f"- {c}" for c in changes) result.append("") result.append("## Current Plan") result.append(tree.to_prompt()) return "\n".join(result) def create_goal_tool_schema() -> dict: """创建 goal 工具的 JSON Schema""" return { "name": "goal", "description": """管理执行计划。目标工具是灵活的支持系统,帮助你组织和追踪工作进度。 使用策略(按需选择): - 全局规划:先规划所有目标,再逐个执行 - 渐进规划:走一步看一步,每次只创建下一个目标 - 动态调整:行动中随时 abandon 不可行的目标,创建新目标 参数: - add: 添加目标(逗号分隔多个) - reason: 创建理由(逗号分隔,与 add 一一对应) - after: 在指定目标后面添加同级目标。使用目标 ID。 - under: 为指定目标添加子目标。使用目标 ID。如已有子目标,追加到最后。 - done: 完成当前目标,值为 summary(记录关键结论) - abandon: 放弃当前目标,值为原因 - focus: 切换焦点到指定目标。使用目标 ID。 位置控制(优先使用 after): - 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层) - after="X": 在目标 X 后面添加兄弟节点(同层级) - under="X": 为目标 X 添加子目标 - after 和 under 不能同时指定 执行顺序: - done → focus → abandon → add - 如果同时指定 done 和 focus,会先完成当前目标,再切换焦点到新目标 示例: - goal(add="分析代码, 实现功能, 测试") - 添加顶层目标 - goal(add="设计接口, 实现代码", under="2") - 为目标2添加子目标 - goal(add="编写文档", after="3") - 在目标3后面添加同级任务 - goal(add="集成测试", after="2.2") - 在目标2.2后面添加同级任务 - goal(done="发现用户模型在 models/user.py") - 完成当前目标 - goal(done="已完成调研", focus="2") - 完成当前目标,切换到目标2 - goal(abandon="方案A需要Redis,环境没有") - 放弃当前目标 注意: - 目标 ID 的格式为 "1", "2", "2.1", "2.2" 等,在计划视图中可以看到 - reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 reason 将为空 """, "parameters": { "type": "object", "properties": { "add": { "type": "string", "description": "添加目标(逗号分隔多个)" }, "reason": { "type": "string", "description": "创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。" }, "after": { "type": "string", "description": "在指定目标后面添加(同层级)。使用目标的 ID,如 \"2\" 或 \"2.1\"。" }, "under": { "type": "string", "description": "为指定目标添加子目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。" }, "done": { "type": "string", "description": "完成当前目标,值为 summary" }, "abandon": { "type": "string", "description": "放弃当前目标,值为原因" }, "focus": { "type": "string", "description": "切换焦点到指定目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。" } }, "required": [] } }