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,172 @@
1
+ """Statistics that account for dependence between observations.
2
+
3
+ Probes written by one reviewer are not independent: near-duplicates ("Many cities,
4
+ including Paris…" / "Many cities, including Rome…") share most of their information.
5
+ We cluster near-duplicate probes and treat each cluster as one observation, scored by
6
+ its mean pass rate. Effective counts feed the protocol's minimum-probe gates, the
7
+ balanced accuracy and the Beta posteriors behind significance and the registry's
8
+ false-discovery estimate.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import random
14
+ import re
15
+ from collections.abc import Sequence
16
+ from dataclasses import dataclass
17
+ from functools import cache
18
+
19
+ import torch
20
+
21
+ _WORDS = re.compile(r"[a-z0-9']+")
22
+
23
+
24
+ def _words(text: str) -> frozenset[str]:
25
+ return frozenset(_WORDS.findall(text.lower()))
26
+
27
+
28
+ def jaccard(a: frozenset[str], b: frozenset[str]) -> float:
29
+ """Word-set overlap of two probes; two empty sets count as identical."""
30
+ union = a | b
31
+ return len(a & b) / len(union) if union else 1.0
32
+
33
+
34
+ def cluster_texts(texts: Sequence[str], threshold: float) -> list[int]:
35
+ """Single-linkage clusters of texts whose word sets overlap by >= ``threshold``.
36
+
37
+ Returns a cluster label per text, numbered in order of first appearance.
38
+ """
39
+ parent = list(range(len(texts)))
40
+
41
+ def find(i: int) -> int:
42
+ while parent[i] != i:
43
+ parent[i] = parent[parent[i]]
44
+ i = parent[i]
45
+ return i
46
+
47
+ words = [_words(t) for t in texts]
48
+ for i in range(len(texts)):
49
+ for j in range(i + 1, len(texts)):
50
+ if jaccard(words[i], words[j]) >= threshold:
51
+ parent[find(j)] = find(i)
52
+ labels: dict[int, int] = {}
53
+ return [labels.setdefault(find(i), len(labels)) for i in range(len(texts))]
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class EffectiveCounts:
58
+ """Probe counts after near-duplicates are clustered (docs/PROTOCOL.md §4)."""
59
+
60
+ raw: int # probes
61
+ clusters: int # independent observations
62
+ passed: float # sum over clusters of the cluster's pass rate
63
+
64
+ @property
65
+ def rate(self) -> float | None:
66
+ """The cluster-weighted pass rate, or ``None`` when there are no probes."""
67
+ return self.passed / self.clusters if self.clusters else None
68
+
69
+
70
+ def effective_counts(
71
+ texts: Sequence[str], passed: Sequence[bool], threshold: float
72
+ ) -> EffectiveCounts:
73
+ """Cluster near-duplicate probes and count each cluster as one observation.
74
+
75
+ One reviewer writes every probe, often as variations on one theme, so probes are not
76
+ independent. A cluster's score is the mean pass rate of its probes (docs/PROTOCOL.md §4).
77
+ """
78
+ labels = cluster_texts(texts, threshold)
79
+ groups: dict[int, list[bool]] = {}
80
+ for label, ok in zip(labels, passed, strict=True):
81
+ groups.setdefault(label, []).append(ok)
82
+ return EffectiveCounts(
83
+ raw=len(texts),
84
+ clusters=len(groups),
85
+ passed=sum(sum(g) / len(g) for g in groups.values()),
86
+ )
87
+
88
+
89
+ def balanced_accuracy(specificity: EffectiveCounts, sensitivity: EffectiveCounts) -> float:
90
+ """The mean of the rates that exist, or 0 when neither rate exists."""
91
+ rates = [r for r in (specificity.rate, sensitivity.rate) if r is not None]
92
+ return sum(rates) / len(rates) if rates else 0.0
93
+
94
+
95
+ def p_balanced_above(
96
+ specificity: EffectiveCounts,
97
+ sensitivity: EffectiveCounts,
98
+ threshold: float,
99
+ *,
100
+ samples: int = 4000,
101
+ seed: int = 0,
102
+ ) -> float:
103
+ """Posterior P((spec + sens) / 2 > threshold) under Beta(1, 1) priors.
104
+
105
+ Fixed ``seed`` keeps it deterministic, so identical records always score the same.
106
+ Many records have the same counts, so the result is cached on them.
107
+ """
108
+ spec = (1 + specificity.passed, 1 + specificity.clusters - specificity.passed)
109
+ sens = (1 + sensitivity.passed, 1 + sensitivity.clusters - sensitivity.passed)
110
+ return _p_beta_mean_above(spec, sens, threshold, samples, seed)
111
+
112
+
113
+ @cache
114
+ def _p_beta_mean_above(
115
+ spec: tuple[float, float], sens: tuple[float, float], threshold: float, samples: int, seed: int
116
+ ) -> float:
117
+ """P((x + y) / 2 > threshold) for x ~ Beta(*spec) and y ~ Beta(*sens), by sampling."""
118
+ rng = random.Random(seed)
119
+ hits = sum(
120
+ (rng.betavariate(*spec) + rng.betavariate(*sens)) / 2 > threshold for _ in range(samples)
121
+ )
122
+ return hits / samples
123
+
124
+
125
+ def expected_false_discoveries(p_useful: Sequence[float]) -> float:
126
+ """Expected number of claimed discoveries that are false: sum of (1 - P(useful))."""
127
+ return sum(1.0 - p for p in p_useful)
128
+
129
+
130
+ def rank_columns(x: torch.Tensor, chunk: int = 1024) -> torch.Tensor:
131
+ """Ranks (0-based) down each column of ``x`` [n, c], averaging ties.
132
+
133
+ Pole strengths are mostly exact zeros, so tie handling matters. Each tied value gets
134
+ the mean of the ranks that it spans, as in ``scipy.stats.rankdata``.
135
+ """
136
+ n = x.shape[0]
137
+ out = torch.empty_like(x, dtype=torch.float32)
138
+ positions = torch.arange(n, dtype=torch.float32)[:, None]
139
+ for start in range(0, x.shape[1], chunk):
140
+ cols = x[:, start : start + chunk].float()
141
+ values, order = cols.sort(dim=0)
142
+ new = torch.ones_like(values, dtype=torch.bool)
143
+ new[1:] = values[1:] != values[:-1]
144
+ group = new.cumsum(0) - 1
145
+ pos = positions.expand_as(values)
146
+ sums = torch.zeros_like(values).scatter_add_(0, group, pos)
147
+ counts = torch.zeros_like(values).scatter_add_(0, group, torch.ones_like(values))
148
+ mean_rank = (sums / counts.clamp(min=1)).gather(0, group)
149
+ out[:, start : start + chunk] = torch.empty_like(values).scatter_(0, order, mean_rank)
150
+ return out
151
+
152
+
153
+ def spearman_columns(target: torch.Tensor, others: torch.Tensor, chunk: int = 1024) -> torch.Tensor:
154
+ """Spearman's rho between ``target`` [n] and each column of ``others`` [n, c]."""
155
+ ranks_t = rank_columns(target[:, None])[:, 0]
156
+ t = ranks_t - ranks_t.mean()
157
+ tt = (t * t).sum()
158
+ rho = torch.zeros(others.shape[1])
159
+ for start in range(0, others.shape[1], chunk):
160
+ o = rank_columns(others[:, start : start + chunk])
161
+ o = o - o.mean(0)
162
+ denom = torch.sqrt(tt * (o * o).sum(0))
163
+ part = (t[:, None] * o).sum(0) / denom
164
+ rho[start : start + chunk] = torch.where(denom > 0, part, torch.zeros_like(part))
165
+ return rho.clamp(-1, 1)
166
+
167
+
168
+ def spearman(x: torch.Tensor, y: torch.Tensor) -> float:
169
+ """Spearman's rho between ``x`` and ``y``; 0 with fewer than three points."""
170
+ if x.numel() < 3:
171
+ return 0.0
172
+ return float(spearman_columns(x.float(), y.float()[:, None])[0])
@@ -0,0 +1,49 @@
1
+ """Schemas for steering samples: what the model writes with a unit clamped high or ablated.
2
+
3
+ The harness continues each neutral prompt greedily in three ways: at baseline, with the
4
+ unit ablated, and with the unit clamped at every position. The clamp uses several
5
+ multiples of the unit's own top activation. You can then read the continuations side
6
+ by side (Turner et al., 2023; Goodfire, "Neural Networks Are Already Clean", 2026).
7
+ Reported, not gated (docs/PROTOCOL.md §4). Neither the proposer nor the reviewer sees
8
+ them.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pydantic import Field, model_validator
14
+
15
+ from openinterp.core.strict import StrictModel
16
+
17
+
18
+ class SteeringSample(StrictModel):
19
+ """One prompt's greedy continuations under each intervention."""
20
+
21
+ prompt: str = Field(min_length=1)
22
+ baseline: str = Field(description="The continuation with no intervention.")
23
+ ablated: str = Field(description="The same with the unit ablated at every position.")
24
+ clamped: list[str] = Field(
25
+ description="The same with the unit clamped at every position, one per `multiples`."
26
+ )
27
+
28
+
29
+ class SteeringSamples(StrictModel):
30
+ """Every prompt's continuations, with the unit clamped high or ablated."""
31
+
32
+ clamp_unit: float = Field(
33
+ gt=0.0,
34
+ description="The strength one multiple stands for: the unit's top activation in the "
35
+ "scan (`max_activation`). A neuron pole is clamped to that signed value.",
36
+ )
37
+ multiples: list[float] = Field(min_length=1)
38
+ max_new_tokens: int = Field(gt=0)
39
+ samples: list[SteeringSample]
40
+
41
+ @model_validator(mode="after")
42
+ def _one_text_per_multiple(self) -> SteeringSamples:
43
+ for s in self.samples:
44
+ if len(s.clamped) != len(self.multiples):
45
+ raise ValueError(
46
+ f"{s.prompt!r} has {len(s.clamped)} clamped texts for "
47
+ f"{len(self.multiples)} multiples"
48
+ )
49
+ return self
@@ -0,0 +1,19 @@
1
+ """Base types shared by the schema modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ UnitFloat = Annotated[float, Field(ge=0.0, le=1.0)]
10
+
11
+
12
+ class StrictModel(BaseModel):
13
+ """The base of every schema.
14
+
15
+ Strict types and ``extra="forbid"`` reject a record that does not match its schema.
16
+ ``frozen`` stops code from changing a model after validation, because records are signed.
17
+ """
18
+
19
+ model_config = ConfigDict(strict=True, extra="forbid", frozen=True)
@@ -0,0 +1,136 @@
1
+ """Takedowns: passages that the project stops publishing on request (``SECURITY.md``).
2
+
3
+ A takedown is one line of ``wiki/takedowns.jsonl``. It names a passage by the hashes of
4
+ its *shingles*: each run of :data:`SHINGLE_WORDS` consecutive words, lowercased. The file
5
+ never holds the text, because that would publish the text again. One passage can appear
6
+ in many records and fields, and the hashes find it in each one.
7
+
8
+ The build removes the text that the shingles cover from everything it publishes
9
+ (:mod:`openinterp.builder.pii`). An experiment page may change outside its Discussion
10
+ section only by that removal (:func:`redact`). The record files and their signatures
11
+ don't change. So a takedown hides the text without breaking reproduction.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import re
19
+ from collections.abc import Iterable, Sequence
20
+ from datetime import date as Date
21
+ from pathlib import Path
22
+
23
+ from pydantic import Field, ValidationError
24
+
25
+ from openinterp.core.paths import DEFAULT_WIKI_DIR
26
+ from openinterp.core.strict import StrictModel
27
+
28
+ SHINGLE_WORDS = 8 # a run this long is rare enough to name one passage
29
+ WORD = re.compile(r"\w+")
30
+ HASH_CHARS = 16 # a 64-bit prefix of SHA-256
31
+
32
+ Span = tuple[int, int]
33
+
34
+
35
+ class Takedown(StrictModel):
36
+ """One request to stop publishing a passage."""
37
+
38
+ id: str = Field(min_length=1, pattern=r"^[a-z0-9-]+$")
39
+ date: Date
40
+ reason: str = Field(
41
+ min_length=1, description="Why, in a few words. It must not hold personal data."
42
+ )
43
+ shingles: list[str] = Field(
44
+ min_length=1, description="The hash of each shingle of the passage (shingle_hashes)."
45
+ )
46
+
47
+
48
+ def takedowns_path(wiki_dir: Path | str = DEFAULT_WIKI_DIR) -> Path:
49
+ """The takedown file of ``wiki_dir``."""
50
+ return Path(wiki_dir) / "takedowns.jsonl"
51
+
52
+
53
+ def parse_takedowns(text: str) -> list[Takedown]:
54
+ """Every takedown in ``text``, in order. Raises ``ValueError`` naming a bad line."""
55
+ out = []
56
+ for number, line in enumerate(text.splitlines(), 1):
57
+ if not line.strip():
58
+ continue
59
+ try:
60
+ out.append(Takedown.model_validate_json(line))
61
+ except ValidationError as exc:
62
+ raise ValueError(f"line {number}: {exc.errors()[0]['msg']}") from exc
63
+ return out
64
+
65
+
66
+ def load_takedowns(wiki_dir: Path | str = DEFAULT_WIKI_DIR) -> list[Takedown]:
67
+ """Every takedown of ``wiki_dir``; none if there is no takedown file."""
68
+ path = takedowns_path(wiki_dir)
69
+ return parse_takedowns(path.read_text(encoding="utf-8")) if path.exists() else []
70
+
71
+
72
+ def _shingles(text: str) -> Iterable[tuple[str, Span]]:
73
+ """Each shingle of ``text``: its hash, and the characters from its first to last word."""
74
+ words = list(WORD.finditer(text))
75
+ for i in range(len(words) - SHINGLE_WORDS + 1):
76
+ run = words[i : i + SHINGLE_WORDS]
77
+ key = " ".join(w.group().lower() for w in run)
78
+ digest = hashlib.sha256(key.encode()).hexdigest()[:HASH_CHARS]
79
+ yield digest, (run[0].start(), run[-1].end())
80
+
81
+
82
+ def shingle_hashes(passage: str) -> list[str]:
83
+ """The hashes that name ``passage``, in order and without repeats.
84
+
85
+ Raises ``ValueError`` for a passage shorter than one shingle.
86
+ """
87
+ hashes = list(dict.fromkeys(h for h, _ in _shingles(passage)))
88
+ if not hashes:
89
+ raise ValueError(f"a passage needs at least {SHINGLE_WORDS} words")
90
+ return hashes
91
+
92
+
93
+ def all_hashes(takedowns: Sequence[Takedown]) -> frozenset[str]:
94
+ """Every shingle hash of ``takedowns``."""
95
+ return frozenset(h for t in takedowns for h in t.shingles)
96
+
97
+
98
+ def covered(text: str, hashes: frozenset[str]) -> tuple[Span, ...]:
99
+ """The spans of ``text`` that the shingles in ``hashes`` cover, sorted and merged."""
100
+ if not hashes:
101
+ return ()
102
+ merged: list[Span] = []
103
+ for digest, (start, end) in _shingles(text):
104
+ if digest not in hashes:
105
+ continue
106
+ if merged and start <= merged[-1][1]:
107
+ merged[-1] = (merged[-1][0], max(merged[-1][1], end))
108
+ else:
109
+ merged.append((start, end))
110
+ return tuple(merged)
111
+
112
+
113
+ def redact(text: str, hashes: frozenset[str], marker: str) -> str:
114
+ """``text`` with ``marker`` in place of each span that ``hashes`` cover."""
115
+ for start, end in reversed(covered(text, hashes)):
116
+ text = text[:start] + marker + text[end:]
117
+ return text
118
+
119
+
120
+ _active: frozenset[str] = frozenset()
121
+
122
+
123
+ def activate(takedowns: Sequence[Takedown]) -> None:
124
+ """Make the build remove what ``takedowns`` name (:func:`active_hashes`)."""
125
+ global _active
126
+ _active = all_hashes(takedowns)
127
+
128
+
129
+ def active_hashes() -> frozenset[str]:
130
+ """The shingle hashes that the build removes. :func:`activate` sets them."""
131
+ return _active
132
+
133
+
134
+ def to_line(takedown: Takedown) -> str:
135
+ """``takedown`` as one line of the takedown file, with its keys in a fixed order."""
136
+ return json.dumps(takedown.model_dump(mode="json")) + "\n"
@@ -0,0 +1,130 @@
1
+ """Token usage of a Claude Code subagent, read from its transcript.
2
+
3
+ Claude Code keeps each subagent's transcript at
4
+ ``~/.claude/projects/<project>/<session>/subagents/agent-<id>.jsonl``, one JSON line per
5
+ message part. Every part of one API response repeats that response's ``usage``. The
6
+ last part carries the final output count. So we count each response once, from its
7
+ last line.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+ from openinterp.core.costs import ExternalUsage, RequestUsage
18
+
19
+
20
+ def _time(value) -> datetime | None:
21
+ try:
22
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
23
+ except (AttributeError, ValueError):
24
+ return None
25
+
26
+
27
+ def claude_dir() -> Path:
28
+ """Claude Code's config directory: ``$CLAUDE_CONFIG_DIR``, else ``~/.claude``."""
29
+ return Path(os.environ.get("CLAUDE_CONFIG_DIR") or Path.home() / ".claude")
30
+
31
+
32
+ def find_transcript(agent: str) -> Path:
33
+ """Return the transcript file of ``agent``.
34
+
35
+ ``agent`` is a transcript path, or the agent ID that Claude Code reports when a
36
+ subagent finishes (``a1b2c3…``, with or without the ``agent-`` prefix).
37
+ """
38
+ path = Path(agent).expanduser()
39
+ if path.suffix == ".jsonl" and path.is_file():
40
+ return path
41
+ name = f"agent-{agent.removeprefix('agent-')}.jsonl"
42
+ found = sorted(
43
+ claude_dir().glob(f"projects/*/*/subagents/{name}"), key=lambda p: p.stat().st_mtime
44
+ )
45
+ if not found:
46
+ raise FileNotFoundError(f"No Claude Code transcript {name} under {claude_dir()}.")
47
+ return found[-1] # the newest, if a resumed session copied it
48
+
49
+
50
+ def usage_from_transcript(path: Path) -> ExternalUsage:
51
+ """Tokens per API request in a Claude Code transcript, for ``open-interpretability usage``.
52
+
53
+ Claude Code writes one line per content block, so a request can have many lines. The
54
+ last line of each request holds its final usage.
55
+ """
56
+ last: dict[str, tuple] = {} # API response id → (time, its last line's message)
57
+ for line in path.read_text().splitlines():
58
+ try:
59
+ entry = json.loads(line)
60
+ except ValueError:
61
+ continue # a line cut off by a crash
62
+ message = entry.get("message") if entry.get("type") == "assistant" else None
63
+ if not isinstance(message, dict) or not message.get("usage"):
64
+ continue
65
+ if message.get("model", "").startswith("<"): # e.g. "<synthetic>" error stubs
66
+ continue
67
+ at = _time(entry.get("timestamp"))
68
+ last[message.get("id") or entry.get("uuid")] = (at, message)
69
+
70
+ requests, models = [], {}
71
+ for at, message in last.values():
72
+ u = message["usage"]
73
+ write_1h = (u.get("cache_creation") or {}).get("ephemeral_1h_input_tokens") or 0
74
+ requests.append(
75
+ RequestUsage(
76
+ at=at,
77
+ input_tokens=u.get("input_tokens") or 0,
78
+ cache_read_tokens=u.get("cache_read_input_tokens") or 0,
79
+ cache_write_5m_tokens=max(
80
+ (u.get("cache_creation_input_tokens") or 0) - write_1h, 0
81
+ ),
82
+ cache_write_1h_tokens=write_1h,
83
+ output_tokens=u.get("output_tokens") or 0,
84
+ )
85
+ )
86
+ model = message.get("model") or "unknown"
87
+ models[model] = models.get(model, 0) + 1
88
+ return ExternalUsage.of(
89
+ requests,
90
+ model=",".join(sorted(models, key=lambda m: -models[m])) or None,
91
+ source=f"claude-code:{path.stem}",
92
+ )
93
+
94
+
95
+ def forbidden_reads(path: Path) -> tuple[list[str], list[str]]:
96
+ """Blocked paths that the reviewer in transcript ``path`` read, and tried to read.
97
+
98
+ The check is :func:`openinterp.core.isolation.forbidden`, for each tool call in the
99
+ transcript. The first list has the calls that completed. The second list has the
100
+ calls that failed, e.g. because the reviewer's hook denied them, so no content was
101
+ read. Each list is in transcript order, without duplicates.
102
+ """
103
+ from openinterp.core.isolation import TOOLS, forbidden
104
+
105
+ calls: dict[str, tuple[str, bool]] = {} # tool_use id → (blocked path, completed)
106
+ for line in path.read_text().splitlines():
107
+ try:
108
+ entry = json.loads(line)
109
+ except ValueError:
110
+ continue
111
+ content = (entry.get("message") or {}).get("content")
112
+ if not isinstance(content, list):
113
+ continue
114
+ cwd = entry.get("cwd") or "."
115
+ for block in content:
116
+ if not isinstance(block, dict):
117
+ continue
118
+ if block.get("type") == "tool_use" and block.get("name") in TOOLS:
119
+ hit = forbidden(block["name"], block.get("input") or {}, cwd, cwd)
120
+ if hit is not None:
121
+ calls[block.get("id")] = (hit, True)
122
+ elif (
123
+ block.get("type") == "tool_result"
124
+ and block.get("tool_use_id") in calls
125
+ and block.get("is_error")
126
+ ):
127
+ calls[block["tool_use_id"]] = (calls[block["tool_use_id"]][0], False)
128
+ read = [hit for hit, completed in calls.values() if completed]
129
+ tried = [hit for hit, completed in calls.values() if not completed]
130
+ return list(dict.fromkeys(read)), list(dict.fromkeys(tried))
@@ -0,0 +1,80 @@
1
+ """A neuron pole's strongest upstream and downstream connections (reported, not gated)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import Field
8
+
9
+ from openinterp.core.strict import StrictModel
10
+
11
+ WiringMethod = Literal["weights", "activations"]
12
+
13
+
14
+ class WiringEdge(StrictModel):
15
+ """One connection between the audited pole and another unit of the same model.
16
+
17
+ ``kind`` and ``layer``/``index`` name the other unit: a neuron (either pole) or an
18
+ attention head. What ``weight`` measures depends on :attr:`Wiring.method`.
19
+ ``channels`` splits a weights reading by the read that it goes through. The reads
20
+ are ``in``, or ``gate`` and ``up`` for a gated MLP, or ``q``, ``k`` and ``v`` for a
21
+ head downstream. ``weight`` is then the channel of largest magnitude.
22
+ """
23
+
24
+ kind: Literal["mlp_neuron", "attn_head"]
25
+ layer: int = Field(ge=0)
26
+ index: int = Field(ge=0)
27
+ weight: float
28
+ channels: dict[str, float] = Field(default_factory=dict)
29
+
30
+
31
+ class Wiring(StrictModel):
32
+ """A unit's top connections, from its weights or from activations.
33
+
34
+ How each kind of unit reads and writes:
35
+
36
+ - A neuron pole reads its MLP input (``gate``/``up``, or ``in``). It writes ``w_out``,
37
+ negated for the negative pole.
38
+ - A dictionary feature reads with its encoder row and writes its decoder row.
39
+ - A head reads through ``q``, ``k`` and ``v``, and writes the subspace ``W_O``.
40
+
41
+ Upstream units write before the unit reads. A head's own layer's MLP runs after the
42
+ head, so it reads the head downstream.
43
+
44
+ *weights* (virtual weights, Elhage et al., 2021). The norms before each read are
45
+ folded in as their gain, and centring for LayerNorm. Their per-token scale is
46
+ ignored. So weights rank units, but have no absolute meaning. Each number is how much
47
+ one unit's read picks up of the other unit's write:
48
+
49
+ - When a vector reads a vector: ``w · r̂``. ``+`` means that a positive activation
50
+ raises the read. Upstream, the unit's own reads are unit-normalised.
51
+ - Otherwise, a norm (≥ 0). For a matrix read, it is the norm of ``R·w``. For an
52
+ upstream head's OV circuit, it is the norm of ``R·W_O·W_V``. Downstream, it is the
53
+ norm of ``W_Q/K/V·w``.
54
+
55
+ *activations* (attribution patching, Syed et al., 2023). Each number is averaged over
56
+ the peaks of the review's prompts where the unit fired:
57
+
58
+ - Upstream: ``Σ_s write_s · ∂strength/∂x_s``. This is the first-order change in
59
+ strength when the earlier unit's direct write is removed from the input. ``+``
60
+ means that the earlier unit excites the unit.
61
+ - Downstream neurons: the exact change in a later neuron's signed activation at the
62
+ peak. The unit's write is removed from that neuron's input only.
63
+ - Downstream heads: ``|W_O Δz|``, how far a later head's write at the peak moves. The
64
+ harness removes the unit's write from one input of the head at a time (``q``,
65
+ ``k`` or ``v``). It removes the write at every position at once.
66
+ """
67
+
68
+ method: WiringMethod
69
+ upstream_neurons: list[WiringEdge]
70
+ upstream_heads: list[WiringEdge] | None = Field(
71
+ default=None, description="None when the architecture's attention isn't supported."
72
+ )
73
+ downstream_neurons: list[WiringEdge]
74
+ downstream_heads: list[WiringEdge] | None = Field(
75
+ default=None,
76
+ description="None when not measured (activations before downstream heads were).",
77
+ )
78
+ prompts: int = Field(
79
+ default=0, ge=0, description="Activations only: prompts whose peaks were averaged."
80
+ )