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
openinterp/crossref.py ADDED
@@ -0,0 +1,442 @@
1
+ """Cross-reference neuron poles with published dictionaries (`open-interpretability crossref`).
2
+
3
+ This takes every neuron pole in the archive whose model has a dictionary preset for its
4
+ layer. It finds the latents whose decoder aligns with the pole's write, and whose encoder
5
+ aligns with its read (:mod:`openinterp.harness.crossref`). Rows go to
6
+ ``docs/analyses/dictionary-alignment.jsonl``, one per pole and dictionary.
7
+
8
+ A row names only the dictionary and each latent's index. A latent's description is with
9
+ the dictionary's host, or in this registry if the latent was audited here. The host is
10
+ Neuronpedia, where :attr:`DictionarySpec.neuronpedia` says it has them. The description
11
+ is looked up when shown, and kept on disk so that each latent is fetched only once
12
+ (``$OPENINTERP_NEURONPEDIA_CACHE``, ``off`` to turn it off). A description from
13
+ Neuronpedia is credited to it and to the model that wrote it.
14
+
15
+ Agreement between a pole's explanation and an aligned latent's is supporting evidence.
16
+ Disagreement is something for the reviewer to probe (`open-interpretability review
17
+ --dictionary-latents`). Neither is gated.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import time
25
+ import urllib.error
26
+ import urllib.request
27
+ from collections import Counter, defaultdict
28
+ from collections.abc import Callable, Iterable
29
+ from pathlib import Path
30
+
31
+ from openinterp import DISTRIBUTION
32
+ from openinterp.core.cache_dirs import cache_root
33
+ from openinterp.core.fence import fence
34
+ from openinterp.core.schemas import AuditRecord, NeuronIdentifier
35
+ from openinterp.core.strict import StrictModel
36
+
37
+ DEFAULT_PATH = Path("docs/analyses/dictionary-alignment.jsonl")
38
+ NEURONPEDIA = "https://www.neuronpedia.org"
39
+ MAX_WAIT = 60.0 # seconds: the longest wait before asking Neuronpedia again
40
+
41
+
42
+ class LatentMatch(StrictModel):
43
+ """One latent of a dictionary, and how well it aligns with the pole."""
44
+
45
+ feature_idx: int
46
+ cosine: float
47
+ channel: str | None = None # the neuron's read channel (gate, up, in), for a read
48
+
49
+
50
+ class AlignmentSide(StrictModel):
51
+ """One side's best matches, by |cosine|.
52
+
53
+ ``chance`` is what a random direction reaches. Also the share of the layer's neurons
54
+ whose best match is weaker.
55
+ """
56
+
57
+ matches: list[LatentMatch]
58
+ chance: float
59
+ percentile: float
60
+
61
+
62
+ class AlignmentRow(StrictModel):
63
+ """One pole against one dictionary.
64
+
65
+ ``writes`` compares each latent's decoder row with the pole's write. + means that the
66
+ latent writes what the pole writes. ``reads`` compares each latent's encoder with the
67
+ neuron's reads. + means that the latent reads what raises the neuron's
68
+ pre-activation. ``reads`` is ``None`` for a dictionary that doesn't read the MLP's
69
+ input.
70
+ """
71
+
72
+ slug: str
73
+ dictionary: str
74
+ repo: str
75
+ revision: str
76
+ path: str
77
+ model_revision: str | None
78
+ writes: AlignmentSide
79
+ reads: AlignmentSide | None = None
80
+
81
+
82
+ def load_rows(path: Path | None = DEFAULT_PATH) -> dict[str, list[AlignmentRow]]:
83
+ """The rows in ``path`` by pole slug; empty if it doesn't exist."""
84
+ out: dict[str, list[AlignmentRow]] = defaultdict(list)
85
+ if path is None or not Path(path).exists():
86
+ return out
87
+ for line in Path(path).read_text().splitlines():
88
+ if line.strip():
89
+ row = AlignmentRow.model_validate_json(line)
90
+ out[row.slug].append(row)
91
+ return out
92
+
93
+
94
+ def dictionaries_for(model_name: str, layer: int) -> list[str]:
95
+ """The dictionary presets that cover ``layer`` of ``model_name``, sorted by name."""
96
+ from openinterp.harness.dictionaries import DICTIONARIES
97
+
98
+ return sorted(
99
+ name
100
+ for name, spec in DICTIONARIES.items()
101
+ if spec.model_name == model_name and layer in spec.layers
102
+ )
103
+
104
+
105
+ def _poles(records: Iterable[AuditRecord]) -> dict[str, tuple[NeuronIdentifier, Counter]]:
106
+ """Each neuron pole in ``records``, with how often each model revision pinned it."""
107
+ poles: dict[str, tuple[NeuronIdentifier, Counter]] = {}
108
+ for record in records:
109
+ ident = record.submission.identifier
110
+ if not isinstance(ident, NeuronIdentifier):
111
+ continue
112
+ prov = record.submission.provenance
113
+ _, revisions = poles.setdefault(ident.slug, (ident, Counter()))
114
+ revisions[prov.model_revision if prov else None] += 1
115
+ return poles
116
+
117
+
118
+ def _todo(records: Iterable[AuditRecord], out: Path):
119
+ """Return ``({(model, dictionary, layer): poles}, {model: revision})`` still to do.
120
+
121
+ These are the pairs that ``out`` has no row for. A model is read at the revision that
122
+ its records pin most often.
123
+ """
124
+ done = {(r.slug, r.dictionary) for rows in load_rows(out).values() for r in rows}
125
+ todo: dict[tuple[str, str, int], list[NeuronIdentifier]] = defaultdict(list)
126
+ revisions: dict[str, Counter] = defaultdict(Counter)
127
+ for slug, (ident, seen) in _poles(records).items():
128
+ revisions[ident.model_name].update(seen)
129
+ for name in dictionaries_for(ident.model_name, ident.layer):
130
+ if (slug, name) not in done:
131
+ todo[(ident.model_name, name, ident.layer)].append(ident)
132
+ return todo, {m: _pinned(seen) for m, seen in revisions.items()}
133
+
134
+
135
+ def crossref(
136
+ records: Iterable[AuditRecord],
137
+ load_runner: Callable[[str, str | None], object],
138
+ out: Path,
139
+ *,
140
+ k: int = 5,
141
+ echo: Callable[[str], None] = print,
142
+ ) -> int:
143
+ """Append a row to ``out`` for each pole and covering dictionary without one yet.
144
+
145
+ Loads each model once. Returns how many rows it wrote.
146
+ """
147
+ from openinterp.core.exceptions import ArchitectureNotSupportedError
148
+ from openinterp.harness.crossref import align_layer
149
+ from openinterp.harness.dictionaries import dictionary_spec
150
+
151
+ todo, revisions = _todo(records, out)
152
+ out.parent.mkdir(parents=True, exist_ok=True)
153
+ wrote, runner, loaded = 0, None, None
154
+ for model_name, name, layer in sorted(todo):
155
+ if loaded != model_name:
156
+ runner, loaded = load_runner(model_name, revisions[model_name]), model_name
157
+ spec, idents = dictionary_spec(name), todo[(model_name, name, layer)]
158
+ try:
159
+ found = align_layer(runner, spec, layer, [(i.neuron_idx, i.pole) for i in idents], k=k)
160
+ except ArchitectureNotSupportedError as exc:
161
+ echo(f"skip {name} L{layer}: {exc}")
162
+ continue
163
+ with out.open("a") as f:
164
+ for ident in idents:
165
+ write, read = found[(ident.neuron_idx, ident.pole)]
166
+ row = AlignmentRow(
167
+ slug=ident.slug,
168
+ dictionary=name,
169
+ repo=spec.repo,
170
+ revision=spec.revision,
171
+ path=spec.file(layer),
172
+ model_revision=revisions[model_name],
173
+ writes=_side(write),
174
+ reads=_side(read) if read is not None else None,
175
+ )
176
+ f.write(row.model_dump_json() + "\n")
177
+ wrote += len(idents)
178
+ echo(f"{name} L{layer}: {len(idents)} poles")
179
+ return wrote
180
+
181
+
182
+ def _pinned(revisions: Counter) -> str | None:
183
+ """The revision most records pin; an unpinned record says nothing about which."""
184
+ pinned = Counter({r: n for r, n in revisions.items() if r is not None})
185
+ return pinned.most_common(1)[0][0] if pinned else None
186
+
187
+
188
+ def _side(side) -> AlignmentSide:
189
+ return AlignmentSide(
190
+ matches=[
191
+ LatentMatch(feature_idx=m.feature_idx, cosine=round(m.cosine, 4), channel=m.channel)
192
+ for m in side.matches
193
+ ],
194
+ chance=round(side.chance, 4),
195
+ percentile=round(side.percentile, 4),
196
+ )
197
+
198
+
199
+ def sort_rows(path: Path) -> None:
200
+ """Rewrite ``path`` in a stable order, so reruns that append don't churn the diff."""
201
+ rows = sorted(
202
+ (r for rows in load_rows(path).values() for r in rows), key=lambda r: (r.slug, r.dictionary)
203
+ )
204
+ path.write_text("".join(r.model_dump_json() + "\n" for r in rows))
205
+
206
+
207
+ def layer_of(row: AlignmentRow) -> int:
208
+ """The layer of the pole that ``row`` is about, read from its slug."""
209
+ return int(row.slug.split("/L")[1].split("/")[0])
210
+
211
+
212
+ def neuronpedia_url(row: AlignmentRow, feature_idx: int, *, api: bool = False) -> str | None:
213
+ """The latent's Neuronpedia page (or API endpoint), where Neuronpedia hosts it."""
214
+ return latent_url(row.dictionary, layer_of(row), feature_idx, api=api)
215
+
216
+
217
+ def latent_url(dictionary: str, layer: int, feature_idx: int, *, api: bool = False) -> str | None:
218
+ """The Neuronpedia page (or API endpoint) of a latent of a dictionary preset.
219
+
220
+ ``None`` where Neuronpedia does not host that dictionary at ``layer``.
221
+ """
222
+ from openinterp.harness.dictionaries import DICTIONARIES
223
+
224
+ spec = DICTIONARIES.get(dictionary)
225
+ source = spec and spec.neuronpedia_source(layer)
226
+ if not source:
227
+ return None
228
+ return f"{NEURONPEDIA}{'/api/feature' if api else ''}/{source}/{feature_idx}"
229
+
230
+
231
+ class Explanation(StrictModel):
232
+ """One Neuronpedia explanation of a latent, with who wrote it.
233
+
234
+ ``explainer`` is the model that wrote it (e.g. ``gpt-4-turbo``), or ``"human"``.
235
+ The author's account is not kept.
236
+ """
237
+
238
+ description: str
239
+ explainer: str
240
+
241
+
242
+ def user_agent() -> str:
243
+ """The User-Agent for requests to Neuronpedia: this package, its version and its repo."""
244
+ from importlib.metadata import PackageNotFoundError, metadata
245
+
246
+ from openinterp import __version__
247
+
248
+ try:
249
+ urls = metadata(DISTRIBUTION).get_all("Project-URL") or []
250
+ except PackageNotFoundError:
251
+ urls = []
252
+ repo = next((u.split(", ", 1)[1] for u in urls if u.startswith("Repository, ")), None)
253
+ return f"openinterp/{__version__}" + (f" (+{repo})" if repo else "")
254
+
255
+
256
+ def _cache_path(url: str) -> Path | None:
257
+ """Where the explanations fetched from ``url`` are kept; None if the cache is off."""
258
+ root = cache_root("OPENINTERP_NEURONPEDIA_CACHE", "neuronpedia")
259
+ if root is None:
260
+ return None
261
+ return root / f"{url.split('/api/feature/', 1)[1]}.json"
262
+
263
+
264
+ def _get_json(url: str, *, timeout: float, retries: int, sleep: Callable, opener: Callable):
265
+ """GET ``url`` as JSON. Wait and try again when the server says it is busy (429, 5xx).
266
+
267
+ Return ``None`` if the request fails or the server stays busy.
268
+ """
269
+ # url is an https Neuronpedia endpoint from latent_url, never a file: URL.
270
+ request = urllib.request.Request(url, headers={"User-Agent": user_agent()}) # noqa: S310
271
+ for attempt in range(retries + 1):
272
+ try:
273
+ with opener(request, timeout=timeout) as response:
274
+ return json.load(response)
275
+ except urllib.error.HTTPError as err:
276
+ busy = err.code == 429 or err.code >= 500
277
+ if not busy or attempt == retries:
278
+ return None
279
+ try:
280
+ wait = float(err.headers.get("Retry-After", ""))
281
+ except (TypeError, ValueError):
282
+ wait = 2.0 * 2**attempt
283
+ sleep(min(wait, MAX_WAIT))
284
+ except (OSError, ValueError):
285
+ return None
286
+ return None
287
+
288
+
289
+ def neuronpedia_explanations(
290
+ row: AlignmentRow,
291
+ feature_idx: int,
292
+ timeout: float = 20,
293
+ **kwargs,
294
+ ) -> list[Explanation] | None:
295
+ """Return the Neuronpedia explanations of latent ``feature_idx`` aligned in ``row``."""
296
+ return latent_explanations(row.dictionary, layer_of(row), feature_idx, timeout, **kwargs)
297
+
298
+
299
+ def latent_explanations(
300
+ dictionary: str,
301
+ layer: int,
302
+ feature_idx: int,
303
+ timeout: float = 20,
304
+ *,
305
+ retries: int = 3,
306
+ sleep: Callable = time.sleep,
307
+ opener: Callable = urllib.request.urlopen,
308
+ ) -> list[Explanation] | None:
309
+ """Return the latent's Neuronpedia explanations, kept on disk after the first fetch.
310
+
311
+ A latent is fetched once: later calls read the cache, also when it has no
312
+ explanations. ``None`` if Neuronpedia does not host the latent or the request fails;
313
+ a failure is not cached.
314
+ """
315
+ url = latent_url(dictionary, layer, feature_idx, api=True)
316
+ if url is None:
317
+ return None
318
+ path = _cache_path(url)
319
+ if path is not None and path.exists():
320
+ return [Explanation.model_validate(e) for e in json.loads(path.read_text())]
321
+ data = _get_json(url, timeout=timeout, retries=retries, sleep=sleep, opener=opener)
322
+ if not isinstance(data, dict):
323
+ return None
324
+ found = [
325
+ Explanation(
326
+ description=text,
327
+ explainer=e.get("explanationModelName")
328
+ or ("human" if e.get("typeName") == "human" else "unknown"),
329
+ )
330
+ for e in data.get("explanations") or []
331
+ if (text := (e.get("description") or "").strip())
332
+ ]
333
+ if path is not None:
334
+ path.parent.mkdir(parents=True, exist_ok=True)
335
+ partial = path.with_suffix(f".{os.getpid()}.part") # reviews can run side by side
336
+ partial.write_text(json.dumps([e.model_dump() for e in found]))
337
+ partial.replace(path)
338
+ return found
339
+
340
+
341
+ def neuronpedia_description(row: AlignmentRow, feature_idx: int, **kwargs) -> Explanation | None:
342
+ """Return the latent's first machine-written Neuronpedia explanation, else its first.
343
+
344
+ ``None`` if it has none or the request fails.
345
+ """
346
+ found = neuronpedia_explanations(row, feature_idx, **kwargs) or []
347
+ return machine_explanation(found) or next(iter(found), None)
348
+
349
+
350
+ def machine_explanation(found: Iterable[Explanation] | None) -> Explanation | None:
351
+ """The first machine-written explanation of ``found``; ``None`` if there is none."""
352
+ return next((e for e in found or [] if e.explainer != "human"), None)
353
+
354
+
355
+ def feature_slug(row: AlignmentRow, feature_idx: int) -> str:
356
+ """The slug the latent would have in this registry, if audited here."""
357
+ model_slug = row.slug.split("/L")[0]
358
+ return f"{model_slug}/L{layer_of(row)}/{row.dictionary}/F{feature_idx}"
359
+
360
+
361
+ def describe(
362
+ rows: Iterable[AlignmentRow],
363
+ registry: dict[str, str] | None = None,
364
+ *,
365
+ fetch: Callable = neuronpedia_description,
366
+ min_percentile: float = 0.0,
367
+ ) -> list[dict]:
368
+ """Return each aligned latent of ``rows`` with its description.
369
+
370
+ The description is the registry's explanation, where the latent was audited here
371
+ (``registry`` maps feature slugs to it). Otherwise it is what ``fetch`` returns.
372
+ ``source`` says where it came from. That is "this registry", or Neuronpedia and the
373
+ model that wrote it.
374
+ """
375
+ registry = registry or {}
376
+ out = []
377
+ for row in rows:
378
+ for side_name in ("writes", "reads"):
379
+ side = getattr(row, side_name)
380
+ if side is None or side.percentile < min_percentile:
381
+ continue
382
+ for m in side.matches:
383
+ if abs(m.cosine) <= side.chance:
384
+ continue
385
+ slug = feature_slug(row, m.feature_idx)
386
+ text, source = registry.get(slug), "this registry"
387
+ if text is None:
388
+ found = fetch(row, m.feature_idx)
389
+ text = found and found.description
390
+ source = found and f"Neuronpedia, {found.explainer}"
391
+ out.append(
392
+ {
393
+ "row": row,
394
+ "side": side_name,
395
+ "match": m,
396
+ "description": text,
397
+ "source": source if text else None,
398
+ }
399
+ )
400
+ return out
401
+
402
+
403
+ def registry_explanations(records: Iterable[AuditRecord]) -> dict[str, str]:
404
+ """The newest explanation of every dictionary latent audited here, by slug."""
405
+ newest: dict[str, AuditRecord] = {}
406
+ for record in records:
407
+ ident = record.submission.identifier
408
+ if ident.kind != "dictionary_feature" or record.verdict is None:
409
+ continue
410
+ old = newest.get(ident.slug)
411
+ if old is None or record.verdict.timestamp > old.verdict.timestamp:
412
+ newest[ident.slug] = record
413
+ return {s: r.submission.natural_language_explanation for s, r in newest.items()}
414
+
415
+
416
+ def format_for_reviewer(described: list[dict]) -> str:
417
+ """Return aligned latents for the reviewer's prompt.
418
+
419
+ They say what published dictionaries think a latent with this pole's weights does.
420
+ Only latents above chance are listed.
421
+ """
422
+ lines = []
423
+ for d in described:
424
+ if not d["description"]:
425
+ continue
426
+ row, m = d["row"], d["match"]
427
+ what = "writes" if d["side"] == "writes" else f"reads (via {m.channel})"
428
+ lines.append(
429
+ f"- {row.dictionary} latent {m.feature_idx} {what} along this pole "
430
+ f"(cosine {m.cosine:+.2f}; chance {getattr(row, d['side']).chance:.2f}): "
431
+ f"{d['description']} [{d['source']}]"
432
+ )
433
+ if not lines:
434
+ return ""
435
+ return (
436
+ "Sparse-dictionary latents whose weights align with this pole's, and what their "
437
+ "authors' descriptions say they do. They are not measurements of this pole. Where "
438
+ "a description disagrees with the hypothesis, write probes that tell them apart; "
439
+ "where it agrees, the hypothesis may still be too broad or too narrow.\n"
440
+ + fence("\n".join(lines))
441
+ + "\n"
442
+ )
@@ -0,0 +1,9 @@
1
+ """Data files shipped with the package: the built-in sample corpus."""
2
+
3
+ from importlib.resources import files
4
+
5
+
6
+ def sample_corpus() -> list[str]:
7
+ """A small, diverse built-in corpus for quick scans. Use --corpus for real audits."""
8
+ text = files(__name__).joinpath("sample_corpus.txt").read_text(encoding="utf-8")
9
+ return [line.strip() for line in text.splitlines() if line.strip()]
@@ -0,0 +1,58 @@
1
+ asshole
2
+ assholes
3
+ blowjob
4
+ blowjobs
5
+ bullshit
6
+ cumshot
7
+ cumshots
8
+ cunt
9
+ cunts
10
+ dildo
11
+ dildos
12
+ faggot
13
+ faggots
14
+ fuck
15
+ fucked
16
+ fucker
17
+ fuckers
18
+ fuckin
19
+ fucking
20
+ fucks
21
+ gangbang
22
+ gook
23
+ gooks
24
+ handjob
25
+ handjobs
26
+ kike
27
+ kikes
28
+ motherfucker
29
+ motherfuckers
30
+ nigga
31
+ niggas
32
+ nigger
33
+ niggers
34
+ porn
35
+ porno
36
+ rape
37
+ raped
38
+ rapes
39
+ raping
40
+ rapist
41
+ rapists
42
+ raghead
43
+ ragheads
44
+ shit
45
+ shithead
46
+ shitheads
47
+ shits
48
+ shitting
49
+ shitty
50
+ slut
51
+ sluts
52
+ towelhead
53
+ towelheads
54
+ tranny
55
+ wetback
56
+ wetbacks
57
+ whore
58
+ whores