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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Neuronpedia's explanations through the adversarial review.
|
|
2
|
+
|
|
3
|
+
The command is `open-interpretability neuronpedia-review`.
|
|
4
|
+
|
|
5
|
+
The score baseline (:mod:`openinterp.neuronpedia_baseline`) asks how well an explanation
|
|
6
|
+
predicts held-out text. This asks whether it survives a reviewer who writes probes to
|
|
7
|
+
break it. Each latent that Neuronpedia explains gets a copy of its newest submission.
|
|
8
|
+
The copy has Neuronpedia's explanation as the hypothesis. It claims no activating tokens
|
|
9
|
+
and a retrospective mechanism, because Neuronpedia's text claims nothing else. A reviewer
|
|
10
|
+
then writes probes from that copy alone, and the harness measures them.
|
|
11
|
+
|
|
12
|
+
Neuronpedia makes no counterfactual predictions. So the comparison uses the gates that
|
|
13
|
+
the probes decide (``PROBE_GATES``), for both explanations. The records stay out of the
|
|
14
|
+
registry: they never change a unit's standing.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from statistics import mean
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from openinterp.baseline import sign_test
|
|
25
|
+
|
|
26
|
+
# Gates that the proposer's own claims decide, not the probes: its predicted edits
|
|
27
|
+
# (counterfactual_reproduction) and its claimed mechanism (lookahead_consistent).
|
|
28
|
+
# independent_review is about who proposed. Neuronpedia claims none of these.
|
|
29
|
+
NOT_PROBE_GATES = frozenset(
|
|
30
|
+
{"counterfactual_reproduction", "lookahead_consistent", "independent_review"}
|
|
31
|
+
)
|
|
32
|
+
METRICS = ("balanced_accuracy", "specificity", "sensitivity")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def probe_gates(gates: dict[str, bool]) -> dict[str, bool]:
|
|
36
|
+
"""The gates of ``gates`` that the probes decide."""
|
|
37
|
+
return {k: v for k, v in gates.items() if k not in NOT_PROBE_GATES}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReviewRow(BaseModel):
|
|
41
|
+
"""One latent: the agent's newest review against the review of Neuronpedia's text."""
|
|
42
|
+
|
|
43
|
+
slug: str
|
|
44
|
+
experiment_id: str # the agent's experiment, whose submission was copied
|
|
45
|
+
url: str | None
|
|
46
|
+
agent_status: str
|
|
47
|
+
agent_explanation: str
|
|
48
|
+
neuronpedia_explanation: str
|
|
49
|
+
explainer: str
|
|
50
|
+
reviewer_model: str | None = None # None while the probes are missing
|
|
51
|
+
agent: dict
|
|
52
|
+
neuronpedia: dict | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def side(metrics) -> dict:
|
|
56
|
+
"""The probe metrics and probe gates of one verdict's ``metrics``."""
|
|
57
|
+
gates = probe_gates(metrics.gates)
|
|
58
|
+
return {
|
|
59
|
+
**{name: getattr(metrics, name) for name in METRICS},
|
|
60
|
+
"gates": gates,
|
|
61
|
+
"probe_verified": all(gates.values()),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def report(rows: Sequence[ReviewRow]) -> str:
|
|
66
|
+
"""Return the comparison as a Markdown page."""
|
|
67
|
+
done = [r for r in rows if r.neuronpedia is not None]
|
|
68
|
+
agent_ok = sum(r.agent["probe_verified"] for r in done)
|
|
69
|
+
np_ok = sum(r.neuronpedia["probe_verified"] for r in done)
|
|
70
|
+
lines = [
|
|
71
|
+
"# Neuronpedia explanations through the adversarial review",
|
|
72
|
+
"",
|
|
73
|
+
"Generated by `open-interpretability neuronpedia-review`. Do not edit. Method: "
|
|
74
|
+
"`openinterp/neuronpedia_review.py` and docs/PROTOCOL.md §4.",
|
|
75
|
+
"",
|
|
76
|
+
f"Explanations from [Neuronpedia](https://www.neuronpedia.org), credited to the model "
|
|
77
|
+
f"that wrote them. Each latent's newest submission was copied with Neuronpedia's "
|
|
78
|
+
f"explanation as the hypothesis and no claimed tokens. A reviewer wrote probes "
|
|
79
|
+
f"for that copy alone. Neuronpedia makes no counterfactual predictions, so both "
|
|
80
|
+
f"sides are judged on the gates that the probes decide: every gate except "
|
|
81
|
+
f"{', '.join(sorted(NOT_PROBE_GATES))}. These reviews are not registry records. "
|
|
82
|
+
f"Reviewed: {len(done)} of {len(rows)} latents.",
|
|
83
|
+
"",
|
|
84
|
+
"Two things favour the agent. Its side is the unit's newest experiment, which is "
|
|
85
|
+
"often a revision written after earlier reviews. And its probes came from that "
|
|
86
|
+
"experiment's own reviewer, which can be another model than the one here.",
|
|
87
|
+
"",
|
|
88
|
+
"| | Agent | Neuronpedia |",
|
|
89
|
+
"| --- | --- | --- |",
|
|
90
|
+
f"| Passes every probe gate | {agent_ok} of {len(done)} | {np_ok} of {len(done)} |",
|
|
91
|
+
]
|
|
92
|
+
lines += [line for name in METRICS if (line := _metric_line(done, name))]
|
|
93
|
+
failed = _failed_gates(done)
|
|
94
|
+
if failed:
|
|
95
|
+
lines += ["", "Probe gates that Neuronpedia's explanations failed: " + failed + "."]
|
|
96
|
+
return "\n".join(lines + ["", *_unit_lines(rows)]) + "\n"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _metric_line(done: Sequence[ReviewRow], name: str) -> str | None:
|
|
100
|
+
"""One table row: the mean of metric ``name`` on each side, with a sign test."""
|
|
101
|
+
pairs = [(r.agent[name], r.neuronpedia[name]) for r in done]
|
|
102
|
+
pairs = [(a, b) for a, b in pairs if a is not None and b is not None]
|
|
103
|
+
if not pairs:
|
|
104
|
+
return None
|
|
105
|
+
p = sign_test([b - a for a, b in pairs])
|
|
106
|
+
return (
|
|
107
|
+
f"| {name.replace('_', ' ').capitalize()} (mean, n = {len(pairs)}) | "
|
|
108
|
+
f"{mean(a for a, _ in pairs):.2f} | {mean(b for _, b in pairs):.2f}"
|
|
109
|
+
f"{'' if p is None else f' (sign test p = {p:.2g})'} |"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _unit_lines(rows: Sequence[ReviewRow]) -> list[str]:
|
|
114
|
+
"""The per-latent table: each side's verdict on the probe gates, and balanced accuracy."""
|
|
115
|
+
lines = [
|
|
116
|
+
"| Unit | Agent | Neuronpedia's explanation | Neuronpedia | Balanced accuracy |",
|
|
117
|
+
"| --- | --- | --- | --- | --- |",
|
|
118
|
+
]
|
|
119
|
+
for r in sorted(rows, key=lambda r: r.slug):
|
|
120
|
+
unit = f"[{r.slug}]({r.url})" if r.url else r.slug
|
|
121
|
+
theirs = "not reviewed yet" if r.neuronpedia is None else _verdict(r.neuronpedia)
|
|
122
|
+
acc = "—"
|
|
123
|
+
if r.neuronpedia is not None:
|
|
124
|
+
acc = f"{r.agent['balanced_accuracy']:.2f} → {r.neuronpedia['balanced_accuracy']:.2f}"
|
|
125
|
+
lines.append(
|
|
126
|
+
f"| {unit} | {_verdict(r.agent)} | {_cell(r.neuronpedia_explanation)} | "
|
|
127
|
+
f"{theirs} | {acc} |"
|
|
128
|
+
)
|
|
129
|
+
return lines
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _verdict(s: dict) -> str:
|
|
133
|
+
return (
|
|
134
|
+
"passes"
|
|
135
|
+
if s["probe_verified"]
|
|
136
|
+
else "fails " + ", ".join(sorted(k for k, ok in s["gates"].items() if not ok))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _failed_gates(done: Sequence[ReviewRow]) -> str:
|
|
141
|
+
from collections import Counter
|
|
142
|
+
|
|
143
|
+
counts = Counter(k for r in done for k, ok in r.neuronpedia["gates"].items() if not ok)
|
|
144
|
+
return ", ".join(f"{k} ×{n}" for k, n in counts.most_common())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _cell(text: str) -> str:
|
|
148
|
+
return " ".join(text.split()).replace("|", "\\|").replace("<", "<")
|
openinterp/planner.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Choose the next experiment: randomised, exploration-balanced, herd-resistant.
|
|
2
|
+
|
|
3
|
+
Every neuron in the registry generates candidate follow-up experiments:
|
|
4
|
+
|
|
5
|
+
- Re-evaluate under the current protocol, or re-review independently.
|
|
6
|
+
- Revise, replicate or challenge.
|
|
7
|
+
- Audit the opposite pole, or try a new hypothesis after a rejection.
|
|
8
|
+
- Rescan a record that was scanned on the built-in sample.
|
|
9
|
+
|
|
10
|
+
A VERIFIED pole also offers its unstudied matches in other configured models
|
|
11
|
+
(``open-interpretability sisters``), audited with its explanation. Any match can be drawn, and any
|
|
12
|
+
pole of another model at random. But the pole's sister is drawn far more often than the
|
|
13
|
+
rest.
|
|
14
|
+
|
|
15
|
+
Each candidate gets a weight from its expected information value. The weight decays
|
|
16
|
+
with how often the neuron has been studied, with recent activity, and with open PRs
|
|
17
|
+
that already claim it. Weights are never zero: repeating an experiment is allowed, and
|
|
18
|
+
a null result is data.
|
|
19
|
+
|
|
20
|
+
With probability ``epsilon``, the planner explores instead. It picks a uniformly random
|
|
21
|
+
new neuron, the only selection that supports unbiased registry statistics. The model
|
|
22
|
+
comes from the configured list (``[tool.openinterp] models`` in ``pyproject.toml``).
|
|
23
|
+
The smallest model below its coverage target gets most of these draws, and the others
|
|
24
|
+
share the rest (``openinterp.planner_models``). Otherwise, the planner
|
|
25
|
+
samples a candidate in proportion to ``weight ** (1 / temperature)``. Sampling,
|
|
26
|
+
rather than taking the best candidate, keeps independent agents from piling onto
|
|
27
|
+
the same neuron. The seed is reported so any choice can be reproduced.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import math
|
|
34
|
+
import random
|
|
35
|
+
import shutil
|
|
36
|
+
import subprocess
|
|
37
|
+
import tomllib
|
|
38
|
+
from collections.abc import Sequence
|
|
39
|
+
from dataclasses import asdict, dataclass, field
|
|
40
|
+
from datetime import UTC, datetime, timedelta
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from openinterp.core.licenses import ModelLicense
|
|
44
|
+
from openinterp.core.paths import DEFAULT_WIKI_DIR
|
|
45
|
+
from openinterp.core.schemas import (
|
|
46
|
+
PROTOCOL_VERSION,
|
|
47
|
+
AuditRecord,
|
|
48
|
+
ReviewPolicy,
|
|
49
|
+
scan_active_share,
|
|
50
|
+
)
|
|
51
|
+
from openinterp.planner_models import (
|
|
52
|
+
DEFAULT_COVERAGE_TARGET,
|
|
53
|
+
DEFAULT_OTHERS_SHARE,
|
|
54
|
+
ModelCoverage,
|
|
55
|
+
explore_weights,
|
|
56
|
+
focus_model,
|
|
57
|
+
model_coverage,
|
|
58
|
+
)
|
|
59
|
+
from openinterp.planner_states import NeuronState, load_states # noqa: F401 - re-exported
|
|
60
|
+
|
|
61
|
+
# Expected information value per experiment kind, before decay.
|
|
62
|
+
# Probability of an explore draw (a random new unit) instead of a follow-up.
|
|
63
|
+
DEFAULT_EPSILON = 0.7
|
|
64
|
+
BASE_WEIGHTS = {
|
|
65
|
+
"reevaluate": 3.0, # verdict predates the current protocol; cheap, no LLM calls
|
|
66
|
+
"independent_review": 2.5, # proposer reviewed its own hypothesis
|
|
67
|
+
"revise": 2.0, # NEEDS_REVISION: failed gates say exactly what to fix
|
|
68
|
+
"rescan": 2.0, # scanned on the built-in sample: its max activation means little
|
|
69
|
+
"replicate": 1.2, # VERIFIED by one reviewer model, or never by an independent one
|
|
70
|
+
"opposite_pole": 1.0, # the other half of a studied neuron
|
|
71
|
+
"new_hypothesis": 0.8, # REJECTED: the null result stands; try another idea
|
|
72
|
+
"challenge": 0.6, # VERIFIED and replicated: hunt for counterexamples
|
|
73
|
+
"sister": 10.0, # a ready hypothesis on its likeliest carrier (others: LINK_WEIGHT)
|
|
74
|
+
}
|
|
75
|
+
SKILLS = {
|
|
76
|
+
"explore": ["native-neuron-audit", "neuron-family-triage", "vocab-projection-lookahead"],
|
|
77
|
+
"reevaluate": [],
|
|
78
|
+
"independent_review": ["interpretability-illusions-review"],
|
|
79
|
+
"revise": ["native-neuron-audit", "explanation-simulation-scoring"],
|
|
80
|
+
"rescan": ["native-neuron-audit", "neuron-family-triage"],
|
|
81
|
+
"replicate": ["interpretability-illusions-review"],
|
|
82
|
+
"opposite_pole": ["native-neuron-audit", "vocab-projection-lookahead"],
|
|
83
|
+
"new_hypothesis": ["native-neuron-audit", "neuron-steering-tests"],
|
|
84
|
+
"challenge": ["interpretability-illusions-review", "neuron-steering-tests"],
|
|
85
|
+
"sister": ["native-neuron-audit", "interpretability-illusions-review"],
|
|
86
|
+
"superposition": ["native-neuron-audit", "interpretability-illusions-review"],
|
|
87
|
+
}
|
|
88
|
+
DEFAULT_MODELS = ("Qwen/Qwen2.5-0.5B",)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Candidate:
|
|
93
|
+
"""One experiment the planner can draw, with its weight and the commands to run it."""
|
|
94
|
+
|
|
95
|
+
kind: str
|
|
96
|
+
model: str
|
|
97
|
+
weight: float
|
|
98
|
+
rationale: str
|
|
99
|
+
commands: list[str]
|
|
100
|
+
slug: str | None = None
|
|
101
|
+
skills: list[str] = field(default_factory=list)
|
|
102
|
+
layer: int | None = None
|
|
103
|
+
record: str | None = None # the record a follow-up builds on
|
|
104
|
+
audit: str | None = None # `open-interpretability audit` options, for kinds that scan
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def claim_key(slug: str) -> str:
|
|
108
|
+
"""Branch-safe form of a neuron slug; PR branches and titles should contain it."""
|
|
109
|
+
return slug.replace("/", "-")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def open_pr_claims() -> list[str]:
|
|
113
|
+
"""Titles and branch names of open PRs (empty if `gh` is unavailable)."""
|
|
114
|
+
if not shutil.which("gh"):
|
|
115
|
+
return []
|
|
116
|
+
try:
|
|
117
|
+
out = subprocess.run(
|
|
118
|
+
[
|
|
119
|
+
"gh",
|
|
120
|
+
"pr",
|
|
121
|
+
"list",
|
|
122
|
+
"--state",
|
|
123
|
+
"open",
|
|
124
|
+
"--limit",
|
|
125
|
+
"200",
|
|
126
|
+
"--json",
|
|
127
|
+
"title,headRefName",
|
|
128
|
+
],
|
|
129
|
+
check=True,
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
timeout=30,
|
|
133
|
+
).stdout
|
|
134
|
+
except (subprocess.SubprocessError, OSError):
|
|
135
|
+
return []
|
|
136
|
+
return [f"{pr['title']} {pr['headRefName']}" for pr in json.loads(out)]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _config(pyproject: Path) -> dict:
|
|
140
|
+
try:
|
|
141
|
+
with open(pyproject, "rb") as fh:
|
|
142
|
+
return tomllib.load(fh).get("tool", {}).get("openinterp", {})
|
|
143
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def configured_models(pyproject: Path = Path("pyproject.toml")) -> tuple[str, ...]:
|
|
148
|
+
"""Models the planner explores: ``[tool.openinterp] models`` (empty if unset)."""
|
|
149
|
+
return tuple(_config(pyproject).get("models", ()))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def configured_windows(pyproject: Path = Path("pyproject.toml")) -> dict[str, int]:
|
|
153
|
+
"""Return ``[tool.openinterp.window_tokens]``, the scan window of some models.
|
|
154
|
+
|
|
155
|
+
These are the models whose context is shorter than the corpus's documents. GPT-2
|
|
156
|
+
reads 1,024 positions.
|
|
157
|
+
"""
|
|
158
|
+
return dict(_config(pyproject).get("window_tokens", {}))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def configured_licenses(pyproject: Path = Path("pyproject.toml")) -> dict[str, ModelLicense]:
|
|
162
|
+
"""Return ``[tool.openinterp.licenses]``, the terms of each model (empty if unset).
|
|
163
|
+
|
|
164
|
+
A malformed entry raises a validation error, so a typo cannot hide a term.
|
|
165
|
+
"""
|
|
166
|
+
licenses = _config(pyproject).get("licenses", {})
|
|
167
|
+
return {model: ModelLicense(**terms) for model, terms in licenses.items()}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def coverage_target(pyproject: Path = Path("pyproject.toml")) -> float:
|
|
171
|
+
"""Return ``[tool.openinterp] coverage_target``, or the default.
|
|
172
|
+
|
|
173
|
+
A model's verified share must pass it before most explore draws move to the next model.
|
|
174
|
+
"""
|
|
175
|
+
return float(_config(pyproject).get("coverage_target", DEFAULT_COVERAGE_TARGET))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def others_share(pyproject: Path = Path("pyproject.toml")) -> float:
|
|
179
|
+
"""Return ``[tool.openinterp] others_share``, or the default.
|
|
180
|
+
|
|
181
|
+
It is the share of explore draws for the models that are not the focus.
|
|
182
|
+
"""
|
|
183
|
+
return float(_config(pyproject).get("others_share", DEFAULT_OTHERS_SHARE))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _decay(state: NeuronState, claims: Sequence[str], now: datetime) -> tuple[float, list[str]]:
|
|
187
|
+
factor, notes = 1.0 / math.sqrt(state.experiments), []
|
|
188
|
+
if state.experiments > 1:
|
|
189
|
+
notes.append(f"{state.experiments} experiments so far")
|
|
190
|
+
age = now - state.last
|
|
191
|
+
if age < timedelta(hours=6):
|
|
192
|
+
factor *= 0.2
|
|
193
|
+
notes.append("studied in the last 6h")
|
|
194
|
+
elif age < timedelta(hours=24):
|
|
195
|
+
factor *= 0.5
|
|
196
|
+
notes.append("studied in the last 24h")
|
|
197
|
+
key = claim_key(state.slug)
|
|
198
|
+
if any(key in c or state.slug in c for c in claims):
|
|
199
|
+
factor *= 0.05
|
|
200
|
+
notes.append("an open PR already claims it")
|
|
201
|
+
return factor, notes
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def candidates_for(state: NeuronState, slugs: set[str], claims: Sequence[str], now: datetime):
|
|
205
|
+
"""The follow-up candidates for one studied unit, weighted."""
|
|
206
|
+
record = state.record
|
|
207
|
+
ident = record.submission.identifier
|
|
208
|
+
commands = _commands(state)
|
|
209
|
+
if record.scanned_on_builtin_sample:
|
|
210
|
+
# Every review kind would inherit a 552-token scan, and so would its default-on
|
|
211
|
+
# gate: rescan on the real corpus instead.
|
|
212
|
+
kinds = _rescan(commands)
|
|
213
|
+
else:
|
|
214
|
+
kinds = _review_kinds(state, commands)
|
|
215
|
+
if _default_on(record):
|
|
216
|
+
# A single default-on pole can't be audited as a detector: a new audit stops at the
|
|
217
|
+
# scan, and a review fails the gate. Re-evaluating can still correct an old verdict.
|
|
218
|
+
kinds = [k for k in kinds if k[0] == "reevaluate"]
|
|
219
|
+
kinds += _opposite_pole(state, slugs, commands)
|
|
220
|
+
|
|
221
|
+
factor, notes = _decay(state, claims, now)
|
|
222
|
+
for kind, why, commands, slug in kinds:
|
|
223
|
+
yield Candidate(
|
|
224
|
+
kind=kind,
|
|
225
|
+
model=ident.model_name,
|
|
226
|
+
weight=BASE_WEIGHTS[kind] * factor,
|
|
227
|
+
rationale="; ".join([why, *notes]),
|
|
228
|
+
commands=commands,
|
|
229
|
+
slug=slug or ident.slug,
|
|
230
|
+
skills=SKILLS[kind],
|
|
231
|
+
layer=ident.layer,
|
|
232
|
+
record=state.record_path.as_posix(),
|
|
233
|
+
audit=_audit_options(commands),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _default_on(record: AuditRecord) -> bool:
|
|
238
|
+
"""Return whether the record shows a default-on unit, by its gate or by its histogram.
|
|
239
|
+
|
|
240
|
+
Records from before protocol 0.8 have no ``not_default_on`` gate, so the histogram is
|
|
241
|
+
read as ``check_default_on`` reads it, with the current policy.
|
|
242
|
+
"""
|
|
243
|
+
metrics = record.verdict.metrics
|
|
244
|
+
if metrics and metrics.gates.get("not_default_on") is False:
|
|
245
|
+
return True
|
|
246
|
+
policy = ReviewPolicy()
|
|
247
|
+
share = scan_active_share(record.submission, policy)
|
|
248
|
+
return share is not None and share >= policy.max_active_share
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _audit_options(commands: list[str]) -> str | None:
|
|
252
|
+
"""The options of the audit command among ``commands``, without --supersedes.
|
|
253
|
+
|
|
254
|
+
The audit command is the one that starts with ``open-interpretability audit``.
|
|
255
|
+
"""
|
|
256
|
+
for command in commands:
|
|
257
|
+
if command.startswith("open-interpretability audit "):
|
|
258
|
+
return command.removeprefix("open-interpretability audit ").split(" --supersedes ")[0]
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class _Commands:
|
|
264
|
+
"""The commands a follow-up on one record runs."""
|
|
265
|
+
|
|
266
|
+
path: str
|
|
267
|
+
target: str # --model, --layer and the unit, as the record was scanned
|
|
268
|
+
review: str
|
|
269
|
+
revise: str
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _commands(state: NeuronState) -> _Commands:
|
|
273
|
+
record = state.record
|
|
274
|
+
ident = record.submission.identifier
|
|
275
|
+
path = state.record_path.as_posix()
|
|
276
|
+
if ident.kind == "attn_head":
|
|
277
|
+
unit = f"--head {ident.head_idx}"
|
|
278
|
+
elif ident.kind == "dictionary_feature":
|
|
279
|
+
unit = f"--dictionary {ident.dictionary} --feature {ident.feature_idx}"
|
|
280
|
+
else:
|
|
281
|
+
unit = f"--neuron {ident.neuron_idx}"
|
|
282
|
+
target = f"--model {ident.model_name} --layer {ident.layer} {unit}"
|
|
283
|
+
provenance = record.submission.provenance
|
|
284
|
+
if provenance and provenance.snippet_tokens: # rescan as the record was scanned
|
|
285
|
+
target += f" --window-tokens {provenance.snippet_tokens}"
|
|
286
|
+
proposer = record.submission.submitting_agent_model
|
|
287
|
+
review = (
|
|
288
|
+
f"open-interpretability verify --audit-file {path} --corpus <scan corpus> "
|
|
289
|
+
f"--agent-model <model other than {proposer}>"
|
|
290
|
+
)
|
|
291
|
+
pole = f" --pole {ident.pole}" if ident.pole else ""
|
|
292
|
+
revise = f"open-interpretability audit {target}{pole} --supersedes {path}"
|
|
293
|
+
return _Commands(path, target, review, revise)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
Kind = tuple[str, str, list[str], str | None] # kind, why, commands, slug (if another unit's)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _review_kinds(state: NeuronState, c: _Commands) -> list[Kind]:
|
|
300
|
+
"""Follow-ups on the record's own verdict: re-evaluate, review, revise or replicate."""
|
|
301
|
+
record, verdict = state.record, state.record.verdict
|
|
302
|
+
kinds: list[Kind] = []
|
|
303
|
+
if verdict.policy is None or verdict.policy.protocol_version != PROTOCOL_VERSION:
|
|
304
|
+
reevaluate = [f"open-interpretability reevaluate --record {c.path}"]
|
|
305
|
+
kinds.append(("reevaluate", "verdict predates the current protocol", reevaluate, None))
|
|
306
|
+
# Not the gate: it passes self-reviews unless the policy requires independence.
|
|
307
|
+
if record.self_reviewed:
|
|
308
|
+
kinds.append(
|
|
309
|
+
("independent_review", "the proposer reviewed its own hypothesis", [c.review], None)
|
|
310
|
+
)
|
|
311
|
+
gates = verdict.metrics.gates if verdict.metrics else {}
|
|
312
|
+
failed = [g for g, ok in gates.items() if not ok]
|
|
313
|
+
resubmit = [
|
|
314
|
+
c.revise,
|
|
315
|
+
"open-interpretability verify --audit-file <new submission> --corpus <scan corpus>",
|
|
316
|
+
]
|
|
317
|
+
if verdict.status == "NEEDS_REVISION":
|
|
318
|
+
kinds.append(("revise", f"failed gates: {', '.join(failed) or 'unknown'}", resubmit, None))
|
|
319
|
+
elif verdict.status == "REJECTED":
|
|
320
|
+
why = "rejected; the null result stands, so try another idea"
|
|
321
|
+
kinds.append(("new_hypothesis", why, resubmit, None))
|
|
322
|
+
elif len(state.reviewers) < 2:
|
|
323
|
+
kinds.append(("replicate", "verified by only one reviewer model", [c.review], None))
|
|
324
|
+
elif not state.independent_reviews:
|
|
325
|
+
kinds.append(("replicate", "every review so far was a self-review", [c.review], None))
|
|
326
|
+
else:
|
|
327
|
+
why = "verified and replicated; look for counterexamples"
|
|
328
|
+
kinds.append(("challenge", why, [c.review], None))
|
|
329
|
+
return kinds
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _rescan(c: _Commands) -> list[Kind]:
|
|
333
|
+
"""Return a new audit on the scan corpus, to supersede a built-in-sample record.
|
|
334
|
+
|
|
335
|
+
Re-evaluating or re-reviewing the record would reuse its scan.
|
|
336
|
+
"""
|
|
337
|
+
why = "scanned on the 552-token built-in sample; rescan on the corpus"
|
|
338
|
+
verify = "open-interpretability verify --audit-file <new submission> --corpus <scan corpus>"
|
|
339
|
+
return [("rescan", why, [c.revise, verify], None)]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _opposite_pole(state: NeuronState, slugs: set[str], c: _Commands) -> list[Kind]:
|
|
343
|
+
ident = state.record.submission.identifier
|
|
344
|
+
other = "negative" if ident.pole == "positive" else "positive"
|
|
345
|
+
other_slug = ident.slug.rsplit("-", 1)[0] + f"-{other}"
|
|
346
|
+
if not ident.pole or other_slug in slugs: # heads and features have no poles
|
|
347
|
+
return []
|
|
348
|
+
audit = f"open-interpretability audit {c.target} --pole {other}"
|
|
349
|
+
return [("opposite_pole", f"the {other} pole is unstudied", [audit], other_slug)]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass
|
|
353
|
+
class Plan:
|
|
354
|
+
"""The planner's choice, the candidates it drew from, and the seed that replays it."""
|
|
355
|
+
|
|
356
|
+
seed: int
|
|
357
|
+
choice: Candidate
|
|
358
|
+
explored: bool
|
|
359
|
+
candidates: list[Candidate]
|
|
360
|
+
explore_models: dict[str, float] = field(default_factory=dict)
|
|
361
|
+
coverage: dict[str, ModelCoverage] = field(default_factory=dict)
|
|
362
|
+
focus: str | None = None # the model that explore draws favour
|
|
363
|
+
|
|
364
|
+
def to_json(self) -> str:
|
|
365
|
+
"""The plan as indented JSON."""
|
|
366
|
+
return json.dumps(asdict(self), indent=2)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def plan_next(
|
|
370
|
+
wiki_dir: Path = DEFAULT_WIKI_DIR,
|
|
371
|
+
*,
|
|
372
|
+
seed: int | None = None,
|
|
373
|
+
epsilon: float = DEFAULT_EPSILON,
|
|
374
|
+
temperature: float = 1.0,
|
|
375
|
+
models: Sequence[str] = (),
|
|
376
|
+
claims: Sequence[str] = (),
|
|
377
|
+
now: datetime | None = None,
|
|
378
|
+
) -> Plan:
|
|
379
|
+
"""Draw the next experiment.
|
|
380
|
+
|
|
381
|
+
Explore draws use ``models`` if given, else the models configured in the
|
|
382
|
+
``pyproject.toml`` next to ``wiki_dir``, else the registry's, else ``DEFAULT_MODELS``.
|
|
383
|
+
"""
|
|
384
|
+
seed = random.SystemRandom().getrandbits(32) if seed is None else seed
|
|
385
|
+
rng = random.Random(seed)
|
|
386
|
+
now = now or datetime.now(UTC)
|
|
387
|
+
states = load_states(wiki_dir)
|
|
388
|
+
slugs = set(states)
|
|
389
|
+
pyproject = Path(wiki_dir).parent / "pyproject.toml"
|
|
390
|
+
pool = _model_pool(models, pyproject, states)
|
|
391
|
+
# Imported here: these modules build their candidates from this module's weights.
|
|
392
|
+
from openinterp.planner_sisters import sister_candidates
|
|
393
|
+
from openinterp.planner_superpositions import superposition_candidates
|
|
394
|
+
|
|
395
|
+
root = Path(wiki_dir).parent
|
|
396
|
+
sisters = sister_candidates(states, claims, now, pool, root, seed)
|
|
397
|
+
extra = [*sisters, *superposition_candidates(states, claims, root)]
|
|
398
|
+
candidates = sorted(
|
|
399
|
+
[*(c for s in states.values() for c in candidates_for(s, slugs, claims, now)), *extra],
|
|
400
|
+
key=lambda c: -c.weight,
|
|
401
|
+
)
|
|
402
|
+
records = (s.record for s in states.values())
|
|
403
|
+
coverage = model_coverage(pool, records, claims, dead(wiki_dir), stopped(wiki_dir))
|
|
404
|
+
target = coverage_target(pyproject)
|
|
405
|
+
explore = explore_weights(coverage, target, others_share(pyproject))
|
|
406
|
+
focus = focus_model(coverage.values(), target)
|
|
407
|
+
if not candidates or rng.random() < epsilon:
|
|
408
|
+
choice = _explore(rng, pool, explore, pyproject, focus, slugs | set(claims))
|
|
409
|
+
return Plan(seed, choice, True, candidates, explore, coverage, focus)
|
|
410
|
+
|
|
411
|
+
weights = [c.weight ** (1.0 / temperature) for c in candidates]
|
|
412
|
+
choice = rng.choices(candidates, weights=weights, k=1)[0]
|
|
413
|
+
return Plan(seed, choice, False, candidates, explore, coverage, focus)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def stopped(wiki_dir: Path) -> list:
|
|
417
|
+
"""Every unit that a scan stopped on (dead or default-on), chosen or drawn at random.
|
|
418
|
+
|
|
419
|
+
Such a unit has no experiment, but it counts as studied.
|
|
420
|
+
"""
|
|
421
|
+
from openinterp.core.draws import load_draws, load_stops
|
|
422
|
+
|
|
423
|
+
wiki_dir = Path(wiki_dir)
|
|
424
|
+
return [d.identifier for d in (*load_draws(wiki_dir), *load_stops(wiki_dir))]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def dead(wiki_dir: Path) -> list:
|
|
428
|
+
"""Every neuron pole that a scan found dead and the weights proved dead.
|
|
429
|
+
|
|
430
|
+
The proof does not depend on how the unit was selected. So this reads the stops of
|
|
431
|
+
chosen units (:func:`load_stops`) as well as random draws. Random-selection
|
|
432
|
+
statistics still read the draws only.
|
|
433
|
+
"""
|
|
434
|
+
from openinterp.core.draws import load_draws, load_stops
|
|
435
|
+
|
|
436
|
+
wiki_dir = Path(wiki_dir)
|
|
437
|
+
return [d.identifier for d in (*load_draws(wiki_dir), *load_stops(wiki_dir)) if d.proven_dead]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _model_pool(models: Sequence[str], pyproject: Path, states) -> list[str]:
|
|
441
|
+
"""``models``, else the configured models, else the registry's, else the defaults."""
|
|
442
|
+
return sorted(
|
|
443
|
+
set(models)
|
|
444
|
+
or set(configured_models(pyproject))
|
|
445
|
+
or {s.record.submission.identifier.model_name for s in states.values()}
|
|
446
|
+
or set(DEFAULT_MODELS)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _explore(
|
|
451
|
+
rng: random.Random, pool: list[str], explore: dict, pyproject: Path, focus: str | None,
|
|
452
|
+
taken: set[str] = frozenset(),
|
|
453
|
+
) -> Candidate: # fmt: skip
|
|
454
|
+
"""A new unit of a model drawn by size and coverage: random, or weighted by effect."""
|
|
455
|
+
from openinterp.planner_effects import effect_draw
|
|
456
|
+
from openinterp.planner_heads import head_draw, neuron_draw
|
|
457
|
+
|
|
458
|
+
model = rng.choices(pool, weights=[explore[m] for m in pool], k=1)[0]
|
|
459
|
+
audit_seed = rng.getrandbits(32)
|
|
460
|
+
window = configured_windows(pyproject).get(model)
|
|
461
|
+
if head := head_draw(model, audit_seed, pyproject, window): # the unit kind, its own draw
|
|
462
|
+
return head
|
|
463
|
+
if weighted := effect_draw(rng, model, pyproject, taken, window):
|
|
464
|
+
return weighted
|
|
465
|
+
why = "the smallest model below its verified target" if model == focus else "coverage"
|
|
466
|
+
return neuron_draw(model, audit_seed, window, explore[model] if len(pool) > 1 else None, why)
|