AQIT 0.1.0
Loading...
Searching...
No Matches
suppression_eval_check.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""`aquin suppression-eval --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 = "suppression-eval-check.json"
12_PNG_NAME = "suppression-eval-check.png"
13
14_STATUS_COLORS = {
15 "suppressed": "#f87171",
16 "softened": "#fbbf24",
17 "unfiltered": "#34d399",
19
20
21def _normalize_suppression_eval_result(result: dict[str, Any]) -> dict[str, Any]:
22 if not isinstance(result, dict):
23 return {"error": "Invalid suppression-eval 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 if isinstance(data.get("suppression"), dict):
32 data = data["suppression"]
33 card = result.get("card")
34 if isinstance(card, dict) and isinstance(card.get("data"), dict):
35 nested = card["data"].get("suppression")
36 if isinstance(nested, dict):
37 data = nested
38
39 return dict(data)
40
41
42def has_suppression_eval_payload(result: dict[str, Any]) -> bool:
44 if data.get("error"):
45 return False
46 return bool(data.get("topics")) or bool(data.get("baseline"))
47
48
50 result: dict[str, Any],
51 *,
52 tool_name: str | None,
53 cwd: str | Path,
54) -> tuple[Path, Path]:
55 cwd = Path(cwd)
56 json_path = cwd / _JSON_NAME
57 png_path = cwd / _PNG_NAME
58
60 payload = {
61 "saved_at": datetime.now(timezone.utc).isoformat(),
62 "tool": tool_name,
63 **flat,
64 }
65 json_path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
66 _plot_suppression_eval_check(flat, png_path)
67 return json_path, png_path
68
69
70def _plot_suppression_eval_check(result: dict[str, Any], png_path: Path) -> None:
71 import matplotlib
72
73 matplotlib.use("Agg")
74 import matplotlib.pyplot as plt
75
76 if result.get("error"):
77 fig, ax = plt.subplots(figsize=(6, 2), facecolor="#0f1117")
78 ax.axis("off")
79 ax.text(0.5, 0.5, f"Error: {result['error']}", ha="center", va="center", wrap=True, color="#e5e7eb")
80 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
81 plt.close(fig)
82 return
83
84 baseline = result.get("baseline") or {}
85 topics = result.get("topics") or []
86 n_topics = len(topics)
87 suppressed = sum(1 for t in topics if t.get("status") == "suppressed")
88 softened = sum(1 for t in topics if t.get("status") == "softened")
89
90 title = f"Suppression eval — {n_topics} topics"
91 subtitle = f"baseline len {baseline.get('mean_length', '—')} · hedge {baseline.get('mean_hedge', '—')} · {suppressed} suppressed · {softened} softened"
92
93 fig, axes = plt.subplots(1, 2, figsize=(11, max(4.5, min(10, n_topics * 0.35 + 2))), facecolor="#0f1117")
94 fig.suptitle(title, color="#e5e7eb", fontsize=11, y=1.02)
95 fig.text(0.5, 0.96, subtitle, ha="center", fontsize=8, color="#9ca3af")
96
97 _plot_suppression_scores(axes[0], topics)
98 _plot_ratio_panel(axes[1], topics)
99
100 for ax in axes:
101 ax.set_facecolor("#0f1117")
102 for spine in ax.spines.values():
103 spine.set_color("#374151")
104
105 fig.tight_layout(rect=(0, 0, 1, 0.93))
106 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
107 plt.close(fig)
108
109
110def _plot_suppression_scores(ax: Any, topics: list[dict[str, Any]]) -> None:
111 ax.set_title("Suppression score by topic", color="#e5e7eb", fontsize=10)
112
113 if not topics:
114 ax.axis("off")
115 ax.text(0.5, 0.5, "No topics", ha="center", va="center", color="#9ca3af")
116 return
117
118 rows = sorted(topics, key=lambda t: float(t.get("suppression_score") or 0), reverse=True)
119 labels = [str(t.get("topic") or "?") for t in rows]
120 vals = [float(t.get("suppression_score") or 0) for t in rows]
121 colors = [_STATUS_COLORS.get(str(t.get("status") or ""), "#78716c") for t in rows]
122
123 y_pos = list(range(len(rows)))
124 ax.barh(y_pos, vals, color=colors, height=0.65, alpha=0.9)
125 ax.set_yticks(y_pos)
126 ax.set_yticklabels(labels, fontsize=8, color="#d1d5db")
127 ax.invert_yaxis()
128 ax.set_xlim(0, 1.05)
129 ax.set_xlabel("Suppression score", color="#9ca3af", fontsize=8)
130 ax.tick_params(axis="x", colors="#6b7280", labelsize=8)
131 ax.grid(axis="x", alpha=0.2, color="#4b5563")
132 for i, v in enumerate(vals):
133 ax.text(min(v + 0.02, 0.98), i, f"{v:.2f}", va="center", fontsize=7, color="#9ca3af")
134
135
136def _plot_ratio_panel(ax: Any, topics: list[dict[str, Any]]) -> None:
137 ax.set_title("Length × hedge vs baseline", color="#e5e7eb", fontsize=10)
138
139 if not topics:
140 ax.axis("off")
141 ax.text(0.5, 0.5, "No topics", ha="center", va="center", color="#9ca3af")
142 return
143
144 labels = [str(t.get("topic") or "?")[:12] for t in topics]
145 x = list(range(len(topics)))
146 width = 0.38
147 len_ratios = [float(t.get("length_ratio") or 0) for t in topics]
148 hedge_ratios = [float(t.get("hedge_ratio") or 0) for t in topics]
149
150 ax.bar([i - width / 2 for i in x], len_ratios, width=width, color="#6366f1", alpha=0.9, label="length×")
151 ax.bar([i + width / 2 for i in x], hedge_ratios, width=width, color="#fbbf24", alpha=0.9, label="hedge×")
152 ax.axhline(1.0, color="#9ca3af", linestyle="--", linewidth=0.8, alpha=0.5)
153 ax.set_xticks(x)
154 ax.set_xticklabels(labels, fontsize=7, color="#d1d5db", rotation=30, ha="right")
155 ax.set_ylabel("Ratio vs baseline", color="#9ca3af", fontsize=8)
156 ax.legend(fontsize=7, facecolor="#0f1117", edgecolor="#374151", labelcolor="#d1d5db")
157 ax.tick_params(colors="#6b7280", labelsize=7)
158 ax.grid(axis="y", alpha=0.2, color="#4b5563")
tuple[Path, Path] write_suppression_eval_check(dict[str, Any] result, *, str|None tool_name, str|Path cwd)
dict[str, Any] _normalize_suppression_eval_result(dict[str, Any] result)
None _plot_suppression_eval_check(dict[str, Any] result, Path png_path)
bool has_suppression_eval_payload(dict[str, Any] result)
None _plot_suppression_scores(Any ax, list[dict[str, Any]] topics)
None _plot_ratio_panel(Any ax, list[dict[str, Any]] topics)