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/cli_score.py
ADDED
|
@@ -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)
|