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,495 @@
1
+ """``open-interpretability audit`` and its helpers: scan caching and prompt/answer files.
2
+
3
+ Corpus loading and the scan itself are in :mod:`openinterp.evidence`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from openinterp.audit_options import ( # noqa: F401 (re-exported)
13
+ open_scan_cache,
14
+ read_superposition,
15
+ resolve_unit,
16
+ scan_cache_option,
17
+ selection_option,
18
+ )
19
+ from openinterp.cli import (
20
+ cli,
21
+ contributor_option,
22
+ recorded_model_option,
23
+ wiki_dir_option,
24
+ with_model_options,
25
+ )
26
+ from openinterp.corpus import DEFAULT_SAMPLE_SEED, DEFAULT_SAMPLE_TOKENS
27
+ from openinterp.evidence import ( # noqa: F401 (re-exported)
28
+ ScanRequest,
29
+ Unauditable,
30
+ read_corpus,
31
+ scan_evidence,
32
+ sha256,
33
+ )
34
+
35
+ REGISTRY_CORPUS = Path("corpus.jsonl")
36
+
37
+
38
+ def load_superseded(path: Path):
39
+ """The archived, reviewed record that ``audit --supersedes`` names."""
40
+ from openinterp.core.schemas import AuditRecord
41
+
42
+ try:
43
+ record = AuditRecord.model_validate_json(path.read_text())
44
+ except ValueError as exc:
45
+ raise click.BadParameter(f"not an audit record: {exc}", param_hint="--supersedes") from exc
46
+ if record.verdict is None:
47
+ raise click.BadParameter("that record has no verdict yet", param_hint="--supersedes")
48
+ return record
49
+
50
+
51
+ def answer_path(prompt_path: Path, suffix: str) -> Path:
52
+ """``review.prompt.md`` or ``review.md`` -> ``review<suffix>``."""
53
+ name = prompt_path.name.removesuffix(".md").removesuffix(".prompt")
54
+ return prompt_path.with_name(name + suffix)
55
+
56
+
57
+ def with_costs(provenance, *parts):
58
+ """``provenance`` with ``parts`` merged into its costs; unchanged if it is ``None``."""
59
+ from openinterp.core.costs import merge
60
+
61
+ if provenance is None:
62
+ return None
63
+ return provenance.model_copy(update={"costs": merge(provenance.costs, *parts)})
64
+
65
+
66
+ def record_draw(exc: Unauditable, random_pick: bool, wiki_dir: Path) -> None:
67
+ """Keep a unit the scan stopped on, so that the registry shows the attempt.
68
+
69
+ Random-selection statistics count a random draw. A chosen unit gets a stop record and a
70
+ page instead, which those statistics do not read.
71
+ """
72
+ from openinterp.builder.wiki import write_stop_page
73
+ from openinterp.cli import _skip_wiki
74
+ from openinterp.core.draws import write_draw
75
+
76
+ if exc.draw is None or _skip_wiki(wiki_dir):
77
+ return
78
+ outcome = exc.draw.outcome.replace("_", "-")
79
+ if random_pick:
80
+ click.echo(f"Recorded the {outcome} draw: {write_draw(exc.draw, wiki_dir)}", err=True)
81
+ else:
82
+ click.echo(f"Recorded the {outcome} stop: {write_stop_page(exc.draw, wiki_dir)}", err=True)
83
+
84
+
85
+ def proposer_context(evidence, wiki_dir: Path, sisters: bool, sister_file=None, superseded=None):
86
+ """Return what other research links to the unit, for the proposer.
87
+
88
+ That is the unit's studied sisters, and the record a revision supersedes. In a sister
89
+ study (``sister_file``), it is only the study's brief, because the explanation is fixed.
90
+ """
91
+ from openinterp.cli_review import sisters_text
92
+ from openinterp.revision import previous_attempt
93
+ from openinterp.sisters import sister_brief
94
+
95
+ if sister_file:
96
+ return sister_brief(load_sister(sister_file, evidence.identifier))
97
+ related = sisters_text(evidence.identifier, wiki_dir, "proposer") if sisters else ""
98
+ return related + (previous_attempt(superseded) if superseded else "")
99
+
100
+
101
+ def load_sister(path: Path, unit):
102
+ """The record ``audit --sister-of`` names, checked against the audited ``unit``."""
103
+ from openinterp.sisters import load_sister_source
104
+
105
+ try:
106
+ return load_sister_source(path, unit)
107
+ except ValueError as exc:
108
+ raise click.BadParameter(str(exc), param_hint="--sister-of") from exc
109
+
110
+
111
+ def write_evidence(evidence, path: Path, full_snippets: bool = False, related: str = "") -> None:
112
+ """Write ``evidence`` to ``path`` with this run's costs, and the proposer prompt beside it."""
113
+ from openinterp.agents.proposer import HypothesisDraft, ProposerAgent
114
+ from openinterp.core import costs
115
+
116
+ evidence = evidence.model_copy(
117
+ update={"provenance": with_costs(evidence.provenance, costs.measured())}
118
+ )
119
+ path.parent.mkdir(parents=True, exist_ok=True)
120
+ path.write_text(evidence.model_dump_json(indent=2))
121
+ prompt_path = path.with_suffix(".prompt.md")
122
+ answer_path = path.with_suffix(".hypothesis.json")
123
+ task = ProposerAgent.proposal_prompt(
124
+ evidence.identifier,
125
+ evidence.top_examples,
126
+ evidence.vocabulary_lens,
127
+ evidence.confounds,
128
+ evidence.correlated_neurons,
129
+ evidence.head_profile,
130
+ evidence.input_attribution,
131
+ evidence.dictionary_profile,
132
+ bool(evidence.first_token_floor),
133
+ full_snippets,
134
+ related,
135
+ superposition_profile=evidence.superposition_profile,
136
+ manifold=evidence.manifold,
137
+ )
138
+ prompt_path.write_text(ProposerAgent.render_prompt(HypothesisDraft, task, answer_path))
139
+ click.echo(f"Unit: {evidence.identifier}")
140
+ click.echo(f"Wrote {path}")
141
+ click.echo(f"Proposer prompt: {prompt_path} (answer in {answer_path})")
142
+
143
+
144
+ @cli.command()
145
+ @click.option(
146
+ "--model",
147
+ "model_name",
148
+ help="Hugging Face repo, e.g. Qwen/Qwen2.5-0.5B. Required unless --evidence.",
149
+ )
150
+ @click.option("--layer", type=int, help="Required unless --random.")
151
+ @click.option("--neuron", type=int, help="Required unless --random or --head.")
152
+ @click.option("--pole", type=click.Choice(["positive", "negative"]), help="Default: positive.")
153
+ @click.option(
154
+ "--head",
155
+ type=int,
156
+ help="Audit this attention head of --layer instead of a neuron (implies --unit head).",
157
+ )
158
+ @click.option(
159
+ "--feature",
160
+ type=int,
161
+ help="Audit this latent of --dictionary on --layer instead of a neuron (implies --unit "
162
+ "feature).",
163
+ )
164
+ @click.option(
165
+ "--dictionary",
166
+ help="Sparse dictionary (transcoder or SAE) whose latent to audit, e.g. "
167
+ "gpt2-small-transcoders or gemma-scope-2b-mlp-saes; it implies --model. See "
168
+ "openinterp/harness/dictionaries.py.",
169
+ )
170
+ @click.option(
171
+ "--superposition",
172
+ "superposition_file",
173
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
174
+ help="Audit the superposition that this JSON file defines: {model, layer, members: "
175
+ "[{neuron_idx, weight}], method, source}. It implies --model and --layer.",
176
+ )
177
+ @click.option(
178
+ "--unit",
179
+ type=click.Choice(["neuron", "head", "feature"]),
180
+ help="What --random samples: an MLP neuron pole (default), an attention head, or a "
181
+ "latent of --dictionary (the learned-basis baseline).",
182
+ )
183
+ @click.option(
184
+ "--random",
185
+ "random_pick",
186
+ is_flag=True,
187
+ help="Sample unspecified layer/neuron/pole (or head) uniformly: the unbiased selection.",
188
+ )
189
+ @click.option("--seed", type=int, help="Seed for --random (recorded in provenance).")
190
+ @click.option(
191
+ "--corpus",
192
+ type=click.Path(exists=True, path_type=Path),
193
+ help="Text file (one snippet per line) or .jsonl with a 'text' field. "
194
+ "Default: ./corpus.jsonl, the registry's scan corpus (see the README).",
195
+ )
196
+ @click.option(
197
+ "--max-tokens",
198
+ default=DEFAULT_SAMPLE_TOKENS,
199
+ show_default=True,
200
+ type=click.IntRange(0),
201
+ help="Scan a seeded sample of whole documents with at most this many tokens, so the "
202
+ "scan's cost and scale don't grow with the corpus (recorded in provenance). "
203
+ "0 scans every document.",
204
+ )
205
+ @click.option(
206
+ "--sample-seed",
207
+ default=DEFAULT_SAMPLE_SEED,
208
+ show_default=True,
209
+ type=int,
210
+ help="Seed for the --max-tokens sample. Keep the default so audits of one corpus read "
211
+ "the same documents.",
212
+ )
213
+ @click.option("--top-k", default=20, show_default=True, help="Examples shown to the proposer.")
214
+ @click.option(
215
+ "--full-snippets",
216
+ is_flag=True,
217
+ help="Show the proposer whole scanned documents, without windowing or dropping "
218
+ "near-duplicates: the prompt before trimming, for A/B comparison.",
219
+ )
220
+ @click.option(
221
+ "--window-tokens",
222
+ type=click.IntRange(2),
223
+ help="Scan each document as overlapping windows of this many tokens (≥ 512 recommended) "
224
+ "instead of whole. Top examples are then windows. Default: whole documents.",
225
+ )
226
+ @click.option(
227
+ "--stride",
228
+ type=click.IntRange(1),
229
+ help="Tokens between window starts (default: half of --window-tokens). Each token counts "
230
+ "once, with at least --window-tokens minus --stride tokens of context.",
231
+ )
232
+ @scan_cache_option
233
+ @click.option(
234
+ "--agent-model",
235
+ default=None,
236
+ callback=recorded_model_option,
237
+ help="Proposer. LiteLLM model string, or external/<name> to answer a prompt file. "
238
+ "With --hypothesis, the name recorded for whoever wrote it (e.g. external/claude-opus-5-5).",
239
+ )
240
+ @contributor_option("proposer")
241
+ @click.option(
242
+ "--evidence-out",
243
+ type=click.Path(dir_okay=False, path_type=Path),
244
+ help="Scan only: write the evidence to this JSON file and the proposer's prompt next to "
245
+ "it (<name>.prompt.md), then exit. Answer the prompt, then run with --evidence.",
246
+ )
247
+ @click.option(
248
+ "--evidence",
249
+ "evidence_file",
250
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
251
+ help="Evidence from --evidence-out; skips the scan. Requires --hypothesis.",
252
+ )
253
+ @click.option(
254
+ "--hypothesis",
255
+ "hypothesis_file",
256
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
257
+ help="HypothesisDraft JSON answering the --evidence-out prompt. No LLM is called.",
258
+ )
259
+ @click.option(
260
+ "--sister-of",
261
+ "sister_file",
262
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
263
+ help="A sister-neuron study: the archived VERIFIED record of a pole in another model "
264
+ "whose explanation this pole is tested on, unchanged. Give it to both steps.",
265
+ )
266
+ @click.option(
267
+ "--sisters/--no-sisters",
268
+ default=True,
269
+ show_default=True,
270
+ help="Show the proposer the pole's sister neurons in other models that this registry "
271
+ "has studied (`open-interpretability sisters`), with their explanations.",
272
+ )
273
+ @click.option(
274
+ "--supersedes",
275
+ "superseded_file",
276
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
277
+ help="Archived record (wiki/experiments/<slug>/<id>.json) this submission revises, e.g. a "
278
+ "NEEDS_REVISION one. Recorded in the submission; it must be on the same unit.",
279
+ )
280
+ @click.option(
281
+ "--out", type=click.Path(path_type=Path), help="Output JSON (default: submissions/<slug>.json)"
282
+ )
283
+ @wiki_dir_option(
284
+ "Registry root. A --random draw the scan stops on (dead or default-on) is recorded "
285
+ "in draws/<slug>/, as an outcome of random selection. A chosen unit is recorded in "
286
+ "stops/<slug>/. Pass --wiki-dir '' to skip.",
287
+ )
288
+ @selection_option
289
+ @with_model_options
290
+ def audit( # long-function: a click command; ScanRequest takes one line per option
291
+ model_name,
292
+ layer,
293
+ neuron,
294
+ pole,
295
+ head,
296
+ feature,
297
+ dictionary,
298
+ superposition_file,
299
+ unit,
300
+ random_pick,
301
+ seed,
302
+ corpus,
303
+ max_tokens,
304
+ sample_seed,
305
+ top_k,
306
+ full_snippets,
307
+ window_tokens,
308
+ stride,
309
+ use_scan_cache,
310
+ selection,
311
+ agent_model,
312
+ contributor,
313
+ evidence_out,
314
+ evidence_file,
315
+ hypothesis_file,
316
+ sister_file,
317
+ sisters,
318
+ superseded_file,
319
+ out,
320
+ wiki_dir,
321
+ device,
322
+ dtype,
323
+ ):
324
+ """Scan a unit, get a hypothesis, and write a submission.
325
+
326
+ The unit is a neuron pole, an attention head, a dictionary feature or a superposition.
327
+
328
+ \b
329
+ In one step, the proposer is an LLM (or external/<name>, answered through exchange/).
330
+ In two steps, any agent can propose without an API key or a waiting process:
331
+ open-interpretability audit --model M --random --evidence-out ev.json
332
+ (answer ev.prompt.md by writing ev.hypothesis.json)
333
+ open-interpretability audit --evidence ev.json --hypothesis ev.hypothesis.json \\
334
+ --agent-model external/<model that wrote it>
335
+ """
336
+ unit = resolve_unit(unit, neuron, pole, head, feature, dictionary)
337
+ superposition = None
338
+ if superposition_file is not None:
339
+ unit, model_name, layer, superposition = read_superposition(superposition_file, unit)
340
+ if unit == "feature" and evidence_file is None:
341
+ model_name = check_dictionary(dictionary, model_name, window_tokens)
342
+ check_steps(evidence_file, hypothesis_file, evidence_out, superseded_file, agent_model)
343
+ check_scan(evidence_file, model_name, stride, window_tokens)
344
+ superseded = superseded_file and load_superseded(superseded_file)
345
+ if evidence_file is not None:
346
+ evidence, draft, runner, agent = load_answer(
347
+ evidence_file, hypothesis_file, agent_model, device, dtype
348
+ )
349
+ else:
350
+ request = ScanRequest(
351
+ model_name=model_name,
352
+ unit=unit,
353
+ layer=layer,
354
+ neuron=neuron,
355
+ pole=pole,
356
+ head=head,
357
+ feature=feature,
358
+ dictionary=dictionary,
359
+ superposition=superposition,
360
+ random_pick=random_pick,
361
+ seed=seed,
362
+ corpus=scan_corpus(corpus),
363
+ top_k=top_k,
364
+ dtype=dtype,
365
+ window_tokens=window_tokens,
366
+ stride=stride,
367
+ max_tokens=max_tokens,
368
+ sample_seed=sample_seed,
369
+ selection=selection,
370
+ )
371
+ runner, evidence = scan_or_record(request, use_scan_cache, wiki_dir, device)
372
+ related = proposer_context(evidence, wiki_dir, sisters, sister_file, superseded)
373
+ if evidence_out is not None:
374
+ write_evidence(evidence, evidence_out, full_snippets, related)
375
+ return
376
+ agent, draft = ask_proposer(agent_model, evidence, full_snippets, related)
377
+ from openinterp.cli_submission import write_submission
378
+
379
+ write_submission(agent, draft, runner, evidence, superseded, out, contributor, sister_file)
380
+
381
+
382
+ def check_dictionary(dictionary, model_name, window_tokens) -> str:
383
+ """The model ``--dictionary`` is for, after checking it fits the scan's windows."""
384
+ from openinterp.harness.dictionaries import dictionary_spec
385
+
386
+ if dictionary is None:
387
+ raise click.UsageError("--unit feature needs --dictionary.")
388
+ try:
389
+ spec = dictionary_spec(dictionary)
390
+ except ValueError as exc:
391
+ raise click.BadParameter(str(exc), param_hint="--dictionary") from exc
392
+ if model_name not in (None, spec.model_name):
393
+ raise click.UsageError(f"{dictionary} is a dictionary for {spec.model_name}.")
394
+ limit = spec.context_tokens
395
+ if limit and (window_tokens is None or window_tokens > limit):
396
+ raise click.UsageError(
397
+ f"{dictionary} only fits contexts of up to {limit} tokens; "
398
+ f"scan with --window-tokens {limit}."
399
+ )
400
+ return spec.model_name
401
+
402
+
403
+ def check_steps(evidence_file, hypothesis_file, evidence_out, superseded_file, agent_model):
404
+ """Options that go together, or apart, across the one- and two-step audits."""
405
+ if (evidence_file is None) != (hypothesis_file is None):
406
+ raise click.UsageError("--evidence and --hypothesis go together.")
407
+ if superseded_file is not None and evidence_out is not None:
408
+ raise click.UsageError(
409
+ "--supersedes goes on the step that writes the submission (with --hypothesis)."
410
+ )
411
+ if evidence_file is not None and evidence_out is not None:
412
+ raise click.UsageError("--evidence-out scans; --evidence reuses a scan. Pick one.")
413
+ if evidence_file is not None and not agent_model:
414
+ raise click.UsageError(
415
+ "--agent-model is required with --hypothesis: name the model that wrote it."
416
+ )
417
+
418
+
419
+ def check_scan(evidence_file, model_name, stride, window_tokens) -> None:
420
+ """Reject scan options that do not go together, before any model loads."""
421
+ if stride is not None and (window_tokens is None or stride > window_tokens):
422
+ raise click.UsageError("--stride needs --window-tokens, and can't exceed it.")
423
+ if evidence_file is None and model_name is None:
424
+ raise click.UsageError("--model is required unless --evidence is given.")
425
+
426
+
427
+ def scan_corpus(corpus: Path | None) -> Path:
428
+ """Return the corpus to scan: ``--corpus``, else the registry's ``corpus.jsonl``.
429
+
430
+ Never the built-in sample, whose ~552 tokens make a scan that says little.
431
+ """
432
+ if corpus is not None:
433
+ return corpus
434
+ if REGISTRY_CORPUS.exists():
435
+ click.echo(f"Scanning {REGISTRY_CORPUS} (no --corpus given).", err=True)
436
+ return REGISTRY_CORPUS
437
+ raise click.UsageError(
438
+ f"No --corpus given and no {REGISTRY_CORPUS} here. Build the registry's scan corpus "
439
+ "with `open-interpretability corpus` (see the README), copy it in, or pass --corpus."
440
+ )
441
+
442
+
443
+ def load_answer(evidence_file, hypothesis_file, agent_model, device, dtype):
444
+ """Step two: the evidence, the external proposer's draft, the model and the agent."""
445
+ from openinterp.agents.proposer import HypothesisDraft, ProposerAgent
446
+ from openinterp.cli import _load_runner # looked up per call, so tests can swap it
447
+ from openinterp.core import costs
448
+ from openinterp.core.schemas import ScanEvidence
449
+ from openinterp.harness.units import attn_implementation
450
+
451
+ evidence = ScanEvidence.model_validate_json(evidence_file.read_text())
452
+ draft = HypothesisDraft.model_validate_json(hypothesis_file.read_text())
453
+ prompt_file = evidence_file.with_suffix(".prompt.md")
454
+ costs.add_external("proposer", agent_model, prompt_file, hypothesis_file)
455
+ attn = attn_implementation(evidence.identifier)
456
+ runner = _load_runner(evidence.identifier.model_name, device, dtype, None, attn)
457
+ return evidence, draft, runner, ProposerAgent(agent_model)
458
+
459
+
460
+ def scan_or_record(request, use_scan_cache: bool, wiki_dir: Path, device):
461
+ """Load the model and scan; keep a unit the scan stops on (see record_draw)."""
462
+ from openinterp.cli import _load_runner # looked up per call, so tests can swap it
463
+
464
+ attn = "eager" if request.unit == "head" else None # heads need attention patterns
465
+ runner = _load_runner(request.model_name, device, request.dtype, None, attn)
466
+ try:
467
+ return runner, scan_evidence(runner, request, open_scan_cache(use_scan_cache))
468
+ except Unauditable as exc:
469
+ record_draw(exc, request.random_pick, wiki_dir)
470
+ raise
471
+
472
+
473
+ def ask_proposer(agent_model, evidence, full_snippets: bool, related: str = ""):
474
+ """Ask the LLM proposer for a hypothesis about ``evidence``'s unit."""
475
+ from openinterp.agents.base import DEFAULT_MODEL
476
+ from openinterp.agents.proposer import ProposerAgent
477
+
478
+ agent = ProposerAgent(agent_model or DEFAULT_MODEL)
479
+ click.echo(f"Asking {agent.model} for a hypothesis…", err=True)
480
+ draft = agent.propose(
481
+ evidence.identifier,
482
+ evidence.top_examples,
483
+ evidence.vocabulary_lens,
484
+ evidence.confounds,
485
+ evidence.correlated_neurons,
486
+ evidence.head_profile,
487
+ evidence.input_attribution,
488
+ evidence.dictionary_profile,
489
+ bool(evidence.first_token_floor),
490
+ full_snippets,
491
+ related,
492
+ superposition_profile=evidence.superposition_profile,
493
+ manifold=evidence.manifold,
494
+ )
495
+ return agent, draft
@@ -0,0 +1,93 @@
1
+ """``open-interpretability evidence``: move slim records' blobs to the dataset (issue #3254).
2
+
3
+ ``push`` runs where the experiment ran. It pushes the blobs of the records that the branch
4
+ adds to ``evidence/<branch>``. CI runs ``upload`` (with ``HF_EVIDENCE_TOKEN``) or ``load``
5
+ (without it), then ``verify``. :mod:`openinterp.evidence_branch` explains why the blobs
6
+ ride on their own branch.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from openinterp.cli import cli, wiki_dir_option
17
+
18
+ base_ref_option = click.option(
19
+ "--base-ref", default="origin/main", show_default=True, help="The branch the PR merges into."
20
+ )
21
+ ref_option = click.option(
22
+ "--ref", required=True, help="The evidence branch, for example FETCH_HEAD after a fetch."
23
+ )
24
+
25
+
26
+ def _added(wiki_dir: Path, base_ref: str) -> list[Path]:
27
+ from openinterp.checks import changed_records
28
+
29
+ return changed_records(wiki_dir, base_ref)
30
+
31
+
32
+ @cli.group()
33
+ def evidence() -> None:
34
+ """Carry slim records' evidence blobs to the public dataset (docs/analyses/repo-size.md)."""
35
+
36
+
37
+ @evidence.command()
38
+ @wiki_dir_option()
39
+ @base_ref_option
40
+ @click.option("--remote", default="origin", show_default=True)
41
+ def push(wiki_dir: Path, base_ref: str, remote: str) -> None:
42
+ """Push the blobs of the records this branch adds to evidence/<branch>."""
43
+ from openinterp.evidence_branch import push as push_blobs
44
+ from openinterp.evidence_branch import record_refs
45
+
46
+ shas = record_refs(_added(wiki_dir, base_ref))
47
+ if not shas:
48
+ click.echo("No slim record on this branch: nothing to push.")
49
+ return
50
+ branch = subprocess.run(
51
+ ["git", "branch", "--show-current"], capture_output=True, text=True, check=True
52
+ ).stdout.strip()
53
+ target = push_blobs(branch, shas, remote)
54
+ click.echo(f"Pushed {len(shas)} evidence blobs to {remote}/{target}.")
55
+
56
+
57
+ @evidence.command()
58
+ @ref_option
59
+ def load(ref: str) -> None:
60
+ """Copy an evidence branch's blobs into the local store, checking each hash."""
61
+ from openinterp.evidence_branch import load_branch
62
+
63
+ click.echo(f"Loaded {len(load_branch(ref))} evidence blobs from {ref}.")
64
+
65
+
66
+ @evidence.command()
67
+ @ref_option
68
+ def upload(ref: str) -> None:
69
+ """Load an evidence branch's blobs, then upload them to the dataset (needs a write token)."""
70
+ from openinterp.core.evidence import EVIDENCE_REPO
71
+ from openinterp.evidence_branch import load_branch
72
+ from openinterp.evidence_branch import upload as upload_blobs
73
+
74
+ shas = load_branch(ref)
75
+ click.echo(f"Uploaded {upload_blobs(shas)} evidence blobs to {EVIDENCE_REPO}.")
76
+
77
+
78
+ @evidence.command()
79
+ @wiki_dir_option()
80
+ @base_ref_option
81
+ def verify(wiki_dir: Path, base_ref: str) -> None:
82
+ """Fail unless each record this branch adds has its evidence, with the right hashes."""
83
+ from openinterp.evidence_branch import unresolved
84
+
85
+ problems = unresolved(_added(wiki_dir, base_ref))
86
+ for problem in problems:
87
+ click.echo(problem, err=True)
88
+ if problems:
89
+ raise click.ClickException(
90
+ f"{len(problems)} records lack their evidence. Run `open-interpretability evidence "
91
+ "push` on the branch, then run the checks again."
92
+ )
93
+ click.echo("Every added record has its evidence.")
@@ -0,0 +1,101 @@
1
+ """Attribute one answer of a model to its labelled neuron poles.
2
+
3
+ The command is ``open-interpretability explain``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from dataclasses import asdict
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+ from openinterp.cli import _load_runner, cli, wiki_dir_option, with_model_options
15
+
16
+
17
+ @cli.command(name="explain")
18
+ @click.argument("prompt")
19
+ @click.option("--model", "model_name", default="Qwen/Qwen2.5-0.5B", show_default=True)
20
+ @click.option("--token", help="The answer token to explain. Default: the model's top token.")
21
+ @click.option("--position", default=-1, show_default=True, help="The position to explain.")
22
+ @click.option("--top", default=20, show_default=True, help="Poles to list.")
23
+ @click.option(
24
+ "--corpus", default="corpus.jsonl", show_default=True, type=click.Path(path_type=Path)
25
+ )
26
+ @click.option(
27
+ "--mean-tokens",
28
+ default=20_000,
29
+ show_default=True,
30
+ help="Tokens of the corpus for each neuron's mean, the attribution's baseline (cached).",
31
+ )
32
+ @click.option("--json", "as_json", is_flag=True, help="Print the result as JSON.")
33
+ @wiki_dir_option()
34
+ @with_model_options
35
+ def explain_command(
36
+ prompt, model_name, token, position, top, corpus, mean_tokens, as_json, wiki_dir, device, dtype
37
+ ):
38
+ """Explain the model's answer to PROMPT as fully as the registry allows.
39
+
40
+ It attributes the answer's logit to every MLP neuron pole, against each neuron's mean
41
+ on the corpus. It labels the top poles from the registry. It reports the share of
42
+ the attribution that VERIFIED poles hold, and checks it by mean-ablating them.
43
+ """
44
+ from openinterp.evidence import read_corpus
45
+ from openinterp.explain import explain, neuron_means, registry_labels
46
+
47
+ runner = _load_runner(model_name, device, dtype)
48
+ means = neuron_means(runner, model_name, read_corpus(corpus)[0], mean_tokens)
49
+ labels = registry_labels(Path(wiki_dir), model_name)
50
+ target = None
51
+ if token is not None:
52
+ ids = runner.tokenizer.encode(token, add_special_tokens=False)
53
+ if len(ids) != 1:
54
+ raise click.BadParameter(
55
+ f"{token!r} is {len(ids)} tokens, not one", param_hint="--token"
56
+ )
57
+ target = ids[0]
58
+ result = explain(
59
+ runner, model_name, prompt, means, labels, token=target, position=position, top=top
60
+ )
61
+ if as_json:
62
+ click.echo(json.dumps(asdict(result) | {"verified_share": result.verified_share}, indent=1))
63
+ return
64
+ _print(result)
65
+
66
+
67
+ def _print(result) -> None:
68
+ click.echo(f"Answer at position {result.position}: {result.token!r} (logit {result.logit:.2f})")
69
+ click.echo(
70
+ f"{'pole':<14} {'attribution':>11} {'share':>6} {'ablated Δ':>9} {'peak/max':>9} status"
71
+ )
72
+ for p in result.poles:
73
+ share = abs(p.attribution) / result.total if result.total else 0.0
74
+ ablated = f"{p.ablation_change:+.2f}" if p.ablation_change is not None else ""
75
+ peak = f"{p.strength / p.max_activation:.2f}" if p.max_activation else "—"
76
+ row = f"{p.label:<14} {p.attribution:>+11.3f} {share:>6.1%} {ablated:>9} {peak:>9}"
77
+ click.echo(f"{row} {p.status}")
78
+ if p.explanation:
79
+ click.echo(f"{'':<14} {p.explanation[:110]}")
80
+ click.echo(
81
+ f"VERIFIED poles hold {result.verified_share:.1%} of the total |attribution|; "
82
+ f"{1 - result.verified_share:.1%} is unexplained."
83
+ )
84
+ joint = result.joint
85
+ if joint.get("poles"):
86
+ changed = (
87
+ f"the top token changes to {joint['new_top_token']!r}"
88
+ if joint["top_token_changed"]
89
+ else "the top token stays"
90
+ )
91
+ click.echo(
92
+ f"Mean-ablating the {joint['poles']} VERIFIED poles above together moves the answer's "
93
+ f"logit by {joint['logit_change']:+.2f}, and {changed}."
94
+ )
95
+ else:
96
+ click.echo("No pole above is VERIFIED, so nothing here is explained yet.")
97
+ click.echo(
98
+ "Attribution is first order and against each neuron's mean. The ablated Δ column is "
99
+ "the exact check: mean-ablating a pole moves the logit by about minus its attribution. "
100
+ "Unverified poles carry no label."
101
+ )