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,229 @@
|
|
|
1
|
+
"""Abstract LLM agent with structured outputs.
|
|
2
|
+
|
|
3
|
+
Two backends: a LiteLLM model string (routed through instructor), or ``external/<name>``,
|
|
4
|
+
where any LLM, agent or person answers a prompt file written to disk.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, ClassVar, TypeVar
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ValidationError
|
|
17
|
+
|
|
18
|
+
from openinterp.core import costs
|
|
19
|
+
from openinterp.core.agent_names import EXTERNAL_PREFIX, spelling_problem
|
|
20
|
+
from openinterp.core.exceptions import AgentError
|
|
21
|
+
from openinterp.core.paths import DEFAULT_EXCHANGE_DIR
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T", bound=BaseModel)
|
|
24
|
+
|
|
25
|
+
DEFAULT_MODEL = os.environ.get("OPENINTERP_AGENT_MODEL", "anthropic/claude-opus-5-5")
|
|
26
|
+
MODEL_ALIASES_ENV = "OPENINTERP_MODEL_ALIASES"
|
|
27
|
+
# LiteLLM providers that cache a prompt prefix only where a ``cache_control`` mark ends it.
|
|
28
|
+
# Others (OpenAI, DeepSeek, Gemini) cache a repeated prefix without a mark.
|
|
29
|
+
CACHE_CONTROL_PROVIDERS = frozenset({"anthropic", "bedrock"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def recorded_model(model: str | None) -> str | None:
|
|
33
|
+
"""The name to record for ``model``, after ``$OPENINTERP_MODEL_ALIASES``.
|
|
34
|
+
|
|
35
|
+
This is for external answerers behind a proxy. An example is Claude Code routed
|
|
36
|
+
through a LiteLLM proxy to Grok, whose subagents report the Claude model they think
|
|
37
|
+
they are. The variable holds ``reported=actual`` pairs separated by commas
|
|
38
|
+
(``claude-haiku-5-5=grok-4-fast,claude-opus-5-5=grok-4``). Or it holds one bare name
|
|
39
|
+
that replaces every external model.
|
|
40
|
+
|
|
41
|
+
Only ``external/<name>`` is rewritten. A LiteLLM string is what gets called, so it
|
|
42
|
+
already names the model. Raises ``ValueError`` for an external name spelled other
|
|
43
|
+
than the site would show it (:func:`openinterp.core.agent_names.spelling_problem`).
|
|
44
|
+
So one model keeps one name.
|
|
45
|
+
"""
|
|
46
|
+
spec = os.environ.get(MODEL_ALIASES_ENV, "").strip()
|
|
47
|
+
if not model or not model.startswith(EXTERNAL_PREFIX):
|
|
48
|
+
return model
|
|
49
|
+
if not spec:
|
|
50
|
+
return _checked(model)
|
|
51
|
+
name = model.removeprefix(EXTERNAL_PREFIX)
|
|
52
|
+
aliases, default = {}, None
|
|
53
|
+
for entry in filter(None, (e.strip() for e in spec.split(","))):
|
|
54
|
+
reported, sep, actual = entry.partition("=")
|
|
55
|
+
if not sep:
|
|
56
|
+
default = entry.removeprefix(EXTERNAL_PREFIX)
|
|
57
|
+
elif reported.strip() and actual.strip():
|
|
58
|
+
aliases[reported.strip().removeprefix(EXTERNAL_PREFIX)] = actual.strip()
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"${MODEL_ALIASES_ENV}: malformed entry {entry!r}")
|
|
61
|
+
actual = aliases.get(name, default or name)
|
|
62
|
+
return _checked(EXTERNAL_PREFIX + actual.removeprefix(EXTERNAL_PREFIX))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _checked(model: str) -> str:
|
|
66
|
+
if problem := spelling_problem(model):
|
|
67
|
+
raise ValueError(f"{model!r}: {problem}")
|
|
68
|
+
return model
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LLMAgent:
|
|
72
|
+
"""Base class for agents that return validated Pydantic objects.
|
|
73
|
+
|
|
74
|
+
``model`` is either a LiteLLM model string (``"anthropic/..."``, ``"openai/..."``,
|
|
75
|
+
``"ollama/..."``) or ``"external/<name>"``. An external model is answered through
|
|
76
|
+
files in ``exchange_dir``. The agent writes ``<Agent>.prompt.md``, with the response's
|
|
77
|
+
JSON schema. Then it waits for ``<Agent>.response.json``. The full model string is what
|
|
78
|
+
gets recorded, so name the model that actually answered, e.g. ``external/gpt-5``.
|
|
79
|
+
Pass ``client`` to inject a pre-built instructor client (useful in tests).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
system_prompt: ClassVar[str] = ""
|
|
83
|
+
role: ClassVar[str] = "agent" # the stage its calls are timed as
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
model: str = DEFAULT_MODEL,
|
|
88
|
+
*,
|
|
89
|
+
temperature: float | None = None,
|
|
90
|
+
max_retries: int = 2,
|
|
91
|
+
client: Any | None = None,
|
|
92
|
+
exchange_dir: Path | None = None,
|
|
93
|
+
timeout: float | None = None,
|
|
94
|
+
poll_interval: float = 2.0,
|
|
95
|
+
) -> None:
|
|
96
|
+
self.model = model
|
|
97
|
+
self.temperature = temperature
|
|
98
|
+
self.max_retries = max_retries
|
|
99
|
+
self._client = client
|
|
100
|
+
self.exchange_dir = exchange_dir or Path(
|
|
101
|
+
os.environ.get("OPENINTERP_EXCHANGE_DIR", DEFAULT_EXCHANGE_DIR)
|
|
102
|
+
)
|
|
103
|
+
self.timeout = timeout or float(os.environ.get("OPENINTERP_EXCHANGE_TIMEOUT", 3600))
|
|
104
|
+
self.poll_interval = poll_interval
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def client(self) -> Any:
|
|
108
|
+
"""The instructor client over LiteLLM, made on first use."""
|
|
109
|
+
if self._client is None:
|
|
110
|
+
import instructor
|
|
111
|
+
import litellm
|
|
112
|
+
|
|
113
|
+
self._client = instructor.from_litellm(litellm.completion)
|
|
114
|
+
return self._client
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def render_prompt(
|
|
118
|
+
cls, response_model: type[BaseModel], prompt: str, response_path: Path | str
|
|
119
|
+
) -> str:
|
|
120
|
+
"""The full prompt for an external answerer: system prompt, JSON schema and task.
|
|
121
|
+
|
|
122
|
+
The parts that are the same for every unit come first, so a provider can cache
|
|
123
|
+
them as a prefix. The task and the response path come last.
|
|
124
|
+
"""
|
|
125
|
+
schema = json.dumps(response_model.model_json_schema(), indent=2)
|
|
126
|
+
return (
|
|
127
|
+
f"# System\n\n{cls.system_prompt}\n\n"
|
|
128
|
+
f"# Response schema\n\n```json\n{schema}\n```\n\n# Prompt\n\n{prompt}\n\n"
|
|
129
|
+
f"# Response\n\nWrite a JSON object matching the schema above to "
|
|
130
|
+
f"`{response_path}`.\n"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def system_message(self) -> dict[str, Any]:
|
|
134
|
+
"""The system message, marked as the end of a cacheable prefix where needed.
|
|
135
|
+
|
|
136
|
+
The response schema goes to the provider as a tool, which comes before the system
|
|
137
|
+
message, so the cached prefix holds both.
|
|
138
|
+
"""
|
|
139
|
+
if self.model.split("/", 1)[0] not in CACHE_CONTROL_PROVIDERS:
|
|
140
|
+
return {"role": "system", "content": self.system_prompt}
|
|
141
|
+
block = {"type": "text", "text": self.system_prompt, "cache_control": {"type": "ephemeral"}}
|
|
142
|
+
return {"role": "system", "content": [block]}
|
|
143
|
+
|
|
144
|
+
def structured(self, response_model: type[T], prompt: str) -> T:
|
|
145
|
+
"""Ask the model for a ``response_model`` and record the call's cost under this role."""
|
|
146
|
+
with costs.stage(self.role):
|
|
147
|
+
if self.model.startswith(EXTERNAL_PREFIX):
|
|
148
|
+
result = self._external(response_model, prompt)
|
|
149
|
+
chars = len(self.system_prompt) + len(prompt)
|
|
150
|
+
call = costs.LLMCall(
|
|
151
|
+
agent=self.role, model=self.model, external=True, prompt_chars=chars
|
|
152
|
+
)
|
|
153
|
+
costs.add_call(call)
|
|
154
|
+
return result
|
|
155
|
+
return self._completion(response_model, prompt)
|
|
156
|
+
|
|
157
|
+
def _completion(self, response_model: type[T], prompt: str) -> T:
|
|
158
|
+
kwargs: dict[str, Any] = {}
|
|
159
|
+
if self.temperature is not None:
|
|
160
|
+
kwargs["temperature"] = self.temperature
|
|
161
|
+
responses: list[Any] = [] # one raw response per attempt, for usage and cost
|
|
162
|
+
client, hook = self.client, responses.append
|
|
163
|
+
if hasattr(client, "on"): # instructor's hooks; a stub client may lack them
|
|
164
|
+
client.on("completion:response", hook)
|
|
165
|
+
try:
|
|
166
|
+
return client.chat.completions.create(
|
|
167
|
+
model=self.model,
|
|
168
|
+
response_model=response_model,
|
|
169
|
+
max_retries=self.max_retries,
|
|
170
|
+
messages=[self.system_message(), {"role": "user", "content": prompt}],
|
|
171
|
+
**kwargs,
|
|
172
|
+
)
|
|
173
|
+
except Exception as exc: # instructor/litellm raise many provider-specific types
|
|
174
|
+
raise AgentError(
|
|
175
|
+
f"{type(self).__name__} ({self.model}) failed to produce "
|
|
176
|
+
f"{response_model.__name__}: {exc}"
|
|
177
|
+
) from exc
|
|
178
|
+
finally:
|
|
179
|
+
if hasattr(client, "off"):
|
|
180
|
+
client.off("completion:response", hook)
|
|
181
|
+
chars = len(self.system_prompt) + len(prompt)
|
|
182
|
+
costs.add_call(
|
|
183
|
+
costs.LLMCall(
|
|
184
|
+
agent=self.role,
|
|
185
|
+
model=self.model,
|
|
186
|
+
prompt_chars=chars,
|
|
187
|
+
**costs.usage_of(responses),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _external(self, response_model: type[T], prompt: str) -> T:
|
|
192
|
+
"""Write the prompt to disk and wait for a valid JSON response from anyone."""
|
|
193
|
+
name = type(self).__name__
|
|
194
|
+
self.exchange_dir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
prompt_path = self.exchange_dir / f"{name}.prompt.md"
|
|
196
|
+
response_path = self.exchange_dir / f"{name}.response.json"
|
|
197
|
+
error_path = self.exchange_dir / f"{name}.error.txt"
|
|
198
|
+
for stale in (response_path, error_path):
|
|
199
|
+
stale.unlink(missing_ok=True)
|
|
200
|
+
_write_whole(prompt_path, self.render_prompt(response_model, prompt, response_path))
|
|
201
|
+
print(f"Waiting for {response_path} (prompt: {prompt_path})…", file=sys.stderr)
|
|
202
|
+
deadline = time.monotonic() + self.timeout
|
|
203
|
+
seen = None # the last unparseable response, rejected only once it stops changing
|
|
204
|
+
while time.monotonic() < deadline:
|
|
205
|
+
text = response_path.read_text() if response_path.exists() else ""
|
|
206
|
+
if text.strip():
|
|
207
|
+
try:
|
|
208
|
+
result = response_model.model_validate_json(text)
|
|
209
|
+
except ValidationError as exc:
|
|
210
|
+
if text != seen: # possibly caught mid-write: look again next poll
|
|
211
|
+
seen = text
|
|
212
|
+
time.sleep(self.poll_interval)
|
|
213
|
+
continue
|
|
214
|
+
seen = None
|
|
215
|
+
_write_whole(error_path, str(exc))
|
|
216
|
+
response_path.unlink()
|
|
217
|
+
print(f"Invalid response, see {error_path}", file=sys.stderr)
|
|
218
|
+
else:
|
|
219
|
+
error_path.unlink(missing_ok=True)
|
|
220
|
+
return result
|
|
221
|
+
time.sleep(self.poll_interval)
|
|
222
|
+
raise AgentError(f"{name} ({self.model}) got no response at {response_path}.")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _write_whole(path: Path, text: str) -> None:
|
|
226
|
+
"""Write ``text`` so a reader polling for ``path`` never sees it half-written."""
|
|
227
|
+
partial = path.with_name(f".{path.name}.partial")
|
|
228
|
+
partial.write_text(text)
|
|
229
|
+
partial.replace(path)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Judge agent: scores an explanation on held-out windows from the explanation alone.
|
|
2
|
+
|
|
3
|
+
Detection asks which windows activate the pole. Fuzzing asks whether the marked tokens
|
|
4
|
+
in each window are the ones that activate it (Paulo et al., 2025). The judge never sees
|
|
5
|
+
activations. A :class:`ScoringTask` holds only the windows' text and markings. The
|
|
6
|
+
labels are measured again when the answers are scored.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from openinterp.agents.base import LLMAgent
|
|
16
|
+
from openinterp.core.costs import ExperimentCosts
|
|
17
|
+
from openinterp.core.fence import FENCE_RULE, fence
|
|
18
|
+
from openinterp.core.schemas import (
|
|
19
|
+
ComponentIdentifier,
|
|
20
|
+
DictionaryProfile,
|
|
21
|
+
HeadProfile,
|
|
22
|
+
SuperpositionProfile,
|
|
23
|
+
)
|
|
24
|
+
from openinterp.score_controls import ControlTask
|
|
25
|
+
|
|
26
|
+
MARK_OPEN, MARK_CLOSE = "<<", ">>"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TaskWindow(BaseModel):
|
|
30
|
+
"""One held-out window as the judge sees it: text, tokens and the marked tokens."""
|
|
31
|
+
|
|
32
|
+
text: str
|
|
33
|
+
tokens: list[str]
|
|
34
|
+
highlighted: list[int] = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ScoringTask(BaseModel):
|
|
38
|
+
"""What ``open-interpretability score --emit-task`` writes: everything but the labels."""
|
|
39
|
+
|
|
40
|
+
identifier: ComponentIdentifier
|
|
41
|
+
head_profile: HeadProfile | None = None # a head's strength is defined by its mean output
|
|
42
|
+
dictionary_profile: DictionaryProfile | None = None # and a feature's by its dictionary
|
|
43
|
+
superposition_profile: SuperpositionProfile | None = (
|
|
44
|
+
None # and a superposition's by its members (#3767)
|
|
45
|
+
)
|
|
46
|
+
# v0.8: the unit isn't measured at a window's first token (NeuronAuditSubmission)
|
|
47
|
+
first_token_floor: bool | None = None
|
|
48
|
+
explanation: str
|
|
49
|
+
threshold: float
|
|
50
|
+
window_tokens: int
|
|
51
|
+
quantiles: int
|
|
52
|
+
quantile_edges: list[float]
|
|
53
|
+
seed: int
|
|
54
|
+
corpus: str
|
|
55
|
+
corpus_sha256: str | None = None
|
|
56
|
+
candidate_windows: int
|
|
57
|
+
active_windows: int
|
|
58
|
+
detection: list[TaskWindow]
|
|
59
|
+
fuzzing: list[TaskWindow]
|
|
60
|
+
controls: list[ControlTask] = Field(default_factory=list) # judged on the same windows
|
|
61
|
+
fuzzing_decoys: str | None = None # "matched-shape" since issue #3505 (ExplanationScores)
|
|
62
|
+
costs: ExperimentCosts | None = None # of drawing the windows
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DetectionAnswer(BaseModel): # noqa: D101 (a docstring would join the JSON schema that the agent reads)
|
|
66
|
+
active: list[int] = Field(
|
|
67
|
+
description="Numbers of the texts in which, going by the explanation, the neuron "
|
|
68
|
+
"fires on at least one token. Leave out every other text."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FuzzingAnswer(BaseModel): # noqa: D101 (a docstring would join the JSON schema that the agent reads)
|
|
73
|
+
correct: list[int] = Field(
|
|
74
|
+
description="Numbers of the texts whose marked tokens are exactly where, going by the "
|
|
75
|
+
"explanation, the neuron fires. Leave out texts marked wrongly."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _show(tokens: Sequence[str], marked: Sequence[int] = ()) -> str:
|
|
80
|
+
hot = set(marked)
|
|
81
|
+
text = "".join(f"{MARK_OPEN}{t}{MARK_CLOSE}" if i in hot else t for i, t in enumerate(tokens))
|
|
82
|
+
return text.replace("\n", "⏎")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ExplanationJudge(LLMAgent):
|
|
86
|
+
"""The judge: applies an explanation to windows, never seeing activations."""
|
|
87
|
+
|
|
88
|
+
role = "judge"
|
|
89
|
+
system_prompt = (
|
|
90
|
+
"You test how well a short explanation of a neuron (or attention head, or dictionary "
|
|
91
|
+
"latent) in a language "
|
|
92
|
+
"model predicts where it fires. You see only the explanation and some short texts, "
|
|
93
|
+
"never its activations. Apply the explanation literally, as written: don't guess "
|
|
94
|
+
"what the neuron really does, and don't reward texts for being on a related topic "
|
|
95
|
+
"if the explanation's condition isn't met. Texts are excerpts and may start or end "
|
|
96
|
+
"mid-word; line breaks are shown as ⏎. " + FENCE_RULE
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def detection_prompt(
|
|
101
|
+
explanation: str, windows: Sequence[TaskWindow], noun: str = "neuron"
|
|
102
|
+
) -> str:
|
|
103
|
+
"""The detection prompt: which of ``windows`` make the unit fire."""
|
|
104
|
+
texts = "\n".join(f"{n}. {_show(w.tokens)}" for n, w in enumerate(windows, 1))
|
|
105
|
+
return (
|
|
106
|
+
f"Explanation of the {noun}:\n\n{fence(explanation)}\n\n"
|
|
107
|
+
f"Any number of the texts below, possibly none or all, make the {noun} fire on at "
|
|
108
|
+
"least one token. List the numbers of the texts in which the explanation says it "
|
|
109
|
+
f"fires.\n\n{fence(texts)}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def fuzzing_prompt(
|
|
114
|
+
explanation: str, windows: Sequence[TaskWindow], noun: str = "neuron"
|
|
115
|
+
) -> str:
|
|
116
|
+
"""The fuzzing prompt: in which ``windows`` the marked tokens are where the unit fires."""
|
|
117
|
+
texts = "\n".join(
|
|
118
|
+
f"{n}. {_show(w.tokens, w.highlighted)}" for n, w in enumerate(windows, 1)
|
|
119
|
+
)
|
|
120
|
+
return (
|
|
121
|
+
f"Explanation of the {noun}:\n\n{fence(explanation)}\n\n"
|
|
122
|
+
f"In each text below, some tokens are marked {MARK_OPEN}like this{MARK_CLOSE}. In "
|
|
123
|
+
f"some texts the marks are exactly the tokens where the {noun} fires; in the rest "
|
|
124
|
+
f"they are on other tokens, or the {noun} doesn't fire in that text at all. List "
|
|
125
|
+
f"the numbers of the texts whose marks are where the explanation says the {noun} "
|
|
126
|
+
f"fires.\n\n{fence(texts)}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def detect(
|
|
130
|
+
self, explanation: str, windows: Sequence[TaskWindow], noun: str = "neuron"
|
|
131
|
+
) -> DetectionAnswer:
|
|
132
|
+
"""Ask the judge the detection question."""
|
|
133
|
+
prompt = self.detection_prompt(explanation, windows, noun)
|
|
134
|
+
return self.structured(DetectionAnswer, prompt)
|
|
135
|
+
|
|
136
|
+
def fuzz(
|
|
137
|
+
self, explanation: str, windows: Sequence[TaskWindow], noun: str = "neuron"
|
|
138
|
+
) -> FuzzingAnswer:
|
|
139
|
+
"""Ask the judge the fuzzing question."""
|
|
140
|
+
return self.structured(FuzzingAnswer, self.fuzzing_prompt(explanation, windows, noun))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def answered(numbers: Sequence[int], count: int, what: str) -> list[bool]:
|
|
144
|
+
"""Per-window yes/no from a judge's list of 1-based text numbers."""
|
|
145
|
+
bad = sorted({n for n in numbers if not 1 <= n <= count})
|
|
146
|
+
if bad:
|
|
147
|
+
raise ValueError(f"{what} answer names texts {bad}, but there are only {count}")
|
|
148
|
+
chosen = set(numbers)
|
|
149
|
+
return [n in chosen for n in range(1, count + 1)]
|