HumboldtJoker commited on
Commit
6b830ac
·
verified ·
1 Parent(s): 3a04c53

Clean up nested duplicate: v2/v2/kintsugi_vendor/kintsugi/skills/dag.py

Browse files
v2/v2/kintsugi_vendor/kintsugi/skills/dag.py DELETED
@@ -1,304 +0,0 @@
1
- """
2
- DAG-based skill composition for Kintsugi.
3
-
4
- Adapted from AgentSkillOS (arXiv:2603.02176). Provides declarative
5
- skill composition via directed acyclic graphs with layer-parallel execution.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import asyncio
11
- import hashlib
12
- import json
13
- import time
14
- import uuid
15
- from collections import deque
16
- from dataclasses import dataclass, field
17
- from typing import Any
18
-
19
- from .base import SkillContext, SkillRequest, SkillResponse
20
- from .registry import SkillRegistry
21
-
22
-
23
- @dataclass
24
- class DAGNode:
25
- """A single node in a skill composition DAG."""
26
-
27
- node_id: str
28
- skill_name: str
29
- sub_task: str
30
- layer: int
31
- input_keys: list[str] = field(default_factory=list)
32
- output_keys: list[str] = field(default_factory=list)
33
-
34
-
35
- @dataclass
36
- class DAGResult:
37
- """Result of executing a complete DAG."""
38
-
39
- dag_id: str
40
- artifacts: dict[str, Any] = field(default_factory=dict)
41
- node_results: dict[str, SkillResponse] = field(default_factory=dict)
42
- node_errors: dict[str, str] = field(default_factory=dict)
43
- success: bool = True
44
- execution_time_ms: float = 0.0
45
- layers_executed: int = 0
46
-
47
-
48
- @dataclass
49
- class SkillDAG:
50
- """Directed acyclic graph of skill compositions."""
51
-
52
- dag_id: str = field(default_factory=lambda: str(uuid.uuid4()))
53
- nodes: dict[str, DAGNode] = field(default_factory=dict)
54
- edges: list[tuple[str, str]] = field(default_factory=list)
55
- strategy: str = "quality" # quality | efficiency | simplicity
56
- metadata: dict[str, Any] = field(default_factory=dict)
57
-
58
- def add_node(self, node: DAGNode) -> None:
59
- self.nodes[node.node_id] = node
60
-
61
- def add_edge(self, source: str, target: str) -> None:
62
- self.edges.append((source, target))
63
-
64
- def layers(self) -> list[list[str]]:
65
- """Return node IDs grouped by layer, ordered by layer index."""
66
- layer_map: dict[int, list[str]] = {}
67
- for node in self.nodes.values():
68
- layer_map.setdefault(node.layer, []).append(node.node_id)
69
- return [layer_map[k] for k in sorted(layer_map.keys())]
70
-
71
- def topological_sort(self) -> list[str]:
72
- """Kahn's algorithm. Returns full execution order respecting edges."""
73
- in_degree: dict[str, int] = {nid: 0 for nid in self.nodes}
74
- adjacency: dict[str, list[str]] = {nid: [] for nid in self.nodes}
75
-
76
- for src, tgt in self.edges:
77
- adjacency[src].append(tgt)
78
- in_degree[tgt] += 1
79
-
80
- queue = deque(
81
- sorted(
82
- [nid for nid, deg in in_degree.items() if deg == 0],
83
- key=lambda nid: self.nodes[nid].layer,
84
- )
85
- )
86
- order: list[str] = []
87
-
88
- while queue:
89
- nid = queue.popleft()
90
- order.append(nid)
91
- for neighbor in adjacency[nid]:
92
- in_degree[neighbor] -= 1
93
- if in_degree[neighbor] == 0:
94
- queue.append(neighbor)
95
-
96
- return order
97
-
98
- def validate(self, registry: SkillRegistry) -> list[str]:
99
- """Validate the DAG. Returns list of error strings (empty = valid)."""
100
- errors: list[str] = []
101
-
102
- # Check all skill_names exist
103
- for node in self.nodes.values():
104
- if node.skill_name not in registry:
105
- errors.append(f"Node '{node.node_id}': skill '{node.skill_name}' not in registry")
106
-
107
- # Check edges reference valid nodes
108
- for src, tgt in self.edges:
109
- if src not in self.nodes:
110
- errors.append(f"Edge source '{src}' not in nodes")
111
- if tgt not in self.nodes:
112
- errors.append(f"Edge target '{tgt}' not in nodes")
113
-
114
- # Check edges respect layer ordering (source.layer < target.layer)
115
- for src, tgt in self.edges:
116
- if src in self.nodes and tgt in self.nodes:
117
- if self.nodes[src].layer >= self.nodes[tgt].layer:
118
- errors.append(
119
- f"Edge ({src} -> {tgt}): source layer {self.nodes[src].layer} "
120
- f"must be < target layer {self.nodes[tgt].layer}"
121
- )
122
-
123
- # Cycle detection via topological sort
124
- topo = self.topological_sort()
125
- if len(topo) != len(self.nodes):
126
- errors.append("DAG contains a cycle")
127
-
128
- return errors
129
-
130
- def content_hash(self) -> str:
131
- """Deterministic hash for provenance signing."""
132
- canonical = json.dumps(
133
- {
134
- "nodes": {
135
- nid: {
136
- "skill_name": n.skill_name,
137
- "sub_task": n.sub_task,
138
- "layer": n.layer,
139
- "input_keys": n.input_keys,
140
- "output_keys": n.output_keys,
141
- }
142
- for nid, n in sorted(self.nodes.items())
143
- },
144
- "edges": sorted(self.edges),
145
- "strategy": self.strategy,
146
- },
147
- sort_keys=True,
148
- )
149
- return hashlib.sha256(canonical.encode()).hexdigest()
150
-
151
-
152
- class DAGExecutor:
153
- """Executes a SkillDAG against a registry with layer-parallel scheduling."""
154
-
155
- def __init__(self, registry: SkillRegistry, max_parallel: int = 4) -> None:
156
- self.registry = registry
157
- self.max_parallel = max_parallel
158
-
159
- async def execute(
160
- self,
161
- dag: SkillDAG,
162
- initial_context: SkillContext,
163
- initial_artifacts: dict[str, Any] | None = None,
164
- ) -> DAGResult:
165
- artifacts: dict[str, Any] = dict(initial_artifacts or {})
166
- node_results: dict[str, SkillResponse] = {}
167
- node_errors: dict[str, str] = {}
168
-
169
- t0 = time.perf_counter()
170
- layers = dag.layers()
171
- layers_executed = 0
172
-
173
- for layer_nodes in layers:
174
- sem = asyncio.Semaphore(self.max_parallel)
175
-
176
- async def _run_node(node_id: str) -> None:
177
- async with sem:
178
- node = dag.nodes[node_id]
179
- chip = self.registry.get(node.skill_name)
180
- if chip is None:
181
- node_errors[node_id] = f"Skill '{node.skill_name}' not found"
182
- return
183
-
184
- # Build request from sub_task + upstream artifacts
185
- input_data = {k: artifacts[k] for k in node.input_keys if k in artifacts}
186
- request = SkillRequest(
187
- intent=node.sub_task,
188
- parameters=input_data,
189
- raw_input=node.sub_task,
190
- )
191
-
192
- try:
193
- response = await chip.handle(request, initial_context)
194
- node_results[node_id] = response
195
-
196
- # Map response data to output artifact keys
197
- if response.success and response.data:
198
- if len(node.output_keys) == 1:
199
- artifacts[node.output_keys[0]] = response.data
200
- else:
201
- for key in node.output_keys:
202
- if key in response.data:
203
- artifacts[key] = response.data[key]
204
- except Exception as exc:
205
- node_errors[node_id] = str(exc)
206
-
207
- await asyncio.gather(*[_run_node(nid) for nid in layer_nodes])
208
- layers_executed += 1
209
-
210
- elapsed_ms = (time.perf_counter() - t0) * 1000.0
211
- success = len(node_errors) == 0
212
-
213
- return DAGResult(
214
- dag_id=dag.dag_id,
215
- artifacts=artifacts,
216
- node_results=node_results,
217
- node_errors=node_errors,
218
- success=success,
219
- execution_time_ms=elapsed_ms,
220
- layers_executed=layers_executed,
221
- )
222
-
223
-
224
- class DAGBuilder:
225
- """Constructs SkillDAGs from BDI intentions or skill sequences."""
226
-
227
- @staticmethod
228
- def from_skill_sequence(
229
- skill_names: list[str],
230
- registry: SkillRegistry,
231
- sub_tasks: list[str] | None = None,
232
- ) -> SkillDAG:
233
- """Build a linear chain DAG from an ordered list of skill names."""
234
- dag = SkillDAG(strategy="simplicity")
235
- prev_output_key: str | None = None
236
-
237
- for i, name in enumerate(skill_names):
238
- task = sub_tasks[i] if sub_tasks and i < len(sub_tasks) else name
239
- output_key = f"step_{i}_out"
240
- input_keys = [prev_output_key] if prev_output_key else []
241
-
242
- node = DAGNode(
243
- node_id=f"node_{i}",
244
- skill_name=name,
245
- sub_task=task,
246
- layer=i,
247
- input_keys=input_keys,
248
- output_keys=[output_key],
249
- )
250
- dag.add_node(node)
251
-
252
- if i > 0:
253
- dag.add_edge(f"node_{i - 1}", f"node_{i}")
254
-
255
- prev_output_key = output_key
256
-
257
- return dag
258
-
259
- @staticmethod
260
- def from_intention(
261
- intention: dict[str, Any],
262
- registry: SkillRegistry,
263
- strategy: str = "quality",
264
- ) -> SkillDAG:
265
- """Build a DAG from a BDI intention structure.
266
-
267
- Expected intention format:
268
- {
269
- "goal": str,
270
- "steps": [
271
- {
272
- "skill": str,
273
- "task": str,
274
- "layer": int,
275
- "inputs": list[str],
276
- "outputs": list[str],
277
- "depends_on": list[str], # node IDs
278
- },
279
- ...
280
- ]
281
- }
282
- """
283
- dag = SkillDAG(
284
- strategy=strategy,
285
- metadata={"goal": intention.get("goal", "")},
286
- )
287
-
288
- steps = intention.get("steps", [])
289
- for i, step in enumerate(steps):
290
- node_id = step.get("id", f"node_{i}")
291
- node = DAGNode(
292
- node_id=node_id,
293
- skill_name=step["skill"],
294
- sub_task=step.get("task", step["skill"]),
295
- layer=step.get("layer", i),
296
- input_keys=step.get("inputs", []),
297
- output_keys=step.get("outputs", [f"{node_id}_out"]),
298
- )
299
- dag.add_node(node)
300
-
301
- for dep in step.get("depends_on", []):
302
- dag.add_edge(dep, node_id)
303
-
304
- return dag