Setting the file. One moment. Build Diagram · Agent Advisor · aws/agent-toolkit-for-aws · Skills Docs23.21
Add Capabilities
81
Creating Amazon Aurora Db Cluster With Instances
104
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
def main
— line 448
This file
- Number
- 23.57
- Position
- 57 of 81
- Type
- Python
- Size
- 21 KB
- Lines
- 473
scripts/build_diagram.py
Python·473 lines·21 KB
,
13 "eks": "Amazon EKS",
14 "lambda": "AWS Lambda",
15 "batch": "AWS Batch",
16 "fargate": "AWS Fargate",
17 "serverless_workers": "Temporal Serverless Workers (Public Preview)",
18 "none": "No viable runtime",
19}
20
21SERVICE_LABELS = {
22 "identity": "Identity",
23 "observability": "Observability",
24 "evaluations": "Evaluations",
25 "optimization": "Optimization",
26 "memory": "Memory",
27 "gateway": "Gateway",
28 "policy": "Policy",
29 "managed_kb": "Managed KB",
30 "code_interpreter": "Code Interpreter",
31 "browser": "Browser",
32 "web_search": "Web Search",
33 "sandbox": "Sandbox",
34 "payments": "Payments",
35 "registry": "Registry",
36}
37
38# Runtimes whose compute layer needs heavy infrastructure execution (clusters,
39# Terraform) handed off to the migration skill. AgentCore, Lambda MicroVMs, and
40# standard Lambda are self-contained deliverables from this advisor — not handoffs.
41# Runtimes that hand the compute layer to the migration skill (their service cards say so):
42# ECS, EKS, Fargate (= ECS), and AWS Batch. AgentCore, standard Lambda, and Lambda MicroVMs
43# are self-contained. Keep in sync with design.md's handoff_required definition.
44HANDOFF_RUNTIMES = {"ecs", "eks", "fargate", "batch"}
45
46
47def unit_runtime(unit):
48 """The runtime a unit ACTUALLY deploys on — effective_runtime under a consolidated
49 platform, falling back to verdict for split runs (or older design.json without the
50 field). The diagram must render where the unit really runs, not its best-fit verdict."""
51 return unit.get("effective_runtime") or unit.get("verdict", "unknown")
52
53
54def units_on_interconnect(units, modes):
55 """Units coupled via any coupling.mode in `modes`, PLUS the target ends of their one-way
56 interacts_with couplings (e.g. a producer mode:queue interacts_with a consumer mode:none —
57 the consumer is on the queue too). A system can MIX couplings (some units on a queue, others
58 via a gateway); each interconnect is computed independently from per-unit coupling.mode, not
59 from the single dominant platform.interconnect value. Returns [] when no unit uses `modes`."""
60 by_id = {u["id"]: u for u in units}
61 on = set()
62 for u in units:
63 coup = u.get("coupling", {})
64 if coup.get("mode") in modes:
65 on.add(u["id"])
66 for tgt in coup.get("interacts_with", []):
67 if tgt in by_id:
68 on.add(tgt)
69 return [u for u in units if u["id"] in on]
70
71
72def units_needing_handoff(units):
73 """Units whose ACTUAL runtime (effective_runtime||verdict) hands the compute layer to
74 the migration skill — i.e. is in HANDOFF_RUNTIMES. Mirrors the single-unit path's handoff node so
75 a multi-unit ECS/EKS/Fargate/Batch system shows the same 'configured by the migration skill'
76 indicator instead of silently dropping it. Returns [] when no unit needs a handoff."""
77 return [u for u in units if unit_runtime(u) in HANDOFF_RUNTIMES]
78
79
80def resolve_runtime(result, confirm):
81 verdict = result.get("verdict")
82 if verdict == "co_recommend":
83 return confirm.get("chosen_runtime") or result.get("co_recommend", ["none"])[0]
84 if verdict == "no_viable_runtime":
85 return "none"
86 return verdict
87
88
89def resolve_services(result, confirm):
90 services = confirm.get("agentcore_services") or result.get("agentcore_services", [])
91 seen, out = set(), []
92 for sid in services:
93 if sid in SERVICE_LABELS and sid not in seen:
94 seen.add(sid)
95 out.append(sid)
96 return out
97
98
99def render_mermaid(runtime, services, model, deployment_model):
100 label = RUNTIME_LABELS.get(runtime, runtime)
101 if runtime == "agentcore" and deployment_model:
102 label = f"{label}<br/>({deployment_model})"
103 lines = ["flowchart TD"]
104 # Primary request/data flow (solid): user invokes the runtime, runtime calls the model.
105 lines.append(f' user["User / Client"]')
106 lines.append(f' rt["{label}"]')
107 lines.append(" user -->|request| rt")
108 # A model-less unit (non-agent: batch/service/light_io that calls no Bedrock model) has no
109 # model node or invoke edge — do not render "Bedrock model: unknown".
110 if model and model != "unknown":
111 lines.append(f' model["Bedrock model:<br/>{model}"]')
112 lines.append(" rt -->|invoke| model")
113 # AgentCore services are cross-cutting capabilities attached to the runtime, NOT
114 # downstream call targets — group them in a subgraph and attach with dotted edges.
115 if services:
116 lines.append(' subgraph svcs["AgentCore services"]')
117 lines.append(" direction LR")
118 for sid in services:
119 lines.append(f' svc_{sid}["{SERVICE_LABELS[sid]}"]')
120 lines.append(" end")
121 lines.append(" rt -.-> svcs")
122 if runtime in HANDOFF_RUNTIMES:
123 lines.append(' handoff["Compute configured by the migration skill"]')
124 lines.append(" rt -.-> handoff")
125 return "\n".join(lines)
126
127
128def render_ascii(runtime, services, model, deployment_model):
129 label = RUNTIME_LABELS.get(runtime, runtime)
130 if runtime == "agentcore" and deployment_model:
131 label = f"{label} ({deployment_model})"
132 # Primary flow: user -> runtime (-> Bedrock model, only if the unit calls one).
133 lines = [
134 "User / Client",
135 " | request",
136 " v",
137 f"[ {label} ]",
138 ]
139 if model and model != "unknown":
140 lines += [f" | invoke", " v", f"Bedrock model: {model}"]
141 # Services are attached capabilities, shown separately (not as call targets).
142 if services:
143 lines.append("")
144 lines.append(f"[ {label} ] .. attached AgentCore services:")
145 for sid in services:
146 lines.append(f" - {SERVICE_LABELS[sid]}")
147 if runtime in HANDOFF_RUNTIMES:
148 lines.append("")
149 lines.append("Note: compute configured by the migration skill")
150 return "\n".join(lines)
151
152
153def render_multi_unit_mermaid(design):
154 lines = ["flowchart TD"]
155 units = design.get("units", [])
156 platform = design.get("platform", {})
157 interconnect = platform.get("interconnect", "none")
158 temporal_block = design.get("temporal", {})
159
160 # Sanitize unit id for mermaid node id (replace hyphens with underscores)
161 def sanitize_id(uid):
162 return uid.replace("-", "_")
163
164 # Check if this is a Temporal system
165 worker_poll_units = [u for u in units if u.get("workload_class") == "temporal_worker_poll"]
166 is_temporal = bool(worker_poll_units or temporal_block)
167
168 if is_temporal:
169 # Temporal topology: Temporal Server → worker_poll unit → Activity units.
170 # The orchestrator label reflects the chosen Way (self-hosted stays self-hosted).
171 way = temporal_block.get("way", "unknown")
172 if way == "self_hosted":
173 orch_label = "Temporal Server<br/>(self-hosted, orchestrator)"
174 elif way == "cloud":
175 orch_label = "Temporal Cloud<br/>(orchestrator)"
176 else:
177 orch_label = "Temporal Server<br/>(orchestrator)"
178 lines.append(f' temporal_cloud["{orch_label}"]')
179
180 # Render each unit as a subgraph
181 for unit in units:
182 uid = unit["id"]
183 sanitized_id = sanitize_id(uid)
184 verdict = unit_runtime(unit)
185 model_rec = unit.get("model_recommendation")
186
187 # Build node label with runtime and model
188 label_parts = [RUNTIME_LABELS.get(verdict, verdict)]
189 if model_rec and model_rec.get("model"):
190 label_parts.append(model_rec["model"])
191 node_label = "<br/>".join(label_parts)
192
193 lines.append(f' subgraph {sanitized_id}["{uid}"]')
194 lines.append(f' {sanitized_id}_node["{node_label}"]')
195 lines.append(' end')
196
197 # Connect Temporal Cloud to worker_poll units
198 for unit in worker_poll_units:
199 sanitized_id = sanitize_id(unit["id"])
200 lines.append(f" temporal_cloud --> {sanitized_id}")
201
202 # Connect each worker fleet ONLY to the Activity units it actually executes —
203 # matched by queue membership (fleet.queues[] contains the Activity's task_queue),
204 # never a cartesian product across all fleets. Fall back to a single-fleet
205 # connect-all only when the fleet/queue data can't disambiguate.
206 non_worker_units = [u for u in units if u.get("workload_class") != "temporal_worker_poll"]
207 single_fleet = len(worker_poll_units) == 1
208 for worker in worker_poll_units:
209 worker_id = sanitize_id(worker["id"])
210 fleet_queues = set(worker.get("queues", []))
211 for activity_unit in non_worker_units:
212 activity_id = sanitize_id(activity_unit["id"])
213 task_queue = activity_unit.get("task_queue", "")
214 if task_queue and fleet_queues:
215 # Only connect when this Activity runs on a queue this fleet polls.
216 if task_queue not in fleet_queues:
217 continue
218 lines.append(f" {worker_id} -->|{task_queue}| {activity_id}")
219 elif single_fleet:
220 # One fleet, no queue metadata to split on: it runs every Activity.
221 if task_queue:
222 lines.append(f" {worker_id} -->|{task_queue}| {activity_id}")
223 else:
224 lines.append(f" {worker_id} --> {activity_id}")
225 # Multiple fleets without queue data: cannot attribute — leave unconnected
226 # rather than draw a false cartesian-product edge.
227 else:
228 # Generic multi-unit topology
229 lines.append(' user["User / Client"]')
230
231 # NOTE: unlike the single-unit path, the multi-unit diagram does NOT draw a per-unit
232 # AgentCore-services subgraph — with N units it would clutter the topology, and the
233 # Generate report already lists each unit's agentcore_services in its per-unit table.
234 # The handoff indicator (below) IS mirrored because it reflects a topology fact.
235
236 # Render each unit as a subgraph
237 agent_session_units = []
238 for unit in units:
239 uid = unit["id"]
240 sanitized_id = sanitize_id(uid)
241 verdict = unit_runtime(unit)
242 model_rec = unit.get("model_recommendation")
243
244 # Build node label with runtime and model
245 label_parts = [RUNTIME_LABELS.get(verdict, verdict)]
246 if model_rec and model_rec.get("model"):
247 label_parts.append(model_rec["model"])
248 node_label = "<br/>".join(label_parts)
249
250 lines.append(f' subgraph {sanitized_id}["{uid}"]')
251 lines.append(f' {sanitized_id}_node["{node_label}"]')
252 lines.append(' end')
253
254 # Track agent_session units for user entry edge
255 if unit.get("workload_class") == "agent_session":
256 agent_session_units.append(sanitized_id)
257
258 # Connect user to agent_session units
259 for uid in agent_session_units:
260 lines.append(f" user -->|request| {uid}")
261
262 # Add interconnect nodes/edges. A system can MIX couplings — some units on a queue,
263 # others via a gateway (Design records only the dominant `platform.interconnect`, but the
264 # per-unit coupling.mode holds the real picture). So draw EACH interconnect that any unit
265 # actually uses, independently — not one exclusive branch keyed on platform.interconnect.
266 if len(units) > 1:
267 has_any_coupling = any("coupling" in u for u in units)
268 queue_units = units_on_interconnect(units, {"queue"})
269 gw_units = units_on_interconnect(units, {"api", "a2a"})
270
271 # No per-unit coupling data at all → fall back to the single platform.interconnect
272 # over all units (legacy behavior for designs that predate per-unit coupling).
273 if not has_any_coupling:
274 if interconnect == "queue":
275 queue_units = units
276 elif interconnect == "gateway":
277 gw_units = units
278
279 if len(queue_units) > 1:
280 lines.append(' queue["Queue"]')
281 # Producer/consumer direction isn't in the data model, so show each coupled
282 # unit's participation with an undirected-style dotted edge (as with gateway).
283 for unit in queue_units:
284 lines.append(f" {sanitize_id(unit['id'])} -.-> queue")
285 if len(gw_units) > 1:
286 lines.append(' gateway["Gateway"]')
287 for unit in gw_units:
288 lines.append(f" {sanitize_id(unit['id'])} -.-> gateway")
289 # single unit, or interconnect none/in_process with no couplings: no edges
290
291 # Handoff indicator — same as the single-unit path, but per unit: any unit on a
292 # HANDOFF_RUNTIME (ecs/eks/fargate/batch) has its compute configured by the migration skill.
293 handoff_units = units_needing_handoff(units)
294 if handoff_units:
295 lines.append(' handoff["Compute configured by the migration skill"]')
296 for unit in handoff_units:
297 lines.append(f" {sanitize_id(unit['id'])} -.-> handoff")
298
299 return "\n".join(lines)
300
301
302def render_multi_unit_ascii(design):
303 lines = ["Multi-unit Architecture:", ""]
304 units = design.get("units", [])
305 platform = design.get("platform", {})
306 interconnect = platform.get("interconnect", "none")
307 temporal_block = design.get("temporal", {})
308
309 # Check if this is a Temporal system
310 worker_poll_units = [u for u in units if u.get("workload_class") == "temporal_worker_poll"]
311 is_temporal = bool(worker_poll_units or temporal_block)
312
313 if is_temporal:
314 # Temporal topology — orchestrator label reflects the chosen Way.
315 way = temporal_block.get("way", "unknown")
316 if way == "self_hosted":
317 lines.append("Temporal Server (self-hosted, orchestrator)")
318 elif way == "cloud":
319 lines.append("Temporal Cloud (orchestrator)")
320 else:
321 lines.append("Temporal Server (orchestrator)")
322 lines.append(" |")
323 lines.append(" v")
324
325 for unit in units:
326 uid = unit["id"]
327 verdict = unit_runtime(unit)
328 model_rec = unit.get("model_recommendation")
329
330 label = RUNTIME_LABELS.get(verdict, verdict)
331 if model_rec and model_rec.get("model"):
332 label += f" ({model_rec['model']})"
333
334 if unit.get("workload_class") == "temporal_worker_poll":
335 lines.append(f" [ {uid}: {label} ] <-- long-polls task queues")
336 else:
337 task_queue = unit.get("task_queue", "")
338 queue_info = f" (task queue: {task_queue})" if task_queue else ""
339 lines.append(f" --> [ {uid}: {label} ]{queue_info}")
340 else:
341 # Generic multi-unit topology
342 lines.append("User / Client")
343 lines.append(" |")
344 lines.append(" v")
345
346 for unit in units:
347 uid = unit["id"]
348 verdict = unit_runtime(unit)
349 model_rec = unit.get("model_recommendation")
350
351 label = RUNTIME_LABELS.get(verdict, verdict)
352 if model_rec and model_rec.get("model"):
353 label += f" ({model_rec['model']})"
354
355 lines.append(f" [ {uid}: {label} ]")
356
357 # A system can mix couplings — draw EACH interconnect any unit actually uses (from
358 # per-unit coupling.mode), not just the single dominant platform.interconnect value.
359 # This mirrors the Mermaid path so a queue+gateway mix doesn't lose the queue here.
360 has_any_coupling = any("coupling" in u for u in units)
361 queue_units = units_on_interconnect(units, {"queue"})
362 gw_units = units_on_interconnect(units, {"api", "a2a"})
363 if not has_any_coupling:
364 if interconnect == "queue":
365 queue_units = units
366 elif interconnect == "gateway":
367 gw_units = units
368 if len(queue_units) > 1:
369 lines.append("")
370 lines.append("Interconnect: Queue — " +
371 ", ".join(u["id"] for u in queue_units))
372 if len(gw_units) > 1:
373 lines.append("")
374 lines.append("Interconnect: Gateway — " +
375 ", ".join(u["id"] for u in gw_units))
376
377 # Handoff indicator (mirrors the Mermaid multi-unit path and the single-unit ASCII note):
378 # any unit on a HANDOFF_RUNTIME has its compute configured by the migration skill.
379 handoff_units = units_needing_handoff(units)
380 if handoff_units:
381 lines.append("")
382 lines.append("Note: compute configured by the migration skill — " +
383 ", ".join(u["id"] for u in handoff_units))
384
385 return "\n".join(lines)
386
387
388def build_diagram(result, confirm, design=None):
389 # If design has multiple units, render multi-unit topology
390 if design is not None:
391 units = design.get("units", [])
392 if len(units) > 1:
393 return {
394 "mermaid": render_multi_unit_mermaid(design),
395 "ascii": render_multi_unit_ascii(design),
396 }
397 if len(units) == 1:
398 # Single-unit design: render from the design UNIT (which carries the resolved
399 # effective_runtime, model_recommendation, deployment_model, agentcore_services),
400 # falling back to the legacy `result`/`confirm` for any field the unit omits. This
401 # fixes the case where `result` is the wrapped scoring-result.json ({"units": {...}})
402 # whose top-level verdict/model are absent (which rendered "runtime None / model
403 # unknown"), while preserving the collapse invariant: given consistent single-unit
404 # data, this path and the legacy path below produce identical output.
405 unit = units[0]
406 runtime = unit_runtime(unit)
407 if runtime in ("none", "no_viable_runtime", "unknown"):
408 runtime = resolve_runtime(result, confirm)
409 if runtime == "none":
410 msg = "No viable runtime — see blocking constraints"
411 return {
412 "mermaid": f'flowchart TD\n n["{msg}"]',
413 "ascii": f"[ {RUNTIME_LABELS['none']} ]\n{msg}",
414 }
415 # Fall back to result/confirm ONLY when the unit OMITS the key — an explicit empty
416 # list is authoritative (the user declined all AgentCore add-ons) and must be kept,
417 # not replaced by scoring defaults like Identity/Observability.
418 if "agentcore_services" in unit:
419 services = unit["agentcore_services"] or []
420 else:
421 services = resolve_services(result, confirm)
422 services = [s for s in services if s in SERVICE_LABELS]
423 model = (unit.get("model_recommendation") or {}).get("model") \
424 or result.get("model_recommendation", {}).get("model", "unknown")
425 deployment_model = unit.get("deployment_model") or result.get("deployment_model")
426 return {
427 "mermaid": render_mermaid(runtime, services, model, deployment_model),
428 "ascii": render_ascii(runtime, services, model, deployment_model),
429 }
430
431 # Legacy single-unit path (no design supplied — e.g. pre-design diagram)
432 runtime = resolve_runtime(result, confirm)
433 if runtime == "none":
434 msg = "No viable runtime — see blocking constraints"
435 return {
436 "mermaid": f'flowchart TD\n n["{msg}"]',
437 "ascii": f"[ {RUNTIME_LABELS['none']} ]\n{msg}",
438 }
439 services = resolve_services(result, confirm)
440 model = result.get("model_recommendation", {}).get("model", "unknown")
441 deployment_model = result.get("deployment_model")
442 return {
443 "mermaid": render_mermaid(runtime, services, model, deployment_model),
444 "ascii": render_ascii(runtime, services, model, deployment_model),
445 }
446
447
448def main(argv=None):
449 import argparse
450 parser = argparse.ArgumentParser(description="agent-advisor diagram composer")
451 parser.add_argument("result", type=pathlib.Path)
452 parser.add_argument("confirm", type=pathlib.Path)
453 parser.add_argument("design", type=pathlib.Path, nargs="?", default=None,
454 help="Optional design.json path for multi-unit topology")
455 args = parser.parse_args(argv)
456 result = json.loads(args.result.read_text())
457 confirm = json.loads(args.confirm.read_text()) if args.confirm.exists() else {}
458 design = json.loads(args.design.read_text()) if args.design and args.design.exists() else None
459 diagram = build_diagram(result, confirm, design=design)
460 out = (
461 "```mermaid\n" + diagram["mermaid"] + "\n```\n\n"
462 "<details><summary>ASCII (plain-text fallback)</summary>\n\n"
463 "```\n" + diagram["ascii"] + "\n```\n\n</details>\n"
464 )
465 out_path = args.result.parent / "diagram.md"
466 out_path.write_text(out)
467 runtime = resolve_runtime(result, confirm) if not design else "multi-unit"
468 print(f"RESULT=ok RUNTIME={runtime}")
469 return 0
470
471
472if __name__ == "__main__":
473 raise SystemExit(main())