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,446 @@
1
+ """``open-interpretability score``: detection, fuzzing and embedding scores for an explanation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import secrets
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from openinterp.cli import cli, recorded_model_option, with_model_options
13
+ from openinterp.harness.explanation_scores import DEFAULT_EMBEDDING_MODEL
14
+
15
+
16
+ def _sibling(task_path: Path, suffix: str) -> Path:
17
+ """``score.task.json`` -> ``score<suffix>``."""
18
+ return task_path.with_name(task_path.name.removesuffix(".json").removesuffix(".task") + suffix)
19
+
20
+
21
+ def _load_submission(path: Path):
22
+ from openinterp.core.schemas import AuditRecord, NeuronAuditSubmission
23
+
24
+ raw = path.read_text()
25
+ if "submission" in json.loads(raw):
26
+ return AuditRecord.model_validate_json(raw).submission
27
+ return NeuronAuditSubmission.model_validate_json(raw)
28
+
29
+
30
+ def _threshold(submission) -> float:
31
+ from openinterp.core.schemas import ReviewPolicy
32
+
33
+ if not submission.max_activation:
34
+ raise click.ClickException("the submission has no max_activation; rescan it first.")
35
+ return ReviewPolicy().activation_threshold * submission.max_activation
36
+
37
+
38
+ @cli.command()
39
+ @click.option(
40
+ "--audit-file",
41
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
42
+ help="Submission or record whose explanation to score. Required unless --task.",
43
+ )
44
+ @click.option(
45
+ "--corpus",
46
+ type=click.Path(exists=True, path_type=Path),
47
+ help="Where held-out windows come from; the scan's top examples are left out. "
48
+ "Use the scan's corpus, so the windows are text the proposer could have seen but didn't.",
49
+ )
50
+ @click.option("--seed", type=int, help="Seed for drawing windows (recorded).")
51
+ @click.option(
52
+ "--judge-model",
53
+ default=None,
54
+ callback=recorded_model_option,
55
+ help="Judge for detection and fuzzing: a LiteLLM model string, or external/<name>. "
56
+ "With --task, the name recorded for whoever answered.",
57
+ )
58
+ @click.option(
59
+ "--embedding-model",
60
+ default=DEFAULT_EMBEDDING_MODEL,
61
+ show_default=True,
62
+ help="Hugging Face sentence encoder for the embedding score; '' to skip it.",
63
+ )
64
+ @click.option(
65
+ "--emit-task",
66
+ type=click.Path(dir_okay=False, path_type=Path),
67
+ help="Draw the windows, write them here (e.g. score.task.json) with the judge's two "
68
+ "prompts next to them (score.detection.prompt.md, score.fuzzing.prompt.md), and exit.",
69
+ )
70
+ @click.option(
71
+ "--task",
72
+ "task_file",
73
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
74
+ help="A task from --emit-task, answered in score.detection.json and score.fuzzing.json "
75
+ "next to it. No LLM is called.",
76
+ )
77
+ @click.option("--window-tokens", default=32, show_default=True, type=click.IntRange(4, 512))
78
+ @click.option("--quantiles", default=5, show_default=True, type=click.IntRange(1, 20))
79
+ @click.option(
80
+ "--per-quantile",
81
+ default=5,
82
+ show_default=True,
83
+ type=click.IntRange(1, 50),
84
+ help="Active windows per activation bin, for each of detection and fuzzing.",
85
+ )
86
+ @click.option("--max-windows", default=4000, show_default=True, type=click.IntRange(1))
87
+ @click.option(
88
+ "--controls",
89
+ is_flag=True,
90
+ help="Also judge the same windows with no explanation and with another unit's, and "
91
+ "report the gaps (openinterp/score_controls.py).",
92
+ )
93
+ @click.option(
94
+ "--out",
95
+ type=click.Path(dir_okay=False, path_type=Path),
96
+ help="ExplanationScores JSON (default with --task: score.scores.json next to it).",
97
+ )
98
+ @with_model_options
99
+ def score(
100
+ audit_file,
101
+ corpus,
102
+ seed,
103
+ judge_model,
104
+ embedding_model,
105
+ emit_task,
106
+ task_file,
107
+ window_tokens,
108
+ quantiles,
109
+ per_quantile,
110
+ max_windows,
111
+ controls,
112
+ out,
113
+ device,
114
+ dtype,
115
+ ):
116
+ """Score an explanation on held-out text: detection, fuzzing and embedding.
117
+
118
+ \b
119
+ Reported, not gated (docs/PROTOCOL.md §4), but every review records them: pass the
120
+ result to `open-interpretability review … --explanation-scores <file>`. In one step the
121
+ judge is an LLM:
122
+ open-interpretability score --audit-file sub.json --corpus corpus.jsonl \\
123
+ --judge-model M --out s.json
124
+ In two steps any agent can judge, without an API key:
125
+ open-interpretability score --audit-file sub.json --corpus corpus.jsonl \\
126
+ --emit-task score.task.json
127
+ (answer score.detection.prompt.md and score.fuzzing.prompt.md)
128
+ open-interpretability score --task score.task.json \\
129
+ --judge-model external/<model that answered>
130
+ """
131
+ from openinterp.agents.base import DEFAULT_MODEL
132
+ from openinterp.cli import _load_unit_runner # looked up per call, so tests can swap it
133
+ from openinterp.core import costs
134
+
135
+ if task_file is not None:
136
+ if audit_file is not None or emit_task is not None:
137
+ raise click.UsageError("--task scores an emitted task; drop --audit-file/--emit-task.")
138
+ if not judge_model:
139
+ raise click.UsageError("--judge-model is required with --task: name who answered.")
140
+ scores = _score_answers(task_file, judge_model, embedding_model, device, dtype)
141
+ out = out or _sibling(task_file, ".scores.json")
142
+ else:
143
+ if audit_file is None:
144
+ raise click.UsageError("--audit-file is required unless --task is given.")
145
+ if emit_task is None and out is None:
146
+ raise click.UsageError("--out is required when scoring in one step.")
147
+ submission = _load_submission(audit_file)
148
+ runner = _load_unit_runner(submission.identifier, device, dtype)
149
+ sampling = (seed, window_tokens, quantiles, per_quantile, max_windows, controls)
150
+ if emit_task is not None:
151
+ task = _draw_task(runner, submission, corpus, *sampling)
152
+ _write_task(task.model_copy(update={"costs": costs.measured()}), emit_task)
153
+ return
154
+ scores = score_submission(
155
+ runner, submission, corpus, judge_model or DEFAULT_MODEL, embedding_model, *sampling
156
+ )
157
+ scores = scores.model_copy(update={"costs": costs.measured()})
158
+ out.parent.mkdir(parents=True, exist_ok=True)
159
+ out.write_text(scores.model_dump_json(indent=2))
160
+ click.echo(_summary(scores))
161
+ click.echo(f"Wrote {out}")
162
+
163
+
164
+ def _score_answers(task_file: Path, judge_model: str, embedding_model, device, dtype):
165
+ """Step two: score an emitted task from the external judge's answers next to it."""
166
+ from openinterp.agents.judge import DetectionAnswer, FuzzingAnswer, ScoringTask
167
+ from openinterp.cli import _load_unit_runner # looked up per call, so tests can swap it
168
+ from openinterp.core import costs
169
+
170
+ task = ScoringTask.model_validate_json(task_file.read_text())
171
+ detection = DetectionAnswer.model_validate_json(
172
+ _sibling(task_file, ".detection.json").read_text()
173
+ )
174
+ fuzzing = ( # no fuzzing prompt when no held-out window was active
175
+ FuzzingAnswer.model_validate_json(_sibling(task_file, ".fuzzing.json").read_text())
176
+ if task.fuzzing
177
+ else FuzzingAnswer(correct=[])
178
+ )
179
+ for kind in ("detection", "fuzzing") if task.fuzzing else ("detection",):
180
+ prompt, answer = (
181
+ _sibling(task_file, f".{kind}.prompt.md"),
182
+ _sibling(task_file, f".{kind}.json"),
183
+ )
184
+ costs.add_external("judge", judge_model, prompt, answer)
185
+ from openinterp.score_controls import read_answers
186
+
187
+ runner = _load_unit_runner(task.identifier, device, dtype)
188
+ controls = read_answers(task, task_file)
189
+ scores = _score_task(task, runner, detection, fuzzing, judge_model, embedding_model, controls)
190
+ return scores.model_copy(update={"costs": costs.merge(task.costs, costs.measured())})
191
+
192
+
193
+ def score_submission(
194
+ runner,
195
+ submission,
196
+ corpus: Path | None,
197
+ judge_model: str,
198
+ embedding_model: str = DEFAULT_EMBEDDING_MODEL,
199
+ seed: int | None = None,
200
+ window_tokens: int = 32,
201
+ quantiles: int = 5,
202
+ per_quantile: int = 5,
203
+ max_windows: int = 4000,
204
+ controls: bool = False,
205
+ ):
206
+ """Draw held-out windows and score the explanation with an LLM judge, in one step."""
207
+ from openinterp.agents.judge import ExplanationJudge, FuzzingAnswer
208
+ from openinterp.score_controls import judge_controls
209
+
210
+ sampling = (seed, window_tokens, quantiles, per_quantile, max_windows, controls)
211
+ task = _draw_task(runner, submission, corpus, *sampling)
212
+ judge = ExplanationJudge(judge_model)
213
+ click.echo(f"Asking {judge.model} to judge {len(task.detection)} windows…", err=True)
214
+ noun = _noun(task.identifier)
215
+ detection = judge.detect(task.explanation, task.detection, noun)
216
+ fuzzing = (
217
+ judge.fuzz(task.explanation, task.fuzzing, noun)
218
+ if task.fuzzing
219
+ else FuzzingAnswer(correct=[])
220
+ )
221
+ judged = judge_controls(judge, task, noun)
222
+ return _score_task(task, runner, detection, fuzzing, judge.model, embedding_model, judged)
223
+
224
+
225
+ def _draw_task(
226
+ runner, submission, corpus, seed, window_tokens, quantiles, per_quantile, cap, controls=False
227
+ ):
228
+ from openinterp.agents.judge import ScoringTask
229
+ from openinterp.cli_evidence import read_corpus
230
+ from openinterp.harness.explanation_scores import DECOYS
231
+ from openinterp.score_controls import control_tasks
232
+
233
+ texts, corpus_name, corpus_sha, _ = read_corpus(corpus)
234
+ seed = secrets.randbits(32) if seed is None else seed
235
+ threshold = _threshold(submission)
236
+ submission = _with_first_token_floor(runner, submission)
237
+ click.echo(f"Measuring held-out {window_tokens}-token windows (seed {seed})…", err=True)
238
+ sample = _sample(
239
+ runner, texts, submission, threshold, seed, window_tokens, cap, quantiles, per_quantile
240
+ )
241
+ windows = _task_windows
242
+ return ScoringTask(
243
+ identifier=submission.identifier,
244
+ head_profile=submission.head_profile,
245
+ dictionary_profile=submission.dictionary_profile,
246
+ superposition_profile=submission.superposition_profile,
247
+ first_token_floor=submission.first_token_floor,
248
+ explanation=submission.natural_language_explanation,
249
+ threshold=threshold,
250
+ window_tokens=window_tokens,
251
+ quantiles=quantiles,
252
+ quantile_edges=sample.quantile_edges,
253
+ seed=seed,
254
+ corpus=corpus_name,
255
+ corpus_sha256=corpus_sha,
256
+ candidate_windows=sample.candidate_windows,
257
+ active_windows=sample.active_windows,
258
+ detection=windows(sample.detection),
259
+ fuzzing=windows(sample.fuzzing),
260
+ controls=control_tasks(submission.identifier.slug, seed) if controls else [],
261
+ fuzzing_decoys=DECOYS,
262
+ )
263
+
264
+
265
+ def _sample(
266
+ runner, texts, submission, threshold, seed, window_tokens, cap, quantiles, per_quantile
267
+ ):
268
+ """Held-out windows of the corpus, leaving out the top examples."""
269
+ from openinterp.core import costs
270
+ from openinterp.harness.explanation_scores import sample_heldout
271
+ from openinterp.harness.units import submission_unit
272
+
273
+ try:
274
+ with costs.stage("heldout_windows"):
275
+ return sample_heldout(
276
+ runner,
277
+ texts,
278
+ submission_unit(submission),
279
+ threshold,
280
+ seed=seed,
281
+ exclude=[e.text for e in submission.top_examples],
282
+ window_tokens=window_tokens,
283
+ max_windows=cap,
284
+ quantiles=quantiles,
285
+ per_quantile=per_quantile,
286
+ )
287
+ except ValueError as exc:
288
+ raise click.ClickException(str(exc)) from exc
289
+
290
+
291
+ def _with_first_token_floor(runner, submission):
292
+ """Return ``submission`` with its first-token floor set.
293
+
294
+ A record from before protocol 0.8 has no floor, so judge it as a review does.
295
+ """
296
+ if submission.first_token_floor is not None:
297
+ return submission
298
+ from functools import partial
299
+
300
+ from openinterp.core.schemas import ReviewPolicy
301
+ from openinterp.harness.scan import has_first_token_floor
302
+ from openinterp.harness.units import submission_unit
303
+
304
+ read = partial(submission_unit(submission).strengths, runner)
305
+ floor = has_first_token_floor(runner, read, submission.max_activation, ReviewPolicy())
306
+ return submission.model_copy(update={"first_token_floor": floor})
307
+
308
+
309
+ def _task_windows(ws):
310
+ from openinterp.agents.judge import TaskWindow
311
+
312
+ return [TaskWindow(text=w.text, tokens=w.tokens, highlighted=list(w.highlighted)) for w in ws]
313
+
314
+
315
+ def _write_task(task, path: Path) -> None:
316
+ from openinterp.agents.judge import DetectionAnswer, ExplanationJudge, FuzzingAnswer
317
+
318
+ path.parent.mkdir(parents=True, exist_ok=True)
319
+ path.write_text(task.model_dump_json(indent=2))
320
+ click.echo(f"Wrote {path} ({len(task.detection)} detection, {len(task.fuzzing)} fuzzing)")
321
+ if not task.active_windows:
322
+ click.echo("No held-out window is active: detection scores false positives only.")
323
+ for kind, model, prompt in [
324
+ ("detection", DetectionAnswer, ExplanationJudge.detection_prompt),
325
+ ("fuzzing", FuzzingAnswer, ExplanationJudge.fuzzing_prompt),
326
+ ]:
327
+ if not getattr(task, kind):
328
+ continue
329
+ prompt_path, answer = _sibling(path, f".{kind}.prompt.md"), _sibling(path, f".{kind}.json")
330
+ text = prompt(task.explanation, getattr(task, kind), _noun(task.identifier))
331
+ prompt_path.write_text(ExplanationJudge.render_prompt(model, text, answer))
332
+ click.echo(f"Judge prompt: {prompt_path} (answer in {answer})")
333
+ if task.controls:
334
+ from openinterp.score_controls import write_prompts
335
+
336
+ write_prompts(task, path, _noun(task.identifier))
337
+ click.echo(f"Control prompts: {len(task.controls)} controls, next to {path}")
338
+
339
+
340
+ def _score_task(task, runner, detection, fuzzing, judge_model, embedding_model, controls=()):
341
+ from openinterp.agents.judge import answered
342
+ from openinterp.core import costs
343
+ from openinterp.core.explanation import ExplanationScores
344
+ from openinterp.harness.explanation_scores import embedding_similarities, remeasure
345
+ from openinterp.harness.units import submission_unit
346
+
347
+ try:
348
+ det = answered(detection.active, len(task.detection), "detection")
349
+ fuz = answered(fuzzing.correct, len(task.fuzzing), "fuzzing")
350
+ except ValueError as exc:
351
+ raise click.ClickException(str(exc)) from exc
352
+ sims = [None] * len(task.detection)
353
+ if embedding_model:
354
+ click.echo(f"Embedding the explanation and windows with {embedding_model}…", err=True)
355
+ with costs.stage("embedding"):
356
+ sims = embedding_similarities(
357
+ embedding_model, task.explanation, [w.text for w in task.detection]
358
+ )
359
+ scores = ExplanationScores(
360
+ slug=task.identifier.slug,
361
+ explanation=task.explanation,
362
+ threshold=task.threshold,
363
+ window_tokens=task.window_tokens,
364
+ quantiles=task.quantiles,
365
+ quantile_edges=task.quantile_edges,
366
+ seed=task.seed,
367
+ corpus=task.corpus,
368
+ corpus_sha256=task.corpus_sha256,
369
+ candidate_windows=task.candidate_windows,
370
+ active_windows=task.active_windows,
371
+ judge_model=judge_model,
372
+ embedding_model=embedding_model or None,
373
+ detection_windows=_unmeasured(task.detection, det, sims),
374
+ fuzzing_windows=_unmeasured(task.fuzzing, fuz, [None] * len(task.fuzzing)),
375
+ controls=list(controls),
376
+ fuzzing_decoys=task.fuzzing_decoys,
377
+ )
378
+ with costs.stage("remeasure"):
379
+ return remeasure(scores, runner, submission_unit(task))
380
+
381
+
382
+ def _unmeasured(windows, answers, similarities):
383
+ """Judged windows awaiting their activations; ``remeasure`` fills them and the labels."""
384
+ from openinterp.core.explanation import ScoredWindow
385
+
386
+ return [
387
+ ScoredWindow(
388
+ text=w.text,
389
+ tokens=w.tokens,
390
+ activations=[],
391
+ highlighted=w.highlighted,
392
+ label=False, # re-derived from a fresh measurement below
393
+ judged=a,
394
+ similarity=s,
395
+ )
396
+ for w, a, s in zip(windows, answers, similarities, strict=True)
397
+ ]
398
+
399
+
400
+ def _noun(identifier) -> str:
401
+ return identifier.noun
402
+
403
+
404
+ def _summary(scores) -> str:
405
+ def judged(s):
406
+ if s is None:
407
+ return "—"
408
+ return (
409
+ f"{s.balanced_accuracy:.2f} (TPR {_num(s.true_positive_rate)}, "
410
+ f"FPR {_num(s.false_positive_rate)})"
411
+ )
412
+
413
+ from openinterp.score_controls import gap_summary
414
+
415
+ line = (
416
+ f"Detection {judged(scores.detection)} · fuzzing {judged(scores.fuzzing)} · "
417
+ f"embedding AUROC {_num(scores.embedding_auroc)} · "
418
+ f"{scores.active_windows}/{scores.candidate_windows} held-out windows active"
419
+ )
420
+ rules = scores.rule_baseline
421
+ if rules:
422
+ best = ", ".join(f"{k} {v:.2f}" for k, v in sorted(rules.items()))
423
+ line += f"\nExplanation-free rules (upper bounds): {best}"
424
+ return line + ("\n" + gap_summary(scores) if scores.controls else "")
425
+
426
+
427
+ def _num(value: float | None) -> str:
428
+ return "—" if value is None else f"{value:.2f}"
429
+
430
+
431
+ def load_scores(path: Path | None, submission):
432
+ """The explanation scores in ``path``, checked against ``submission``, or ``None``."""
433
+ from openinterp.core.explanation import ExplanationScores
434
+
435
+ if path is None:
436
+ return None
437
+ scores = ExplanationScores.model_validate_json(path.read_text())
438
+ if scores.slug != submission.identifier.slug:
439
+ raise click.UsageError(f"{path} scores {scores.slug}, not {submission.identifier.slug}.")
440
+ if scores.explanation != submission.natural_language_explanation:
441
+ raise click.UsageError(
442
+ f"{path} scores a different explanation; re-run open-interpretability score."
443
+ )
444
+ if not math.isclose(scores.threshold, _threshold(submission), rel_tol=1e-9):
445
+ raise click.UsageError(f"{path} used another activation threshold; re-run it.")
446
+ return scores
@@ -0,0 +1,86 @@
1
+ """``open-interpretability sisters``: VERIFIED neuron poles matched to other models' poles.
2
+
3
+ See :mod:`openinterp.sisters`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from openinterp.cli import _load_runner, cli, wiki_dir_option
13
+ from openinterp.sisters import DEFAULT_PATH
14
+
15
+
16
+ @cli.command("sisters")
17
+ @wiki_dir_option()
18
+ @click.option(
19
+ "--model",
20
+ "model_name",
21
+ default=None,
22
+ help="Match only this model's VERIFIED poles. Default: every configured model "
23
+ "([tool.openinterp] models in pyproject.toml).",
24
+ )
25
+ @click.option(
26
+ "--other",
27
+ "others",
28
+ multiple=True,
29
+ help="Search only this model (repeatable). Default: every other configured model, "
30
+ "and the model's larger siblings (openinterp.sisters.SIBLINGS).",
31
+ )
32
+ @click.option(
33
+ "--corpus",
34
+ default=Path("corpus.jsonl"),
35
+ show_default=True,
36
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
37
+ help="Texts both models read; each document is one forward pass.",
38
+ )
39
+ @click.option(
40
+ "--out",
41
+ "path",
42
+ default=DEFAULT_PATH,
43
+ show_default=True,
44
+ type=click.Path(dir_okay=False, path_type=Path),
45
+ help="JSONL of sister rows, one per pole and other model.",
46
+ )
47
+ @click.option("-k", "k", default=5, show_default=True, help="Poles kept per pole and model.")
48
+ @click.option("--device", default="auto", show_default=True, help="cpu, cuda, mps or auto.")
49
+ def sisters_command(wiki_dir, model_name, others, corpus, path, k, device):
50
+ """Find each VERIFIED pole's best-correlated poles in other models.
51
+
52
+ Both models read --corpus. They are compared where both have read the same text
53
+ (token ends they share), so they may tokenize differently. Every pole of every layer
54
+ of the other model is correlated with each VERIFIED pole. Then each match is
55
+ correlated with every pole of the first model, so a row says whether the match is
56
+ mutual. Poles already in --out for that
57
+ model are skipped, so a run can resume.
58
+ """
59
+ from openinterp.builder.wiki import load_archive
60
+ from openinterp.evidence import read_corpus
61
+ from openinterp.planner import configured_models
62
+ from openinterp.sisters import pairs, sisters, sort_rows
63
+
64
+ todo = pairs(configured_models(wiki_dir.parent / "pyproject.toml"))
65
+ if model_name:
66
+ todo = [p for p in todo if p[0] == model_name] or pairs([model_name])
67
+ if others:
68
+ todo = [(a, b) for a in {a for a, _ in todo} or {model_name} for b in others if a != b]
69
+ if not todo:
70
+ raise click.UsageError("no model pairs to match; pass --model and --other")
71
+ texts, corpus_name, corpus_sha, _ = read_corpus(corpus)
72
+ from openinterp.core.errata import load_errata
73
+
74
+ records = load_archive(wiki_dir)
75
+
76
+ def load(name, revision): # not cached: each stage frees its model before the next
77
+ return _load_runner(name, device, "float32", revision)
78
+
79
+ wrote = sisters(
80
+ records, load, path, texts, todo, corpus=corpus_name, corpus_sha256=corpus_sha,
81
+ errata=load_errata(wiki_dir), k=k, echo=click.echo,
82
+ ) # fmt: skip
83
+ for (source, other), n in wrote.items():
84
+ click.echo(f"Matched {n} poles of {source} against {other} → {path}")
85
+ if path.exists():
86
+ sort_rows(path)