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,471 @@
1
+ """Proposer agent: turns top-activating snippets into a falsifiable hypothesis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from openinterp import wordnet
11
+ from openinterp.agents.base import LLMAgent
12
+ from openinterp.agents.proposer_profiles import ( # noqa: F401 (re-exported)
13
+ format_dictionary_profile,
14
+ format_head_profile,
15
+ format_superposition_profile,
16
+ )
17
+ from openinterp.core.attribution import InputAttribution
18
+ from openinterp.core.confounds import MATCH_LABEL_SETS, NO_MATCH, NOT_SHOWN_TO_PROPOSER
19
+ from openinterp.core.fence import FENCE_RULE, fence
20
+ from openinterp.core.manifold import format_for_proposer as format_manifold
21
+ from openinterp.core.schemas import (
22
+ ActivatingExample,
23
+ ComponentIdentifier,
24
+ ConfoundProfile,
25
+ CorrelatedNeuron,
26
+ CounterfactualPair,
27
+ DictionaryProfile,
28
+ ExperimentProvenance,
29
+ HeadProfile,
30
+ Histogram,
31
+ LookaheadProfile,
32
+ NeuronAuditSubmission,
33
+ VocabularyLens,
34
+ )
35
+ from openinterp.harness.hooks import NeuronHookRunner
36
+ from openinterp.harness.lookahead import ablation_profile
37
+ from openinterp.harness.units import Unit, max_strength, unit_of
38
+
39
+
40
+ class CounterfactualDraft(BaseModel): # noqa: D101 (a docstring would join the JSON schema that the agent reads)
41
+ base_prompt: str = Field(
42
+ description="A prompt the hypothesis predicts strongly activates the pole."
43
+ )
44
+ counterfactual_prompt: str = Field(
45
+ description="A minimal edit of base_prompt that the hypothesis predicts changes activation."
46
+ )
47
+ expected_delta: float = Field(
48
+ ge=-1.0,
49
+ le=1.0,
50
+ description="Predicted (base - counterfactual) activation as a fraction of max activation.",
51
+ )
52
+ rationale: str
53
+
54
+
55
+ class HypothesisDraft(BaseModel): # noqa: D101 (a docstring would join the JSON schema that the agent reads)
56
+ explanation: str = Field(
57
+ description="One or two precise sentences: what input feature or upcoming-token "
58
+ "preparation makes this pole fire."
59
+ )
60
+ activating_tokens: list[str] = Field(description="Representative tokens where the pole peaks.")
61
+ mechanism: Literal["retrospective", "lookahead"] = Field(
62
+ description="retrospective = reacts to tokens already seen; "
63
+ "lookahead = prepares a specific upcoming token."
64
+ )
65
+ target_upcoming_tokens: list[str] = Field(
66
+ default_factory=list, description="For lookahead neurons: the tokens being prepared."
67
+ )
68
+ counterfactuals: list[CounterfactualDraft] = Field(min_length=3, max_length=10)
69
+ wordnet_synset: str | None = Field(
70
+ default=None,
71
+ description="If the hypothesis claims a WordNet noun category, its synset "
72
+ "(e.g. 'city.n.01'); otherwise null.",
73
+ )
74
+
75
+
76
+ # Agents see a window of tokens around each example's peak, not the whole scanned
77
+ # document. They also don't see near-duplicates of an earlier example. --full-snippets
78
+ # turns both off.
79
+ SNIPPET_BEFORE = 48
80
+ SNIPPET_AFTER = 16
81
+ NEAR_DUPLICATE = 0.8
82
+
83
+
84
+ def snippet_window(ex: ActivatingExample, full: bool = False) -> range:
85
+ """The token positions that the agents see of ``ex``: around its peak, or all with ``full``."""
86
+ if full:
87
+ return range(len(ex.tokens))
88
+ peak = ex.max_token_index
89
+ return range(max(0, peak - SNIPPET_BEFORE), min(len(ex.tokens), peak + SNIPPET_AFTER + 1))
90
+
91
+
92
+ def snippet(ex: ActivatingExample, full: bool = False) -> str:
93
+ """The example's text with the peak token in [[ ]], cut to its window with … marks."""
94
+ window = snippet_window(ex, full)
95
+ marked = "".join(
96
+ f"[[{ex.tokens[i]}]]" if i == ex.max_token_index else ex.tokens[i] for i in window
97
+ )
98
+ start = "…" if window.start > 0 else ""
99
+ end = "…" if window.stop < len(ex.tokens) else ""
100
+ return start + marked + end
101
+
102
+
103
+ def label_note(ex: ActivatingExample) -> str:
104
+ """The peak token's labels (``"language fr"``), or ``""`` if the scan had none."""
105
+ labels = (ex.peak_labels or {}).items()
106
+ shown = ((n, label) for n, label in labels if n not in NOT_SHOWN_TO_PROPOSER)
107
+ return ", ".join(f"{name} {label}" for name, label in shown if label != NO_MATCH)
108
+
109
+
110
+ def distinct_examples(
111
+ examples: Sequence[ActivatingExample], limit: int, full: bool = False
112
+ ) -> tuple[list[tuple[int, ActivatingExample]], list[int]]:
113
+ """Return the first ``limit`` examples, numbered from 1, and the numbers dropped.
114
+
115
+ An example is dropped when its window shares at least NEAR_DUPLICATE of its distinct
116
+ tokens with the window of an earlier kept example. Numbers stay those of the scan's
117
+ order, which input attribution cites.
118
+ """
119
+ kept: list[tuple[int, ActivatingExample]] = []
120
+ dropped: list[int] = []
121
+ seen: list[set[str]] = []
122
+ for n, ex in enumerate(examples[:limit], 1):
123
+ tokens = {ex.tokens[i] for i in snippet_window(ex)}
124
+ if not full and any(len(tokens & s) >= NEAR_DUPLICATE * len(tokens | s) for s in seen):
125
+ dropped.append(n)
126
+ continue
127
+ seen.append(tokens)
128
+ kept.append((n, ex))
129
+ return kept, dropped
130
+
131
+
132
+ def dropped_note(dropped: Sequence[int]) -> str:
133
+ """The note that lists examples left out as near-duplicates, or "" for none."""
134
+ if not dropped:
135
+ return ""
136
+ numbers = ", ".join(map(str, dropped))
137
+ return f"(Omitted as near-duplicates of an earlier example: {numbers}.)\n"
138
+
139
+
140
+ def format_review_examples(examples: Sequence[ActivatingExample], full: bool = False) -> str:
141
+ """The reviewer's view of the top 10 examples: text only, no strengths."""
142
+ if full: # the prompt before windowing, for A/B comparison
143
+ listed = "\n".join(f"{n}. {_noted(e)}{e.text}" for n, e in enumerate(examples[:10], 1))
144
+ return f"Top-activating examples:\n{fence(listed or '(none)')}\n\n"
145
+ kept, dropped = distinct_examples(examples, 10)
146
+ listed = "\n".join(f"{n}. {_noted(e)}{snippet(e)}" for n, e in kept) or "(none)"
147
+ note = "\n" + dropped_note(dropped).rstrip() if dropped else ""
148
+ head = "Top-activating examples (peak token in [[ ]], … where cut):"
149
+ return f"{head}\n{fence(listed)}{note}\n\n"
150
+
151
+
152
+ def _noted(ex: ActivatingExample) -> str:
153
+ note = label_note(ex)
154
+ return f"({note}) " if note else ""
155
+
156
+
157
+ def format_examples(
158
+ examples: Sequence[ActivatingExample], max_examples: int = 20, full: bool = False
159
+ ) -> str:
160
+ """Render examples with the peak token in [[double brackets]] and per-token strengths."""
161
+ lines = []
162
+ kept, dropped = distinct_examples(examples, max_examples, full)
163
+ for n, ex in kept:
164
+ marked = snippet(ex, full)
165
+ window = range(max(0, ex.max_token_index - 3), min(len(ex.tokens), ex.max_token_index + 2))
166
+ context = ", ".join(f"{ex.tokens[i]!r}:{ex.activations[i]:.2f}" for i in window)
167
+ note = label_note(ex)
168
+ head = f"max {ex.max_activation:.3f}" + (f", {note}" if note else "")
169
+ lines.append(f"{n}. ({head}) {marked}\n around peak: {context}")
170
+ if ex.attends_to:
171
+ attended = ", ".join(
172
+ f"{ex.tokens[i]!r} (token {i - ex.max_token_index:+d}, weight {w:.2f})"
173
+ for i, w in zip(ex.attends_to, ex.attention_weights, strict=True)
174
+ )
175
+ lines.append(f" peak attends to: {attended}")
176
+ return "\n".join(lines) + ("\n" + dropped_note(dropped).rstrip() if dropped else "")
177
+
178
+
179
+ def format_input_attribution(attribution: InputAttribution) -> str:
180
+ """Where in the context the pole's peaks come from, for the proposer and the reviewer."""
181
+ if not attribution.examples:
182
+ return ""
183
+ n = len(attribution.examples)
184
+ text = (
185
+ f"Input attribution (integrated gradients over the input tokens, at the peak of "
186
+ f"{n} top examples; the path integral converged on {attribution.converged}): "
187
+ )
188
+ if attribution.distances:
189
+ shares = {d.min_distance: d.share for d in attribution.distances}
190
+ far = sum(d.share for d in attribution.distances if d.min_distance > 16)
191
+ text += (
192
+ f"{shares[0]:.0%} of it is on the peak token itself, {shares[1]:.0%} on the "
193
+ f"token before and {far:.0%} more than 16 tokens back (mean distance "
194
+ f"{attribution.mean_distance:.1f} tokens). "
195
+ )
196
+ if attribution.ablation_agreement is not None:
197
+ text += (
198
+ f"Replacing one token at a time agrees with it at rho = "
199
+ f"{attribution.ablation_agreement:+.2f}. "
200
+ )
201
+ lines = [
202
+ text + "Tokens driving each peak most, as offset from the peak: share of the "
203
+ "activation (attribution / effect of replacing that token alone; trust the second "
204
+ "where an example did not converge):"
205
+ ]
206
+ for ex in attribution.examples:
207
+ tokens = ", ".join(
208
+ f"{t.token!r} ({t.offset:+d}) {t.attribution:.2f}/"
209
+ + ("?" if t.ablation_effect is None else f"{t.ablation_effect:.2f}")
210
+ for t in ex.top
211
+ )
212
+ note = "" if ex.converged else " (not converged)"
213
+ lines.append(f" example {ex.example + 1}, peak {ex.peak_token!r}{note}: {tokens}")
214
+ return "\n".join(lines) + "\n"
215
+
216
+
217
+ # Protocol 0.8: how a unit with a first-token floor is read, for every agent that writes
218
+ # prompts for one.
219
+ FIRST_TOKEN_UNMEASURED = (
220
+ "This unit has a first-token floor: at a text's first token it is active whatever the " # noqa: S105 (prompt text, not a password)
221
+ "token is, so it is not measured there. A prompt's strength is its peak over every "
222
+ "token but the first, so every prompt needs at least two tokens, and a trigger that "
223
+ "opens a prompt is not seen. Its examples show the first token's strength, but it "
224
+ "never counts.\n"
225
+ )
226
+
227
+
228
+ class ProposerAgent(LLMAgent):
229
+ """The proposer: writes a falsifiable hypothesis from the scan evidence."""
230
+
231
+ role = "proposer"
232
+ system_prompt = (
233
+ "You are a mechanistic interpretability researcher auditing a single unit, a native "
234
+ "MLP neuron pole or attention head, or a latent of a sparse dictionary trained on an "
235
+ "MLP layer, in an open-weight language model. Activations "
236
+ "are strengths (non-negative; larger = more active). Propose the most specific "
237
+ "hypothesis that explains the evidence and that could be falsified. Decide explicitly "
238
+ "whether the unit reacts to what has already been read (retrospective) or prepares a "
239
+ "particular upcoming token (lookahead): lookahead neurons often peak on the token "
240
+ "*before* the predicted one, and their promoted-token list is informative. Write "
241
+ "minimal-pair "
242
+ "counterfactuals that change as little text as possible, including at least one "
243
+ "control pair with expected_delta near 0. " + FENCE_RULE
244
+ )
245
+
246
+ def propose(
247
+ self,
248
+ identifier: ComponentIdentifier,
249
+ examples: Sequence[ActivatingExample],
250
+ lens: VocabularyLens | None = None,
251
+ confounds: ConfoundProfile | None = None,
252
+ correlated: Sequence[CorrelatedNeuron] = (),
253
+ head_profile: HeadProfile | None = None,
254
+ attribution: InputAttribution | None = None,
255
+ dictionary_profile: DictionaryProfile | None = None,
256
+ first_token_floor: bool = False,
257
+ full_snippets: bool = False,
258
+ related: str = "",
259
+ superposition_profile=None,
260
+ manifold=None,
261
+ ) -> HypothesisDraft:
262
+ """Ask the proposer for a hypothesis about ``identifier``."""
263
+ prompt = self.proposal_prompt(
264
+ identifier,
265
+ examples,
266
+ lens,
267
+ confounds,
268
+ correlated,
269
+ head_profile,
270
+ attribution,
271
+ dictionary_profile,
272
+ first_token_floor,
273
+ full_snippets,
274
+ related,
275
+ superposition_profile,
276
+ manifold,
277
+ )
278
+ return self.structured(HypothesisDraft, prompt)
279
+
280
+ @staticmethod
281
+ def proposal_prompt(
282
+ identifier: ComponentIdentifier,
283
+ examples: Sequence[ActivatingExample],
284
+ lens: VocabularyLens | None = None,
285
+ confounds: ConfoundProfile | None = None,
286
+ correlated: Sequence[CorrelatedNeuron] = (),
287
+ head_profile: HeadProfile | None = None,
288
+ attribution: InputAttribution | None = None,
289
+ dictionary_profile: DictionaryProfile | None = None,
290
+ first_token_floor: bool = False,
291
+ full_snippets: bool = False,
292
+ related: str = "",
293
+ superposition_profile=None,
294
+ manifold=None,
295
+ ) -> str:
296
+ """Return the task text that the proposer answers.
297
+
298
+ It gives the scan evidence for one unit, then what other research links to it.
299
+ ``related`` is the unit's studied sister neurons
300
+ (:func:`openinterp.sisters.format_sisters`).
301
+ """
302
+ prompt = f"{identifier.noun.capitalize()}: {identifier}\n\n"
303
+ if first_token_floor:
304
+ prompt += FIRST_TOKEN_UNMEASURED + "\n"
305
+ if head_profile is not None:
306
+ prompt += format_head_profile(head_profile) + "\n"
307
+ if dictionary_profile is not None:
308
+ prompt += format_dictionary_profile(dictionary_profile) + "\n"
309
+ if superposition_profile is not None:
310
+ prompt += format_superposition_profile(superposition_profile) + "\n"
311
+ window = (
312
+ ""
313
+ if full_snippets
314
+ else f"; up to {SNIPPET_BEFORE} tokens before it and {SNIPPET_AFTER} after, … where cut"
315
+ )
316
+ prompt += (
317
+ f"Top-activating snippets (peak token in [[ ]]{window}):\n"
318
+ f"{fence(format_examples(examples, full=full_snippets))}\n\n"
319
+ )
320
+ if lens is not None:
321
+ prompt += format_lens(lens, identifier)
322
+ if confounds is not None:
323
+ prompt += format_confounds(confounds)
324
+ prompt += wordnet.format_for_proposer(examples, wordnet.lexicon())
325
+ if correlated:
326
+ prompt += format_correlated(correlated)
327
+ if manifold is not None:
328
+ prompt += format_manifold(manifold)
329
+ if attribution is not None:
330
+ prompt += "\n" + format_input_attribution(attribution)
331
+ if related:
332
+ prompt += "\n" + related
333
+ return prompt
334
+
335
+ def build_submission(
336
+ self,
337
+ identifier: ComponentIdentifier,
338
+ draft: HypothesisDraft,
339
+ *,
340
+ runner: NeuronHookRunner | None = None,
341
+ examples: Sequence[ActivatingExample] = (),
342
+ histogram: Histogram | None = None,
343
+ confounds: ConfoundProfile | None = None,
344
+ correlated: Sequence[CorrelatedNeuron] = (),
345
+ provenance: ExperimentProvenance | None = None,
346
+ vocabulary_lens: VocabularyLens | None = None,
347
+ input_attribution: InputAttribution | None = None,
348
+ head_profile: HeadProfile | None = None,
349
+ dictionary_profile: DictionaryProfile | None = None,
350
+ first_token_floor: bool | None = None,
351
+ superposition_profile=None,
352
+ manifold=None,
353
+ ) -> NeuronAuditSubmission:
354
+ """Convert a draft into a submission, measuring evidence with ``runner`` if given."""
355
+ max_act = max((e.max_activation for e in examples), default=0.0) or None
356
+ pairs, lookahead = draft_claims(draft)
357
+ is_head = identifier.kind == "attn_head"
358
+ if runner is not None and max_act:
359
+ unit = unit_of(
360
+ identifier, head_profile, dictionary_profile, bool(first_token_floor),
361
+ superposition_profile,
362
+ ) # fmt: skip
363
+ pairs, lookahead = measure_claims(runner, unit, pairs, lookahead, max_act)
364
+
365
+ return NeuronAuditSubmission(
366
+ identifier=identifier,
367
+ natural_language_explanation=draft.explanation,
368
+ activating_tokens=list(draft.activating_tokens),
369
+ counterfactuals=pairs,
370
+ lookahead=lookahead,
371
+ submitting_agent_model=self.model,
372
+ max_activation=max_act,
373
+ top_examples=list(examples),
374
+ activation_histogram=histogram,
375
+ first_token_floor=first_token_floor,
376
+ model_num_layers=runner.n_layers if runner is not None else None,
377
+ model_mlp_width=runner.mlp_width(identifier.layer) if runner is not None else None,
378
+ model_n_heads=runner.n_heads if runner is not None and is_head else None,
379
+ provenance=provenance,
380
+ confounds=confounds,
381
+ correlated_neurons=list(correlated),
382
+ vocabulary_lens=vocabulary_lens,
383
+ input_attribution=input_attribution,
384
+ head_profile=head_profile,
385
+ dictionary_profile=dictionary_profile,
386
+ superposition_profile=superposition_profile,
387
+ manifold=manifold,
388
+ )
389
+
390
+
391
+ def format_lens(lens: VocabularyLens, identifier: ComponentIdentifier) -> str:
392
+ """The proposer's note on the tokens that the unit promotes and suppresses."""
393
+ return (
394
+ f"Tokens this {'pole' if identifier.pole else identifier.noun} promotes via the "
395
+ f"Jacobian lens (its output direction "
396
+ f"transported to the final layer; reliable in middle layers): "
397
+ f"{', '.join(map(repr, lens.jacobian_promoted))}; suppresses: "
398
+ f"{', '.join(map(repr, lens.jacobian_suppressed))}. "
399
+ f"Logit lens (no transport): {', '.join(map(repr, lens.logit_promoted))}. "
400
+ f"Lens agreement {lens.overlap:.0%}; peakedness (kurtosis) "
401
+ f"{lens.jacobian_kurtosis:.1f}, where ~3 means no specific token set.\n"
402
+ )
403
+
404
+
405
+ def format_confounds(confounds: ConfoundProfile) -> str:
406
+ """The proposer's note on how much token identity, position and labels explain."""
407
+ top = ", ".join(repr(t.token) for t in confounds.trigger_tokens[:10])
408
+ labels = "".join(
409
+ f" {name.capitalize()} explains {c.r2:.0%} ({c.mutual_information_bits:.3f} bits); "
410
+ f"active tokens by {name}: "
411
+ f"{', '.join(f'{s.label} {s.active}/{s.count}' for s in c.levels[:10])}."
412
+ for name, c in (confounds.labels or {}).items()
413
+ if name not in NOT_SHOWN_TO_PROPOSER
414
+ and (name not in MATCH_LABEL_SETS or c.has_active_match)
415
+ )
416
+ return (
417
+ f"\nConfounds: token identity explains {confounds.token_r2:.0%} of activation "
418
+ f"variance, position {confounds.position_r2:.0%}.{labels} Highest mean-activation "
419
+ f"tokens: {top}. Your hypothesis must predict activity better than "
420
+ f"'fires on these tokens'.\n"
421
+ )
422
+
423
+
424
+ def format_correlated(correlated: Sequence[CorrelatedNeuron]) -> str:
425
+ """The proposer's note on the poles in the layer that co-activate with the unit."""
426
+ pairs = ", ".join(
427
+ f"N{c.neuron_idx}{'+' if c.pole == 'positive' else '-'} "
428
+ f"(co-activation lift >= {c.coactivation_lift_lower:.1f}, rho={c.spearman:+.2f})"
429
+ for c in correlated[:5]
430
+ )
431
+ return f"Neuron poles in this layer that co-activate with it: {pairs}.\n"
432
+
433
+
434
+ def draft_claims(draft: HypothesisDraft) -> tuple[list[CounterfactualPair], LookaheadProfile]:
435
+ """The draft's counterfactual pairs (dropping unedited ones) and mechanism, unmeasured."""
436
+ pairs = [
437
+ CounterfactualPair(
438
+ base_prompt=cf.base_prompt,
439
+ counterfactual_prompt=cf.counterfactual_prompt,
440
+ expected_delta=cf.expected_delta,
441
+ )
442
+ for cf in draft.counterfactuals
443
+ if cf.base_prompt != cf.counterfactual_prompt
444
+ ]
445
+ lookahead = LookaheadProfile(
446
+ is_lookahead=draft.mechanism == "lookahead",
447
+ target_upcoming_tokens=list(draft.target_upcoming_tokens),
448
+ kl_divergence=0.0,
449
+ )
450
+ return pairs, lookahead
451
+
452
+
453
+ def measure_claims(runner, unit: Unit, pairs, lookahead: LookaheadProfile, max_act: float):
454
+ """Each pair's observed delta, and the ablation KL on the base prompts."""
455
+ pairs = [
456
+ pair.model_copy(update={"observed_delta": observed_delta(runner, unit, pair, max_act)})
457
+ for pair in pairs
458
+ ]
459
+ prompts = [p.base_prompt for p in pairs]
460
+ measured = ablation_profile(runner, prompts, unit.ablation(runner))
461
+ # Keep the proposer's claim; record the measured KL as evidence for review.
462
+ return pairs, lookahead.model_copy(update={"kl_divergence": measured.kl_divergence})
463
+
464
+
465
+ def observed_delta(
466
+ runner: NeuronHookRunner, unit: Unit, pair: CounterfactualPair, scale: float
467
+ ) -> float:
468
+ """Measured (base - counterfactual) max strength, normalised by ``scale``."""
469
+ base = max_strength(runner, unit, pair.base_prompt)
470
+ cf = max_strength(runner, unit, pair.counterfactual_prompt)
471
+ return round((base - cf) / scale, 6)
@@ -0,0 +1,79 @@
1
+ """How the proposer's prompt describes a unit that is not a neuron pole.
2
+
3
+ A head is described by its attention-pattern scores, and a dictionary feature by its
4
+ dictionary's fit. A superposition is described by its member neurons and their weights.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from openinterp.core.schemas import DictionaryProfile, HeadProfile, SuperpositionProfile
10
+
11
+ SHOWN_MEMBERS = 20 # the largest members by weight that the prompt lists
12
+
13
+
14
+ def format_head_profile(profile: HeadProfile) -> str:
15
+ """The proposer's note on what a head's strength is, and its pattern scores."""
16
+ return (
17
+ f"Strength is |W_O (z - mean z)|: how far the head's write to the residual stream is "
18
+ f"from its average write. Attention-pattern scores on {profile.pattern_sequences} "
19
+ f"random {profile.pattern_length}-token sequences repeated twice (0-1; mean attention "
20
+ f"weight): previous token {profile.previous_token_score:.2f}, duplicate token "
21
+ f"{profile.duplicate_token_score:.2f}, induction (to the token after an earlier copy "
22
+ f"of the current one) {profile.induction_score:.2f}.\n"
23
+ )
24
+
25
+
26
+ def format_dictionary_profile(profile: DictionaryProfile) -> str:
27
+ """The proposer's note on what a dictionary latent's strength is, and its fit."""
28
+ if profile.kind == "basis": # says nothing of which basis, so a null arm stays blind
29
+ return (
30
+ f"This is one direction in this layer's MLP hidden space (one of a basis of "
31
+ f"{profile.n_features}), read where native neurons are: the input to the down "
32
+ f"projection. Strength is the positive part of the hidden activations' "
33
+ f"projection onto it; it writes that strength x the direction, through the "
34
+ f"down projection, to the MLP output. It is non-zero on {profile.density:.2%} of "
35
+ f"scanned tokens.\n"
36
+ )
37
+ residual = profile.output_site == "resid_post"
38
+ output = "the residual stream after this layer" if residual else "the MLP output"
39
+ fit = ""
40
+ if profile.explained_variance is not None:
41
+ fit = (
42
+ f" On the scan the whole dictionary reconstructs "
43
+ f"{profile.explained_variance:.0%} of {output}'s variance with "
44
+ f"{profile.mean_l0:.0f} active latents per token."
45
+ )
46
+ if profile.kind == "spd":
47
+ return (
48
+ f"This is one latent ({profile.n_features} in this layer) of a parameter "
49
+ f"decomposition of this layer's MLP down projection into rank-1 pieces, not a "
50
+ f"native neuron. It reads where native neurons are: the input to the down "
51
+ f"projection. Strength is the positive part of the hidden activations' "
52
+ f"projection onto the piece's input direction, scaled by the piece's causal "
53
+ f"importance on that token (0 to 1: how much ablating the piece would change "
54
+ f"the model's output). It writes strength x the piece's output direction to the "
55
+ f"MLP output. It is non-zero on {profile.density:.2%} of scanned tokens.{fit}\n"
56
+ )
57
+ trained = output if residual else "this layer's MLP"
58
+ return (
59
+ f"This is one latent of a sparse {profile.kind} ({profile.n_features} latents) "
60
+ f"trained on {trained}, not a native neuron. Strength is the latent's "
61
+ f"activation (non-negative); it writes activation x its decoder vector to "
62
+ f"{output}. It is non-zero on {profile.density:.2%} of scanned tokens.{fit}\n"
63
+ )
64
+
65
+
66
+ def format_superposition_profile(profile: SuperpositionProfile) -> str:
67
+ """The superposition's members, largest weight first, and how it was found."""
68
+ members = sorted(profile.members, key=lambda m: -abs(m.weight))
69
+ top = abs(members[0].weight)
70
+ shown = ", ".join(f"N{m.neuron_idx} ({m.weight / top:+.2f})" for m in members[:SHOWN_MEMBERS])
71
+ more = len(members) - SHOWN_MEMBERS
72
+ rest = f", and {more} more" if more > 0 else ""
73
+ return (
74
+ f"This unit is a superposition: one direction over {len(members)} of the layer's "
75
+ f"neurons, active where the weighted sum of their activations is positive. Members, "
76
+ f"by relative weight: {shown}{rest}. It was found by {profile.method} "
77
+ f"({profile.source}). Explain what the direction as a whole responds to, not one "
78
+ "neuron."
79
+ )