open-interpretability 0.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -0,0 +1,173 @@
1
+ """What the reviewer measures on the model.
2
+
3
+ It measures probe and counterfactual activity, the lookahead check, and the causal and
4
+ wiring readouts. :mod:`openinterp.agents.reviewer` turns these into metrics and a verdict.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from functools import partial
11
+
12
+ from openinterp.core.exceptions import ArchitectureNotSupportedError
13
+ from openinterp.core.schemas import AdversarialTestResult, ReviewPolicy
14
+ from openinterp.harness.causal import causal_metrics
15
+ from openinterp.harness.covariance import joint_ablation
16
+ from openinterp.harness.layer_major import map_layer_major, prompt_tokens
17
+ from openinterp.harness.lookahead import ablation_profile
18
+ from openinterp.harness.scan import has_first_token_floor
19
+ from openinterp.harness.units import peak_strength, submission_unit
20
+ from openinterp.harness.wiring_activations import activation_wiring
21
+
22
+
23
+ def settle_first_token_floor(submission, runner, policy: ReviewPolicy):
24
+ """Return (submission, unit, scale).
25
+
26
+ If the scan predates protocol 0.8, ``first_token_floor`` is judged here. From 0.8, the
27
+ scan judges it.
28
+ """
29
+ unit = submission_unit(submission)
30
+ scale = submission.max_activation or max(
31
+ peak_strength(runner, unit, p.base_prompt) or 0.0 for p in submission.counterfactuals
32
+ )
33
+ if submission.first_token_floor is None:
34
+ read = partial(unit.strengths, runner)
35
+ floor = has_first_token_floor(runner, read, scale, policy)
36
+ submission = submission.model_copy(update={"first_token_floor": floor})
37
+ unit = submission_unit(submission)
38
+ return submission, unit, scale
39
+
40
+
41
+ def measure_probes(runner, unit, probes, scale: float, threshold: float):
42
+ """Each probe's result, plus the measured probes and whether each was active.
43
+
44
+ A one-token prompt to a unit with a first-token floor has nothing measured: it is
45
+ recorded as unmeasured and counts in no metric.
46
+ """
47
+ results: list[AdversarialTestResult] = []
48
+ measured_probes, actual = [], []
49
+ texts = [probe.text for probe in probes]
50
+ strengths = map_layer_major(
51
+ runner, partial(peak_strength, runner, unit), texts,
52
+ tokens=prompt_tokens(runner, texts), logits=False,
53
+ ) # fmt: skip
54
+ for probe, strength in zip(probes, strengths, strict=True):
55
+ active = strength is not None and scale > 0 and strength >= threshold
56
+ if strength is not None:
57
+ measured_probes.append(probe)
58
+ actual.append(active)
59
+ results.append(
60
+ AdversarialTestResult(
61
+ kind="positive_holdout" if probe.expected_active else "hard_negative",
62
+ prompt=probe.text,
63
+ passed=strength is not None and active == probe.expected_active,
64
+ expected_active=probe.expected_active,
65
+ activation=None if strength is None else round(strength, 6),
66
+ detail=probe.rationale,
67
+ measured=strength is not None,
68
+ )
69
+ )
70
+ return results, measured_probes, actual
71
+
72
+
73
+ def measure_counterfactuals(runner, unit, submission, scale: float, policy: ReviewPolicy):
74
+ """Measure each counterfactual pair's Δ, and whether it matches the expected Δ."""
75
+ results = []
76
+ pairs = submission.counterfactuals
77
+ texts = [t for pair in pairs for t in (pair.base_prompt, pair.counterfactual_prompt)]
78
+ strengths = map_layer_major(
79
+ runner, partial(peak_strength, runner, unit), texts,
80
+ tokens=prompt_tokens(runner, texts), logits=False,
81
+ ) # fmt: skip
82
+ for pair, base, cf in zip(pairs, strengths[::2], strengths[1::2], strict=True):
83
+ both = base is not None and cf is not None
84
+ observed = (base - cf) / scale if both and scale > 0 else 0.0
85
+ results.append(
86
+ AdversarialTestResult(
87
+ kind="counterfactual",
88
+ prompt=f"{pair.base_prompt} -> {pair.counterfactual_prompt}",
89
+ passed=both and scale > 0 and _delta_agrees(pair.expected_delta, observed, policy),
90
+ expected_delta=pair.expected_delta,
91
+ observed_delta=round(observed, 6) if both else None,
92
+ measured=both,
93
+ )
94
+ )
95
+ return results
96
+
97
+
98
+ def lookahead_result(runner, unit, submission) -> AdversarialTestResult:
99
+ """Whether ablating the unit on the base prompts agrees with the claimed mechanism."""
100
+ prompts = [p.base_prompt for p in submission.counterfactuals]
101
+ measured = ablation_profile(runner, prompts, unit.ablation(runner))
102
+ claimed = submission.lookahead.is_lookahead
103
+ return AdversarialTestResult(
104
+ kind="lookahead_consistency",
105
+ prompt="; ".join(p.base_prompt for p in submission.counterfactuals),
106
+ passed=measured.is_lookahead == claimed,
107
+ detail=(
108
+ f"claimed {'lookahead' if claimed else 'retrospective'}; measured "
109
+ f"{'lookahead' if measured.is_lookahead else 'retrospective'} "
110
+ f"(KL={measured.kl_divergence:.4g}, tokens={measured.target_upcoming_tokens[:5]})"
111
+ ),
112
+ )
113
+
114
+
115
+ def causal_readouts(submission, runner, probes, policy: ReviewPolicy, scale: float):
116
+ """(causal metrics, joint ablation with the unit's correlated partners)."""
117
+ causal = causal_metrics(
118
+ submission,
119
+ runner,
120
+ freeze_downstream=policy.freeze_downstream,
121
+ null_neurons=policy.causal_null_neurons,
122
+ min_change=policy.delta_abs_tolerance * scale,
123
+ )
124
+ return causal, _joint_ablation(submission, runner, probes, policy)
125
+
126
+
127
+ def _joint_ablation(submission, runner, probes, policy):
128
+ # Ranked by co-activation lift (the scan's order); Spearman is the fallback signal.
129
+ partners: list[int] = []
130
+ for c in submission.correlated_neurons:
131
+ lifted = c.coactivation_lift_lower >= policy.min_partner_lift and (
132
+ c.coactive_tokens >= policy.min_partner_coactive
133
+ )
134
+ if (lifted or abs(c.spearman) >= policy.min_partner_correlation) and (
135
+ c.neuron_idx not in partners
136
+ ):
137
+ partners.append(c.neuron_idx)
138
+ partners = partners[: policy.joint_ablation_partners]
139
+ if not partners or submission.identifier.kind != "mlp_neuron_pole":
140
+ return None
141
+ ident = submission.identifier
142
+ prompts = [p.base_prompt for p in submission.counterfactuals]
143
+ prompts += [p.text for p in probes if p.expected_active]
144
+ return joint_ablation(runner, prompts, ident.layer, ident.neuron_idx, ident.pole, partners)
145
+
146
+
147
+ def _activation_wiring(submission, runner, probes, threshold):
148
+ """Return wiring by attribution at the unit's peak, and how many prompts it fired on.
149
+
150
+ The prompts are those that it should fire on: the counterfactuals' base prompts and
151
+ the holdouts.
152
+ """
153
+ if threshold <= 0:
154
+ return None
155
+ prompts = [p.base_prompt for p in submission.counterfactuals]
156
+ prompts += [p.text for p in probes if p.expected_active]
157
+ try:
158
+ return activation_wiring(runner, submission_unit(submission), prompts, threshold)
159
+ except ArchitectureNotSupportedError:
160
+ return None
161
+
162
+
163
+ def _delta_agrees(expected: float, observed: float, policy: ReviewPolicy) -> bool:
164
+ """Observed Δ must land within max(abs_tol, rel_tol·|expected|) of the prediction.
165
+
166
+ Non-control pairs (|expected| > abs_tol) must also have the predicted sign.
167
+ """
168
+ tolerance = max(policy.delta_abs_tolerance, policy.delta_rel_tolerance * abs(expected))
169
+ if abs(observed - expected) > tolerance:
170
+ return False
171
+ if abs(expected) <= policy.delta_abs_tolerance:
172
+ return True
173
+ return math.copysign(1, observed) == math.copysign(1, expected)
@@ -0,0 +1,498 @@
1
+ """Reviewer agent: stress-tests a submitted hypothesis against the model itself.
2
+
3
+ The LLM's only job is to write adversarial probes. :func:`evaluate_submission` computes
4
+ the verdict deterministically from measurements. It uses a :class:`ReviewPolicy`, which
5
+ is stored in the verdict. So any decision can be reproduced from the record and the model
6
+ weights (see docs/PROTOCOL.md).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+ from datetime import UTC, datetime
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from openinterp import prior_art, wordnet
17
+ from openinterp.agents.base import LLMAgent
18
+ from openinterp.agents.proposer import (
19
+ FIRST_TOKEN_UNMEASURED,
20
+ format_input_attribution,
21
+ format_review_examples,
22
+ )
23
+ from openinterp.agents.review_measurements import ( # noqa: F401 (_delta_agrees re-exported)
24
+ _activation_wiring,
25
+ _delta_agrees,
26
+ causal_readouts,
27
+ lookahead_result,
28
+ measure_counterfactuals,
29
+ measure_probes,
30
+ settle_first_token_floor,
31
+ )
32
+ from openinterp.core import costs
33
+ from openinterp.core.agent_names import same_agent
34
+ from openinterp.core.confounds import LANGUAGE, SYNTAX_NODE
35
+ from openinterp.core.fence import FENCE_RULE, fence
36
+ from openinterp.core.schemas import (
37
+ MAX_PROBES,
38
+ AdversarialTestResult,
39
+ NeuronAuditSubmission,
40
+ ReviewMetrics,
41
+ ReviewPolicy,
42
+ VerdictStatus,
43
+ VerificationVerdict,
44
+ scan_active_share,
45
+ )
46
+ from openinterp.core.stats import balanced_accuracy, effective_counts
47
+ from openinterp.harness.confounds import (
48
+ LENGTH_RATIO_WARN, # noqa: F401 - re-exported
49
+ baseline_predictions,
50
+ length_predictions,
51
+ length_ratio, # noqa: F401 - re-exported
52
+ length_warning,
53
+ )
54
+ from openinterp.harness.environment import run_environment
55
+ from openinterp.harness.hooks import NeuronHookRunner
56
+
57
+
58
+ class AdversarialProbe(BaseModel): # noqa: D101 (a docstring would join the JSON schema that the agent reads)
59
+ text: str
60
+ expected_active: bool = Field(
61
+ description="What the *hypothesis* predicts. Hard negatives are near-misses predicted "
62
+ "inactive; positive holdouts are novel cases predicted active."
63
+ )
64
+ rationale: str
65
+
66
+
67
+ def split(n: int) -> tuple[int, int]:
68
+ """The hard negatives and positive holdouts that a set of ``n`` probes asks for."""
69
+ holdouts = max(4, round(n / 3)) # the system prompt asks for at least four
70
+ return n - holdouts, holdouts
71
+
72
+
73
+ class ProbeSet(BaseModel): # noqa: D101 (a docstring would join the JSON schema that the agent reads)
74
+ probes: list[AdversarialProbe] = Field(min_length=10, max_length=MAX_PROBES)
75
+
76
+
77
+ DEFAULT_POLICY = ReviewPolicy()
78
+
79
+ # Probes in two groups of very different lengths can be told apart by length alone. Since
80
+ # protocol 0.9 the confound baseline includes that rule (issue #3525), so the reviewer is
81
+ # asked to match lengths (and warned: harness.confounds.length_warning).
82
+ LENGTH_MATCHED = (
83
+ " Give the hard negatives and the positive holdouts similar lengths in tokens. The "
84
+ "review compares the hypothesis with a rule that reads only a probe's length. If your "
85
+ "holdouts are much longer than your negatives, that rule can explain them as well as "
86
+ "the hypothesis does, and the hypothesis fails."
87
+ )
88
+ HEAD_ACTIVITY = (
89
+ "A head counts as active at a token when its write to the residual stream there is far "
90
+ "from its average write: what it attends to and copies at that token is unusual. A "
91
+ "probe is active if any of its tokens after the first is. Probes are measured on their "
92
+ "own, so any earlier context a probe relies on (e.g. a repeated phrase) must be inside "
93
+ "it.\n"
94
+ "The head reads tokens, not words. When a hypothesis talks about repeats or matches, "
95
+ "they are of tokens, and a token usually includes the space before it, so ' My' "
96
+ "(after a space) and 'My' (at the start of the text or after a newline) are different "
97
+ "tokens, while '. My' later in the text repeats '. My' earlier, however short or "
98
+ "common. Label each probe by the repeats its tokens make, not by the words you read.\n"
99
+ )
100
+
101
+ LANGUAGE_PROBES = (
102
+ "The scan corpus is labelled by language, and each example above gives its language. "
103
+ "If the hypothesis claims the feature whatever the language, include positive holdouts "
104
+ "that express it in other languages, translated. If it names or implies one language, "
105
+ "include hard negatives in another language written in the same script (e.g. Spanish "
106
+ "against Portuguese, Russian against Ukrainian) that keep the examples' words and "
107
+ "topic as far as that language allows.\n"
108
+ )
109
+
110
+
111
+ SYNTAX_PROBES = (
112
+ "The scan corpus is labelled by syntax, and each code example above gives its peak "
113
+ "token's syntax node, bracket depths, indent depth and syntax construct. Include hard "
114
+ "negatives that keep the token and change its syntactic role: `return` as a keyword, "
115
+ "inside a string and inside a comment. If the hypothesis claims a feature whatever the "
116
+ "programming language, include positive holdouts in another programming language.\n"
117
+ )
118
+
119
+
120
+ def has_syntax_labels(submission: NeuronAuditSubmission) -> bool:
121
+ """Does a top example say what syntax its peak token has?"""
122
+ return any(SYNTAX_NODE in (e.peak_labels or {}) for e in submission.top_examples)
123
+
124
+
125
+ def has_language_labels(submission: NeuronAuditSubmission) -> bool:
126
+ """Do the submission's top examples say which language each is in?"""
127
+ return any(LANGUAGE in (e.peak_labels or {}) for e in submission.top_examples)
128
+
129
+
130
+ class ReviewerAgent(LLMAgent):
131
+ """The reviewer: writes probes designed to break the hypothesis."""
132
+
133
+ role = "reviewer"
134
+ system_prompt = (
135
+ "You are an adversarial peer reviewer for mechanistic interpretability claims. Given "
136
+ "a hypothesis about what makes a neuron pole (or an attention head, or a sparse "
137
+ "dictionary latent) fire, write probes "
138
+ "designed to break it. "
139
+ "Two thirds of the probes must be hard negatives: near-miss sentences that share "
140
+ "surface features with the activating examples (same words in other senses, same "
141
+ "topic without the claimed feature, the feature in an unusual position) but which "
142
+ "the hypothesis predicts should NOT activate the pole. The rest (at least four) are "
143
+ "positive holdouts: novel contexts unlike the provided examples that the hypothesis "
144
+ "predicts SHOULD activate it, including paraphrases of the claimed feature. Label each "
145
+ "probe with what the hypothesis predicts, not with what you believe the neuron will do. "
146
+ + FENCE_RULE
147
+ )
148
+
149
+ def generate_probes(
150
+ self,
151
+ submission: NeuronAuditSubmission,
152
+ n: int = MAX_PROBES,
153
+ full_snippets: bool = False,
154
+ related: str = "",
155
+ ) -> ProbeSet:
156
+ """Ask the reviewer for ``n`` probes."""
157
+ prompt = self.probe_prompt(submission, n, full_snippets, related)
158
+ return self.structured(ProbeSet, prompt)
159
+
160
+ @staticmethod
161
+ def probe_prompt(
162
+ submission: NeuronAuditSubmission,
163
+ n: int = MAX_PROBES,
164
+ full_snippets: bool = False,
165
+ related: str = "",
166
+ ) -> str:
167
+ """Return the task text that the reviewer answers.
168
+
169
+ It shows no activations of candidate probes. ``related`` is what other analyses
170
+ link to the unit, so that the reviewer can probe where they disagree. That is
171
+ published latents aligned with the unit
172
+ (:func:`openinterp.crossref.format_for_reviewer`), and its studied sister neurons
173
+ (:func:`openinterp.sisters.format_sisters`). The submission's prior art is always
174
+ shown (:func:`openinterp.prior_art.format_for_reviewer`).
175
+ """
176
+ mechanism = "lookahead" if submission.lookahead.is_lookahead else "retrospective"
177
+ claim = (
178
+ f"Hypothesis: {submission.natural_language_explanation}\n"
179
+ f"Claimed activating tokens: {submission.activating_tokens}\n"
180
+ f"Claimed mechanism: {mechanism}"
181
+ f" {submission.lookahead.target_upcoming_tokens or ''}"
182
+ )
183
+ is_head = submission.identifier.kind == "attn_head"
184
+ context = ""
185
+ if submission.input_attribution is not None:
186
+ context = format_input_attribution(submission.input_attribution)
187
+ if context:
188
+ context += (
189
+ "Probes are measured on their own, so a probe contains only the context "
190
+ "you write into it: vary the tokens above, not just the peak token.\n"
191
+ )
192
+ return (
193
+ f"{submission.identifier.noun.capitalize()}: {submission.identifier}\n"
194
+ + (HEAD_ACTIVITY if is_head else "")
195
+ + (FIRST_TOKEN_UNMEASURED if submission.first_token_floor else "")
196
+ + f"The submitted hypothesis, which your probes test:\n{fence(claim)}\n"
197
+ + format_review_examples(submission.top_examples, full_snippets)
198
+ + (LANGUAGE_PROBES if has_language_labels(submission) else "")
199
+ + (SYNTAX_PROBES if has_syntax_labels(submission) else "")
200
+ + wordnet.format_for_reviewer(submission.top_examples, wordnet.lexicon())
201
+ + (f"{context}\n" if context else "")
202
+ + (f"{related}\n" if related else "")
203
+ + (f"{art}\n" if (art := prior_art.format_for_reviewer(submission.prior_art)) else "")
204
+ + "Write {} probes: {} hard negatives and {} positive holdouts.".format(n, *split(n))
205
+ + LENGTH_MATCHED
206
+ )
207
+
208
+ @staticmethod
209
+ def token_check_prompt(
210
+ submission: NeuronAuditSubmission, probes: Sequence[AdversarialProbe], tokenizer
211
+ ) -> str:
212
+ """Return a second-pass prompt for the reviewer: its probes as the model's tokens.
213
+
214
+ Then the reviewer can fix labels that it gave by reading words. It shows no
215
+ activations: only the tokenizer runs. For a head, each probe also lists the token
216
+ runs that repeat an earlier one.
217
+ """
218
+ is_head = submission.identifier.kind == "attn_head"
219
+ blocks = []
220
+ for n, probe in enumerate(probes):
221
+ ids = tokenizer.encode(probe.text)
222
+ tokens = [tokenizer.decode([i]) for i in ids]
223
+ label = "active" if probe.expected_active else "inactive"
224
+ lines = [
225
+ f"{n}. expected {label}: {probe.text!r}",
226
+ f" tokens: {' | '.join(repr(t) for t in tokens)}",
227
+ ]
228
+ if is_head:
229
+ runs = [f"{''.join(tokens[a:b])!r} ({b - a})" for a, b in repeated_runs(ids)]
230
+ lines.append(f" repeated token runs: {', '.join(runs) or 'none'}")
231
+ blocks.append("\n".join(lines))
232
+ repeats = (
233
+ "Each 'repeated token runs' entry is a longest run of tokens that also occurs "
234
+ "earlier in the probe, with its length in tokens.\n"
235
+ if is_head
236
+ else ""
237
+ )
238
+ unequal = length_warning(probes, tokenizer, bool(submission.first_token_floor))
239
+ rebalance = f"{unequal} Rewrite some probes to match the lengths. " if unequal else ""
240
+ return (
241
+ ReviewerAgent.probe_prompt(submission, len(probes))
242
+ + "\n\nYou wrote the probes below. Here they are as the model's tokens. "
243
+ + repeats
244
+ + rebalance
245
+ + "Check each label against what the hypothesis predicts for these tokens, not "
246
+ "for the words. Where a label is wrong, rewrite the probe so it tests what you "
247
+ "meant (preferred, to keep the share of hard negatives), or relabel it. Leave "
248
+ "the other probes unchanged and write the whole probe set again.\n\n"
249
+ + fence("\n\n".join(blocks))
250
+ )
251
+
252
+ def review(
253
+ self,
254
+ submission: NeuronAuditSubmission,
255
+ runner: NeuronHookRunner,
256
+ *,
257
+ probes: Sequence[AdversarialProbe] | None = None,
258
+ policy: ReviewPolicy = DEFAULT_POLICY,
259
+ ) -> VerificationVerdict:
260
+ """Get probes (unless given), measure them and return the verdict."""
261
+ if probes is None:
262
+ probes = self.generate_probes(submission).probes
263
+ return evaluate_submission(submission, runner, probes, self.model, policy)
264
+
265
+
266
+ def repeated_runs(ids: Sequence[int]) -> list[tuple[int, int]]:
267
+ """Maximal runs ``ids[a:b]`` that also occur ending earlier in ``ids``, as ``(a, b)``.
268
+
269
+ ``match[j]`` is the longest run ending at ``j`` that ends at some earlier position
270
+ too; a run is reported where it stops growing.
271
+ """
272
+ match = [0] * len(ids)
273
+ for j in range(len(ids)):
274
+ for i in range(j):
275
+ k = 0
276
+ while k <= i and ids[i - k] == ids[j - k]:
277
+ k += 1
278
+ match[j] = max(match[j], k)
279
+ return [
280
+ (j - match[j] + 1, j + 1)
281
+ for j in range(len(ids))
282
+ if match[j] and (j + 1 == len(ids) or match[j + 1] != match[j] + 1)
283
+ ]
284
+
285
+
286
+ def evaluate_submission(
287
+ submission: NeuronAuditSubmission,
288
+ runner: NeuronHookRunner,
289
+ probes: Sequence[AdversarialProbe],
290
+ reviewer_model: str,
291
+ policy: ReviewPolicy = DEFAULT_POLICY,
292
+ *,
293
+ wire: bool = True,
294
+ ) -> VerificationVerdict:
295
+ """Measure ``probes`` and the submission's own claims, and decide.
296
+
297
+ ``wire=False`` skips the activation wiring (reported, not gated), for example when
298
+ reproducing a record.
299
+ """
300
+ lap = costs.laps()
301
+ submission, unit, scale = settle_first_token_floor(submission, runner, policy)
302
+ threshold = policy.activation_threshold * scale
303
+ results, measured_probes, actual = measure_probes(runner, unit, probes, scale, threshold)
304
+ results += measure_counterfactuals(runner, unit, submission, scale, policy)
305
+ results.append(lookahead_result(runner, unit, submission))
306
+ lap("probe_measurement")
307
+ metrics = review_metrics(
308
+ results,
309
+ policy,
310
+ independent=not same_agent(reviewer_model, submission.submitting_agent_model),
311
+ baseline=confound_baseline(submission, runner, measured_probes, actual, threshold, policy),
312
+ active_share=scan_active_share(submission, policy),
313
+ first_token_floor=submission.first_token_floor,
314
+ )
315
+ lap("gates")
316
+ causal, joint = causal_readouts(submission, runner, probes, policy, scale)
317
+ lap("causal_metrics")
318
+ wiring = _activation_wiring(submission, runner, probes, threshold) if wire else None
319
+ if wire:
320
+ lap("wiring")
321
+ return VerificationVerdict(
322
+ status=decide(metrics, policy),
323
+ confidence_score=metrics.balanced_accuracy,
324
+ adversarial_test_results=results,
325
+ reviewer_agent_model=reviewer_model,
326
+ timestamp=datetime.now(UTC),
327
+ causal_metrics=causal,
328
+ metrics=metrics,
329
+ policy=policy,
330
+ joint_ablation=joint,
331
+ environment=run_environment(runner),
332
+ wiring=wiring,
333
+ notes=_summary(results, metrics),
334
+ )
335
+
336
+
337
+ def _probe_score(
338
+ probes: Sequence[AdversarialProbe],
339
+ predicted: Sequence[bool],
340
+ actual: Sequence[bool],
341
+ policy: ReviewPolicy,
342
+ ) -> float:
343
+ """Return the balanced accuracy of ``predicted`` vs ``actual`` activity.
344
+
345
+ It is split by the hypothesis's labels (hard negatives / holdouts), with near-duplicate
346
+ probes clustered.
347
+ """
348
+
349
+ def subset(expected: bool):
350
+ rows = [
351
+ (p.text, pr == a)
352
+ for p, pr, a in zip(probes, predicted, actual, strict=True)
353
+ if p.expected_active == expected
354
+ ]
355
+ return effective_counts(
356
+ [t for t, _ in rows], [ok for _, ok in rows], policy.probe_cluster_threshold
357
+ )
358
+
359
+ return balanced_accuracy(subset(False), subset(True))
360
+
361
+
362
+ def confound_baseline(
363
+ submission: NeuronAuditSubmission,
364
+ runner: NeuronHookRunner,
365
+ probes: Sequence[AdversarialProbe],
366
+ actual: Sequence[bool],
367
+ threshold: float,
368
+ policy: ReviewPolicy,
369
+ ) -> tuple[str, float] | None:
370
+ """Return the best trivial explanation of the probes' activity: (name, accuracy).
371
+
372
+ The candidates are the token-identity and position rules, and "never active". Since
373
+ protocol 0.9 they also include each "at least n tokens" rule of probe length (#3525).
374
+ ``None`` when the submission has no confound profile (scan predates protocol 0.3).
375
+ """
376
+ if submission.confounds is None or runner.tokenizer is None or not probes:
377
+ return None
378
+ texts = [p.text for p in probes]
379
+ skip_first = bool(submission.first_token_floor)
380
+ candidates = baseline_predictions(
381
+ submission.confounds, runner.tokenizer, texts, threshold, skip_first
382
+ )
383
+ candidates |= length_predictions(runner.tokenizer, texts, skip_first) # protocol 0.9
384
+ candidates["never active"] = [False] * len(probes)
385
+ return best_baseline(candidates, probes, actual, policy)
386
+
387
+
388
+ def best_baseline(
389
+ candidates: dict[str, Sequence[bool]],
390
+ probes: Sequence[AdversarialProbe],
391
+ actual: Sequence[bool],
392
+ policy: ReviewPolicy,
393
+ ) -> tuple[str, float]:
394
+ """The candidate rule that explains the probes' activity best: (name, balanced accuracy).
395
+
396
+ Ties go to the first name in sorted order.
397
+ """
398
+ scored = {name: _probe_score(probes, pred, actual, policy) for name, pred in candidates.items()}
399
+ best = max(sorted(scored), key=lambda name: scored[name])
400
+ return best, round(scored[best], 6)
401
+
402
+
403
+ def _pass_rate(results: Sequence[AdversarialTestResult], kinds: set[str]) -> float | None:
404
+ subset = [r for r in results if r.kind in kinds and r.measured]
405
+ return sum(r.passed for r in subset) / len(subset) if subset else None
406
+
407
+
408
+ def review_metrics(
409
+ results: Sequence[AdversarialTestResult],
410
+ policy: ReviewPolicy,
411
+ *,
412
+ independent: bool,
413
+ baseline: tuple[str, float] | None = None,
414
+ active_share: float | None = None,
415
+ first_token_floor: bool | None = None,
416
+ ) -> ReviewMetrics:
417
+ """The rates and gates of the decision rule, from the measured results (docs/PROTOCOL.md §4)."""
418
+
419
+ def counts(kind: str):
420
+ rows = [r for r in results if r.kind == kind and r.measured]
421
+ return effective_counts(
422
+ [r.prompt for r in rows], [r.passed for r in rows], policy.probe_cluster_threshold
423
+ )
424
+
425
+ negatives, holdouts = counts("hard_negative"), counts("positive_holdout")
426
+ reproduction = _pass_rate(results, {"counterfactual"})
427
+ lookahead = all(r.passed for r in results if r.kind == "lookahead_consistency")
428
+ balanced = balanced_accuracy(negatives, holdouts)
429
+ # The baseline's accuracy is stored rounded, so compare at the same precision. A tie
430
+ # passes (protocol 0.8). Before 0.8, 10/11 lost to a rounded-up 10/11.
431
+ beats = not policy.require_confound_control or (
432
+ baseline is not None and round(balanced, 6) >= baseline[1]
433
+ )
434
+
435
+ gates = _gates(
436
+ policy, negatives, holdouts, reproduction, lookahead, independent, beats, active_share
437
+ )
438
+ return ReviewMetrics(
439
+ specificity=None if negatives.rate is None else round(negatives.rate, 6),
440
+ sensitivity=None if holdouts.rate is None else round(holdouts.rate, 6),
441
+ balanced_accuracy=round(balanced, 6),
442
+ counterfactual_reproduction=reproduction,
443
+ lookahead_consistent=lookahead,
444
+ independent_review=independent,
445
+ gates=gates,
446
+ effective_hard_negatives=negatives.clusters,
447
+ effective_holdouts=holdouts.clusters,
448
+ confound_baseline=baseline[0] if baseline else None,
449
+ confound_baseline_accuracy=baseline[1] if baseline else None,
450
+ active_share=None if active_share is None else round(active_share, 6),
451
+ first_token_floor=first_token_floor,
452
+ )
453
+
454
+
455
+ def _gates(
456
+ policy, negatives, holdouts, reproduction, lookahead, independent, beats, active_share
457
+ ) -> dict[str, bool]:
458
+ return {
459
+ "enough_hard_negatives": negatives.clusters >= policy.min_hard_negatives,
460
+ "enough_holdouts": holdouts.clusters >= policy.min_holdouts,
461
+ "specificity": (negatives.rate or 0.0) >= policy.min_specificity,
462
+ "sensitivity": (holdouts.rate or 0.0) >= policy.min_sensitivity,
463
+ "counterfactual_reproduction": (reproduction or 0.0)
464
+ >= policy.min_counterfactual_reproduction,
465
+ "lookahead_consistent": lookahead,
466
+ "independent_review": independent or not policy.require_independent_review,
467
+ "beats_confound_baseline": beats,
468
+ "not_default_on": active_share is None or active_share < policy.max_active_share,
469
+ }
470
+
471
+
472
+ def decide(metrics: ReviewMetrics, policy: ReviewPolicy) -> VerdictStatus:
473
+ """VERIFIED if every gate passes; REJECTED if adequately probed yet near chance."""
474
+ if all(metrics.gates.values()):
475
+ return "VERIFIED"
476
+ adequately_probed = metrics.gates["enough_hard_negatives"] and metrics.gates["enough_holdouts"]
477
+ if adequately_probed and metrics.balanced_accuracy < policy.reject_below_balanced_accuracy:
478
+ return "REJECTED"
479
+ return "NEEDS_REVISION"
480
+
481
+
482
+ def _summary(results: Sequence[AdversarialTestResult], metrics: ReviewMetrics) -> str:
483
+ counts: dict[str, list[int]] = {}
484
+ for r in results:
485
+ passed, total = counts.setdefault(r.kind, [0, 0])
486
+ counts[r.kind] = [passed + r.passed, total + 1]
487
+ tally = "; ".join(f"{kind}: {p}/{t} passed" for kind, (p, t) in counts.items())
488
+ failed = [gate for gate, ok in metrics.gates.items() if not ok]
489
+ return tally + (f". Failed gates: {', '.join(failed)}." if failed else ". All gates passed.")
490
+
491
+
492
+ def probes_from_verdict(verdict: VerificationVerdict) -> list[AdversarialProbe]:
493
+ """Recover the probe set stored in a verdict, to re-evaluate it under another policy."""
494
+ return [
495
+ AdversarialProbe(text=r.prompt, expected_active=bool(r.expected_active), rationale=r.detail)
496
+ for r in verdict.adversarial_test_results
497
+ if r.kind in {"hard_negative", "positive_holdout"}
498
+ ]