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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
openinterp/core/stats.py
ADDED
|
@@ -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
|
+
)
|