sre-incident-env / scripts /generate_state_graphs.py
Maverick98's picture
Add Gradio landing UI + Playground + pre-rendered state graphs
d131948
#!/usr/bin/env python3
"""Pre-generate state-graph SVGs for every scenario.
Calls mermaid.ink once per scenario, validates the response is a real SVG,
writes to outputs/state_graphs/<scenario_id>.svg.
Run this once before deploying:
uv run python scripts/generate_state_graphs.py
The Gradio UI then serves these files via the /state_graphs/ static mount,
so there's no per-request network call to mermaid.ink at runtime.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import httpx
# Make project root importable
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from server.ui_data import build_mermaid, load_scenarios, mermaid_to_url
OUT_DIR = Path(__file__).resolve().parent.parent / "outputs" / "state_graphs"
def fetch_svg(mermaid_code: str) -> str | None:
"""Hit mermaid.ink, return SVG text or None on failure."""
url = mermaid_to_url(mermaid_code)
try:
resp = httpx.get(url, timeout=20.0, follow_redirects=True)
except Exception as e:
print(f" ERROR: HTTP request failed: {e}")
return None
if resp.status_code != 200:
print(f" ERROR: status {resp.status_code}")
return None
text = resp.text
if not text.lstrip().startswith("<svg"):
print(f" ERROR: response doesn't start with <svg (first 80 chars): {text[:80]}")
return None
# Drop the @import that some browsers/iframes block
text = re.sub(r"@import url\([^)]+\);", "", text)
# Merge our sizing rules into the existing <svg style="..."> attribute,
# or add a new one if missing — never duplicate.
extra = "max-width:100%;height:auto;display:block;margin:0 auto"
if re.search(r'<svg[^>]*\sstyle="', text):
text = re.sub(
r'(<svg[^>]*\sstyle=")([^"]*)(")',
lambda m: f'{m.group(1)}{extra};{m.group(2)}{m.group(3)}',
text,
count=1,
)
else:
text = re.sub(
r"<svg([^>]*)>",
rf'<svg\1 style="{extra}">',
text,
count=1,
)
return text
def validate_svg(svg_text: str, scenario_id: str) -> tuple[bool, list[str]]:
"""Sanity checks on a generated SVG.
Returns (ok, list of issues).
"""
issues: list[str] = []
if len(svg_text) < 1000:
issues.append(f"too small ({len(svg_text)} bytes)")
if "<svg" not in svg_text:
issues.append("missing <svg> root tag")
if "</svg>" not in svg_text:
issues.append("missing </svg> closing tag")
# Mermaid renders graph-related elements
if 'class="flowchart"' not in svg_text and "flowchart" not in svg_text:
issues.append("no flowchart class — mermaid may have failed to parse")
# Check that we got at least a few node rects
rect_count = svg_text.count("<rect")
if rect_count < 3:
issues.append(f"only {rect_count} <rect> elements (expected ≥ 3 for state nodes)")
return (len(issues) == 0, issues)
def main() -> int:
OUT_DIR.mkdir(parents=True, exist_ok=True)
scenarios = load_scenarios()
print(f"Generating state-graph SVGs for {len(scenarios)} scenarios → {OUT_DIR}")
print("=" * 70)
successes = 0
failures: list[str] = []
for s in scenarios:
sid = s["id"]
print(f"\n[{sid}]")
mermaid_src = build_mermaid(s)
print(f" mermaid LOC: {len(mermaid_src.splitlines())}")
svg = fetch_svg(mermaid_src)
if svg is None:
print(f" ✗ FAILED to fetch")
failures.append(sid)
continue
ok, issues = validate_svg(svg, sid)
if not ok:
print(f" ✗ VALIDATION FAILED:")
for issue in issues:
print(f" - {issue}")
failures.append(sid)
continue
out_path = OUT_DIR / f"{sid}.svg"
out_path.write_text(svg)
print(f" ✓ wrote {out_path.name} ({len(svg):,} bytes, {svg.count('<rect')} rects, {svg.count('<path')} paths)")
successes += 1
print("\n" + "=" * 70)
print(f"SUMMARY: {successes}/{len(scenarios)} succeeded")
if failures:
print(f"FAILED: {failures}")
return 1
print("All state graphs generated and validated.")
return 0
if __name__ == "__main__":
sys.exit(main())