open-interpretability 0.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -0,0 +1,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)]