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,148 @@
1
+ """Neuronpedia's explanations through the adversarial review.
2
+
3
+ The command is `open-interpretability neuronpedia-review`.
4
+
5
+ The score baseline (:mod:`openinterp.neuronpedia_baseline`) asks how well an explanation
6
+ predicts held-out text. This asks whether it survives a reviewer who writes probes to
7
+ break it. Each latent that Neuronpedia explains gets a copy of its newest submission.
8
+ The copy has Neuronpedia's explanation as the hypothesis. It claims no activating tokens
9
+ and a retrospective mechanism, because Neuronpedia's text claims nothing else. A reviewer
10
+ then writes probes from that copy alone, and the harness measures them.
11
+
12
+ Neuronpedia makes no counterfactual predictions. So the comparison uses the gates that
13
+ the probes decide (``PROBE_GATES``), for both explanations. The records stay out of the
14
+ registry: they never change a unit's standing.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Sequence
20
+ from statistics import mean
21
+
22
+ from pydantic import BaseModel
23
+
24
+ from openinterp.baseline import sign_test
25
+
26
+ # Gates that the proposer's own claims decide, not the probes: its predicted edits
27
+ # (counterfactual_reproduction) and its claimed mechanism (lookahead_consistent).
28
+ # independent_review is about who proposed. Neuronpedia claims none of these.
29
+ NOT_PROBE_GATES = frozenset(
30
+ {"counterfactual_reproduction", "lookahead_consistent", "independent_review"}
31
+ )
32
+ METRICS = ("balanced_accuracy", "specificity", "sensitivity")
33
+
34
+
35
+ def probe_gates(gates: dict[str, bool]) -> dict[str, bool]:
36
+ """The gates of ``gates`` that the probes decide."""
37
+ return {k: v for k, v in gates.items() if k not in NOT_PROBE_GATES}
38
+
39
+
40
+ class ReviewRow(BaseModel):
41
+ """One latent: the agent's newest review against the review of Neuronpedia's text."""
42
+
43
+ slug: str
44
+ experiment_id: str # the agent's experiment, whose submission was copied
45
+ url: str | None
46
+ agent_status: str
47
+ agent_explanation: str
48
+ neuronpedia_explanation: str
49
+ explainer: str
50
+ reviewer_model: str | None = None # None while the probes are missing
51
+ agent: dict
52
+ neuronpedia: dict | None = None
53
+
54
+
55
+ def side(metrics) -> dict:
56
+ """The probe metrics and probe gates of one verdict's ``metrics``."""
57
+ gates = probe_gates(metrics.gates)
58
+ return {
59
+ **{name: getattr(metrics, name) for name in METRICS},
60
+ "gates": gates,
61
+ "probe_verified": all(gates.values()),
62
+ }
63
+
64
+
65
+ def report(rows: Sequence[ReviewRow]) -> str:
66
+ """Return the comparison as a Markdown page."""
67
+ done = [r for r in rows if r.neuronpedia is not None]
68
+ agent_ok = sum(r.agent["probe_verified"] for r in done)
69
+ np_ok = sum(r.neuronpedia["probe_verified"] for r in done)
70
+ lines = [
71
+ "# Neuronpedia explanations through the adversarial review",
72
+ "",
73
+ "Generated by `open-interpretability neuronpedia-review`. Do not edit. Method: "
74
+ "`openinterp/neuronpedia_review.py` and docs/PROTOCOL.md §4.",
75
+ "",
76
+ f"Explanations from [Neuronpedia](https://www.neuronpedia.org), credited to the model "
77
+ f"that wrote them. Each latent's newest submission was copied with Neuronpedia's "
78
+ f"explanation as the hypothesis and no claimed tokens. A reviewer wrote probes "
79
+ f"for that copy alone. Neuronpedia makes no counterfactual predictions, so both "
80
+ f"sides are judged on the gates that the probes decide: every gate except "
81
+ f"{', '.join(sorted(NOT_PROBE_GATES))}. These reviews are not registry records. "
82
+ f"Reviewed: {len(done)} of {len(rows)} latents.",
83
+ "",
84
+ "Two things favour the agent. Its side is the unit's newest experiment, which is "
85
+ "often a revision written after earlier reviews. And its probes came from that "
86
+ "experiment's own reviewer, which can be another model than the one here.",
87
+ "",
88
+ "| | Agent | Neuronpedia |",
89
+ "| --- | --- | --- |",
90
+ f"| Passes every probe gate | {agent_ok} of {len(done)} | {np_ok} of {len(done)} |",
91
+ ]
92
+ lines += [line for name in METRICS if (line := _metric_line(done, name))]
93
+ failed = _failed_gates(done)
94
+ if failed:
95
+ lines += ["", "Probe gates that Neuronpedia's explanations failed: " + failed + "."]
96
+ return "\n".join(lines + ["", *_unit_lines(rows)]) + "\n"
97
+
98
+
99
+ def _metric_line(done: Sequence[ReviewRow], name: str) -> str | None:
100
+ """One table row: the mean of metric ``name`` on each side, with a sign test."""
101
+ pairs = [(r.agent[name], r.neuronpedia[name]) for r in done]
102
+ pairs = [(a, b) for a, b in pairs if a is not None and b is not None]
103
+ if not pairs:
104
+ return None
105
+ p = sign_test([b - a for a, b in pairs])
106
+ return (
107
+ f"| {name.replace('_', ' ').capitalize()} (mean, n = {len(pairs)}) | "
108
+ f"{mean(a for a, _ in pairs):.2f} | {mean(b for _, b in pairs):.2f}"
109
+ f"{'' if p is None else f' (sign test p = {p:.2g})'} |"
110
+ )
111
+
112
+
113
+ def _unit_lines(rows: Sequence[ReviewRow]) -> list[str]:
114
+ """The per-latent table: each side's verdict on the probe gates, and balanced accuracy."""
115
+ lines = [
116
+ "| Unit | Agent | Neuronpedia's explanation | Neuronpedia | Balanced accuracy |",
117
+ "| --- | --- | --- | --- | --- |",
118
+ ]
119
+ for r in sorted(rows, key=lambda r: r.slug):
120
+ unit = f"[{r.slug}]({r.url})" if r.url else r.slug
121
+ theirs = "not reviewed yet" if r.neuronpedia is None else _verdict(r.neuronpedia)
122
+ acc = "—"
123
+ if r.neuronpedia is not None:
124
+ acc = f"{r.agent['balanced_accuracy']:.2f} → {r.neuronpedia['balanced_accuracy']:.2f}"
125
+ lines.append(
126
+ f"| {unit} | {_verdict(r.agent)} | {_cell(r.neuronpedia_explanation)} | "
127
+ f"{theirs} | {acc} |"
128
+ )
129
+ return lines
130
+
131
+
132
+ def _verdict(s: dict) -> str:
133
+ return (
134
+ "passes"
135
+ if s["probe_verified"]
136
+ else "fails " + ", ".join(sorted(k for k, ok in s["gates"].items() if not ok))
137
+ )
138
+
139
+
140
+ def _failed_gates(done: Sequence[ReviewRow]) -> str:
141
+ from collections import Counter
142
+
143
+ counts = Counter(k for r in done for k, ok in r.neuronpedia["gates"].items() if not ok)
144
+ return ", ".join(f"{k} ×{n}" for k, n in counts.most_common())
145
+
146
+
147
+ def _cell(text: str) -> str:
148
+ return " ".join(text.split()).replace("|", "\\|").replace("<", "&lt;")
openinterp/planner.py ADDED
@@ -0,0 +1,466 @@
1
+ """Choose the next experiment: randomised, exploration-balanced, herd-resistant.
2
+
3
+ Every neuron in the registry generates candidate follow-up experiments:
4
+
5
+ - Re-evaluate under the current protocol, or re-review independently.
6
+ - Revise, replicate or challenge.
7
+ - Audit the opposite pole, or try a new hypothesis after a rejection.
8
+ - Rescan a record that was scanned on the built-in sample.
9
+
10
+ A VERIFIED pole also offers its unstudied matches in other configured models
11
+ (``open-interpretability sisters``), audited with its explanation. Any match can be drawn, and any
12
+ pole of another model at random. But the pole's sister is drawn far more often than the
13
+ rest.
14
+
15
+ Each candidate gets a weight from its expected information value. The weight decays
16
+ with how often the neuron has been studied, with recent activity, and with open PRs
17
+ that already claim it. Weights are never zero: repeating an experiment is allowed, and
18
+ a null result is data.
19
+
20
+ With probability ``epsilon``, the planner explores instead. It picks a uniformly random
21
+ new neuron, the only selection that supports unbiased registry statistics. The model
22
+ comes from the configured list (``[tool.openinterp] models`` in ``pyproject.toml``).
23
+ The smallest model below its coverage target gets most of these draws, and the others
24
+ share the rest (``openinterp.planner_models``). Otherwise, the planner
25
+ samples a candidate in proportion to ``weight ** (1 / temperature)``. Sampling,
26
+ rather than taking the best candidate, keeps independent agents from piling onto
27
+ the same neuron. The seed is reported so any choice can be reproduced.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import math
34
+ import random
35
+ import shutil
36
+ import subprocess
37
+ import tomllib
38
+ from collections.abc import Sequence
39
+ from dataclasses import asdict, dataclass, field
40
+ from datetime import UTC, datetime, timedelta
41
+ from pathlib import Path
42
+
43
+ from openinterp.core.licenses import ModelLicense
44
+ from openinterp.core.paths import DEFAULT_WIKI_DIR
45
+ from openinterp.core.schemas import (
46
+ PROTOCOL_VERSION,
47
+ AuditRecord,
48
+ ReviewPolicy,
49
+ scan_active_share,
50
+ )
51
+ from openinterp.planner_models import (
52
+ DEFAULT_COVERAGE_TARGET,
53
+ DEFAULT_OTHERS_SHARE,
54
+ ModelCoverage,
55
+ explore_weights,
56
+ focus_model,
57
+ model_coverage,
58
+ )
59
+ from openinterp.planner_states import NeuronState, load_states # noqa: F401 - re-exported
60
+
61
+ # Expected information value per experiment kind, before decay.
62
+ # Probability of an explore draw (a random new unit) instead of a follow-up.
63
+ DEFAULT_EPSILON = 0.7
64
+ BASE_WEIGHTS = {
65
+ "reevaluate": 3.0, # verdict predates the current protocol; cheap, no LLM calls
66
+ "independent_review": 2.5, # proposer reviewed its own hypothesis
67
+ "revise": 2.0, # NEEDS_REVISION: failed gates say exactly what to fix
68
+ "rescan": 2.0, # scanned on the built-in sample: its max activation means little
69
+ "replicate": 1.2, # VERIFIED by one reviewer model, or never by an independent one
70
+ "opposite_pole": 1.0, # the other half of a studied neuron
71
+ "new_hypothesis": 0.8, # REJECTED: the null result stands; try another idea
72
+ "challenge": 0.6, # VERIFIED and replicated: hunt for counterexamples
73
+ "sister": 10.0, # a ready hypothesis on its likeliest carrier (others: LINK_WEIGHT)
74
+ }
75
+ SKILLS = {
76
+ "explore": ["native-neuron-audit", "neuron-family-triage", "vocab-projection-lookahead"],
77
+ "reevaluate": [],
78
+ "independent_review": ["interpretability-illusions-review"],
79
+ "revise": ["native-neuron-audit", "explanation-simulation-scoring"],
80
+ "rescan": ["native-neuron-audit", "neuron-family-triage"],
81
+ "replicate": ["interpretability-illusions-review"],
82
+ "opposite_pole": ["native-neuron-audit", "vocab-projection-lookahead"],
83
+ "new_hypothesis": ["native-neuron-audit", "neuron-steering-tests"],
84
+ "challenge": ["interpretability-illusions-review", "neuron-steering-tests"],
85
+ "sister": ["native-neuron-audit", "interpretability-illusions-review"],
86
+ "superposition": ["native-neuron-audit", "interpretability-illusions-review"],
87
+ }
88
+ DEFAULT_MODELS = ("Qwen/Qwen2.5-0.5B",)
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class Candidate:
93
+ """One experiment the planner can draw, with its weight and the commands to run it."""
94
+
95
+ kind: str
96
+ model: str
97
+ weight: float
98
+ rationale: str
99
+ commands: list[str]
100
+ slug: str | None = None
101
+ skills: list[str] = field(default_factory=list)
102
+ layer: int | None = None
103
+ record: str | None = None # the record a follow-up builds on
104
+ audit: str | None = None # `open-interpretability audit` options, for kinds that scan
105
+
106
+
107
+ def claim_key(slug: str) -> str:
108
+ """Branch-safe form of a neuron slug; PR branches and titles should contain it."""
109
+ return slug.replace("/", "-")
110
+
111
+
112
+ def open_pr_claims() -> list[str]:
113
+ """Titles and branch names of open PRs (empty if `gh` is unavailable)."""
114
+ if not shutil.which("gh"):
115
+ return []
116
+ try:
117
+ out = subprocess.run(
118
+ [
119
+ "gh",
120
+ "pr",
121
+ "list",
122
+ "--state",
123
+ "open",
124
+ "--limit",
125
+ "200",
126
+ "--json",
127
+ "title,headRefName",
128
+ ],
129
+ check=True,
130
+ capture_output=True,
131
+ text=True,
132
+ timeout=30,
133
+ ).stdout
134
+ except (subprocess.SubprocessError, OSError):
135
+ return []
136
+ return [f"{pr['title']} {pr['headRefName']}" for pr in json.loads(out)]
137
+
138
+
139
+ def _config(pyproject: Path) -> dict:
140
+ try:
141
+ with open(pyproject, "rb") as fh:
142
+ return tomllib.load(fh).get("tool", {}).get("openinterp", {})
143
+ except (OSError, tomllib.TOMLDecodeError):
144
+ return {}
145
+
146
+
147
+ def configured_models(pyproject: Path = Path("pyproject.toml")) -> tuple[str, ...]:
148
+ """Models the planner explores: ``[tool.openinterp] models`` (empty if unset)."""
149
+ return tuple(_config(pyproject).get("models", ()))
150
+
151
+
152
+ def configured_windows(pyproject: Path = Path("pyproject.toml")) -> dict[str, int]:
153
+ """Return ``[tool.openinterp.window_tokens]``, the scan window of some models.
154
+
155
+ These are the models whose context is shorter than the corpus's documents. GPT-2
156
+ reads 1,024 positions.
157
+ """
158
+ return dict(_config(pyproject).get("window_tokens", {}))
159
+
160
+
161
+ def configured_licenses(pyproject: Path = Path("pyproject.toml")) -> dict[str, ModelLicense]:
162
+ """Return ``[tool.openinterp.licenses]``, the terms of each model (empty if unset).
163
+
164
+ A malformed entry raises a validation error, so a typo cannot hide a term.
165
+ """
166
+ licenses = _config(pyproject).get("licenses", {})
167
+ return {model: ModelLicense(**terms) for model, terms in licenses.items()}
168
+
169
+
170
+ def coverage_target(pyproject: Path = Path("pyproject.toml")) -> float:
171
+ """Return ``[tool.openinterp] coverage_target``, or the default.
172
+
173
+ A model's verified share must pass it before most explore draws move to the next model.
174
+ """
175
+ return float(_config(pyproject).get("coverage_target", DEFAULT_COVERAGE_TARGET))
176
+
177
+
178
+ def others_share(pyproject: Path = Path("pyproject.toml")) -> float:
179
+ """Return ``[tool.openinterp] others_share``, or the default.
180
+
181
+ It is the share of explore draws for the models that are not the focus.
182
+ """
183
+ return float(_config(pyproject).get("others_share", DEFAULT_OTHERS_SHARE))
184
+
185
+
186
+ def _decay(state: NeuronState, claims: Sequence[str], now: datetime) -> tuple[float, list[str]]:
187
+ factor, notes = 1.0 / math.sqrt(state.experiments), []
188
+ if state.experiments > 1:
189
+ notes.append(f"{state.experiments} experiments so far")
190
+ age = now - state.last
191
+ if age < timedelta(hours=6):
192
+ factor *= 0.2
193
+ notes.append("studied in the last 6h")
194
+ elif age < timedelta(hours=24):
195
+ factor *= 0.5
196
+ notes.append("studied in the last 24h")
197
+ key = claim_key(state.slug)
198
+ if any(key in c or state.slug in c for c in claims):
199
+ factor *= 0.05
200
+ notes.append("an open PR already claims it")
201
+ return factor, notes
202
+
203
+
204
+ def candidates_for(state: NeuronState, slugs: set[str], claims: Sequence[str], now: datetime):
205
+ """The follow-up candidates for one studied unit, weighted."""
206
+ record = state.record
207
+ ident = record.submission.identifier
208
+ commands = _commands(state)
209
+ if record.scanned_on_builtin_sample:
210
+ # Every review kind would inherit a 552-token scan, and so would its default-on
211
+ # gate: rescan on the real corpus instead.
212
+ kinds = _rescan(commands)
213
+ else:
214
+ kinds = _review_kinds(state, commands)
215
+ if _default_on(record):
216
+ # A single default-on pole can't be audited as a detector: a new audit stops at the
217
+ # scan, and a review fails the gate. Re-evaluating can still correct an old verdict.
218
+ kinds = [k for k in kinds if k[0] == "reevaluate"]
219
+ kinds += _opposite_pole(state, slugs, commands)
220
+
221
+ factor, notes = _decay(state, claims, now)
222
+ for kind, why, commands, slug in kinds:
223
+ yield Candidate(
224
+ kind=kind,
225
+ model=ident.model_name,
226
+ weight=BASE_WEIGHTS[kind] * factor,
227
+ rationale="; ".join([why, *notes]),
228
+ commands=commands,
229
+ slug=slug or ident.slug,
230
+ skills=SKILLS[kind],
231
+ layer=ident.layer,
232
+ record=state.record_path.as_posix(),
233
+ audit=_audit_options(commands),
234
+ )
235
+
236
+
237
+ def _default_on(record: AuditRecord) -> bool:
238
+ """Return whether the record shows a default-on unit, by its gate or by its histogram.
239
+
240
+ Records from before protocol 0.8 have no ``not_default_on`` gate, so the histogram is
241
+ read as ``check_default_on`` reads it, with the current policy.
242
+ """
243
+ metrics = record.verdict.metrics
244
+ if metrics and metrics.gates.get("not_default_on") is False:
245
+ return True
246
+ policy = ReviewPolicy()
247
+ share = scan_active_share(record.submission, policy)
248
+ return share is not None and share >= policy.max_active_share
249
+
250
+
251
+ def _audit_options(commands: list[str]) -> str | None:
252
+ """The options of the audit command among ``commands``, without --supersedes.
253
+
254
+ The audit command is the one that starts with ``open-interpretability audit``.
255
+ """
256
+ for command in commands:
257
+ if command.startswith("open-interpretability audit "):
258
+ return command.removeprefix("open-interpretability audit ").split(" --supersedes ")[0]
259
+ return None
260
+
261
+
262
+ @dataclass(frozen=True)
263
+ class _Commands:
264
+ """The commands a follow-up on one record runs."""
265
+
266
+ path: str
267
+ target: str # --model, --layer and the unit, as the record was scanned
268
+ review: str
269
+ revise: str
270
+
271
+
272
+ def _commands(state: NeuronState) -> _Commands:
273
+ record = state.record
274
+ ident = record.submission.identifier
275
+ path = state.record_path.as_posix()
276
+ if ident.kind == "attn_head":
277
+ unit = f"--head {ident.head_idx}"
278
+ elif ident.kind == "dictionary_feature":
279
+ unit = f"--dictionary {ident.dictionary} --feature {ident.feature_idx}"
280
+ else:
281
+ unit = f"--neuron {ident.neuron_idx}"
282
+ target = f"--model {ident.model_name} --layer {ident.layer} {unit}"
283
+ provenance = record.submission.provenance
284
+ if provenance and provenance.snippet_tokens: # rescan as the record was scanned
285
+ target += f" --window-tokens {provenance.snippet_tokens}"
286
+ proposer = record.submission.submitting_agent_model
287
+ review = (
288
+ f"open-interpretability verify --audit-file {path} --corpus <scan corpus> "
289
+ f"--agent-model <model other than {proposer}>"
290
+ )
291
+ pole = f" --pole {ident.pole}" if ident.pole else ""
292
+ revise = f"open-interpretability audit {target}{pole} --supersedes {path}"
293
+ return _Commands(path, target, review, revise)
294
+
295
+
296
+ Kind = tuple[str, str, list[str], str | None] # kind, why, commands, slug (if another unit's)
297
+
298
+
299
+ def _review_kinds(state: NeuronState, c: _Commands) -> list[Kind]:
300
+ """Follow-ups on the record's own verdict: re-evaluate, review, revise or replicate."""
301
+ record, verdict = state.record, state.record.verdict
302
+ kinds: list[Kind] = []
303
+ if verdict.policy is None or verdict.policy.protocol_version != PROTOCOL_VERSION:
304
+ reevaluate = [f"open-interpretability reevaluate --record {c.path}"]
305
+ kinds.append(("reevaluate", "verdict predates the current protocol", reevaluate, None))
306
+ # Not the gate: it passes self-reviews unless the policy requires independence.
307
+ if record.self_reviewed:
308
+ kinds.append(
309
+ ("independent_review", "the proposer reviewed its own hypothesis", [c.review], None)
310
+ )
311
+ gates = verdict.metrics.gates if verdict.metrics else {}
312
+ failed = [g for g, ok in gates.items() if not ok]
313
+ resubmit = [
314
+ c.revise,
315
+ "open-interpretability verify --audit-file <new submission> --corpus <scan corpus>",
316
+ ]
317
+ if verdict.status == "NEEDS_REVISION":
318
+ kinds.append(("revise", f"failed gates: {', '.join(failed) or 'unknown'}", resubmit, None))
319
+ elif verdict.status == "REJECTED":
320
+ why = "rejected; the null result stands, so try another idea"
321
+ kinds.append(("new_hypothesis", why, resubmit, None))
322
+ elif len(state.reviewers) < 2:
323
+ kinds.append(("replicate", "verified by only one reviewer model", [c.review], None))
324
+ elif not state.independent_reviews:
325
+ kinds.append(("replicate", "every review so far was a self-review", [c.review], None))
326
+ else:
327
+ why = "verified and replicated; look for counterexamples"
328
+ kinds.append(("challenge", why, [c.review], None))
329
+ return kinds
330
+
331
+
332
+ def _rescan(c: _Commands) -> list[Kind]:
333
+ """Return a new audit on the scan corpus, to supersede a built-in-sample record.
334
+
335
+ Re-evaluating or re-reviewing the record would reuse its scan.
336
+ """
337
+ why = "scanned on the 552-token built-in sample; rescan on the corpus"
338
+ verify = "open-interpretability verify --audit-file <new submission> --corpus <scan corpus>"
339
+ return [("rescan", why, [c.revise, verify], None)]
340
+
341
+
342
+ def _opposite_pole(state: NeuronState, slugs: set[str], c: _Commands) -> list[Kind]:
343
+ ident = state.record.submission.identifier
344
+ other = "negative" if ident.pole == "positive" else "positive"
345
+ other_slug = ident.slug.rsplit("-", 1)[0] + f"-{other}"
346
+ if not ident.pole or other_slug in slugs: # heads and features have no poles
347
+ return []
348
+ audit = f"open-interpretability audit {c.target} --pole {other}"
349
+ return [("opposite_pole", f"the {other} pole is unstudied", [audit], other_slug)]
350
+
351
+
352
+ @dataclass
353
+ class Plan:
354
+ """The planner's choice, the candidates it drew from, and the seed that replays it."""
355
+
356
+ seed: int
357
+ choice: Candidate
358
+ explored: bool
359
+ candidates: list[Candidate]
360
+ explore_models: dict[str, float] = field(default_factory=dict)
361
+ coverage: dict[str, ModelCoverage] = field(default_factory=dict)
362
+ focus: str | None = None # the model that explore draws favour
363
+
364
+ def to_json(self) -> str:
365
+ """The plan as indented JSON."""
366
+ return json.dumps(asdict(self), indent=2)
367
+
368
+
369
+ def plan_next(
370
+ wiki_dir: Path = DEFAULT_WIKI_DIR,
371
+ *,
372
+ seed: int | None = None,
373
+ epsilon: float = DEFAULT_EPSILON,
374
+ temperature: float = 1.0,
375
+ models: Sequence[str] = (),
376
+ claims: Sequence[str] = (),
377
+ now: datetime | None = None,
378
+ ) -> Plan:
379
+ """Draw the next experiment.
380
+
381
+ Explore draws use ``models`` if given, else the models configured in the
382
+ ``pyproject.toml`` next to ``wiki_dir``, else the registry's, else ``DEFAULT_MODELS``.
383
+ """
384
+ seed = random.SystemRandom().getrandbits(32) if seed is None else seed
385
+ rng = random.Random(seed)
386
+ now = now or datetime.now(UTC)
387
+ states = load_states(wiki_dir)
388
+ slugs = set(states)
389
+ pyproject = Path(wiki_dir).parent / "pyproject.toml"
390
+ pool = _model_pool(models, pyproject, states)
391
+ # Imported here: these modules build their candidates from this module's weights.
392
+ from openinterp.planner_sisters import sister_candidates
393
+ from openinterp.planner_superpositions import superposition_candidates
394
+
395
+ root = Path(wiki_dir).parent
396
+ sisters = sister_candidates(states, claims, now, pool, root, seed)
397
+ extra = [*sisters, *superposition_candidates(states, claims, root)]
398
+ candidates = sorted(
399
+ [*(c for s in states.values() for c in candidates_for(s, slugs, claims, now)), *extra],
400
+ key=lambda c: -c.weight,
401
+ )
402
+ records = (s.record for s in states.values())
403
+ coverage = model_coverage(pool, records, claims, dead(wiki_dir), stopped(wiki_dir))
404
+ target = coverage_target(pyproject)
405
+ explore = explore_weights(coverage, target, others_share(pyproject))
406
+ focus = focus_model(coverage.values(), target)
407
+ if not candidates or rng.random() < epsilon:
408
+ choice = _explore(rng, pool, explore, pyproject, focus, slugs | set(claims))
409
+ return Plan(seed, choice, True, candidates, explore, coverage, focus)
410
+
411
+ weights = [c.weight ** (1.0 / temperature) for c in candidates]
412
+ choice = rng.choices(candidates, weights=weights, k=1)[0]
413
+ return Plan(seed, choice, False, candidates, explore, coverage, focus)
414
+
415
+
416
+ def stopped(wiki_dir: Path) -> list:
417
+ """Every unit that a scan stopped on (dead or default-on), chosen or drawn at random.
418
+
419
+ Such a unit has no experiment, but it counts as studied.
420
+ """
421
+ from openinterp.core.draws import load_draws, load_stops
422
+
423
+ wiki_dir = Path(wiki_dir)
424
+ return [d.identifier for d in (*load_draws(wiki_dir), *load_stops(wiki_dir))]
425
+
426
+
427
+ def dead(wiki_dir: Path) -> list:
428
+ """Every neuron pole that a scan found dead and the weights proved dead.
429
+
430
+ The proof does not depend on how the unit was selected. So this reads the stops of
431
+ chosen units (:func:`load_stops`) as well as random draws. Random-selection
432
+ statistics still read the draws only.
433
+ """
434
+ from openinterp.core.draws import load_draws, load_stops
435
+
436
+ wiki_dir = Path(wiki_dir)
437
+ return [d.identifier for d in (*load_draws(wiki_dir), *load_stops(wiki_dir)) if d.proven_dead]
438
+
439
+
440
+ def _model_pool(models: Sequence[str], pyproject: Path, states) -> list[str]:
441
+ """``models``, else the configured models, else the registry's, else the defaults."""
442
+ return sorted(
443
+ set(models)
444
+ or set(configured_models(pyproject))
445
+ or {s.record.submission.identifier.model_name for s in states.values()}
446
+ or set(DEFAULT_MODELS)
447
+ )
448
+
449
+
450
+ def _explore(
451
+ rng: random.Random, pool: list[str], explore: dict, pyproject: Path, focus: str | None,
452
+ taken: set[str] = frozenset(),
453
+ ) -> Candidate: # fmt: skip
454
+ """A new unit of a model drawn by size and coverage: random, or weighted by effect."""
455
+ from openinterp.planner_effects import effect_draw
456
+ from openinterp.planner_heads import head_draw, neuron_draw
457
+
458
+ model = rng.choices(pool, weights=[explore[m] for m in pool], k=1)[0]
459
+ audit_seed = rng.getrandbits(32)
460
+ window = configured_windows(pyproject).get(model)
461
+ if head := head_draw(model, audit_seed, pyproject, window): # the unit kind, its own draw
462
+ return head
463
+ if weighted := effect_draw(rng, model, pyproject, taken, window):
464
+ return weighted
465
+ why = "the smallest model below its verified target" if model == focus else "coverage"
466
+ return neuron_draw(model, audit_seed, window, explore[model] if len(pool) > 1 else None, why)