"""GoalPlanner — 目标分析与计划生成 用户给定自然语言目标后,自动生成结构化执行计划,包含任务拆解、 依赖关系、并行度识别。作为 Orchestrator._decompose_task() 的前置增强层。 执行流程: 1. 通过结构化目标分解(规则/模板)生成初始方案 2. 如果初始方案有效则跳过 LLM 调用 3. 否则将初始方案作为上下文注入 LLM prompt,LLM 细化调整 4. 识别能力缺口,请求人工介入 5. 通过 AskHumanTool 请求确认/修改 """ from __future__ import annotations import logging import re import uuid from typing import Any from agentkit.core.plan_schema import ( ExecutionPlan, PlanStep, PlanStepStatus, SkillGap, SkillGapLevel, ) logger = logging.getLogger(__name__) class GoalPlanner: """目标分析与计划生成器 将自然语言目标分解为结构化执行计划,包含任务拆解、 依赖关系和并行度识别。 使用方式: planner = GoalPlanner() plan = await planner.generate_plan( goal="调研 3 个竞品 SEO 策略并生成对比报告", context={}, available_skills=["web_search", "seo_analyzer", "report_generator"], ) """ def __init__(self, llm_gateway: Any = None, max_parallel: int = 5): """ Args: llm_gateway: LLM Gateway,用于细化计划(可选) max_parallel: 最大并行步骤数 """ self._llm_gateway = llm_gateway self._max_parallel = max_parallel async def generate_plan( self, goal: str, context: dict[str, Any] | None = None, available_skills: list[str] | None = None, ) -> ExecutionPlan: """生成结构化执行计划 Args: goal: 自然语言目标 context: 上下文信息(如已有数据、约束条件等) available_skills: 可用 Skill 列表 Returns: ExecutionPlan: 结构化执行计划 """ context = context or {} available_skills = available_skills or [] # 1. 通过规则/模板生成初始方案 plan = self._rule_based_decompose(goal, context, available_skills) # 2. 识别能力缺口 plan.skill_gaps = self._identify_skill_gaps(plan, available_skills) # 3. 如果有 LLM Gateway 且初始方案不够精确,让 LLM 细化 if self._llm_gateway and self._should_refine_with_llm(plan): plan = await self._llm_refine_plan(goal, plan, context, available_skills) # 细化后重新识别能力缺口 plan.skill_gaps = self._identify_skill_gaps(plan, available_skills) # 4. 构建并行组 plan.parallel_groups = self._build_parallel_groups(plan.steps) return plan def _rule_based_decompose( self, goal: str, context: dict[str, Any], available_skills: list[str], ) -> ExecutionPlan: """基于规则/模板的目标分解 使用启发式规则识别目标中的并列结构和顺序依赖, 生成初始执行计划。 """ steps: list[PlanStep] = [] # 识别并列结构:如"3 个竞品"、"3个方案"、"A、B、C" parallel_items = self._extract_parallel_items(goal) if parallel_items and len(parallel_items) > 1: # 有并列结构:每个并列项生成一个并行步骤 + 汇总步骤 steps = self._decompose_parallel_goal(goal, parallel_items, available_skills) else: # 无明显并列结构:尝试识别顺序步骤 sequential_parts = self._extract_sequential_parts(goal) if len(sequential_parts) > 1: steps = self._decompose_sequential_goal(goal, sequential_parts, available_skills) else: # 单步任务 steps = self._decompose_simple_goal(goal, available_skills) return ExecutionPlan( goal=goal, steps=steps, ) def _extract_parallel_items(self, goal: str) -> list[str]: """从目标中提取并列项 识别模式: - "N 个 X":如"3 个竞品"、"5 个方案" - "A、B、C":顿号分隔的并列项 - "A, B, C":逗号分隔的并列项 """ items: list[str] = [] # 模式1:"N 个 X" — 识别数量+类别 count_match = re.search(r"(\d+)\s*个\s*(.+?)(?:的|和|并|以及|,|,|$)", goal) if count_match: count = int(count_match.group(1)) category = count_match.group(2).strip() # 生成 N 个并列项 for i in range(1, count + 1): items.append(f"{category} {i}") return items # 模式2:顿号分隔 — "竞品A、竞品B、竞品C" if "、" in goal: # 提取顿号分隔的片段 parts = re.split(r"[、]", goal) # 过滤掉太短的片段(可能是标点噪声) meaningful = [p.strip() for p in parts if len(p.strip()) > 1] if len(meaningful) >= 2: items = meaningful return items # 模式3:英文逗号分隔 — "A, B, C" if "," in goal: parts = goal.split(",") meaningful = [p.strip() for p in parts if len(p.strip()) > 1] if len(meaningful) >= 2: items = meaningful return items return items def _extract_sequential_parts(self, goal: str) -> list[str]: """从目标中提取顺序步骤 识别模式: - "并":如"调研并生成报告" - "然后"/"接着"/"再":顺序连接词 - "→"/"->":箭头分隔 """ parts: list[str] = [] # 模式1:箭头分隔 if "→" in goal or "->" in goal: separator = "→" if "→" in goal else "->" parts = [p.strip() for p in goal.split(separator) if p.strip()] return parts # 模式2:顺序连接词 sequential_patterns = [ r"(.+?)然后(.+)", r"(.+?)接着(.+)", r"(.+?)之后再(.+)", ] for pattern in sequential_patterns: match = re.search(pattern, goal) if match: parts = [g.strip() for g in match.groups() if g.strip()] return parts # 模式3:"并" 连接 — 如"调研并生成报告" if "并" in goal: match = re.search(r"(.+?)并(.+)", goal) if match: parts = [g.strip() for g in match.groups() if g.strip()] return parts return parts def _decompose_parallel_goal( self, goal: str, parallel_items: list[str], available_skills: list[str], ) -> list[PlanStep]: """分解包含并列结构的目标 生成 N 个并行步骤 + 1 个汇总步骤。 """ steps: list[PlanStep] = [] parallel_step_ids: list[str] = [] # 为每个并列项生成一个并行步骤 for i, item in enumerate(parallel_items): step_id = f"step-{i}" required_skills = self._infer_required_skills(item, available_skills) steps.append(PlanStep( step_id=step_id, name=f"处理: {item}", description=f"对「{item}」执行相关操作", dependencies=[], parallel_group=0, required_skills=required_skills, )) parallel_step_ids.append(step_id) # 汇总步骤:依赖所有并行步骤 summary_skills = self._infer_required_skills("汇总 生成 报告", available_skills) steps.append(PlanStep( step_id=f"step-{len(parallel_items)}", name="汇总结果", description="汇总所有并行步骤的结果,生成最终输出", dependencies=parallel_step_ids, parallel_group=1, required_skills=summary_skills, )) return steps def _decompose_sequential_goal( self, goal: str, sequential_parts: list[str], available_skills: list[str], ) -> list[PlanStep]: """分解包含顺序步骤的目标""" steps: list[PlanStep] = [] for i, part in enumerate(sequential_parts): step_id = f"step-{i}" dependencies = [f"step-{i - 1}"] if i > 0 else [] required_skills = self._infer_required_skills(part, available_skills) steps.append(PlanStep( step_id=step_id, name=part[:50], # 截取前 50 字符作为名称 description=part, dependencies=dependencies, parallel_group=i, required_skills=required_skills, )) return steps def _decompose_simple_goal( self, goal: str, available_skills: list[str], ) -> list[PlanStep]: """分解简单目标为单步计划""" required_skills = self._infer_required_skills(goal, available_skills) return [ PlanStep( step_id="step-0", name=goal[:50], description=goal, dependencies=[], parallel_group=0, required_skills=required_skills, ) ] def _infer_required_skills(self, text: str, available_skills: list[str]) -> list[str]: """根据文本推断所需的 Skill 基于关键词匹配,将文本中的意图映射到可用 Skill。 """ skill_keywords: dict[str, list[str]] = { "web_search": ["搜索", "查询", "查找", "调研", "search", "find", "lookup"], "seo_analyzer": ["seo", "搜索引擎优化", "关键词", "排名"], "report_generator": ["报告", "汇总", "总结", "生成", "对比", "report", "summary"], "data_analyzer": ["分析", "统计", "数据", "analyze", "data"], "document_writer": ["写", "撰写", "文档", "write", "document"], "code_generator": ["代码", "编程", "开发", "code", "develop"], } text_lower = text.lower() matched: list[str] = [] for skill, keywords in skill_keywords.items(): if skill not in available_skills: continue if any(kw in text_lower for kw in keywords): matched.append(skill) return matched def _identify_skill_gaps( self, plan: ExecutionPlan, available_skills: list[str] ) -> list[SkillGap]: """识别能力缺口 检查每个步骤所需的 Skill 是否可用,标注缺口。 """ gaps: list[SkillGap] = [] available_set = set(available_skills) for step in plan.steps: for skill in step.required_skills: if skill not in available_set: gaps.append(SkillGap( step_name=step.name, required_skill=skill, level=SkillGapLevel.HIGH, suggestion=f"请安装或注册 '{skill}' Skill,或手动完成该步骤", )) # 如果步骤没有匹配到任何 Skill,标注缺口 if not step.required_skills: if not available_skills: # 无可用 Skill 时标注为 HIGH gaps.append(SkillGap( step_name=step.name, required_skill="(无可用 Skill)", level=SkillGapLevel.HIGH, suggestion="当前无可用 Skill,请注册所需 Skill 或手动完成该步骤", )) else: # 有 Skill 但未匹配到时标注为 MEDIUM gaps.append(SkillGap( step_name=step.name, required_skill="(未匹配)", level=SkillGapLevel.MEDIUM, suggestion=f"无法自动匹配 Skill,可用 Skill: {', '.join(available_skills[:5])}", )) return gaps def _should_refine_with_llm(self, plan: ExecutionPlan) -> bool: """判断是否需要 LLM 细化 当初始方案步骤描述过于简单、能力缺口较多、或所有步骤 都没有匹配到 Skill 时,需要 LLM 细化。 """ # 如果所有步骤都没有匹配到任何 Skill,让 LLM 重新评估 if plan.steps and all(not s.required_skills for s in plan.steps): return True # 如果有较多能力缺口,让 LLM 重新评估 if len(plan.skill_gaps) > len(plan.steps): return True return False async def _llm_refine_plan( self, goal: str, initial_plan: ExecutionPlan, context: dict[str, Any], available_skills: list[str], ) -> ExecutionPlan: """使用 LLM 细化执行计划 将初始方案作为上下文注入 LLM prompt,让 LLM 细化调整。 """ import json # 构建初始方案摘要 initial_summary = json.dumps( [s.to_dict() for s in initial_plan.steps], ensure_ascii=False, indent=2, ) skills_str = ", ".join(available_skills) if available_skills else "无" prompt = ( f"Refine the following execution plan for the given goal.\n\n" f"Goal: {goal}\n\n" f"Initial Plan (generated by rules):\n{initial_summary}\n\n" f"Available Skills: {skills_str}\n\n" f"Context: {json.dumps(context, ensure_ascii=False) if context else 'None'}\n\n" 'Respond ONLY with a JSON array of steps: ' '[{"name": "...", "description": "...", "dependencies": [], ' '"required_skills": [...]}]\n' "The dependencies field lists step indices (0-based) that must complete first.\n" "Each step should have a clear, specific description (at least 20 characters).\n" "Do not include any other text." ) try: response = await self._llm_gateway.chat( messages=[{"role": "user", "content": prompt}], model="default", ) step_defs = json.loads(response.content) if not isinstance(step_defs, list) or not step_defs: return initial_plan steps: list[PlanStep] = [] for i, defn in enumerate(step_defs): depends_on = [f"step-{j}" for j in defn.get("dependencies", [])] steps.append(PlanStep( step_id=f"step-{i}", name=defn.get("name", f"Step {i}"), description=defn.get("description", ""), dependencies=depends_on, parallel_group=0, # 后续由 _build_parallel_groups 重新计算 required_skills=defn.get("required_skills", []), )) return ExecutionPlan( goal=goal, steps=steps, metadata={"refined_by_llm": True}, ) except Exception as e: logger.warning(f"LLM plan refinement failed, using initial plan: {e}") return initial_plan def _build_parallel_groups(self, steps: list[PlanStep]) -> list[list[str]]: """构建并行执行组 基于依赖关系拓扑排序,无依赖的步骤分到同一组并行执行。 复用 Orchestrator._build_parallel_groups() 的拓扑排序逻辑。 """ step_map = {s.step_id: s for s in steps} completed: set[str] = set() groups: list[list[str]] = [] remaining = set(s.step_id for s in steps) while remaining: # 找到所有依赖已满足的步骤 ready = [] for sid in remaining: step = step_map[sid] if all(dep in completed for dep in step.dependencies): ready.append(sid) if not ready: # 循环依赖 — 将剩余步骤放入一组 groups.append(list(remaining)) break # 限制组大小 group = ready[: self._max_parallel] groups.append(group) for sid in group: completed.add(sid) remaining.discard(sid) # 更新步骤的 parallel_group 字段 for group_idx, group in enumerate(groups): for sid in group: step = step_map.get(sid) if step: step.parallel_group = group_idx return groups def update_plan_from_feedback( self, plan: ExecutionPlan, modifications: dict[str, Any], ) -> ExecutionPlan: """根据用户反馈更新计划 Args: plan: 原始执行计划 modifications: 修改内容,可包含: - add_steps: 新增步骤列表 - remove_steps: 要移除的步骤 ID 列表 - update_steps: 要更新的步骤 {step_id: {field: value}} - reorder: 是否重新排序 Returns: 更新后的 ExecutionPlan """ steps = list(plan.steps) # 移除步骤 remove_ids = set(modifications.get("remove_steps", [])) if remove_ids: steps = [s for s in steps if s.step_id not in remove_ids] # 清理依赖引用 for step in steps: step.dependencies = [d for d in step.dependencies if d not in remove_ids] # 更新步骤 update_map: dict[str, dict] = modifications.get("update_steps", {}) for step in steps: if step.step_id in update_map: updates = update_map[step.step_id] for field_name, value in updates.items(): if hasattr(step, field_name): setattr(step, field_name, value) # 新增步骤 add_steps = modifications.get("add_steps", []) for new_step_def in add_steps: step_id = new_step_def.get("step_id", f"step-{len(steps)}") # 确保唯一性 existing_ids = {s.step_id for s in steps} while step_id in existing_ids: step_id = f"step-{uuid.uuid4().hex[:4]}" steps.append(PlanStep( step_id=step_id, name=new_step_def.get("name", "New Step"), description=new_step_def.get("description", ""), dependencies=new_step_def.get("dependencies", []), required_skills=new_step_def.get("required_skills", []), )) # 重新构建并行组 parallel_groups = self._build_parallel_groups(steps) return ExecutionPlan( plan_id=plan.plan_id, goal=plan.goal, steps=steps, parallel_groups=parallel_groups, skill_gaps=plan.skill_gaps, # 保留原有缺口信息 confirmed=False, # 修改后需要重新确认 metadata=plan.metadata, ) def validate_plan(self, plan: ExecutionPlan) -> list[str]: """验证执行计划的合法性 Returns: 错误信息列表,空列表表示验证通过 """ errors: list[str] = [] step_ids = {s.step_id for s in plan.steps} # 检查依赖引用是否存在 for step in plan.steps: for dep in step.dependencies: if dep not in step_ids: errors.append(f"步骤 '{step.step_id}' 依赖不存在的步骤 '{dep}'") # 检查循环依赖 visited: set[str] = set() in_stack: set[str] = set() def has_cycle(sid: str) -> bool: if sid in in_stack: return True if sid in visited: return False visited.add(sid) in_stack.add(sid) step = plan.get_step(sid) if step: for dep in step.dependencies: if has_cycle(dep): return True in_stack.discard(sid) return False for step in plan.steps: if has_cycle(step.step_id): errors.append(f"检测到循环依赖,涉及步骤 '{step.step_id}'") break # 检查并行组与步骤的一致性 grouped_ids: set[str] = set() for group in plan.parallel_groups: for sid in group: if sid not in step_ids: errors.append(f"并行组包含不存在的步骤 '{sid}'") if sid in grouped_ids: errors.append(f"步骤 '{sid}' 出现在多个并行组中") grouped_ids.add(sid) ungrouped = step_ids - grouped_ids if ungrouped: errors.append(f"步骤未分配到并行组: {', '.join(ungrouped)}") return errors