AQIT 0.1.0
Loading...
Searching...
No Matches
red_team_check.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""`aquin red-team --check` — save JSON + diagram to the current working directory."""
3
4from __future__ import annotations
5
6import json
7from datetime import datetime, timezone
8from pathlib import Path
9from typing import Any
10
11_JSON_NAME = "red-team-check.json"
12_PNG_NAME = "red-team-check.png"
13
14_STATUS_COLORS = {
15 "pass": "#34d399",
16 "warn": "#fbbf24",
17 "fail": "#f87171",
19
20
21def _normalize_red_team_result(result: dict[str, Any]) -> dict[str, Any]:
22 if not isinstance(result, dict):
23 return {"error": "Invalid red-team result"}
24
25 if result.get("error"):
26 return dict(result)
27
28 data = result
29 if isinstance(result.get("content"), dict):
30 data = result["content"]
31 card = result.get("card")
32 if isinstance(card, dict) and isinstance(card.get("data"), dict):
33 data = card["data"]
34
35 return dict(data)
36
37
38def has_red_team_payload(result: dict[str, Any]) -> bool:
39 data = _normalize_red_team_result(result)
40 if data.get("error"):
41 return False
42 return bool(data.get("vectors")) or data.get("composite_score") is not None
43
44
45def _json_safe_payload(flat: dict[str, Any]) -> dict[str, Any]:
46 """Omit bulky per-probe raw blobs from check export."""
47 out = dict(flat)
48 vectors = []
49 for row in flat.get("vectors") or []:
50 if not isinstance(row, dict):
51 continue
52 slim = {k: v for k, v in row.items() if k != "raw"}
53 vectors.append(slim)
54 out["vectors"] = vectors
55 return out
56
57
59 result: dict[str, Any],
60 *,
61 tool_name: str | None,
62 cwd: str | Path,
63) -> tuple[Path, Path]:
64 cwd = Path(cwd)
65 json_path = cwd / _JSON_NAME
66 png_path = cwd / _PNG_NAME
67
68 flat = _normalize_red_team_result(result)
69 payload = {
70 "saved_at": datetime.now(timezone.utc).isoformat(),
71 "tool": tool_name,
72 **_json_safe_payload(flat),
73 }
74 json_path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
75 _plot_red_team_check(flat, png_path)
76 return json_path, png_path
77
78
79def _plot_red_team_check(result: dict[str, Any], png_path: Path) -> None:
80 import matplotlib
81
82 matplotlib.use("Agg")
83 import matplotlib.pyplot as plt
84
85 if result.get("error"):
86 fig, ax = plt.subplots(figsize=(6, 2), facecolor="#0f1117")
87 ax.axis("off")
88 ax.text(0.5, 0.5, f"Error: {result['error']}", ha="center", va="center", wrap=True, color="#e5e7eb")
89 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
90 plt.close(fig)
91 return
92
93 model = str(result.get("model_id") or "")
94 composite = float(result.get("composite_score") or 0)
95 vectors = result.get("vectors") or []
96 n_pass = sum(1 for v in vectors if v.get("status") == "pass")
97 n_warn = sum(1 for v in vectors if v.get("status") == "warn")
98 n_fail = sum(1 for v in vectors if v.get("status") == "fail")
99
100 title = f"Red team — {model}" if model else "Red team"
101 subtitle = f"composite {composite:.0%} · {n_pass} pass · {n_warn} warn · {n_fail} fail"
102
103 fig, axes = plt.subplots(1, 2, figsize=(11, max(4.5, min(10, len(vectors) * 0.45 + 2))), facecolor="#0f1117")
104 fig.suptitle(title, color="#e5e7eb", fontsize=11, y=1.02)
105 fig.text(0.5, 0.96, subtitle, ha="center", fontsize=8, color="#9ca3af")
106
107 _plot_vector_scores(axes[0], vectors)
108 _plot_composite_panel(axes[1], composite, n_pass, n_warn, n_fail)
109
110 for ax in axes:
111 ax.set_facecolor("#0f1117")
112 for spine in ax.spines.values():
113 spine.set_color("#374151")
114
115 fig.tight_layout(rect=(0, 0, 1, 0.93))
116 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
117 plt.close(fig)
118
119
120def _plot_vector_scores(ax: Any, vectors: list[dict[str, Any]]) -> None:
121 ax.set_title("Attack vector robustness", color="#e5e7eb", fontsize=10)
122
123 if not vectors:
124 ax.axis("off")
125 ax.text(0.5, 0.5, "No vectors", ha="center", va="center", color="#9ca3af")
126 return
127
128 rows = sorted(vectors, key=lambda v: float(v.get("score") or 0))
129 labels = [str(v.get("label") or v.get("id") or "?") for v in rows]
130 vals = [float(v.get("score") or 0) for v in rows]
131 colors = [_STATUS_COLORS.get(str(v.get("status") or ""), "#78716c") for v in rows]
132
133 y_pos = list(range(len(rows)))
134 ax.barh(y_pos, vals, color=colors, height=0.65, alpha=0.9)
135 ax.set_yticks(y_pos)
136 ax.set_yticklabels(labels, fontsize=8, color="#d1d5db")
137 ax.invert_yaxis()
138 ax.set_xlim(0, 1.05)
139 ax.set_xlabel("Robustness score (0–1)", color="#9ca3af", fontsize=8)
140 ax.tick_params(axis="x", colors="#6b7280", labelsize=8)
141 ax.grid(axis="x", alpha=0.2, color="#4b5563")
142 for i, v in enumerate(vals):
143 ax.text(min(v + 0.02, 0.98), i, f"{v:.0%}", va="center", fontsize=7, color="#9ca3af")
144
145
146def _plot_composite_panel(ax: Any, composite: float, n_pass: int, n_warn: int, n_fail: int) -> None:
147 ax.set_title("Composite & status mix", color="#e5e7eb", fontsize=10)
148
149 ax.barh([0], [composite], color=_score_color(composite), height=0.35, alpha=0.92)
150 ax.set_xlim(0, 1.05)
151 ax.set_yticks([0])
152 ax.set_yticklabels(["composite"], fontsize=9, color="#d1d5db")
153 ax.set_xlabel("Composite score", color="#9ca3af", fontsize=8)
154 ax.text(min(composite + 0.02, 0.98), 0, f"{composite:.0%}", va="center", fontsize=9, color="#9ca3af")
155 ax.tick_params(axis="x", colors="#6b7280", labelsize=8)
156 ax.grid(axis="x", alpha=0.2, color="#4b5563")
157
158 counts = [n_pass, n_warn, n_fail]
159 if any(counts):
160 inset = ax.inset_axes([0.08, 0.08, 0.84, 0.42])
161 inset.set_facecolor("#0f1117")
162 labels = ["pass", "warn", "fail"]
163 colors = [_STATUS_COLORS[k] for k in labels]
164 x = list(range(3))
165 inset.bar(x, counts, color=colors, width=0.6, alpha=0.9)
166 inset.set_xticks(x)
167 inset.set_xticklabels(labels, fontsize=8, color="#d1d5db")
168 inset.tick_params(axis="y", colors="#6b7280", labelsize=7)
169 for spine in inset.spines.values():
170 spine.set_color("#374151")
171
172
173def _score_color(val: float) -> str:
174 if val >= 0.65:
175 return "#34d399"
176 if val >= 0.35:
177 return "#fbbf24"
178 return "#f87171"
None _plot_red_team_check(dict[str, Any] result, Path png_path)
bool has_red_team_payload(dict[str, Any] result)
None _plot_vector_scores(Any ax, list[dict[str, Any]] vectors)
str _score_color(float val)
tuple[Path, Path] write_red_team_check(dict[str, Any] result, *, str|None tool_name, str|Path cwd)
dict[str, Any] _normalize_red_team_result(dict[str, Any] result)
dict[str, Any] _json_safe_payload(dict[str, Any] flat)
None _plot_composite_panel(Any ax, float composite, int n_pass, int n_warn, int n_fail)