Coverage for agentos/orchestration/task_decomposer.py: 28%
280 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 21:26 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 21:26 +0800
1"""
2AgentOS v1.14.7 — Intelligent Task Decomposer 2.0.
4Replaces the simplistic 234-line single-prompt decomposer with:
5- DAG cycle detection (Kahn's + DFS fallback)
6- Dynamic re-planning on partial failure
7- Task dependency validation
8- Parallelism detection (independent sub-tasks)
9- Confidence scoring per sub-task
10- Observable decomposition trace
12Architecture:
13 TaskInput → Decomposer.decompose() → TaskDAG
14 Partial failure → Decomposer.replan(failed_node) → new_path
15"""
17from __future__ import annotations
19import json
20import logging
21import uuid
22from collections import deque
23from collections.abc import Callable
24from dataclasses import dataclass, field
25from enum import StrEnum
26from typing import Any
28logger = logging.getLogger(__name__)
31# ── Types ────────────────────────────────────
34class TaskNodeStatus(StrEnum):
35 PENDING = "pending"
36 RUNNING = "running"
37 COMPLETED = "completed"
38 FAILED = "failed"
39 SKIPPED = "skipped"
42class DecompositionStrategy(StrEnum):
43 """分解策略。"""
45 TOP_DOWN = "top_down" # 从目标逐层拆解
46 BOTTOM_UP = "bottom_up" # 从子任务聚合
47 RECURSIVE = "recursive" # 递归分解直到原子任务
48 HEURISTIC = "heuristic" # 基于规则/模式匹配
51@dataclass
52class TaskEdge:
53 """DAG 边:from_node 完成后才能执行 to_node。"""
55 from_node: str
56 to_node: str
57 dependency_type: str = "hard" # hard / soft
60@dataclass
61class TaskNode:
62 """DAG 节点:单个可执行单元。"""
64 id: str = field(default_factory=lambda: f"tn-{uuid.uuid4().hex[:8]}")
65 description: str = ""
66 input_schema: dict = field(default_factory=dict)
67 output_schema: dict = field(default_factory=dict)
68 agent_type: str = "default" # 推荐执行 Agent 类型
69 estimated_duration_s: float = 0.0
70 confidence: float = 1.0 # 0~1,分解置信度
71 retry_policy: str = "once" # once / retry_n / fallback
72 max_retries: int = 1
73 status: TaskNodeStatus = TaskNodeStatus.PENDING
74 result: Any = None
75 error: str = ""
78@dataclass
79class TaskDAG:
80 """完整的任务 DAG。"""
82 dag_id: str = field(default_factory=lambda: f"dag-{uuid.uuid4().hex[:8]}")
83 root_task: str = "" # 原始任务描述
84 nodes: dict[str, TaskNode] = field(default_factory=dict)
85 edges: list[TaskEdge] = field(default_factory=list)
86 strategy: DecompositionStrategy = DecompositionStrategy.TOP_DOWN
87 metadata: dict[str, Any] = field(default_factory=dict)
88 created_at: float = 0.0
90 def in_degree_map(self) -> dict[str, int]:
91 """计算每个节点的入度。"""
92 indeg: dict[str, int] = {nid: 0 for nid in self.nodes}
93 for e in self.edges:
94 indeg[e.to_node] = indeg.get(e.to_node, 0) + 1
95 return indeg
97 def adjacency_map(self) -> dict[str, list[str]]:
98 """邻接表。"""
99 adj: dict[str, list[str]] = {nid: [] for nid in self.nodes}
100 for e in self.edges:
101 adj[e.from_node].append(e.to_node)
102 return adj
104 def topological_order(self) -> list[str]:
105 """Kahn 算法拓扑排序,遇循环抛 ValueError。"""
106 indeg = self.in_degree_map()
107 adj = self.adjacency_map()
108 queue = deque([nid for nid, d in indeg.items() if d == 0])
109 order: list[str] = []
111 while queue:
112 node = queue.popleft()
113 order.append(node)
114 for neighbor in adj.get(node, []):
115 indeg[neighbor] -= 1
116 if indeg[neighbor] == 0:
117 queue.append(neighbor)
119 if len(order) != len(self.nodes):
120 remaining = set(self.nodes) - set(order)
121 raise ValueError(
122 f"Cycle detected in DAG. {len(remaining)} nodes in cycle: "
123 f"{list(remaining)[:5]}..."
124 )
126 return order
128 def detect_cycles(self) -> set[str]:
129 """检测并返回所有参与循环的节点 ID 集合。"""
130 indeg = self.in_degree_map()
131 adj = self.adjacency_map()
132 queue = deque([nid for nid, d in indeg.items() if d == 0])
133 acyclic: set[str] = set()
135 while queue:
136 node = queue.popleft()
137 acyclic.add(node)
138 for neighbor in adj.get(node, []):
139 indeg[neighbor] -= 1
140 if indeg[neighbor] == 0:
141 queue.append(neighbor)
143 return set(self.nodes) - acyclic
145 def parallel_groups(self) -> list[list[str]]:
146 """按拓扑层级分组,同一组内可并行执行。"""
147 order = self.topological_order()
148 indeg = self.in_degree_map()
149 adj = self.adjacency_map()
151 groups: list[list[str]] = []
152 remaining = set(order)
154 while remaining:
155 # 所有入度为 0 的当前批次
156 batch = sorted([n for n in remaining if indeg.get(n, 0) == 0])
157 if not batch:
158 break
159 groups.append(batch)
160 for n in batch:
161 remaining.discard(n)
162 for neighbor in adj.get(n, []):
163 indeg[neighbor] -= 1
165 return groups
168@dataclass
169class DecompositionTrace:
170 """分解过程可观测性记录。"""
172 iteration: int
173 action: str # split / merge / refine / replan
174 node_before: TaskNode | None = None
175 nodes_after: list[TaskNode] = field(default_factory=list)
176 reason: str = ""
179# ── Decomposer ───────────────────────────────
182class TaskDecomposer:
183 """智能任务分解器。
185 核心能力:
186 1. 将复杂任务拆解为可执行的 DAG
187 2. 检测并拒绝循环依赖
188 3. 在部分失败时动态重规划
189 4. 输出可观测的分解轨迹
191 Usage:
192 decomposer = TaskDecomposer()
193 dag = decomposer.decompose("从 10GB 日志中提取异常并生成日报")
194 order = dag.topological_order()
195 for nid in order:
196 execute(dag.nodes[nid])
197 # 部分节点失败后
198 new_dag = decomposer.replan(dag, failed_nodes=["tn-xxx"])
199 """
201 MAX_DEPTH = 8 # 最大递归深度
202 MIN_NODE_DURATION = 1.0 # 最小节点估算时长(秒),低于此不再分解
203 MAX_NODES = 50 # 最多节点数
205 def __init__(
206 self,
207 strategy: DecompositionStrategy = DecompositionStrategy.RECURSIVE,
208 llm_call: Callable | None = None,
209 ):
210 self._strategy = strategy
211 self._llm_call = llm_call
212 self._trace: list[DecompositionTrace] = []
213 self._iteration = 0
215 # ── Public API ─────────────────────────
217 def decompose(
218 self,
219 task: str,
220 context: dict[str, Any] | None = None,
221 ) -> TaskDAG:
222 """将任务分解为 DAG。
224 Args:
225 task: 任务描述
226 context: 补充上下文(已训练的 Agent、可用工具等)
228 Returns:
229 完整的 TaskDAG
230 """
231 self._trace = []
232 self._iteration = 0
234 dag = TaskDAG(
235 root_task=task,
236 strategy=self._strategy,
237 metadata=context or {},
238 created_at=__import__("time").time(),
239 )
241 root = self._create_node(task, confidence=1.0)
242 dag.nodes[root.id] = root
244 # 递归分解
245 self._decompose_recursive(dag, root.id, depth=0)
247 # 验证无循环
248 cycles = dag.detect_cycles()
249 if cycles:
250 logger.warning(f"Decomposition produced cycle: {cycles}. Re-resolving.")
251 dag = self._break_cycles(dag, cycles)
253 return dag
255 def replan(
256 self,
257 dag: TaskDAG,
258 failed_nodes: list[str],
259 ) -> TaskDAG:
260 """在部分节点执行失败后动态重规划。
262 Args:
263 dag: 当前 DAG(含已执行的节点状态)
264 failed_nodes: 失败节点 ID 列表
266 Returns:
267 重规划后的新 DAG(仅影响失败节点及其下游)
268 """
269 self._iteration += 1
271 affected = self._collect_downstream(dag, failed_nodes)
273 new_dag = TaskDAG(
274 dag_id=f"{dag.dag_id}-replan-{self._iteration}",
275 root_task=dag.root_task,
276 metadata=dag.metadata,
277 created_at=__import__("time").time(),
278 )
280 # 保留不受影响的节点和边
281 for nid, node in dag.nodes.items():
282 if nid not in affected:
283 new_dag.nodes[nid] = node
285 for edge in dag.edges:
286 if edge.from_node not in affected and edge.to_node not in affected:
287 new_dag.edges.append(edge)
289 # 为每个失败节点构建替代路径
290 for nid in failed_nodes:
291 node = dag.nodes[nid]
292 original_desc = node.description
294 alt_node = self._create_node(
295 f"[Retry Plan] {original_desc}",
296 confidence=node.confidence * 0.8, # 降信心
297 )
298 alt_node.retry_policy = "retry_n"
299 alt_node.max_retries = node.max_retries + 1
300 new_dag.nodes[alt_node.id] = alt_node
302 # 重连边:失败节点上游 → 新节点,新节点 → 失败节点下游
303 incoming = [e for e in dag.edges if e.to_node == nid]
304 outgoing = [e for e in dag.edges if e.from_node == nid]
306 for e in incoming:
307 if e.from_node not in affected:
308 new_dag.edges.append(TaskEdge(from_node=e.from_node, to_node=alt_node.id))
309 for e in outgoing:
310 if e.to_node not in affected:
311 new_dag.edges.append(TaskEdge(from_node=alt_node.id, to_node=e.to_node))
313 self._trace.append(
314 DecompositionTrace(
315 iteration=self._iteration,
316 action="replan",
317 node_before=node,
318 nodes_after=[alt_node],
319 reason=f"Node {nid} failed: {node.error or 'unknown'}",
320 )
321 )
323 return new_dag
325 def get_trace(self) -> list[DecompositionTrace]:
326 """获取完整的分解轨迹(用于可观测性)。"""
327 return list(self._trace)
329 def validate_dag(self, dag: TaskDAG) -> tuple[bool, str]:
330 """验证 DAG 的结构完整性。
332 Returns:
333 (is_valid, error_message)
334 """
335 # 空 DAG
336 if not dag.nodes:
337 return False, "DAG has no nodes"
339 # 循环检测
340 cycles = dag.detect_cycles()
341 if cycles:
342 return False, f"DAG contains cycles: {cycles}"
344 # 孤立节点检测
345 connected: set[str] = set()
346 for e in dag.edges:
347 connected.add(e.from_node)
348 connected.add(e.to_node)
349 isolated = set(dag.nodes) - connected
350 if isolated and len(dag.nodes) > 1:
351 logger.warning(f"Isolated nodes: {isolated}")
353 # 拓扑可达性(至少存在一条从源到汇的路径)
354 try:
355 dag.topological_order()
356 except ValueError as e:
357 return False, str(e)
359 return True, "valid"
361 # ── Internal ────────────────────────────
363 def _create_node(self, description: str, confidence: float = 1.0) -> TaskNode:
364 return TaskNode(
365 description=description,
366 confidence=confidence,
367 estimated_duration_s=max(1.0, len(description.split()) * 0.5),
368 )
370 def _decompose_recursive(self, dag: TaskDAG, node_id: str, depth: int):
371 """递归分解节点直到达到原子粒度。"""
372 if depth >= self.MAX_DEPTH:
373 return
375 node = dag.nodes.get(node_id)
376 if not node:
377 return
379 # 判断是否继续分解
380 if self._is_atomic(node, depth):
381 return
383 self._iteration += 1
385 # 调用 LLM 或启发式规则生成子任务
386 sub_tasks = self._generate_sub_tasks(node, dag.metadata)
388 if not sub_tasks or len(sub_tasks) <= 1:
389 return # 无法继续分解
391 # 移除原节点,插入子节点和边
392 dag.nodes.pop(node_id)
393 for i, sub in enumerate(sub_tasks):
394 dag.nodes[sub.id] = sub
395 # 子任务按顺序或并行链接
396 if i > 0:
397 dag.edges.append(
398 TaskEdge(
399 from_node=sub_tasks[i - 1].id,
400 to_node=sub.id,
401 )
402 )
404 # 重连原节点的入边和出边
405 incoming = [e for e in dag.edges if e.to_node == node_id]
406 outgoing = [e for e in dag.edges if e.from_node == node_id]
408 # 移除旧边
409 dag.edges = [e for e in dag.edges if e.to_node != node_id and e.from_node != node_id]
411 if sub_tasks:
412 first = sub_tasks[0]
413 for e in incoming:
414 dag.edges.append(TaskEdge(from_node=e.from_node, to_node=first.id))
415 last = sub_tasks[-1]
416 for e in outgoing:
417 dag.edges.append(TaskEdge(from_node=last.id, to_node=e.to_node))
419 self._trace.append(
420 DecompositionTrace(
421 iteration=self._iteration,
422 action="split",
423 node_before=node,
424 nodes_after=sub_tasks,
425 reason=f"Decomposed at depth {depth}",
426 )
427 )
429 # 递归分解子任务
430 if len(dag.nodes) < self.MAX_NODES:
431 for sub in sub_tasks:
432 if sub.id in dag.nodes:
433 self._decompose_recursive(dag, sub.id, depth + 1)
435 def _is_atomic(self, node: TaskNode, depth: int) -> bool:
436 """判断节点是否已达到原子粒度,无需进一步分解。"""
437 # 规则 1:估算时长够短
438 if node.estimated_duration_s < self.MIN_NODE_DURATION:
439 return True
440 # 规则 2:节点数已接近上限
441 if depth > self.MAX_DEPTH - 1:
442 return True
443 # 规则 3:描述过于简单(单步骤)
444 if len(node.description.split()) < 5:
445 return True
446 return False
448 def _generate_sub_tasks(self, node: TaskNode, context: dict[str, Any]) -> list[TaskNode]:
449 """生成节点的子任务列表。
451 优先使用 LLM 调用,降级为启发式规则。
452 """
453 if self._llm_call:
454 return self._llm_generate(node, context)
455 return self._heuristic_generate(node)
457 def _llm_generate(self, node: TaskNode, context: dict[str, Any]) -> list[TaskNode]:
458 """通过 LLM 调用生成子任务。"""
459 prompt = f"""Break down the following task into 2-5 subtasks.
461Task: {node.description}
462Context: {json.dumps(context, default=str) if context else 'None'}
464Output JSON array of subtasks, each with:
465- description: string
466- agent_type: string (default/planner/executor/analyst)
467- estimated_duration_s: float
469Only respond with the JSON array, no other text."""
470 try:
471 result = self._llm_call(prompt)
472 items = json.loads(result) if isinstance(result, str) else result
473 return [
474 TaskNode(
475 description=item["description"],
476 agent_type=item.get("agent_type", "default"),
477 estimated_duration_s=item.get("estimated_duration_s", 5.0),
478 confidence=0.7,
479 )
480 for item in items
481 ]
482 except Exception as e:
483 logger.warning(f"LLM decomposition failed: {e}, falling back to heuristic")
484 return self._heuristic_generate(node)
486 def _heuristic_generate(self, node: TaskNode) -> list[TaskNode]:
487 """启发式任务分解 — 基于关键词和模式匹配。"""
488 desc = node.description.lower()
489 subtasks: list[TaskNode] = []
491 # 模式 1:提取/收集 → 分析 → 生成
492 if any(kw in desc for kw in ("extract", "collect", "fetch", "retrieve", "提取", "收集")):
493 subtasks.append(
494 self._create_node(f"Phase 1: Collect data for: {node.description[:60]}")
495 )
496 subtasks.append(self._create_node("Phase 2: Analyze/process collected data"))
497 subtasks.append(
498 self._create_node(f"Phase 3: Generate output/report for: {node.description[:60]}")
499 )
501 # 模式 2:对比/比较
502 elif any(kw in desc for kw in ("compare", "vs", "对比", "比较", "versus")):
503 parts = desc.replace("compare ", "").replace("对比 ", "").split(" vs ")
504 if len(parts) < 2:
505 parts = desc.replace("compare ", "").split(" and ")
506 if len(parts) >= 2:
507 subtasks.append(self._create_node(f"Analyze: {parts[0].strip()}"))
508 subtasks.append(self._create_node(f"Analyze: {parts[1].strip()}"))
509 subtasks.append(self._create_node("Synthesize comparison results"))
511 # 模式 3:transform/convert/migrate
512 elif any(kw in desc for kw in ("transform", "convert", "migrate", "转换", "迁移")):
513 subtasks.append(self._create_node("Validate source data integrity"))
514 subtasks.append(self._create_node(f"Execute transformation: {node.description[:60]}"))
515 subtasks.append(self._create_node("Verify output correctness"))
517 # 模式 4:default — 按步骤拆
518 else:
519 subtasks.append(self._create_node(f"Plan: outline steps for '{node.description[:60]}'"))
520 subtasks.append(self._create_node(f"Execute: carry out '{node.description[:60]}'"))
521 subtasks.append(
522 self._create_node(f"Validate: check results of '{node.description[:60]}'")
523 )
525 for sub in subtasks:
526 sub.confidence = 0.6 # 启发式分解信心较低
527 return subtasks
529 def _collect_downstream(self, dag: TaskDAG, failed_nodes: list[str]) -> set[str]:
530 """收集失败节点及所有下游节点。"""
531 adj = dag.adjacency_map()
532 affected: set[str] = set()
534 queue = deque(failed_nodes)
535 while queue:
536 nid = queue.popleft()
537 if nid in affected:
538 continue
539 affected.add(nid)
540 for neighbor in adj.get(nid, []):
541 if neighbor not in affected:
542 queue.append(neighbor)
544 return affected
546 def _break_cycles(self, dag: TaskDAG, cycles: set[str]) -> TaskDAG:
547 """打破循环 — 移除循环中置信度最低的边。"""
548 cycle_edges = [e for e in dag.edges if e.from_node in cycles and e.to_node in cycles]
549 if cycle_edges:
550 # 移除第一条循环边(可改进为最小置信度边)
551 dag.edges.remove(cycle_edges[0])
552 logger.info(
553 f"Removed edge {cycle_edges[0].from_node}→{cycle_edges[0].to_node} to break cycle"
554 )
555 return dag
558# ── Quick Start ──────────────────────────────
561def create_decomposer(
562 strategy: DecompositionStrategy = DecompositionStrategy.RECURSIVE,
563 llm_call: Callable | None = None,
564) -> TaskDecomposer:
565 return TaskDecomposer(strategy=strategy, llm_call=llm_call)