AQIT 0.1.0
Loading...
Searching...
No Matches
audit_check.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""`aquin audit --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 = "audit-check.json"
12_PNG_NAME = "audit-check.png"
13
14_STATUS_COLORS = {
15 "suppressed": "#f87171",
16 "softened": "#fbbf24",
17 "unfiltered": "#34d399",
19
20
21def _normalize_audit_result(result: dict[str, Any]) -> dict[str, Any]:
22 if not isinstance(result, dict):
23 return {"error": "Invalid audit result"}
24
25 data = result
26 card = result.get("card")
27 if isinstance(card, dict) and isinstance(card.get("data"), dict):
28 data = {**card["data"], "model_id": result.get("model_id") or card["data"].get("model_id")}
29 elif isinstance(result.get("content"), dict):
30 data = result["content"]
31
32 if result.get("error"):
33 data = {**data, "error": result["error"]}
34 return data
35
36
37def has_audit_payload(result: dict[str, Any]) -> bool:
38 data = _normalize_audit_result(result)
39 return any(data.get(k) for k in ("consistency", "suppression", "boundary"))
40
43 result: dict[str, Any],
44 *,
45 tool_name: str | None,
46 cwd: str | Path,
47) -> tuple[Path, Path]:
48 cwd = Path(cwd)
49 json_path = cwd / _JSON_NAME
50 png_path = cwd / _PNG_NAME
51
52 flat = _normalize_audit_result(result)
53 payload = {
54 "saved_at": datetime.now(timezone.utc).isoformat(),
55 "tool": tool_name,
56 **flat,
57 }
58 json_path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
59 _plot_audit_check(flat, png_path)
60 return json_path, png_path
61
62
63def _plot_audit_check(result: dict[str, Any], png_path: Path) -> None:
64 import matplotlib
65
66 matplotlib.use("Agg")
67 import matplotlib.pyplot as plt
68
69 if result.get("error") and not has_audit_payload(result):
70 fig, ax = plt.subplots(figsize=(6, 2), facecolor="#0f1117")
71 ax.axis("off")
72 ax.text(0.5, 0.5, f"Error: {result['error']}", ha="center", va="center", wrap=True, color="#e5e7eb")
73 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
74 plt.close(fig)
75 return
76
77 model = str(result.get("model_id") or "")
78 title = f"Audit — {model}" if model else "Audit"
79
80 fig, axes = plt.subplots(1, 3, figsize=(12, 4.8), facecolor="#0f1117")
81 fig.suptitle(title, color="#e5e7eb", fontsize=11, y=1.02)
82
83 _plot_consistency_panel(axes[0], result.get("consistency") or {})
84 _plot_suppression_panel(axes[1], result.get("suppression") or {})
85 _plot_boundary_panel(axes[2], result.get("boundary") or {})
86
87 for ax in axes:
88 ax.set_facecolor("#0f1117")
89 for spine in ax.spines.values():
90 spine.set_color("#374151")
91
92 fig.tight_layout()
93 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
94 plt.close(fig)
95
96
97def _plot_consistency_panel(ax: Any, data: dict[str, Any]) -> None:
98 ax.set_title("Consistency", color="#e5e7eb", fontsize=10)
99
100 score = data.get("consistency_score")
101 query = _trunc(str(data.get("query") or ""), 40)
102 variants = data.get("variants") or []
103
104 if score is None and not variants:
105 ax.axis("off")
106 ax.text(0.5, 0.5, "No data", ha="center", va="center", color="#9ca3af")
107 return
108
109 if score is not None:
110 color = _score_color(float(score))
111 ax.barh([0], [float(score)], color=color, height=0.5, alpha=0.92)
112 ax.set_xlim(0, 1.05)
113 ax.set_yticks([0])
114 ax.set_yticklabels(["score"], fontsize=9, color="#d1d5db")
115 ax.set_xlabel("Consistency (0–1)", color="#9ca3af", fontsize=8)
116 ax.text(min(float(score) + 0.02, 0.98), 0, f"{float(score):.3f}", va="center", fontsize=8, color="#9ca3af")
117 if query:
118 ax.set_title(f"Consistency\n{query}", color="#e5e7eb", fontsize=9)
119 elif variants:
120 labels = [f"v{i}" for i in range(len(variants))]
121 kls = [float(v.get("kl_from_anchor") or 0) for v in variants]
122 ax.barh(labels, kls, color="#6366f1", height=0.65, alpha=0.9)
123 ax.invert_yaxis()
124 ax.set_xlabel("KL from anchor", color="#9ca3af", fontsize=8)
125
126 ax.tick_params(colors="#6b7280", labelsize=8)
127 ax.grid(axis="x", alpha=0.2, color="#4b5563")
128
129
130def _plot_suppression_panel(ax: Any, data: dict[str, Any]) -> None:
131 ax.set_title("Suppression", color="#e5e7eb", fontsize=10)
132 topics = data.get("topics") or []
133
134 if not topics:
135 ax.axis("off")
136 ax.text(0.5, 0.5, "No data", ha="center", va="center", color="#9ca3af")
137 return
138
139 rows = sorted(topics, key=lambda t: float(t.get("suppression_score") or 0), reverse=True)[:8]
140 labels = [str(t.get("topic") or "?") for t in rows]
141 vals = [float(t.get("suppression_score") or 0) for t in rows]
142 colors = [_STATUS_COLORS.get(str(t.get("status") or ""), "#78716c") for t in rows]
143
144 y_pos = list(range(len(rows)))
145 ax.barh(y_pos, vals, color=colors, height=0.65, alpha=0.9)
146 ax.set_yticks(y_pos)
147 ax.set_yticklabels(labels, fontsize=8, color="#d1d5db")
148 ax.invert_yaxis()
149 ax.set_xlim(0, 1.05)
150 ax.set_xlabel("Suppression score", color="#9ca3af", fontsize=8)
151 ax.tick_params(axis="x", colors="#6b7280", labelsize=8)
152 ax.grid(axis="x", alpha=0.2, color="#4b5563")
153 for i, v in enumerate(vals):
154 ax.text(min(v + 0.02, 0.98), i, f"{v:.2f}", va="center", fontsize=7, color="#9ca3af")
155
156
157def _plot_boundary_panel(ax: Any, data: dict[str, Any]) -> None:
158 mean_rob = data.get("mean_robustness")
159 probes = data.get("probes") or []
160 ax.set_title("Boundary / robustness", color="#e5e7eb", fontsize=10)
162 if not probes and mean_rob is None:
163 ax.axis("off")
164 ax.text(0.5, 0.5, "No data", ha="center", va="center", color="#9ca3af")
165 return
166
167 if len(probes) <= 1 and mean_rob is not None:
168 color = _score_color(float(mean_rob))
169 ax.barh([0], [float(mean_rob)], color=color, height=0.5, alpha=0.92)
170 ax.set_xlim(0, 1.05)
171 ax.set_yticks([0])
172 ax.set_yticklabels(["mean"], fontsize=9, color="#d1d5db")
173 ax.set_xlabel("Robustness (0–1)", color="#9ca3af", fontsize=8)
174 ax.text(min(float(mean_rob) + 0.02, 0.98), 0, f"{float(mean_rob):.3f}", va="center", fontsize=8, color="#9ca3af")
175 ax.tick_params(colors="#6b7280", labelsize=8)
176 ax.grid(axis="x", alpha=0.2, color="#4b5563")
177 return
178
179 labels = [_trunc(str(p.get("prompt") or ""), 18) for p in probes[:8]]
180 vals = [float(p.get("robustness_score") or 0) for p in probes[:8]]
181 colors = [_score_color(v) for v in vals]
182 y_pos = list(range(len(labels)))
183 ax.barh(y_pos, vals, color=colors, height=0.65, alpha=0.9)
184 ax.set_yticks(y_pos)
185 ax.set_yticklabels(labels, fontsize=7, color="#d1d5db")
186 ax.invert_yaxis()
187 ax.set_xlim(0, 1.05)
188 ax.set_xlabel("Robustness", color="#9ca3af", fontsize=8)
189 ax.tick_params(axis="x", colors="#6b7280", labelsize=7)
190 ax.grid(axis="x", alpha=0.2, color="#4b5563")
191
192
193def _score_color(val: float) -> str:
194 if val >= 0.75:
195 return "#34d399"
196 if val >= 0.5:
197 return "#fbbf24"
198 return "#f87171"
199
200
201def _trunc(text: str, limit: int) -> str:
202 t = text.strip()
203 if len(t) <= limit:
204 return t or "·"
205 return t[: limit - 1] + "…"
None _plot_suppression_panel(Any ax, dict[str, Any] data)
None _plot_consistency_panel(Any ax, dict[str, Any] data)
str _trunc(str text, int limit)
None _plot_audit_check(dict[str, Any] result, Path png_path)
None _plot_boundary_panel(Any ax, dict[str, Any] data)
bool has_audit_payload(dict[str, Any] result)
tuple[Path, Path] write_audit_check(dict[str, Any] result, *, str|None tool_name, str|Path cwd)
dict[str, Any] _normalize_audit_result(dict[str, Any] result)
str _score_color(float val)