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,244 @@
1
+ """Errata: harness bugs that affected signed records (``docs/PROTOCOL.md`` §5).
2
+
3
+ Records cannot be rewritten. A maintainer who finds a bug that affected some of them
4
+ appends an erratum to ``wiki/errata.jsonl``. Then they sign the file with
5
+ ``open-interpretability sign wiki/errata.jsonl``. Each line is one :class:`Erratum`. It names the
6
+ records it covers (a list of record paths, a :class:`ErratumRule`, or both), the bug, and
7
+ the commit that fixed it. It either lists :class:`Edit` objects for the records, or sets
8
+ the records aside.
9
+
10
+ The record files never change. The site build applies the edits to a copy of each covered
11
+ record (:func:`apply_errata`). So pages, standings and ``registry.parquet`` read the
12
+ corrected values. ``open-interpretability reproduce`` checks the corrected record, and skips a
13
+ record that is set aside.
14
+
15
+ ``open-interpretability check`` verifies the file (:mod:`openinterp.errata_checks`). Errata have
16
+ no page of their own. The site shows each erratum on the experiments it covers, with the old
17
+ and new value of each edit. The standing rule (:mod:`openinterp.builder.standing`) leaves
18
+ out the records that are set aside.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from collections.abc import Sequence
25
+ from dataclasses import dataclass
26
+ from datetime import date as Date
27
+ from pathlib import Path
28
+ from typing import TYPE_CHECKING, Any, Literal
29
+
30
+ from pydantic import AwareDatetime, Field, JsonValue, model_validator
31
+
32
+ from openinterp.core.paths import DEFAULT_WIKI_DIR
33
+ from openinterp.core.strict import StrictModel
34
+
35
+ if TYPE_CHECKING:
36
+ from openinterp.core.schemas import AuditRecord
37
+
38
+ ERRATA_FILE = "errata.jsonl"
39
+ """The errata file's name in the wiki directory."""
40
+
41
+ FIXED_PATHS = ("submission.identifier", "verdict.timestamp")
42
+ """Fields an edit can't change: they give the record's unit and id, so its place."""
43
+
44
+
45
+ class Edit(StrictModel):
46
+ """One corrected field of a record: its dotted path and its new value.
47
+
48
+ The path names keys and list indices of the record's JSON, for example
49
+ ``verdict.status`` or ``submission.top_examples.0.max_activation``. The field must exist.
50
+ """
51
+
52
+ path: str = Field(pattern=r"^[a-z_]+(\.([a-z_]+|[0-9]+))*$")
53
+ value: JsonValue
54
+
55
+ @model_validator(mode="after")
56
+ def _not_fixed(self) -> Edit:
57
+ if any(self.path == p or self.path.startswith(p + ".") for p in FIXED_PATHS):
58
+ raise ValueError(f"an edit can't change {self.path}: it places the record")
59
+ return self
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class AppliedEdit:
64
+ """An edit as applied to one record: which erratum made it, and the value before."""
65
+
66
+ erratum: str # the erratum's id
67
+ path: str
68
+ before: Any
69
+ after: Any
70
+
71
+
72
+ class ErratumRule(StrictModel):
73
+ """A rule that selects records. A record matches when it matches every field given.
74
+
75
+ A field that is ``None`` matches every record. A record without a review policy has no
76
+ protocol version, so a rule with ``protocol_versions`` does not match it.
77
+ """
78
+
79
+ models: list[str] | None = Field(default=None, min_length=1, description="Model names.")
80
+ kinds: list[Literal["mlp_neuron_pole", "attn_head", "dictionary_feature"]] | None = Field(
81
+ default=None, min_length=1
82
+ )
83
+ protocol_versions: list[str] | None = Field(default=None, min_length=1)
84
+ harness_versions: list[str] | None = Field(default=None, min_length=1)
85
+ reviewed_from: AwareDatetime | None = Field(
86
+ default=None, description="First verdict time that matches (inclusive)."
87
+ )
88
+ reviewed_before: AwareDatetime | None = Field(
89
+ default=None, description="Verdict time from which records no longer match."
90
+ )
91
+
92
+ @model_validator(mode="after")
93
+ def _selects_something(self) -> ErratumRule:
94
+ if all(value is None for value in self.model_dump().values()):
95
+ raise ValueError("a rule needs at least one field; list the records instead")
96
+ return self
97
+
98
+ def matches(self, record: AuditRecord) -> bool:
99
+ """Return whether ``record`` matches every field of the rule."""
100
+ ident, verdict = record.submission.identifier, record.verdict
101
+ provenance = record.submission.provenance
102
+ policy = verdict.policy if verdict is not None else None
103
+ checks = (
104
+ (self.models, ident.model_name),
105
+ (self.kinds, ident.kind),
106
+ (self.protocol_versions, policy.protocol_version if policy else None),
107
+ (self.harness_versions, provenance.harness_version if provenance else None),
108
+ )
109
+ if any(allowed is not None and value not in allowed for allowed, value in checks):
110
+ return False
111
+ when = verdict.timestamp if verdict is not None else None
112
+ if self.reviewed_from is not None and (when is None or when < self.reviewed_from):
113
+ return False
114
+ return self.reviewed_before is None or (when is not None and when < self.reviewed_before)
115
+
116
+
117
+ class Erratum(StrictModel):
118
+ """One harness bug and the records it affected. One line of ``wiki/errata.jsonl``."""
119
+
120
+ id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]*$", description="Unique; the site's anchor.")
121
+ date: Date
122
+ title: str = Field(min_length=1, description="One line for the banner.")
123
+ bug: str = Field(min_length=1, description="What was wrong, and what it did to records.")
124
+ fixed_in: str = Field(pattern=r"^[0-9a-f]{7,40}$", description="The commit that fixed it.")
125
+ edits: list[Edit] = Field(default_factory=list, description="Corrections to each record.")
126
+ set_aside: bool = Field(default=False, description="The records count toward no status.")
127
+ records: list[str] = Field(
128
+ default_factory=list,
129
+ description="Covered records, as paths relative to wiki/experiments/.",
130
+ )
131
+ rule: ErratumRule | None = None
132
+
133
+ @model_validator(mode="after")
134
+ def _covers_something(self) -> Erratum:
135
+ if not self.records and self.rule is None:
136
+ raise ValueError("an erratum needs records, a rule, or both")
137
+ if bool(self.edits) == self.set_aside:
138
+ raise ValueError("an erratum either lists edits or sets its records aside")
139
+ return self
140
+
141
+ def covers(self, record: AuditRecord) -> bool:
142
+ """Return whether the erratum covers ``record``: it is listed or matches the rule."""
143
+ return record_ref(record) in self.records or (
144
+ self.rule is not None and self.rule.matches(record)
145
+ )
146
+
147
+
148
+ def errata_path(wiki_dir: Path | str = DEFAULT_WIKI_DIR) -> Path:
149
+ """Where the errata file is: ``<wiki_dir>/errata.jsonl``."""
150
+ return Path(wiki_dir) / ERRATA_FILE
151
+
152
+
153
+ def record_ref(record: AuditRecord) -> str:
154
+ """How an erratum names ``record``: its path relative to ``wiki/experiments/``."""
155
+ from openinterp.builder.wiki import archive_path
156
+ from openinterp.core.paths import experiments_dir
157
+
158
+ return archive_path(Path(), record).relative_to(experiments_dir(Path())).as_posix()
159
+
160
+
161
+ def parse_errata(text: str) -> list[Erratum]:
162
+ """Parse the content of an errata file. Raises ``ValueError`` naming the bad line."""
163
+ errata = []
164
+ for number, line in enumerate(text.splitlines(), start=1):
165
+ try:
166
+ errata.append(Erratum.model_validate_json(line))
167
+ except ValueError as exc:
168
+ raise ValueError(f"line {number}: {exc}") from exc
169
+ return errata
170
+
171
+
172
+ def load_errata(wiki_dir: Path | str = DEFAULT_WIKI_DIR) -> list[Erratum]:
173
+ """Every erratum in ``wiki_dir``, in file order; none if there is no errata file."""
174
+ path = errata_path(wiki_dir)
175
+ return parse_errata(path.read_text(encoding="utf-8")) if path.exists() else []
176
+
177
+
178
+ def covering(errata: Sequence[Erratum], record: AuditRecord) -> list[Erratum]:
179
+ """The errata in ``errata`` that cover ``record``, in file order."""
180
+ return [erratum for erratum in errata if erratum.covers(record)]
181
+
182
+
183
+ def apply_errata(
184
+ record: AuditRecord, errata: Sequence[Erratum]
185
+ ) -> tuple[AuditRecord, list[AppliedEdit]]:
186
+ """``record`` with the edits of ``errata`` applied in order, and what each edit changed.
187
+
188
+ Returns ``record`` itself when no erratum edits it. Raises ``ValueError`` when an edit's
189
+ path is not in the record, or the edited record is not valid.
190
+ """
191
+ from openinterp.core.schemas import AuditRecord
192
+
193
+ edits = [(erratum.id, edit) for erratum in errata for edit in erratum.edits]
194
+ if not edits:
195
+ return record, []
196
+ data = record.model_dump(mode="json")
197
+ applied = []
198
+ for erratum_id, edit in edits:
199
+ before = _replace(data, edit.path, edit.value)
200
+ applied.append(AppliedEdit(erratum_id, edit.path, before, edit.value))
201
+ try:
202
+ return AuditRecord.model_validate_json(json.dumps(data)), applied
203
+ except ValueError as exc:
204
+ raise ValueError(f"the edited record is not valid: {exc}") from exc
205
+
206
+
207
+ def _replace(data: Any, path: str, value: Any) -> Any:
208
+ """Set the field at dotted ``path`` in ``data`` to ``value``; return its old value."""
209
+ *parents, last = path.split(".")
210
+ node = data
211
+ try:
212
+ for part in parents:
213
+ node = node[int(part)] if isinstance(node, list) else node[part]
214
+ key = int(last) if isinstance(node, list) else last
215
+ before = node[key]
216
+ except (KeyError, IndexError, TypeError, ValueError):
217
+ raise ValueError(f"the record has no field {path}") from None
218
+ node[key] = value
219
+ return before
220
+
221
+
222
+ def corrected(record: AuditRecord, errata: Sequence[Erratum]) -> tuple[AuditRecord, bool]:
223
+ """``record`` with the edits of the errata that cover it, and whether one sets it aside."""
224
+ found = covering(errata, record)
225
+ return apply_errata(record, found)[0], any(erratum.set_aside for erratum in found)
226
+
227
+
228
+ def standing_records(
229
+ records: Sequence[AuditRecord], errata: Sequence[Erratum]
230
+ ) -> list[AuditRecord]:
231
+ """The records that count toward a status, with their errata's edits applied.
232
+
233
+ This is the standing rule's first step (:mod:`openinterp.builder.standing`), for readers
234
+ that don't build the site's tree. A record that an erratum sets aside is left out. The
235
+ exception is a unit whose records are all set aside: then its status rests on them, so
236
+ they stay. The order of ``records`` is kept.
237
+ """
238
+ fixed = [(record, *corrected(record, errata)) for record in records]
239
+ counted = {r.submission.identifier.slug for r, _, aside in fixed if not aside}
240
+ return [
241
+ edited
242
+ for record, edited, aside in fixed
243
+ if not aside or record.submission.identifier.slug not in counted
244
+ ]
@@ -0,0 +1,160 @@
1
+ """Bulky record evidence in a content-addressed store, outside the repository (issue #3254).
2
+
3
+ Two fields make up about 59% of a record: ``submission.top_examples`` and
4
+ ``verdict.explanation_scores``. :func:`split` takes them out of a record's JSON. Each one
5
+ becomes a reference, ``{"evidence_sha256": "<hash>"}``, to a blob that holds the field's
6
+ exact JSON. :func:`resolve` puts each blob back, and refuses a blob whose hash is wrong.
7
+
8
+ A record's signature covers its file's bytes, so it covers the hashes. A hash covers its
9
+ blob. So a signed slim record still vouches for all of its evidence. Records are
10
+ write-once, so the records that are already whole stay whole.
11
+
12
+ Blobs live in a local store, ``~/.cache/openinterp/evidence/`` by default
13
+ (``$OPENINTERP_EVIDENCE_DIR``). A blob that isn't there is fetched, read-only, from the
14
+ public dataset :data:`EVIDENCE_REPO`. :func:`openinterp.builder.wiki.archive_record` writes
15
+ slim records unless ``OPENINTERP_OFFLOAD_EVIDENCE=0``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import os
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ FIELDS = (("submission", "top_examples"), ("verdict", "explanation_scores"))
27
+ """The record fields that :func:`split` moves to the store: (parent key, field)."""
28
+ REF_KEY = "evidence_sha256"
29
+ EVIDENCE_REPO = "sackfield/open-interpretability-evidence"
30
+ """The public Hugging Face dataset that holds the blobs, by path ``<sha[:2]>/<sha>.json``."""
31
+ DIR_ENV = "OPENINTERP_EVIDENCE_DIR"
32
+ OFFLOAD_ENV = "OPENINTERP_OFFLOAD_EVIDENCE" # "0" makes archive_record write whole records
33
+
34
+
35
+ class EvidenceMissing(ValueError):
36
+ """A record refers to a blob that neither the local store nor the dataset holds."""
37
+
38
+
39
+ def offload_enabled() -> bool:
40
+ """Whether new archived records are written slim: yes, unless the variable is ``0``.
41
+
42
+ Phase 3 of issue #3254 turned this on. CI uploads the blobs from ``evidence/<branch>``.
43
+ """
44
+ return os.environ.get(OFFLOAD_ENV, "1") != "0"
45
+
46
+
47
+ def store_dir() -> Path:
48
+ """The local blob store: ``$OPENINTERP_EVIDENCE_DIR``, else ``~/.cache/openinterp/evidence``."""
49
+ return Path(os.environ.get(DIR_ENV) or Path.home() / ".cache" / "openinterp" / "evidence")
50
+
51
+
52
+ def blob_name(sha: str) -> str:
53
+ """A blob's path in the store and in the dataset."""
54
+ return f"{sha[:2]}/{sha}.json"
55
+
56
+
57
+ def canonical(value: Any) -> bytes:
58
+ """The bytes that a blob holds for ``value``: sorted keys, no spaces, UTF-8."""
59
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
60
+
61
+
62
+ def sha256(data: bytes) -> str:
63
+ """The hex SHA-256 of ``data``."""
64
+ return hashlib.sha256(data).hexdigest()
65
+
66
+
67
+ def is_ref(value: Any) -> bool:
68
+ """Whether ``value`` is a reference that :func:`split` wrote."""
69
+ return isinstance(value, dict) and set(value) == {REF_KEY}
70
+
71
+
72
+ def put(data: bytes, root: Path | None = None) -> str:
73
+ """Store ``data`` in the local store, and return its hash."""
74
+ sha = sha256(data)
75
+ path = (root or store_dir()) / blob_name(sha)
76
+ if not path.exists():
77
+ path.parent.mkdir(parents=True, exist_ok=True)
78
+ tmp = path.with_suffix(f".{os.getpid()}.tmp")
79
+ tmp.write_bytes(data)
80
+ tmp.replace(path) # atomic: a reader sees no blob or a whole one
81
+ return sha
82
+
83
+
84
+ def _fetch(sha: str) -> bytes | None:
85
+ """The blob from the public dataset, or ``None`` if it can't be had."""
86
+ try:
87
+ from huggingface_hub import hf_hub_download
88
+
89
+ local = hf_hub_download(EVIDENCE_REPO, blob_name(sha), repo_type="dataset")
90
+ except Exception: # noqa: BLE001 - offline, no dataset yet, or no such blob
91
+ return None
92
+ return Path(local).read_bytes()
93
+
94
+
95
+ def get(sha: str, root: Path | None = None, fetch=None) -> bytes:
96
+ """The blob with hash ``sha``: from the local store, else fetched and stored.
97
+
98
+ ``fetch`` defaults to the read-only fetch from :data:`EVIDENCE_REPO`. Raises
99
+ :class:`EvidenceMissing` when the blob can't be had or its hash is wrong.
100
+ """
101
+ path = (root or store_dir()) / blob_name(sha)
102
+ data = path.read_bytes() if path.exists() else (fetch or _fetch)(sha)
103
+ if data is None:
104
+ raise EvidenceMissing(
105
+ f"evidence blob {sha} is not in {root or store_dir()} or {EVIDENCE_REPO}"
106
+ )
107
+ if sha256(data) != sha:
108
+ raise EvidenceMissing(f"evidence blob {sha} has the wrong hash; the store holds a bad copy")
109
+ if not path.exists():
110
+ put(data, root)
111
+ return data
112
+
113
+
114
+ def split(data: dict, root: Path | None = None) -> dict:
115
+ """A copy of a record's JSON with each field in :data:`FIELDS` moved to the store.
116
+
117
+ A field that is missing, ``None`` or already a reference stays as it is.
118
+ """
119
+ out = json.loads(json.dumps(data))
120
+ for parent, field in FIELDS:
121
+ node = out.get(parent)
122
+ if not isinstance(node, dict) or node.get(field) is None or is_ref(node[field]):
123
+ continue
124
+ node[field] = {REF_KEY: put(canonical(node[field]), root)}
125
+ return out
126
+
127
+
128
+ def resolve(data: Any, root: Path | None = None, fetch=None) -> Any:
129
+ """``data`` with each reference in :data:`FIELDS` replaced by its blob's value.
130
+
131
+ Data with no reference comes back as it is, so whole records cost nothing.
132
+ """
133
+ if not isinstance(data, dict):
134
+ return data
135
+ refs = [
136
+ (parent, field)
137
+ for parent, field in FIELDS
138
+ if isinstance(data.get(parent), dict) and is_ref(data[parent].get(field))
139
+ ]
140
+ if not refs:
141
+ return data
142
+ out = dict(data)
143
+ for parent, field in refs:
144
+ sha = data[parent][field][REF_KEY]
145
+ out[parent] = {**out[parent], field: json.loads(get(sha, root, fetch))}
146
+ return out
147
+
148
+
149
+ def resolved_text(json_data: str | bytes | bytearray) -> str | bytes | bytearray:
150
+ """A record's JSON text with its references resolved; whole records come back as given."""
151
+ text = json_data.decode() if isinstance(json_data, bytes | bytearray) else json_data
152
+ if REF_KEY not in text:
153
+ return json_data
154
+ return json.dumps(resolve(json.loads(text)))
155
+
156
+
157
+ def resolved_dict_text(obj: Any) -> str | None:
158
+ """A record dict's JSON text with its references resolved, or ``None`` if it has none."""
159
+ resolved = resolve(obj)
160
+ return None if resolved is obj else json.dumps(resolved)
@@ -0,0 +1,93 @@
1
+ """What a scan shows of a unit.
2
+
3
+ A scan shows a unit's top-activating examples and the histogram of its strengths. For a
4
+ neuron, it shows the same for the other pole.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import Field, model_validator
10
+
11
+ from openinterp.core.identifiers import Pole
12
+ from openinterp.core.strict import StrictModel, UnitFloat
13
+
14
+
15
+ class Histogram(StrictModel):
16
+ """Counts of strengths in bins; ``bin_edges`` has one more entry than ``counts``."""
17
+
18
+ bin_edges: list[float] = Field(min_length=2)
19
+ counts: list[int]
20
+
21
+ @model_validator(mode="after")
22
+ def _shape(self) -> Histogram:
23
+ if len(self.counts) != len(self.bin_edges) - 1:
24
+ raise ValueError("counts must have exactly len(bin_edges) - 1 entries")
25
+ return self
26
+
27
+ def share_at_least(self, line: float) -> float:
28
+ """Return the share of values at or above ``line``.
29
+
30
+ Only bins wholly above ``line`` count, so the result is a lower bound, within one
31
+ bin of the exact share.
32
+ """
33
+ total = sum(self.counts)
34
+ above = sum(n for lo, n in zip(self.bin_edges, self.counts, strict=False) if lo >= line)
35
+ return above / total if total else 0.0
36
+
37
+
38
+ class ActivatingExample(StrictModel):
39
+ """A text snippet with per-token strengths (non-negative magnitudes).
40
+
41
+ For an attention head, ``attends_to`` lists the tokens the peak token attends to
42
+ most, strongest first, with their attention weights in ``attention_weights``.
43
+ ``peak_labels`` gives the peak token's label in each of the scan's label sets. A
44
+ document label, such as language, is the label of the token's document. It is
45
+ ``None`` when the scan had no label sets, or the peak token has no label in any.
46
+ """
47
+
48
+ text: str
49
+ tokens: list[str]
50
+ activations: list[float]
51
+ max_activation: float = Field(ge=0.0)
52
+ max_token_index: int = Field(ge=0)
53
+ attends_to: list[int] = Field(default_factory=list)
54
+ attention_weights: list[UnitFloat] = Field(default_factory=list)
55
+ peak_labels: dict[str, str] | None = None
56
+
57
+ @model_validator(mode="after")
58
+ def _aligned(self) -> ActivatingExample:
59
+ if len(self.tokens) != len(self.activations):
60
+ raise ValueError("tokens and activations must be the same length")
61
+ if self.tokens and self.max_token_index >= len(self.tokens):
62
+ raise ValueError("max_token_index out of range")
63
+ if len(self.attends_to) != len(self.attention_weights):
64
+ raise ValueError("attends_to and attention_weights must be the same length")
65
+ if any(not 0 <= i <= self.max_token_index for i in self.attends_to):
66
+ raise ValueError("attends_to must index tokens up to the peak (attention is causal)")
67
+ return self
68
+
69
+
70
+ class OppositePole(StrictModel):
71
+ """A neuron's other pole, read from the same scan as the audited pole.
72
+
73
+ The neuron's page shows it beside the audited pole. It is reported, not gated, and
74
+ neither agent sees it.
75
+ """
76
+
77
+ pole: Pole
78
+ top_examples: list[ActivatingExample]
79
+ activation_histogram: Histogram | None = None
80
+
81
+
82
+ def check_opposite_pole(model) -> None:
83
+ """Reject an ``opposite_pole`` that the unit cannot have.
84
+
85
+ Only a neuron pole has an opposite pole, and it must not be the audited pole.
86
+ """
87
+ if model.opposite_pole is None:
88
+ return
89
+ pole = model.identifier.pole
90
+ if pole is None:
91
+ raise ValueError("opposite_pole is only for neuron poles")
92
+ if model.opposite_pole.pole == pole:
93
+ raise ValueError("opposite_pole must be the pole the identifier does not name")
@@ -0,0 +1,29 @@
1
+ """Exception hierarchy for openinterp."""
2
+
3
+
4
+ class OpenInterpError(Exception):
5
+ """Base class for all openinterp errors."""
6
+
7
+
8
+ class ArchitectureNotSupportedError(OpenInterpError):
9
+ """The hook runner could not locate MLP layers on the given model."""
10
+
11
+
12
+ class PatchingError(OpenInterpError):
13
+ """An activation patching run could not be performed as requested."""
14
+
15
+
16
+ class ShapeMismatchError(PatchingError):
17
+ """Source and target activations have incompatible shapes."""
18
+
19
+
20
+ class AgentError(OpenInterpError):
21
+ """An LLM agent failed to produce a valid structured response."""
22
+
23
+
24
+ class RegistryError(OpenInterpError):
25
+ """An audit record on disk is missing or malformed."""
26
+
27
+
28
+ class SyncError(OpenInterpError):
29
+ """Deploying the static site to object storage failed."""
@@ -0,0 +1,130 @@
1
+ """Schemas for scoring an explanation on held-out text (detection, fuzzing, embedding).
2
+
3
+ These follow Delphi (Paulo et al., 2025). A judge sees only the explanation.
4
+
5
+ - *detection*: the judge picks out the held-out windows that activate the pole.
6
+ - *fuzzing*: the judge says whether the marked tokens in a window activate the pole.
7
+ - *embedding*: an embedding model ranks windows by similarity to the explanation.
8
+
9
+ The scores are reported, not gated (docs/PROTOCOL.md §4).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pydantic import Field
15
+
16
+ from openinterp.core.costs import ExperimentCosts
17
+ from openinterp.core.strict import StrictModel, UnitFloat
18
+
19
+
20
+ class ScoredWindow(StrictModel):
21
+ """A held-out window as the judge saw it, measured on its own (like a probe)."""
22
+
23
+ text: str
24
+ tokens: list[str]
25
+ activations: list[float]
26
+ quantile: int | None = Field(
27
+ default=None,
28
+ ge=0,
29
+ description="Activation bin of the window's peak among active held-out windows "
30
+ "(0 = weakest); None when the window is inactive.",
31
+ )
32
+ highlighted: list[int] = Field(
33
+ default_factory=list, description="Fuzzing: indices of the tokens marked for the judge."
34
+ )
35
+ label: bool = Field(
36
+ description="What a perfect judge answers: the window is active (detection), or its "
37
+ "marked tokens are exactly its active tokens (fuzzing)."
38
+ )
39
+ judged: bool | None = Field(default=None, description="The judge's answer.")
40
+ similarity: float | None = Field(
41
+ default=None, description="Embedding: cosine between the explanation and the window."
42
+ )
43
+
44
+
45
+ class JudgeScore(StrictModel):
46
+ """How well the judge's answers match the labels.
47
+
48
+ ``recall_by_quantile[q]`` is the share of positive windows in activation bin ``q``
49
+ that the judge got right. It is ``None`` when the bin had no positive windows. It
50
+ shows the weak activations, which the top activations can otherwise hide.
51
+ """
52
+
53
+ balanced_accuracy: UnitFloat
54
+ true_positive_rate: UnitFloat | None
55
+ false_positive_rate: UnitFloat | None
56
+ recall_by_quantile: list[UnitFloat | None]
57
+
58
+
59
+ class ControlScore(StrictModel):
60
+ """The judge's scores for a control explanation, on the same windows as the real one.
61
+
62
+ A control shows how much of a score comes from the windows and not from the
63
+ explanation. ``empty`` gives the judge no explanation. ``other_unit`` gives it the
64
+ explanation of another unit of the same model. A real explanation should score well
65
+ above both.
66
+ """
67
+
68
+ kind: str = Field(description="'empty' or 'other_unit'.")
69
+ explanation: str = Field(description="What the judge saw; '' for the empty control.")
70
+ source_slug: str | None = Field(
71
+ default=None, description="other_unit: the unit whose explanation this is."
72
+ )
73
+ detection_judged: list[bool] = Field(default_factory=list)
74
+ fuzzing_judged: list[bool] = Field(default_factory=list)
75
+ detection: JudgeScore | None = None
76
+ fuzzing: JudgeScore | None = None
77
+
78
+
79
+ class ExplanationScores(StrictModel):
80
+ """Delphi-style scores of ``explanation`` on windows the proposer never saw.
81
+
82
+ Windows are cut from corpus snippets outside the scan's top examples and measured on
83
+ their own, exactly as the judge reads them. Active windows (peak ≥ ``threshold``) are
84
+ drawn evenly from ``quantiles`` bins of their peak strength, split at
85
+ ``quantile_edges``, and matched with as many inactive ones.
86
+ """
87
+
88
+ slug: str = Field(description="The unit scored: its identifier's slug.")
89
+ explanation: str = Field(min_length=1)
90
+ threshold: float = Field(gt=0.0, description="Pole strength counted as active.")
91
+ window_tokens: int = Field(gt=0)
92
+ quantiles: int = Field(gt=0)
93
+ quantile_edges: list[float]
94
+ seed: int
95
+ corpus: str
96
+ corpus_sha256: str | None = None
97
+ candidate_windows: int = Field(ge=0, description="Held-out windows measured.")
98
+ active_windows: int = Field(ge=0, description="Of those, windows reaching the threshold.")
99
+ judge_model: str | None = None
100
+ embedding_model: str | None = None
101
+ detection: JudgeScore | None = None
102
+ fuzzing: JudgeScore | None = None
103
+ embedding_auroc: UnitFloat | None = Field(
104
+ default=None,
105
+ description="AUROC of the embedding similarity for active vs inactive detection windows.",
106
+ )
107
+ detection_windows: list[ScoredWindow] = Field(default_factory=list)
108
+ fuzzing_windows: list[ScoredWindow] = Field(default_factory=list)
109
+ fuzzing_decoys: str | None = Field(
110
+ default=None,
111
+ description="How fuzzing decoys were marked: 'matched-shape' (the real marks' count "
112
+ "and spacing, moved) since issue #3505. None for older scores, whose decoys were "
113
+ "random tokens that explanation-free rules could tell apart.",
114
+ )
115
+ rule_baseline: dict[str, float] = Field(
116
+ default_factory=dict,
117
+ description="Balanced accuracy of explanation-free rules on these windows, best cut "
118
+ "chosen on them, so an upper bound: detection_length, fuzzing_contiguous, "
119
+ "fuzzing_count. A score means something only above these.",
120
+ )
121
+ controls: list[ControlScore] = Field(
122
+ default_factory=list,
123
+ description="The same windows judged with control explanations "
124
+ "(open-interpretability score --controls). Each control's answers are scored against "
125
+ "these windows' labels.",
126
+ )
127
+ costs: ExperimentCosts | None = Field(
128
+ default=None,
129
+ description="What scoring cost; a review moves it into the record's provenance.",
130
+ )