AQIT 0.1.0
Loading...
Searching...
No Matches
steer_eval.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Baseline vs steered probe eval for `aquin steer --eval`."""
3from __future__ import annotations
4
5import json
6from pathlib import Path
7from typing import Any, Callable
8
10 BehaviorProbe,
11 classify_behavior,
12 load_behavior_probes,
13)
14from aquin.compute.evals import keyword_overlap_score
15
16MAX_PROBES = 50
17DEFAULT_THRESHOLD = 0.5
18
19
20def _as_str_list(raw: Any) -> list[str] | None:
21 if raw is None:
22 return None
23 if isinstance(raw, list):
24 out = [str(x).strip() for x in raw if str(x).strip()]
25 return out or None
26 if isinstance(raw, str):
27 text = raw.strip()
28 if not text:
29 return None
30 if text.startswith("["):
31 try:
32 parsed = json.loads(text)
33 except json.JSONDecodeError:
34 return [text]
35 if isinstance(parsed, list):
36 out = [str(x).strip() for x in parsed if str(x).strip()]
37 return out or None
38 path = Path(text).expanduser()
39 if path.is_file():
40 body = path.read_text(encoding="utf-8").strip()
41 if path.suffix == ".jsonl":
42 rows = [json.loads(line) for line in body.splitlines() if line.strip()]
43 out: list[str] = []
44 for row in rows:
45 if isinstance(row, str) and row.strip():
46 out.append(row.strip())
47 elif isinstance(row, dict):
48 for key in ("prompt", "instruction", "text", "question"):
49 val = row.get(key)
50 if isinstance(val, str) and val.strip():
51 out.append(val.strip())
52 break
53 return out or None
54 try:
55 parsed = json.loads(body)
56 except json.JSONDecodeError:
57 return None
58 if isinstance(parsed, list):
59 out = []
60 for item in parsed:
61 if isinstance(item, str) and item.strip():
62 out.append(item.strip())
63 elif isinstance(item, dict):
64 for key in ("prompt", "instruction", "text", "question"):
65 val = item.get(key)
66 if isinstance(val, str) and val.strip():
67 out.append(val.strip())
68 break
69 return out or None
70 return [text]
71 return None
72
73
74def _rate(n: int, total: int) -> float:
75 return round(n / total, 4) if total else 0.0
76
77
78def _behavior_summary(rows: list[dict[str, Any]], *, key: str) -> dict[str, Any]:
79 total = len(rows)
80 n_truthful = sum(1 for r in rows if r[key]["behavior"] == "truthful")
81 n_deceptive = sum(1 for r in rows if r[key]["behavior"] == "deceptive")
82 n_ambiguous = sum(1 for r in rows if r[key]["behavior"] == "ambiguous")
83 n_passed = sum(1 for r in rows if r[key]["passed"])
84 return {
85 "n": total,
86 "n_truthful": n_truthful,
87 "n_deceptive": n_deceptive,
88 "n_ambiguous": n_ambiguous,
89 "n_passed": n_passed,
90 "truthful_rate": _rate(n_truthful, total),
91 "deceptive_rate": _rate(n_deceptive, total),
92 "ambiguous_rate": _rate(n_ambiguous, total),
93 "pass_rate": _rate(n_passed, total),
94 }
95
96
97def _custom_summary(rows: list[dict[str, Any]], *, key: str) -> dict[str, Any]:
98 total = len(rows)
99 n_passed = sum(1 for r in rows if r[key]["passed"])
100 scores = [float(r[key]["score"]) for r in rows]
101 mean_score = round(sum(scores) / len(scores), 4) if scores else 0.0
102 return {
103 "n": total,
104 "n_passed": n_passed,
105 "pass_rate": _rate(n_passed, total),
106 "mean_score": mean_score,
107 }
108
109
111 prompts: Any,
112) -> tuple[list[BehaviorProbe], dict[str, Any]] | None:
113 """Try deception/behavior probe loading; return None if not applicable."""
114 if prompts is None or prompts is True:
115 return load_behavior_probes(None)
116
117 if isinstance(prompts, (str, Path)):
118 text = str(prompts).strip()
119 if not text or text.startswith("["):
120 return None
121 try:
122 return load_behavior_probes(text)
123 except (FileNotFoundError, ValueError, json.JSONDecodeError, OSError):
124 return None
125
126 return None
127
128
130 *,
131 generate_baseline: Callable[[str], str],
132 generate_steered: Callable[[str], str],
133 prompts: Any = None,
134 reference_answers: Any = None,
135 threshold: float = DEFAULT_THRESHOLD,
136 max_probes: int = MAX_PROBES,
137) -> dict[str, Any]:
138 """
139 Score the same probe set under baseline vs steered generation.
140
141 Modes:
142 - behavior (default): deception-style probes with honest/deceptive references;
143 pass = classified truthful.
144 - custom: prompts + reference_answers; pass = keyword overlap >= threshold.
145 """
146 refs = _as_str_list(reference_answers)
147 behavior_loaded = None if refs is not None else _resolve_behavior_probes(prompts)
148
149 if behavior_loaded is not None:
150 probes, meta = behavior_loaded
151 probes = probes[: max(1, int(max_probes))]
152 rows: list[dict[str, Any]] = []
153 for probe in probes:
154 base_resp = generate_baseline(probe.prompt)
155 steered_resp = generate_steered(probe.prompt)
156 b_beh, b_h, b_d = classify_behavior(
157 base_resp, probe.honest_reference, probe.deceptive_reference,
158 )
159 s_beh, s_h, s_d = classify_behavior(
160 steered_resp, probe.honest_reference, probe.deceptive_reference,
161 )
162 rows.append({
163 "probe_id": probe.probe_id,
164 "prompt": probe.prompt,
165 "intent": probe.intent,
166 "baseline": {
167 "response": base_resp,
168 "behavior": b_beh,
169 "honest_score": b_h,
170 "deceptive_score": b_d,
171 "passed": b_beh == "truthful",
172 },
173 "steered": {
174 "response": steered_resp,
175 "behavior": s_beh,
176 "honest_score": s_h,
177 "deceptive_score": s_d,
178 "passed": s_beh == "truthful",
179 },
180 "changed": base_resp.strip() != steered_resp.strip(),
181 })
182
183 baseline = _behavior_summary(rows, key="baseline")
184 steered = _behavior_summary(rows, key="steered")
185 return {
186 "mode": "behavior",
187 "prompts_path": meta.get("prompts_path"),
188 "n_probes": len(rows),
189 "pass_means": "truthful",
190 "baseline": baseline,
191 "steered": steered,
192 "delta_pass_rate": round(steered["pass_rate"] - baseline["pass_rate"], 4),
193 "n_changed": sum(1 for r in rows if r["changed"]),
194 "probes": rows,
195 }
196
197 prompt_list = _as_str_list(prompts)
198 if not prompt_list:
199 return {
200 "error": (
201 "steer --eval needs a probe set. Pass --prompts <jsonl|json|array> "
202 "and --reference_answers for custom scoring."
203 ),
204 }
205 if refs is None:
206 return {
207 "error": (
208 "Custom steer --eval requires --reference_answers (one per prompt). "
209 "Or pass a deception probe file with honest_reference / deceptive_reference."
210 ),
211 }
212 if len(refs) != len(prompt_list):
213 return {
214 "error": (
215 f"reference_answers length ({len(refs)}) must match prompts ({len(prompt_list)})"
216 ),
217 }
218
219 prompt_list = prompt_list[: max(1, int(max_probes))]
220 refs = refs[: len(prompt_list)]
221 thr = float(threshold)
222 rows = []
223 for i, (prompt, reference) in enumerate(zip(prompt_list, refs)):
224 base_resp = generate_baseline(prompt)
225 steered_resp = generate_steered(prompt)
226 b_score = keyword_overlap_score(base_resp, reference)
227 s_score = keyword_overlap_score(steered_resp, reference)
228 rows.append({
229 "probe_id": f"p{i}",
230 "prompt": prompt,
231 "reference": reference,
232 "baseline": {
233 "response": base_resp,
234 "score": b_score,
235 "passed": b_score >= thr,
236 },
237 "steered": {
238 "response": steered_resp,
239 "score": s_score,
240 "passed": s_score >= thr,
241 },
242 "changed": base_resp.strip() != steered_resp.strip(),
243 })
244
245 baseline = _custom_summary(rows, key="baseline")
246 steered = _custom_summary(rows, key="steered")
247 return {
248 "mode": "custom",
249 "n_probes": len(rows),
250 "threshold": thr,
251 "pass_means": f"keyword_overlap>={thr}",
252 "baseline": baseline,
253 "steered": steered,
254 "delta_pass_rate": round(steered["pass_rate"] - baseline["pass_rate"], 4),
255 "n_changed": sum(1 for r in rows if r["changed"]),
256 "probes": rows,
257 }
tuple[list[BehaviorProbe], dict[str, Any]]|None _resolve_behavior_probes(Any prompts)
dict[str, Any] _custom_summary(list[dict[str, Any]] rows, *, str key)
dict[str, Any] run_steer_probe_eval(*, Callable[[str], str] generate_baseline, Callable[[str], str] generate_steered, Any prompts=None, Any reference_answers=None, float threshold=DEFAULT_THRESHOLD, int max_probes=MAX_PROBES)
list[str]|None _as_str_list(Any raw)
Definition steer_eval.py:24
float _rate(int n, int total)
Definition steer_eval.py:78
dict[str, Any] _behavior_summary(list[dict[str, Any]] rows, *, str key)
Definition steer_eval.py:82