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,450 @@
1
+ """Reproduce an experiment record from its JSON before it merges.
2
+
3
+ The command is `open-interpretability reproduce`.
4
+
5
+ `open-interpretability check` only validates a record's shape. This runs again every
6
+ measurement that the verdict rests on. It uses the model revision and dtype that the record pins:
7
+
8
+ 1. The scan evidence: each top example's per-token strengths. Also the scale
9
+ (``max_activation``) that every "active" threshold is a fraction of.
10
+ 2. Each counterfactual pair's observed Δ.
11
+ 3. The review itself, with the record's stored probes and its own ``ReviewPolicy`` (no
12
+ LLM calls). That is every probe's activation and pass/fail, every gate and the status.
13
+
14
+ Given the scan's corpus, it also draws the record's corpus sample again (protocol 0.7),
15
+ from its seed and token budget. It checks that the sample hashes the same and holds
16
+ every top example.
17
+
18
+ Numbers must agree within ``atol × scale + rtol × |recorded|``. Anything the decision
19
+ rule reads (pass/fail, gates, status) must agree exactly. Causal metrics, joint
20
+ ablation and explanation scores are reported but not gated, so a difference there is
21
+ only a warning. Explanation scores are re-derived from re-measured windows and the
22
+ judge's stored answers; the judge and embedding model are not re-run.
23
+
24
+ A record made in a 16-bit dtype (bfloat16 or float16) is reproduced in float32. Its
25
+ numbers are compared within :data:`HALF_TOLERANCE`, because 16-bit rounding moves a
26
+ number by a few percent from one device to another (issue #2995). float32 is also about
27
+ 10 times faster than bfloat16 on a CPU. ``half_precision.py`` has the other rules.
28
+
29
+ A record judged under another protocol version can't be reproduced by this harness:
30
+ its decision rule was different. Re-evaluate it instead (`open-interpretability reevaluate`). A
31
+ library upgrade can still shift its activations. So ``measurements_only`` compares its
32
+ scan evidence and counterfactuals, and leaves out the review.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import hashlib
38
+ import math
39
+ from collections.abc import Sequence
40
+ from dataclasses import dataclass, field
41
+ from datetime import date
42
+ from functools import partial
43
+ from pathlib import Path
44
+
45
+ from openinterp.core.schemas import PROTOCOL_VERSION, AuditRecord, RunEnvironment
46
+ from openinterp.half_precision import HALF, recorded_pass, rederived, reproduction_dtype
47
+ from openinterp.harness.environment import run_environment
48
+ from openinterp.harness.hooks import NeuronHookRunner
49
+ from openinterp.harness.layer_major import map_layer_major, prompt_tokens
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Tolerance:
54
+ """Allowed |reproduced − recorded| = atol × scale + rtol × |recorded|.
55
+
56
+ The defaults absorb float32 differences between devices and library versions
57
+ (records round activations to 5–6 decimals) but not an edited number.
58
+ """
59
+
60
+ atol: float = 1e-3 # fraction of the neuron's max activation
61
+ rtol: float = 1e-2
62
+
63
+ def close(self, recorded: float | None, reproduced: float | None, scale: float) -> bool:
64
+ """True when ``reproduced`` is within tolerance of ``recorded``, or both are ``None``."""
65
+ if recorded is None or reproduced is None:
66
+ return recorded is None and reproduced is None
67
+ return abs(reproduced - recorded) <= self.atol * scale + self.rtol * abs(recorded)
68
+
69
+ def for_dtype(self, dtype: str) -> Tolerance:
70
+ """This tolerance, widened to :data:`HALF_TOLERANCE` for a record in a 16-bit dtype."""
71
+ if dtype not in HALF:
72
+ return self
73
+ return Tolerance(
74
+ atol=max(self.atol, HALF_TOLERANCE.atol), rtol=max(self.rtol, HALF_TOLERANCE.rtol)
75
+ )
76
+
77
+
78
+ DEFAULT_TOLERANCE = Tolerance()
79
+ # Set from cross-device re-measurements (docs/analyses/half-precision-reproduce.md).
80
+ # At a peak it allows 7%, so a 10% edit of a peak still fails.
81
+ HALF_TOLERANCE = Tolerance(atol=0.05, rtol=0.02)
82
+ # Tokens of one top example that a 16-bit record may have outside HALF_TOLERANCE. Deep
83
+ # layers on unusual text can move a few tokens by up to 22% of the peak; 3 was the most.
84
+ HALF_STRAY_TOKENS = 4
85
+
86
+
87
+ @dataclass
88
+ class ReproductionReport:
89
+ """The result of reproducing one record: the mismatches, the warnings and what was compared."""
90
+
91
+ path: Path | None
92
+ record: AuditRecord
93
+ mismatches: list[str] = field(default_factory=list)
94
+ warnings: list[str] = field(default_factory=list)
95
+ compared: int = 0
96
+
97
+ @property
98
+ def ok(self) -> bool:
99
+ """True when nothing that the decision rule reads differed."""
100
+ return not self.mismatches
101
+
102
+ def fail(self, message: str) -> None:
103
+ """Record a mismatch."""
104
+ self.mismatches.append(message)
105
+
106
+
107
+ def pinned_environment(record: AuditRecord) -> tuple[str | None, str]:
108
+ """(model revision, dtype) to load the model with, from the record's own pins."""
109
+ env = record.verdict.environment if record.verdict else None
110
+ prov = record.submission.provenance
111
+ revision = (env and env.model_revision) or (prov and prov.model_revision)
112
+ dtype = (env and env.dtype) or (prov and prov.dtype) or "float32"
113
+ return revision, dtype
114
+
115
+
116
+ def check_protocol(record: AuditRecord) -> str | None:
117
+ """Why this harness can't reproduce ``record``'s verdict, or ``None`` if it can."""
118
+ if record.verdict is None:
119
+ return "record has no verdict"
120
+ policy = record.verdict.policy
121
+ version = policy.protocol_version if policy else "< 0.2"
122
+ if version != PROTOCOL_VERSION:
123
+ return (
124
+ f"judged under protocol {version}, but this harness implements "
125
+ f"{PROTOCOL_VERSION}; reproduce it with that openinterp version, or re-evaluate it"
126
+ )
127
+ return None
128
+
129
+
130
+ def rotating_sample(paths: Sequence[Path], size: int, day: date) -> list[Path]:
131
+ """Return the ``size`` records of ``paths`` that ``day`` selects.
132
+
133
+ The order comes from a hash of each path, so one day's records are spread over models
134
+ and layers. A new record doesn't move the others in the order. Consecutive days take
135
+ consecutive windows, so a registry of N records is covered in N / ``size`` days.
136
+ """
137
+ order = sorted(paths, key=lambda p: hashlib.sha256(Path(p).as_posix().encode()).hexdigest())
138
+ if size >= len(order):
139
+ return order
140
+ start = day.toordinal() * size % len(order)
141
+ return (order + order)[start : start + size]
142
+
143
+
144
+ def reproduce_record(
145
+ record: AuditRecord,
146
+ runner: NeuronHookRunner,
147
+ tolerance: Tolerance = DEFAULT_TOLERANCE,
148
+ path: Path | None = None,
149
+ corpus: tuple[list[str], str] | None = None,
150
+ *,
151
+ measurements_only: bool = False,
152
+ ) -> ReproductionReport:
153
+ """Reproduce ``record`` with ``runner``.
154
+
155
+ ``corpus`` is (documents, SHA-256 of the corpus file), to draw the scan's sample from.
156
+ With ``measurements_only``, a record from another protocol version gets its scan
157
+ evidence and counterfactuals compared, and a warning that the review was left out.
158
+ """
159
+ report = ReproductionReport(path=path, record=record)
160
+ reason = check_protocol(record)
161
+ if reason and (record.verdict is None or not measurements_only):
162
+ report.fail(reason)
163
+ return report
164
+ from openinterp.harness.units import submission_unit
165
+
166
+ sub, verdict = record.submission, record.verdict
167
+ unit = submission_unit(sub)
168
+ half = pinned_environment(record)[1] in HALF
169
+ tolerance = tolerance.for_dtype(pinned_environment(record)[1])
170
+ _compare_environment(record, run_environment(runner), report)
171
+
172
+ scale = sub.max_activation or 0.0
173
+ _check_scale(sub, unit, runner, scale, report)
174
+
175
+ # 1. Scan evidence.
176
+ if corpus is not None:
177
+ _check_sample(sub.provenance, sub.top_examples, runner, *corpus, report)
178
+ stray = HALF_STRAY_TOKENS if half else 0
179
+ _check_examples(sub.top_examples, unit, runner, tolerance, scale, report, stray)
180
+
181
+ # 2. Counterfactuals as submitted.
182
+ _check_counterfactuals(sub, unit, runner, tolerance, scale, report)
183
+ if reason:
184
+ report.warnings.append(f"measurements only, because the record was {reason}")
185
+ return report
186
+
187
+ # 3. The review, under the record's own policy and reviewer.
188
+ _compare_verdicts(verdict, _review_again(record, runner), tolerance, scale, report, half=half)
189
+
190
+ # 4. Explanation scores (reported, not gated): re-measure the windows, keep the answers.
191
+ if verdict.explanation_scores is not None:
192
+ _compare_explanation_scores(
193
+ verdict.explanation_scores, runner, unit, tolerance, scale, report
194
+ )
195
+ return report
196
+
197
+
198
+ def _review_again(record: AuditRecord, runner):
199
+ """The record's probes and pairs measured again, under its own policy and reviewer."""
200
+ from openinterp.agents.reviewer import evaluate_submission, probes_from_verdict
201
+
202
+ verdict = record.verdict
203
+ return evaluate_submission(
204
+ record.submission,
205
+ runner,
206
+ probes_from_verdict(verdict),
207
+ verdict.reviewer_agent_model,
208
+ verdict.policy,
209
+ wire=False, # reported, not gated: nothing to compare
210
+ )
211
+
212
+
213
+ def _check_scale(sub, unit, runner, scale: float, report) -> None:
214
+ """``max_activation`` is the top example's peak, and the first-token floor reproduces."""
215
+ peak = max((e.max_activation for e in sub.top_examples), default=None)
216
+ if peak is not None and not math.isclose(scale, peak, rel_tol=1e-9, abs_tol=1e-12):
217
+ report.fail(f"max_activation {scale} is not the top example's peak {peak}")
218
+ if sub.first_token_floor is not None: # judged against the scan's scale (protocol 0.8)
219
+ from functools import partial
220
+
221
+ from openinterp.core.schemas import ReviewPolicy
222
+ from openinterp.harness.scan import has_first_token_floor
223
+
224
+ read = partial(unit.strengths, runner)
225
+ if has_first_token_floor(runner, read, scale, ReviewPolicy()) != sub.first_token_floor:
226
+ report.fail(f"first_token_floor {sub.first_token_floor} does not reproduce")
227
+
228
+
229
+ def _check_counterfactuals(sub, unit, runner, tolerance: Tolerance, scale: float, report):
230
+ from openinterp.agents.proposer import observed_delta
231
+
232
+ pairs = [(n, p) for n, p in enumerate(sub.counterfactuals, 1) if p.observed_delta is not None]
233
+ if not scale:
234
+ return
235
+ texts = [t for _, p in pairs for t in (p.base_prompt, p.counterfactual_prompt)]
236
+ deltas = map_layer_major(
237
+ runner, lambda item: observed_delta(runner, unit, item[1], scale), pairs,
238
+ tokens=prompt_tokens(runner, texts), logits=False,
239
+ ) # fmt: skip
240
+ for (n, pair), delta in zip(pairs, deltas, strict=True):
241
+ report.compared += 1
242
+ if not tolerance.close(pair.observed_delta, delta, 1.0):
243
+ report.fail(
244
+ f"counterfactual {n}: observed_delta recorded {pair.observed_delta}, "
245
+ f"reproduced {delta}"
246
+ )
247
+
248
+
249
+ def _check_examples(
250
+ examples, unit, runner, tolerance: Tolerance, scale: float, report, stray: int = 0
251
+ ) -> None:
252
+ """Each top example's tokens and activations, measured layer-major when streamed.
253
+
254
+ Up to ``stray`` tokens of each example may differ beyond ``tolerance``, as a warning.
255
+ """
256
+ texts = [ex.text for ex in examples]
257
+ measured = map_layer_major(
258
+ runner, partial(unit.strengths, runner), texts,
259
+ tokens=prompt_tokens(runner, texts), logits=False,
260
+ ) # fmt: skip
261
+ for n, (ex, strengths) in enumerate(zip(examples, measured, strict=True), 1):
262
+ _check_example(n, ex, unit, runner, tolerance, scale, report, strengths, stray)
263
+
264
+
265
+ def _check_example(
266
+ n: int, ex, unit, runner, tolerance: Tolerance, scale: float, report, strengths, stray=0
267
+ ) -> None:
268
+ """Top example ``n``'s tokens and per-token activations (``strengths``), and its peak."""
269
+ tokens = runner.encode(ex.text)["input_ids"][0].tolist()
270
+ words = [runner.tokenizer.decode([i]) for i in tokens] if runner.tokenizer else None
271
+ if words is not None and words != ex.tokens:
272
+ report.fail(f"top example {n}: tokens differ from the record")
273
+ return
274
+ values = strengths[0].tolist()
275
+ report.compared += len(values)
276
+ bad = [
277
+ i
278
+ for i, (a, b) in enumerate(zip(ex.activations, values, strict=False))
279
+ if not tolerance.close(a, b, scale)
280
+ ]
281
+ if bad and len(bad) <= stray and len(values) == len(ex.activations):
282
+ report.warnings.append(
283
+ f"top example {n}: {len(bad)} token(s) differ beyond the 16-bit tolerance, "
284
+ f"e.g. token {bad[0]} (recorded {ex.activations[bad[0]]}, "
285
+ f"reproduced {round(values[bad[0]], 6)})"
286
+ )
287
+ bad = []
288
+ if len(values) != len(ex.activations) or bad:
289
+ where = f"token {bad[0]}" if bad else "length"
290
+ report.fail(
291
+ f"top example {n}: activations differ at {where} "
292
+ f"(recorded {ex.activations[bad[0]] if bad else len(ex.activations)}, "
293
+ f"reproduced {round(values[bad[0]], 6) if bad else len(values)})"
294
+ )
295
+ measured = values[int(unit.skip_first) :]
296
+ if not tolerance.close(ex.max_activation, max(measured, default=0.0), scale):
297
+ report.fail(f"top example {n}: max_activation differs")
298
+
299
+
300
+ def _check_sample(prov, examples, runner, texts, corpus_sha, report) -> None:
301
+ """Re-draw the record's corpus sample: the same documents, holding every top example."""
302
+ from openinterp.corpus import sample_tokens
303
+
304
+ sample = prov and prov.corpus_sample
305
+ if sample is None:
306
+ return
307
+ if corpus_sha != prov.corpus_sha256:
308
+ report.warnings.append("the corpus given is not the scan's; its sample was not checked")
309
+ return
310
+ drawn, redrawn = sample_tokens(runner.tokenizer, texts, sample.max_tokens, sample.seed)
311
+ if redrawn != sample:
312
+ report.fail(f"corpus sample recorded {sample}, re-drawn {redrawn}")
313
+ return
314
+ for n, ex in enumerate(examples, 1):
315
+ if not any(ex.text in doc for doc in drawn):
316
+ report.fail(f"top example {n} is not in the corpus sample")
317
+
318
+
319
+ def _compare_explanation_scores(
320
+ old, runner, unit, tolerance: Tolerance, scale: float, report
321
+ ) -> None:
322
+ from openinterp.harness.explanation_scores import remeasure
323
+
324
+ new = remeasure(old, runner, unit)
325
+ for kind in ("detection", "fuzzing"):
326
+ pairs = zip(getattr(old, f"{kind}_windows"), getattr(new, f"{kind}_windows"), strict=True)
327
+ for n, (a, b) in enumerate(pairs, 1):
328
+ report.compared += 1
329
+ same = len(a.activations) == len(b.activations) and all(
330
+ tolerance.close(x, y, scale)
331
+ for x, y in zip(a.activations, b.activations, strict=True)
332
+ )
333
+ if not same or a.label != b.label:
334
+ report.warnings.append(
335
+ f"explanation scores: {kind} window {n} re-measures differently "
336
+ f"(label {a.label} -> {b.label})"
337
+ )
338
+ for name in ("detection", "fuzzing", "embedding_auroc"):
339
+ if getattr(old, name) != getattr(new, name):
340
+ report.warnings.append(f"explanation scores: {name} differs (recorded, not gated)")
341
+
342
+
343
+ def _compare_environment(record: AuditRecord, now: RunEnvironment, report) -> None:
344
+ revision, dtype = pinned_environment(record)
345
+ if revision and now.model_revision and revision != now.model_revision:
346
+ report.fail(f"model revision {now.model_revision} is not the pinned {revision}")
347
+ if dtype not in {"auto", now.dtype} and reproduction_dtype(dtype) != now.dtype:
348
+ report.fail(f"loaded {now.dtype} weights; the record pins {dtype}")
349
+ elif dtype in HALF and now.dtype != dtype:
350
+ report.warnings.append(
351
+ f"reproduced in {now.dtype}; the record pins {dtype}, so its numbers are compared "
352
+ "within the 16-bit tolerance"
353
+ )
354
+ then = record.verdict.environment
355
+ if then is None:
356
+ report.warnings.append("record predates environment pinning; device and versions unknown")
357
+ return
358
+ names = ("device", "device_name", "torch_version", "transformers_version")
359
+ for name in (*names, "experts_implementation"):
360
+ if name == "device_name" and then.device_name is None: # recorded before 0.0.19
361
+ continue
362
+ if getattr(then, name) != getattr(now, name):
363
+ report.warnings.append(
364
+ f"{name}: recorded {getattr(then, name)}, now {getattr(now, name)}"
365
+ )
366
+
367
+
368
+ def _compare_verdicts(
369
+ old, new, tolerance: Tolerance, scale: float, report, *, half: bool = False
370
+ ) -> None:
371
+ """Each test's numbers and result, then the decision.
372
+
373
+ For a 16-bit record (``half``), a result may flip where 16-bit rounding moves a number
374
+ across its threshold. The flip is only a warning when the numbers agree within
375
+ ``tolerance`` and the recorded result follows from the recorded number. The recorded
376
+ decision must then follow exactly from the recorded results.
377
+ """
378
+ if len(old.adversarial_test_results) != len(new.adversarial_test_results):
379
+ report.fail("the record's test results don't match the probes and pairs it stores")
380
+ return
381
+ flips = 0
382
+ for n, (a, b) in enumerate(
383
+ zip(old.adversarial_test_results, new.adversarial_test_results, strict=True), 1
384
+ ):
385
+ flips += _compare_result(n, a, b, old.policy, tolerance, scale, report, half)
386
+
387
+ if flips and old.metrics is None:
388
+ report.fail("a result flipped, and the record has no metrics to re-derive")
389
+ elif flips:
390
+ _compare_decision(old, rederived(old), report)
391
+ if (old.metrics and old.metrics.gates) != new.metrics.gates or old.status != new.status:
392
+ report.warnings.append(
393
+ f"reproduced decision {new.status} with gates {new.metrics.gates}; the "
394
+ "recorded decision follows from the recorded results"
395
+ )
396
+ else:
397
+ _compare_decision(old, new, report)
398
+
399
+
400
+ def _compare_result(n, a, b, policy, tolerance, scale: float, report, half: bool) -> int:
401
+ """Compare recorded test ``a`` with reproduced test ``b``. Return 1 for a 16-bit flip."""
402
+ label = f"test {n} ({a.kind}: {a.prompt[:40]!r})"
403
+ if (a.kind, a.prompt) != (b.kind, b.prompt):
404
+ report.fail(f"{label}: does not match the stored probe")
405
+ return 0
406
+ report.compared += 1
407
+ close = tolerance.close(a.activation, b.activation, scale)
408
+ if not close:
409
+ report.fail(f"{label}: activation recorded {a.activation}, reproduced {b.activation}")
410
+ if not tolerance.close(a.observed_delta, b.observed_delta, 1.0):
411
+ close = False
412
+ report.fail(f"{label}: Δ recorded {a.observed_delta}, reproduced {b.observed_delta}")
413
+ follows = recorded_pass(a, policy, scale)
414
+ if half and follows not in {None, a.passed}:
415
+ report.fail(f"{label}: recorded passed={a.passed} does not follow from its number")
416
+ elif a.passed != b.passed:
417
+ if half and close and follows is not None:
418
+ report.warnings.append(
419
+ f"{label}: passed={a.passed} recorded, {b.passed} reproduced, at its "
420
+ "threshold within 16-bit rounding"
421
+ )
422
+ return 1
423
+ report.fail(f"{label}: recorded passed={a.passed}, reproduced passed={b.passed}")
424
+ return 0
425
+
426
+
427
+ def _compare_decision(old, new, report) -> None:
428
+ """Gates, balanced accuracy and status must match; causal metrics only warn."""
429
+ if old.metrics is None or old.metrics.gates != new.metrics.gates:
430
+ report.fail(
431
+ f"gates recorded {old.metrics.gates if old.metrics else None}, "
432
+ f"reproduced {new.metrics.gates}"
433
+ )
434
+ if not math.isclose(old.confidence_score, new.confidence_score, abs_tol=1e-6):
435
+ report.fail(
436
+ f"balanced accuracy recorded {old.confidence_score}, reproduced {new.confidence_score}"
437
+ )
438
+ if old.status != new.status:
439
+ report.fail(f"status recorded {old.status}, reproduced {new.status}")
440
+
441
+ oc, nc = old.causal_metrics, new.causal_metrics
442
+ if (oc is None) != (nc is None) or (
443
+ oc
444
+ and nc
445
+ and (
446
+ abs(oc.sufficiency_score - nc.sufficiency_score) > 0.1
447
+ or abs(oc.necessity_score - nc.necessity_score) > 0.1
448
+ )
449
+ ):
450
+ report.warnings.append("causal metrics differ (recorded, not gated)")
openinterp/revision.py ADDED
@@ -0,0 +1,45 @@
1
+ """What a revision's proposer sees of the record that it supersedes (issue #3246).
2
+
3
+ ``open-interpretability audit --supersedes <record>`` revises an earlier hypothesis about the same
4
+ unit. Without this, the proposer wrote its new hypothesis from the scan alone and could
5
+ repeat the old mistake. The brief gives the old hypothesis, its verdict, the gates it
6
+ failed and the probes it got wrong. The proposer may see the measurements: only the
7
+ reviewer must not.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from openinterp.core.fence import fence
13
+
14
+ MAX_WRONG = 12 # wrong probes listed, so a long probe set doesn't flood the prompt
15
+ PROBES = ("hard_negative", "positive_holdout")
16
+
17
+
18
+ def previous_attempt(record) -> str:
19
+ """Return the brief on ``record`` for the proposer of the hypothesis that supersedes it."""
20
+ verdict = record.verdict
21
+ gates = verdict.metrics.gates if verdict.metrics else {}
22
+ failed = [gate for gate, ok in gates.items() if not ok]
23
+ wrong = [
24
+ r
25
+ for r in verdict.adversarial_test_results
26
+ if r.kind in PROBES and r.measured and not r.passed
27
+ ]
28
+ lines = [
29
+ "This hypothesis revises an earlier one about this unit. Fix what failed, and "
30
+ "keep what the earlier review confirmed.",
31
+ f"Earlier hypothesis:\n{fence(record.submission.natural_language_explanation)}",
32
+ f"Its verdict: {verdict.status}. Failed gates: {', '.join(failed) or 'none'}.",
33
+ ]
34
+ if wrong:
35
+ lines.append(f"Probes it got wrong ({min(len(wrong), MAX_WRONG)} of {len(wrong)}):")
36
+ lines += [_probe_line(r) for r in wrong[:MAX_WRONG]]
37
+ return "\n".join(lines) + "\n"
38
+
39
+
40
+ def _probe_line(result) -> str:
41
+ """One wrong probe: what the hypothesis predicted, and what was measured."""
42
+ predicted = "active" if result.expected_active else "inactive"
43
+ measured = "n/a" if result.activation is None else f"{result.activation:.2f}"
44
+ kind = result.kind.replace("_", " ")
45
+ return f"- {kind}, predicted {predicted}, activation {measured}: {result.prompt!r}"
@@ -0,0 +1,35 @@
1
+ """Keep loaded models across commands that run in one process (``open-interpretability loop``).
2
+
3
+ Each ``open-interpretability`` command loads its model afresh, which is right for one command
4
+ per process. ``open-interpretability loop`` runs many commands in one process, so it turns this
5
+ cache on. Then each experiment on a model after the first skips the load. The cache is off by
6
+ default.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import functools
12
+
13
+ _runners: dict[tuple, object] | None = None # None: the cache is off
14
+
15
+
16
+ def keep() -> None:
17
+ """Turn the cache on for the rest of this process."""
18
+ global _runners
19
+ if _runners is None:
20
+ _runners = {}
21
+
22
+
23
+ def cached(load):
24
+ """Wrap ``load`` so that, while the cache is on, equal arguments return one runner."""
25
+
26
+ @functools.wraps(load)
27
+ def wrapper(*args, **kwargs):
28
+ if _runners is None:
29
+ return load(*args, **kwargs)
30
+ key = (args, tuple(sorted(kwargs.items())))
31
+ if key not in _runners:
32
+ _runners[key] = load(*args, **kwargs)
33
+ return _runners[key]
34
+
35
+ return wrapper