"""Render the MuleGuard system architecture + data-flow diagrams to PNG.""" from __future__ import annotations import matplotlib matplotlib.use("Agg") import matplotlib.patches as mpatches import matplotlib.pyplot as plt from matplotlib.patches import FancyArrowPatch, FancyBboxPatch from src import config NAVY, BLUE, TEAL, RED, GREY = "#1f4e79", "#2e86c1", "#16a085", "#c0392b", "#7f8c8d" def _box(ax, x, y, w, h, text, color, tcolor="white", fs=10): ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.02,rounding_size=0.08", fc=color, ec="white", lw=1.5)) ax.text(x + w / 2, y + h / 2, text, ha="center", va="center", color=tcolor, fontsize=fs, weight="bold", wrap=True) def _arrow(ax, x1, y1, x2, y2, color=GREY): ax.add_patch(FancyArrowPatch((x1, y1), (x2, y2), arrowstyle="-|>", mutation_scale=16, color=color, lw=1.8)) def architecture() -> None: fig, ax = plt.subplots(figsize=(12, 6.8)) ax.set_xlim(0, 12); ax.set_ylim(0, 7); ax.axis("off") ax.text(6, 6.7, "MuleGuard — System Architecture", ha="center", fontsize=15, weight="bold", color=NAVY) # Ingestion _box(ax, 0.3, 4.6, 2.4, 1.6, "INGESTION\n\n• Financial txns\n• FMS / TMS alerts\n• Govt cyber-fraud\n tickets (I4C/NCRP)\n• Cross-channel data", NAVY, fs=8.5) # Feature pipeline _box(ax, 3.2, 4.7, 2.3, 1.4, "FEATURE PIPELINE\n\nclean · impute ·\none-hot · select\n+ anomaly score", BLUE, fs=9) # Model _box(ax, 6.0, 4.7, 2.3, 1.4, "ML MODEL\n\nLightGBM +\nIsolationForest\ncalibrated · SHAP", TEAL, fs=9) # Artifacts _box(ax, 6.0, 2.7, 2.3, 1.2, "ARTIFACTS\nmodel · threshold\nfeatures · explainer", GREY, fs=8.5) # Scoring API _box(ax, 8.9, 4.7, 2.7, 1.4, "SCORING API (FastAPI)\n\n/score → risk 0-100,\ntier, decision,\nreason codes, alert", RED, fs=9) # Simulator _box(ax, 3.2, 2.6, 2.3, 1.3, "FEED SIMULATOR\nstreams accounts\n& tickets → API", BLUE, fs=9) # Dashboard _box(ax, 8.9, 2.5, 2.7, 1.5, "ANALYST CONSOLE (Streamlit)\nalert queue · drill-down\nexplanations · model perf", NAVY, fs=8.5) # Outcome _box(ax, 4.4, 0.5, 3.2, 1.1, "OUTCOME: block circulation of\nfraudulent proceeds via mule accounts", RED, fs=9.5) _arrow(ax, 2.7, 5.4, 3.2, 5.4) _arrow(ax, 5.5, 5.4, 6.0, 5.4) _arrow(ax, 8.3, 5.4, 8.9, 5.4) _arrow(ax, 7.15, 4.7, 7.15, 3.9) # model -> artifacts _arrow(ax, 5.5, 3.25, 6.0, 3.25, BLUE) # simulator -> artifacts/api path _arrow(ax, 4.35, 4.7, 4.35, 3.9, GREY) # pipeline <-> simulator _arrow(ax, 10.25, 4.7, 10.25, 4.0, RED) # api -> dashboard _arrow(ax, 10.25, 2.5, 7.6, 1.6, NAVY) # dashboard -> outcome _arrow(ax, 8.9, 3.25, 8.3, 5.0, GREY) # artifacts -> api (load) fig.tight_layout() out = config.ROOT / "docs" / "architecture.png" fig.savefig(out, dpi=140, bbox_inches="tight"); plt.close(fig) print("Wrote", out) def pipeline_flow() -> None: fig, ax = plt.subplots(figsize=(12, 2.6)) ax.set_xlim(0, 12); ax.set_ylim(0, 2); ax.axis("off") steps = [("Raw\n3,924 feats", NAVY), ("Drop leakage\n(F3912, F2230)", RED), ("Clean +\nencode", BLUE), ("Impute +\nanomaly score", TEAL), ("CV feature\nselection → 103", BLUE), ("LightGBM +\ncalibration", TEAL), ("Threshold\ntuning (F2)", GREY), ("Risk 0-100\n+ SHAP", NAVY)] w = 1.32; gap = (12 - len(steps) * w) / (len(steps) + 1) x = gap for i, (t, c) in enumerate(steps): _box(ax, x, 0.6, w, 0.9, t, c, fs=7.5) if i < len(steps) - 1: _arrow(ax, x + w, 1.05, x + w + gap, 1.05) x += w + gap ax.text(6, 1.8, "Modeling Pipeline", ha="center", fontsize=13, weight="bold", color=NAVY) fig.tight_layout() out = config.ROOT / "docs" / "pipeline_flow.png" fig.savefig(out, dpi=140, bbox_inches="tight"); plt.close(fig) print("Wrote", out) if __name__ == "__main__": architecture() pipeline_flow()