| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- """
- 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": []
- }
- }
|