MuleGuard / docs /make_architecture.py
MuleGuard
MuleGuard: end-to-end mule-account detection + HF Space deploy
af879c2
Raw
History Blame Contribute Delete
4.1 kB
"""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()