brainpatch 1.2.0__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.
- brainpatch/__init__.py +92 -0
- brainpatch/backends/__init__.py +19 -0
- brainpatch/backends/llamacpp.py +383 -0
- brainpatch/backends/mlx_backend.py +213 -0
- brainpatch/backends/transformers_backend.py +473 -0
- brainpatch/backends/vllm_backend.py +299 -0
- brainpatch/backends/vllm_worker.py +129 -0
- brainpatch/cli.py +825 -0
- brainpatch/config.py +245 -0
- brainpatch/datasets/__init__.py +20 -0
- brainpatch/datasets/contrast_sets.py +64 -0
- brainpatch/evaluation/__init__.py +28 -0
- brainpatch/evaluation/metrics.py +223 -0
- brainpatch/patch/__init__.py +64 -0
- brainpatch/patch/compiler.py +324 -0
- brainpatch/patch/format.py +489 -0
- brainpatch/patch/loader.py +312 -0
- brainpatch/patch/registry.py +300 -0
- brainpatch/patch/tensors.py +236 -0
- brainpatch/patch/validation.py +157 -0
- brainpatch/paths.py +184 -0
- brainpatch/py.typed +0 -0
- brainpatch/research/__init__.py +16 -0
- brainpatch/research/antisycophancy.py +348 -0
- brainpatch/research/behaviour_eval.py +711 -0
- brainpatch/research/generation_eval.py +346 -0
- brainpatch/research/ml/__init__.py +35 -0
- brainpatch/research/ml/activation_store.py +232 -0
- brainpatch/research/ml/causal.py +386 -0
- brainpatch/research/ml/corpus.py +165 -0
- brainpatch/research/ml/evaluation.py +188 -0
- brainpatch/research/ml/extraction.py +464 -0
- brainpatch/research/ml/feature_analysis.py +317 -0
- brainpatch/research/ml/generation.py +109 -0
- brainpatch/research/ml/hooks.py +183 -0
- brainpatch/research/ml/intervention.py +274 -0
- brainpatch/research/ml/model.py +219 -0
- brainpatch/research/ml/patch_search.py +337 -0
- brainpatch/research/ml/runtime.py +343 -0
- brainpatch/research/ml/sae.py +383 -0
- brainpatch/research/ml/training.py +376 -0
- brainpatch/research/stance_rubric.py +170 -0
- brainpatch/research/sycophancy_data.py +982 -0
- brainpatch/research/sycophancy_data_r1.py +1701 -0
- brainpatch/research/sycophancy_data_v2.py +1649 -0
- brainpatch/research/sycophancy_data_v3.py +2288 -0
- brainpatch/research/sycophancy_v2_build.py +362 -0
- brainpatch/research/sycophancy_v3_build.py +188 -0
- brainpatch/research/utility_probe.py +139 -0
- brainpatch/runtime/__init__.py +50 -0
- brainpatch/runtime/auto.py +157 -0
- brainpatch/runtime/base.py +311 -0
- brainpatch/runtime/capabilities.py +96 -0
- brainpatch/runtime/model.py +260 -0
- brainpatch/runtime/scheduling.py +13 -0
- brainpatch/schemas/__init__.py +35 -0
- brainpatch/schemas/contrast.py +161 -0
- brainpatch/schemas/feature.py +193 -0
- brainpatch/schemas/manifest.py +167 -0
- brainpatch/schemas/patch.py +379 -0
- brainpatch/schemas/patch_io.py +88 -0
- brainpatch/schemas/sae.py +146 -0
- brainpatch/server/__init__.py +11 -0
- brainpatch/server/app.py +269 -0
- brainpatch/steering/__init__.py +13 -0
- brainpatch/steering/plan.py +177 -0
- brainpatch/steering/schedule.py +138 -0
- brainpatch/ui/__init__.py +11 -0
- brainpatch/ui/app.py +201 -0
- brainpatch/verify/__init__.py +66 -0
- brainpatch/verify/behavioural.py +156 -0
- brainpatch/verify/checks.py +204 -0
- brainpatch/verify/corruptions.py +335 -0
- brainpatch/verify/report.py +133 -0
- brainpatch/verify/vectors.py +95 -0
- brainpatch/verify/workflow.py +331 -0
- brainpatch-1.2.0.dist-info/METADATA +556 -0
- brainpatch-1.2.0.dist-info/RECORD +82 -0
- brainpatch-1.2.0.dist-info/WHEEL +5 -0
- brainpatch-1.2.0.dist-info/entry_points.txt +2 -0
- brainpatch-1.2.0.dist-info/licenses/LICENSE +190 -0
- brainpatch-1.2.0.dist-info/top_level.txt +1 -0
brainpatch/cli.py
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
"""The ``brainpatch`` command-line interface.
|
|
2
|
+
|
|
3
|
+
Product-first. The commands a *user* needs -- install, list, inspect, run, chat,
|
|
4
|
+
compare, serve, doctor -- work with only the core package plus whichever
|
|
5
|
+
inference backend they chose. Nothing here requires Modal, a hosted service, or
|
|
6
|
+
network access once a patch and model are local.
|
|
7
|
+
|
|
8
|
+
Backends are imported lazily, so ``brainpatch list`` and ``brainpatch inspect``
|
|
9
|
+
run instantly on a machine with no ML stack at all. ``brainpatch doctor`` is
|
|
10
|
+
built to work in exactly that situation, since its whole job is reporting what
|
|
11
|
+
is and is not installed.
|
|
12
|
+
|
|
13
|
+
Research commands live under ``brainpatch research`` and are documented as
|
|
14
|
+
requiring the ``research`` extra.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
|
|
29
|
+
from brainpatch import __version__
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
err_console = Console(stderr=True)
|
|
33
|
+
|
|
34
|
+
app = typer.Typer(
|
|
35
|
+
name="brainpatch",
|
|
36
|
+
help="Portable, reversible activation-space interventions for frozen language models.",
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
add_completion=False,
|
|
39
|
+
)
|
|
40
|
+
research_app = typer.Typer(
|
|
41
|
+
help="Patch-authoring tools. Requires: pip install 'brainpatch[research]'",
|
|
42
|
+
no_args_is_help=True,
|
|
43
|
+
)
|
|
44
|
+
app.add_typer(research_app, name="research")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _fail(message: str, code: int = 1) -> None:
|
|
48
|
+
err_console.print(f"[red]error:[/red] {message}")
|
|
49
|
+
raise typer.Exit(code=code)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.callback(invoke_without_command=True)
|
|
53
|
+
def _root(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
version: bool = typer.Option(False, "--version", help="Print the version and exit."),
|
|
56
|
+
) -> None:
|
|
57
|
+
if version:
|
|
58
|
+
console.print(f"brainpatch {__version__}")
|
|
59
|
+
raise typer.Exit()
|
|
60
|
+
if ctx.invoked_subcommand is None:
|
|
61
|
+
console.print(ctx.get_help())
|
|
62
|
+
raise typer.Exit()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# patch management
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command()
|
|
71
|
+
def install(
|
|
72
|
+
ref: str = typer.Argument(..., help="Path to a .brainpatch file, or a 'owner/repo' HF reference."),
|
|
73
|
+
force: bool = typer.Option(False, "--force", help="Replace an already-installed patch."),
|
|
74
|
+
offline: bool = typer.Option(False, "--offline", help="Refuse any network access."),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Install a patch into the local registry (~/.brainpatch).
|
|
77
|
+
|
|
78
|
+
Downloads only the patch artifact -- never the base model.
|
|
79
|
+
"""
|
|
80
|
+
from brainpatch.patch.registry import RegistryError, default_registry
|
|
81
|
+
|
|
82
|
+
registry = default_registry()
|
|
83
|
+
try:
|
|
84
|
+
installed = registry.install(ref, overwrite=force, offline=offline)
|
|
85
|
+
except RegistryError as exc:
|
|
86
|
+
_fail(str(exc))
|
|
87
|
+
except Exception as exc: # noqa: BLE001
|
|
88
|
+
_fail(f"could not install {ref!r}: {exc}")
|
|
89
|
+
|
|
90
|
+
loaded = installed.load()
|
|
91
|
+
manifest = loaded.manifest
|
|
92
|
+
console.print(f"[green]Installed:[/green] [bold]{installed.name}[/bold]")
|
|
93
|
+
console.print(f" model: {manifest.base_model.model_id}")
|
|
94
|
+
console.print(f" size: {installed.size_bytes / 1024:.1f} KB")
|
|
95
|
+
console.print(f" evidence: {_evidence_markup(manifest.evidence_level)}")
|
|
96
|
+
if manifest.evidence_level in {"none", "correlational"}:
|
|
97
|
+
console.print(
|
|
98
|
+
" [yellow]This patch has no validated behavioural effect. "
|
|
99
|
+
"Its name is a label, not a claim.[/yellow]"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def uninstall(name: str = typer.Argument(..., help="Installed patch name.")) -> None:
|
|
105
|
+
"""Remove a patch from the local registry."""
|
|
106
|
+
from brainpatch.patch.registry import RegistryError, default_registry
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
default_registry().uninstall(name)
|
|
110
|
+
except RegistryError as exc:
|
|
111
|
+
_fail(str(exc))
|
|
112
|
+
console.print(f"[green]Uninstalled[/green] {name}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command("list")
|
|
116
|
+
def list_command(
|
|
117
|
+
json_output: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""List installed patches."""
|
|
120
|
+
from brainpatch.patch.registry import default_registry
|
|
121
|
+
|
|
122
|
+
installed = default_registry().list_patches()
|
|
123
|
+
if json_output:
|
|
124
|
+
rows = []
|
|
125
|
+
for item in installed:
|
|
126
|
+
loaded = item.load()
|
|
127
|
+
rows.append({**loaded.describe(), "installed_bytes": item.size_bytes})
|
|
128
|
+
console.print_json(json.dumps(rows))
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if not installed:
|
|
132
|
+
console.print("No patches installed.")
|
|
133
|
+
console.print("\n brainpatch install <file.brainpatch>")
|
|
134
|
+
console.print(" brainpatch install owner/repo")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
table = Table(title="Installed BrainPatches")
|
|
138
|
+
table.add_column("name")
|
|
139
|
+
table.add_column("base model")
|
|
140
|
+
table.add_column("layers", justify="right")
|
|
141
|
+
table.add_column("size", justify="right")
|
|
142
|
+
table.add_column("evidence")
|
|
143
|
+
for item in installed:
|
|
144
|
+
manifest = item.load().manifest
|
|
145
|
+
table.add_row(
|
|
146
|
+
item.name,
|
|
147
|
+
manifest.base_model.model_id,
|
|
148
|
+
",".join(str(layer) for layer in manifest.layers),
|
|
149
|
+
f"{item.size_bytes / 1024:.1f} KB",
|
|
150
|
+
_evidence_markup(manifest.evidence_level),
|
|
151
|
+
)
|
|
152
|
+
console.print(table)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command()
|
|
156
|
+
def inspect(
|
|
157
|
+
patch: str = typer.Argument(..., help="Installed name or path to a .brainpatch file."),
|
|
158
|
+
json_output: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Show everything a patch declares, including what it does not claim."""
|
|
161
|
+
loaded = _load_patch_arg(patch)
|
|
162
|
+
manifest = loaded.manifest
|
|
163
|
+
|
|
164
|
+
if json_output:
|
|
165
|
+
console.print_json(json.dumps(manifest.to_dict()))
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
console.print(
|
|
169
|
+
Panel(
|
|
170
|
+
manifest.description or "[dim]no description[/dim]",
|
|
171
|
+
title=f"[bold]{manifest.name}[/bold] (format {manifest.format_version})",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
table = Table(show_header=False, box=None)
|
|
176
|
+
spec = manifest.base_model
|
|
177
|
+
table.add_row("base model", spec.model_id)
|
|
178
|
+
table.add_row("revision", spec.revision or "[dim]unpinned[/dim]")
|
|
179
|
+
table.add_row("architecture", spec.architecture or "[dim]unknown[/dim]")
|
|
180
|
+
table.add_row("geometry", f"hidden {spec.hidden_size}, {spec.num_layers} layers")
|
|
181
|
+
table.add_row("evidence", _evidence_markup(manifest.evidence_level))
|
|
182
|
+
table.add_row("strength", f"default {manifest.default_strength}, max ±{manifest.max_abs_strength}")
|
|
183
|
+
table.add_row("license", manifest.license)
|
|
184
|
+
table.add_row("size", f"{loaded.archive_bytes / 1024:.1f} KB")
|
|
185
|
+
console.print(table)
|
|
186
|
+
|
|
187
|
+
console.print("\n[bold]interventions[/bold]")
|
|
188
|
+
for item in manifest.interventions:
|
|
189
|
+
tensor = loaded.vectors.get(item.vector)
|
|
190
|
+
dims = tensor.shape[0] if tensor else "?"
|
|
191
|
+
console.print(
|
|
192
|
+
f" L{item.layer:<3} {item.hook:<14} {item.vector:<12} "
|
|
193
|
+
f"coefficient={item.coefficient:+.4f} ({dims}-d {tensor.dtype if tensor else ''})"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if manifest.compatibility:
|
|
197
|
+
console.print("\n[bold]backend compatibility[/bold]")
|
|
198
|
+
for backend, entry in sorted(manifest.compatibility.items()):
|
|
199
|
+
status = str(entry.get("status", "unsupported"))
|
|
200
|
+
colour = {"verified": "green", "experimental": "yellow"}.get(status, "red")
|
|
201
|
+
extra = {k: v for k, v in entry.items() if k != "status"}
|
|
202
|
+
suffix = f" {extra}" if extra else ""
|
|
203
|
+
console.print(f" {backend:<14} [{colour}]{status}[/{colour}]{suffix}")
|
|
204
|
+
else:
|
|
205
|
+
console.print("\n[yellow]No backend compatibility recorded.[/yellow]")
|
|
206
|
+
|
|
207
|
+
if manifest.evaluation:
|
|
208
|
+
console.print("\n[bold]recorded evaluation[/bold]")
|
|
209
|
+
console.print_json(json.dumps(manifest.evaluation))
|
|
210
|
+
else:
|
|
211
|
+
console.print("\n[yellow]No evaluation recorded: this patch has no measured effect.[/yellow]")
|
|
212
|
+
|
|
213
|
+
if manifest.provenance:
|
|
214
|
+
console.print("\n[bold]provenance[/bold] [dim](research metadata; unused at runtime)[/dim]")
|
|
215
|
+
console.print_json(json.dumps(manifest.provenance))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command()
|
|
219
|
+
def validate(
|
|
220
|
+
patch: str = typer.Argument(..., help="Installed name or path to a .brainpatch file."),
|
|
221
|
+
model: Optional[str] = typer.Option(None, "--model", help="Check against a loaded model too."),
|
|
222
|
+
backend: str = typer.Option("auto", "--backend"),
|
|
223
|
+
mode: str = typer.Option("strict", "--mode", help="strict | architecture | unsafe"),
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Validate a patch archive or a directory of patches."""
|
|
226
|
+
target = Path(patch).expanduser()
|
|
227
|
+
if target.is_dir():
|
|
228
|
+
# A directory may hold runtime artifacts, research patches, or both.
|
|
229
|
+
from brainpatch.patch.loader import PatchLoadError
|
|
230
|
+
from brainpatch.patch.loader import load_patch as load_runtime
|
|
231
|
+
from brainpatch.schemas.patch import PatchValidationError
|
|
232
|
+
from brainpatch.schemas.patch_io import load_patch as load_research
|
|
233
|
+
|
|
234
|
+
files = sorted(list(target.glob("*.brainpatch")) + list(target.glob("*.json")))
|
|
235
|
+
if not files:
|
|
236
|
+
console.print(f"[yellow]no patches found in {target}[/yellow]")
|
|
237
|
+
raise typer.Exit()
|
|
238
|
+
failed = 0
|
|
239
|
+
for path in files:
|
|
240
|
+
try:
|
|
241
|
+
if path.suffix == ".brainpatch":
|
|
242
|
+
console.print(f"[green]ok[/green] {path.name}: {load_runtime(path).manifest.summary()}")
|
|
243
|
+
else:
|
|
244
|
+
console.print(f"[green]ok[/green] {path.name}: {load_research(path).summary()}")
|
|
245
|
+
except (PatchLoadError, PatchValidationError, OSError) as exc:
|
|
246
|
+
failed += 1
|
|
247
|
+
console.print(f"[red]invalid[/red] {path.name}: {exc}")
|
|
248
|
+
if failed:
|
|
249
|
+
raise typer.Exit(code=1)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
loaded = _load_patch_arg(patch)
|
|
253
|
+
console.print(f"[green]ok[/green] archive, checksums and manifest are valid")
|
|
254
|
+
console.print(f" {loaded.manifest.summary()}")
|
|
255
|
+
|
|
256
|
+
if model is None:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
from brainpatch.runtime.model import BrainPatchedModel
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
patched = BrainPatchedModel.from_pretrained(model, backend=backend)
|
|
263
|
+
except Exception as exc: # noqa: BLE001
|
|
264
|
+
_fail(f"could not load {model!r}: {exc}")
|
|
265
|
+
report = patched.backend.validate_patch(loaded, mode=mode) # type: ignore[arg-type]
|
|
266
|
+
for warning in report.warnings:
|
|
267
|
+
console.print(f"[yellow]warning[/yellow] {warning}")
|
|
268
|
+
if report.ok:
|
|
269
|
+
console.print(f"[green]ok[/green] compatible with {model} in '{mode}' mode")
|
|
270
|
+
else:
|
|
271
|
+
for error in report.errors:
|
|
272
|
+
console.print(f"[red]incompatible[/red] {error}")
|
|
273
|
+
raise typer.Exit(code=1)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command()
|
|
277
|
+
def verify(
|
|
278
|
+
patch: str = typer.Argument(..., help="Installed name or path to a .brainpatch file."),
|
|
279
|
+
reference: Optional[str] = typer.Option(
|
|
280
|
+
None, "--reference", help="Trusted artifact to compare direction and magnitude against."
|
|
281
|
+
),
|
|
282
|
+
model: Optional[str] = typer.Option(
|
|
283
|
+
None, "--model", help="Load this model to check where the runtime actually intervenes."
|
|
284
|
+
),
|
|
285
|
+
backend: str = typer.Option("auto", "--backend"),
|
|
286
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Check that a patch encodes the intervention it claims to.
|
|
289
|
+
|
|
290
|
+
`validate` answers "is this file well-formed and loadable". `verify` answers
|
|
291
|
+
"does it do what it says". Those are different questions: in a controlled
|
|
292
|
+
corruption study every one of eleven real defects passed schema, checksum,
|
|
293
|
+
shape and model-compatibility checks, and layer/site defects were invisible to
|
|
294
|
+
every file-level check -- only tracing the runtime found them.
|
|
295
|
+
|
|
296
|
+
Checks run in four levels, and each one that cannot run is reported as
|
|
297
|
+
`skipped` rather than omitted:
|
|
298
|
+
|
|
299
|
+
structural always (checksums, schema, shape, declared model)
|
|
300
|
+
numerical needs --reference (signed direction, delta norm, coefficient)
|
|
301
|
+
execution needs --model (layer, site and schedule as the runtime applies them)
|
|
302
|
+
behavioural see docs/verification.md
|
|
303
|
+
"""
|
|
304
|
+
import json as _json
|
|
305
|
+
|
|
306
|
+
from brainpatch.verify import verify_artifact
|
|
307
|
+
|
|
308
|
+
target = Path(patch).expanduser()
|
|
309
|
+
if not target.is_file():
|
|
310
|
+
installed = _load_patch_arg(patch)
|
|
311
|
+
target = Path(installed.path) if getattr(installed, "path", None) else target
|
|
312
|
+
if not target.is_file():
|
|
313
|
+
_fail(f"no artifact found for {patch!r}")
|
|
314
|
+
|
|
315
|
+
loaded_backend = None
|
|
316
|
+
if model is not None:
|
|
317
|
+
from brainpatch.runtime.model import BrainPatchedModel
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
loaded_backend = BrainPatchedModel.from_pretrained(model, backend=backend).backend
|
|
321
|
+
except Exception as exc: # noqa: BLE001
|
|
322
|
+
_fail(f"could not load {model!r}: {exc}")
|
|
323
|
+
|
|
324
|
+
result = verify_artifact(
|
|
325
|
+
target,
|
|
326
|
+
reference=Path(reference).expanduser() if reference else None,
|
|
327
|
+
backend=loaded_backend,
|
|
328
|
+
expect_model=model,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if json_output:
|
|
332
|
+
console.print_json(_json.dumps(result.to_dict()))
|
|
333
|
+
raise typer.Exit(code=0 if result.faithful else 1)
|
|
334
|
+
|
|
335
|
+
colours = {"pass": "green", "fail": "red", "skipped": "yellow"}
|
|
336
|
+
width = max(len(c.name) for c in result.checks)
|
|
337
|
+
current = None
|
|
338
|
+
for check in result.checks:
|
|
339
|
+
if check.level != current:
|
|
340
|
+
current = check.level
|
|
341
|
+
console.print()
|
|
342
|
+
console.print(f"[bold]{current}[/bold]")
|
|
343
|
+
colour = colours[check.status]
|
|
344
|
+
console.print(
|
|
345
|
+
f" {check.name.ljust(width)} [{colour}]{check.status.upper()}[/{colour}] {check.detail}"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
console.print()
|
|
349
|
+
if result.skipped_levels:
|
|
350
|
+
console.print(
|
|
351
|
+
f"[yellow]unverified:[/yellow] {', '.join(result.skipped_levels)} "
|
|
352
|
+
"-- not checked is not the same as checked and fine"
|
|
353
|
+
)
|
|
354
|
+
verdict_colour = "green" if result.faithful else "red"
|
|
355
|
+
console.print(f"[{verdict_colour}]{result.verdict}[/{verdict_colour}]")
|
|
356
|
+
raise typer.Exit(code=0 if result.faithful else 1)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# running
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@app.command()
|
|
365
|
+
def run(
|
|
366
|
+
prompt: str = typer.Argument(..., help="The prompt to complete."),
|
|
367
|
+
model: str = typer.Option(..., "--model", "-m", help="Base model id or GGUF path."),
|
|
368
|
+
patch: list[str] = typer.Option([], "--patch", "-p", help="Patch(es) to apply. Repeatable."),
|
|
369
|
+
backend: str = typer.Option("auto", "--backend", "-b"),
|
|
370
|
+
device: str = typer.Option("auto", "--device"),
|
|
371
|
+
strength: Optional[float] = typer.Option(None, "--strength", "-s"),
|
|
372
|
+
max_new_tokens: int = typer.Option(128, "--max-tokens"),
|
|
373
|
+
temperature: float = typer.Option(0.0, "--temperature"),
|
|
374
|
+
mode: str = typer.Option("strict", "--compatibility", help="strict | architecture | unsafe"),
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Generate a completion with patches applied."""
|
|
377
|
+
model_obj, _ = _load_model_with_patches(
|
|
378
|
+
model, patch, backend, device, strength, mode
|
|
379
|
+
)
|
|
380
|
+
from brainpatch.runtime.base import GenerationConfig
|
|
381
|
+
|
|
382
|
+
cfg = GenerationConfig(max_new_tokens=max_new_tokens, temperature=temperature)
|
|
383
|
+
console.print(model_obj.generate(prompt, cfg))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command()
|
|
387
|
+
def compare(
|
|
388
|
+
model: str = typer.Option(..., "--model", "-m"),
|
|
389
|
+
patch: list[str] = typer.Option(..., "--patch", "-p", help="Patch(es) to apply."),
|
|
390
|
+
prompt: str = typer.Option(..., "--prompt"),
|
|
391
|
+
backend: str = typer.Option("auto", "--backend", "-b"),
|
|
392
|
+
device: str = typer.Option("auto", "--device"),
|
|
393
|
+
strength: Optional[float] = typer.Option(None, "--strength", "-s"),
|
|
394
|
+
max_new_tokens: int = typer.Option(128, "--max-tokens"),
|
|
395
|
+
temperature: float = typer.Option(0.0, "--temperature"),
|
|
396
|
+
mode: str = typer.Option("strict", "--compatibility"),
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Generate the same prompt with the patch off and on, side by side."""
|
|
399
|
+
model_obj, _ = _load_model_with_patches(model, patch, backend, device, strength, mode)
|
|
400
|
+
from brainpatch.runtime.base import GenerationConfig
|
|
401
|
+
|
|
402
|
+
cfg = GenerationConfig(max_new_tokens=max_new_tokens, temperature=temperature)
|
|
403
|
+
result = model_obj.compare(prompt, cfg)
|
|
404
|
+
|
|
405
|
+
console.print(Panel(result["baseline"], title="[bold]BASELINE[/bold]", border_style="dim"))
|
|
406
|
+
console.print(Panel(result["patched"], title="[bold]PATCHED[/bold]", border_style="cyan"))
|
|
407
|
+
if result["baseline"] == result["patched"]:
|
|
408
|
+
console.print(
|
|
409
|
+
"[yellow]Outputs are identical.[/yellow] The patch had no effect at this "
|
|
410
|
+
"strength on this prompt -- try raising --strength, but check the patch's "
|
|
411
|
+
"measured dose-response first."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@app.command()
|
|
416
|
+
def chat(
|
|
417
|
+
model: str = typer.Option(..., "--model", "-m"),
|
|
418
|
+
patch: list[str] = typer.Option([], "--patch", "-p"),
|
|
419
|
+
backend: str = typer.Option("auto", "--backend", "-b"),
|
|
420
|
+
device: str = typer.Option("auto", "--device"),
|
|
421
|
+
strength: Optional[float] = typer.Option(None, "--strength", "-s"),
|
|
422
|
+
max_new_tokens: int = typer.Option(256, "--max-tokens"),
|
|
423
|
+
temperature: float = typer.Option(0.7, "--temperature"),
|
|
424
|
+
mode: str = typer.Option("strict", "--compatibility"),
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Interactive chat. Type /help for in-session commands."""
|
|
427
|
+
model_obj, handles = _load_model_with_patches(model, patch, backend, device, strength, mode)
|
|
428
|
+
from brainpatch.runtime.base import GenerationConfig
|
|
429
|
+
|
|
430
|
+
cfg = GenerationConfig(max_new_tokens=max_new_tokens, temperature=temperature)
|
|
431
|
+
console.print(
|
|
432
|
+
Panel(
|
|
433
|
+
"/patches list patches\n"
|
|
434
|
+
"/strength <name> <value>\n"
|
|
435
|
+
"/on <name> /off <name>\n"
|
|
436
|
+
"/quit",
|
|
437
|
+
title="brainpatch chat",
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
while True:
|
|
441
|
+
try:
|
|
442
|
+
line = console.input("[bold cyan]you[/bold cyan] > ").strip()
|
|
443
|
+
except (EOFError, KeyboardInterrupt):
|
|
444
|
+
console.print()
|
|
445
|
+
break
|
|
446
|
+
if not line:
|
|
447
|
+
continue
|
|
448
|
+
if line.startswith("/"):
|
|
449
|
+
if _handle_chat_command(line, model_obj):
|
|
450
|
+
break
|
|
451
|
+
continue
|
|
452
|
+
console.print("[bold green]model[/bold green] >", end=" ")
|
|
453
|
+
console.print(model_obj.generate(line, cfg))
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _handle_chat_command(line: str, model_obj: Any) -> bool:
|
|
457
|
+
"""Handle a ``/`` command. Returns True to exit the loop."""
|
|
458
|
+
parts = line.split()
|
|
459
|
+
command = parts[0].lower()
|
|
460
|
+
if command in {"/quit", "/exit", "/q"}:
|
|
461
|
+
return True
|
|
462
|
+
if command == "/help":
|
|
463
|
+
console.print("/patches /strength <name> <v> /on <name> /off <name> /quit")
|
|
464
|
+
elif command == "/patches":
|
|
465
|
+
for name in model_obj.list_patches():
|
|
466
|
+
console.print(f" {model_obj.patch(name)!r}")
|
|
467
|
+
elif command == "/strength" and len(parts) == 3:
|
|
468
|
+
try:
|
|
469
|
+
actual = model_obj.set_patch_strength(parts[1], float(parts[2]))
|
|
470
|
+
console.print(f" {parts[1]} strength -> {actual}")
|
|
471
|
+
except (KeyError, ValueError) as exc:
|
|
472
|
+
console.print(f"[red]{exc}[/red]")
|
|
473
|
+
elif command in {"/on", "/off"} and len(parts) == 2:
|
|
474
|
+
try:
|
|
475
|
+
model_obj.enable_patch(parts[1]) if command == "/on" else model_obj.disable_patch(parts[1])
|
|
476
|
+
console.print(f" {parts[1]} {'enabled' if command == '/on' else 'disabled'}")
|
|
477
|
+
except KeyError as exc:
|
|
478
|
+
console.print(f"[red]{exc}[/red]")
|
|
479
|
+
else:
|
|
480
|
+
console.print("[yellow]unknown command; /help for the list[/yellow]")
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@app.command()
|
|
485
|
+
def serve(
|
|
486
|
+
model: str = typer.Option(..., "--model", "-m"),
|
|
487
|
+
patch: list[str] = typer.Option([], "--patch", "-p"),
|
|
488
|
+
backend: str = typer.Option("auto", "--backend", "-b"),
|
|
489
|
+
device: str = typer.Option("auto", "--device"),
|
|
490
|
+
strength: Optional[float] = typer.Option(None, "--strength", "-s"),
|
|
491
|
+
host: str = typer.Option("127.0.0.1", "--host"),
|
|
492
|
+
port: int = typer.Option(8000, "--port"),
|
|
493
|
+
mode: str = typer.Option("strict", "--compatibility"),
|
|
494
|
+
) -> None:
|
|
495
|
+
"""Serve an OpenAI-compatible HTTP API with patches applied."""
|
|
496
|
+
try:
|
|
497
|
+
import uvicorn
|
|
498
|
+
except ModuleNotFoundError:
|
|
499
|
+
_fail("serving needs FastAPI and uvicorn -- pip install 'brainpatch[server]'")
|
|
500
|
+
|
|
501
|
+
from brainpatch.server.app import build_app
|
|
502
|
+
|
|
503
|
+
model_obj, _ = _load_model_with_patches(model, patch, backend, device, strength, mode)
|
|
504
|
+
console.print(f"[green]serving[/green] http://{host}:{port}/v1 (backend: {backend})")
|
|
505
|
+
console.print(f" patches: {', '.join(model_obj.list_patches()) or 'none'}")
|
|
506
|
+
uvicorn.run(build_app(model_obj), host=host, port=port, log_level="info")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@app.command()
|
|
510
|
+
def ui(
|
|
511
|
+
model: Optional[str] = typer.Option(None, "--model", "-m"),
|
|
512
|
+
backend: str = typer.Option("auto", "--backend", "-b"),
|
|
513
|
+
device: str = typer.Option("auto", "--device"),
|
|
514
|
+
host: str = typer.Option("127.0.0.1", "--host"),
|
|
515
|
+
port: int = typer.Option(7860, "--port"),
|
|
516
|
+
share: bool = typer.Option(False, "--share", help="Expose a public Gradio link."),
|
|
517
|
+
) -> None:
|
|
518
|
+
"""Launch the local web UI. Runs entirely on your machine."""
|
|
519
|
+
try:
|
|
520
|
+
from brainpatch.ui.app import launch
|
|
521
|
+
except ModuleNotFoundError:
|
|
522
|
+
_fail("the UI needs Gradio -- pip install 'brainpatch[ui]'")
|
|
523
|
+
launch(model=model, backend=backend, device=device, host=host, port=port, share=share)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
# environment
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@app.command()
|
|
532
|
+
def doctor(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
533
|
+
"""Report which inference engines are installed and usable."""
|
|
534
|
+
from brainpatch.runtime.auto import environment_report
|
|
535
|
+
|
|
536
|
+
report = environment_report()
|
|
537
|
+
if json_output:
|
|
538
|
+
console.print_json(json.dumps(report))
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
console.print(f"[bold]BrainPatch {report['brainpatch_version']}[/bold]")
|
|
542
|
+
console.print(f" Python {report['python']}")
|
|
543
|
+
console.print(f" Platform {report['platform']}")
|
|
544
|
+
console.print(f" Registry {report['registry_home']}")
|
|
545
|
+
console.print(f" Patches {len(report['installed_patches'])} installed")
|
|
546
|
+
console.print()
|
|
547
|
+
|
|
548
|
+
table = Table(title="Backends")
|
|
549
|
+
table.add_column("backend")
|
|
550
|
+
table.add_column("status")
|
|
551
|
+
table.add_column("detail")
|
|
552
|
+
for entry in report["backends"]:
|
|
553
|
+
ok = entry["available"]
|
|
554
|
+
table.add_row(
|
|
555
|
+
entry["backend"],
|
|
556
|
+
"[green]available[/green]" if ok else "[red]unavailable[/red]",
|
|
557
|
+
entry["detail"],
|
|
558
|
+
)
|
|
559
|
+
console.print(table)
|
|
560
|
+
|
|
561
|
+
if not any(e["available"] for e in report["backends"]):
|
|
562
|
+
console.print(
|
|
563
|
+
"\n[yellow]No backend available.[/yellow] Install one:\n"
|
|
564
|
+
" pip install 'brainpatch[transformers]'"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@app.command()
|
|
569
|
+
def backends(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
570
|
+
"""Show the capability matrix for every backend."""
|
|
571
|
+
from brainpatch.runtime.auto import available_backends
|
|
572
|
+
from brainpatch.runtime.capabilities import CAPABILITY_FLAGS
|
|
573
|
+
|
|
574
|
+
statuses = available_backends()
|
|
575
|
+
if json_output:
|
|
576
|
+
console.print_json(json.dumps([s.to_dict() for s in statuses]))
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
table = Table(title="Backend capabilities")
|
|
580
|
+
table.add_column("capability")
|
|
581
|
+
for status in statuses:
|
|
582
|
+
table.add_column(status.name, justify="center")
|
|
583
|
+
|
|
584
|
+
for flag in CAPABILITY_FLAGS:
|
|
585
|
+
row = [flag]
|
|
586
|
+
for status in statuses:
|
|
587
|
+
caps = status.capabilities
|
|
588
|
+
if caps is None:
|
|
589
|
+
row.append("?")
|
|
590
|
+
else:
|
|
591
|
+
row.append("[green]yes[/green]" if caps.supports(flag) else "[dim]no[/dim]")
|
|
592
|
+
table.add_row(*row)
|
|
593
|
+
console.print(table)
|
|
594
|
+
|
|
595
|
+
console.print("\n[dim]Notes on unsupported capabilities:[/dim]")
|
|
596
|
+
for status in statuses:
|
|
597
|
+
if status.capabilities and status.capabilities.notes:
|
|
598
|
+
console.print(f"\n[bold]{status.name}[/bold]")
|
|
599
|
+
for flag, note in status.capabilities.notes.items():
|
|
600
|
+
console.print(f" {flag}: {note}")
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@app.command()
|
|
604
|
+
def benchmark(
|
|
605
|
+
model: str = typer.Option(..., "--model", "-m"),
|
|
606
|
+
patch: list[str] = typer.Option([], "--patch", "-p"),
|
|
607
|
+
backend: str = typer.Option("auto", "--backend", "-b"),
|
|
608
|
+
device: str = typer.Option("auto", "--device"),
|
|
609
|
+
max_new_tokens: int = typer.Option(128, "--max-tokens"),
|
|
610
|
+
runs: int = typer.Option(3, "--runs"),
|
|
611
|
+
prompt: str = typer.Option("Explain how a bicycle works.", "--prompt"),
|
|
612
|
+
) -> None:
|
|
613
|
+
"""Measure patched vs unpatched throughput and patch load time."""
|
|
614
|
+
import time
|
|
615
|
+
|
|
616
|
+
from brainpatch.runtime.base import GenerationConfig
|
|
617
|
+
|
|
618
|
+
model_obj, _ = _load_model_with_patches(model, patch, backend, device, None, "strict")
|
|
619
|
+
cfg = GenerationConfig(max_new_tokens=max_new_tokens)
|
|
620
|
+
|
|
621
|
+
def timed(enabled: bool) -> tuple[float, int]:
|
|
622
|
+
for name in model_obj.list_patches():
|
|
623
|
+
model_obj.backend.set_enabled(name, enabled)
|
|
624
|
+
durations, tokens = [], 0
|
|
625
|
+
for _ in range(runs):
|
|
626
|
+
start = time.perf_counter()
|
|
627
|
+
text = model_obj.generate(prompt, cfg)
|
|
628
|
+
durations.append(time.perf_counter() - start)
|
|
629
|
+
tokens = max(tokens, len(text.split()))
|
|
630
|
+
return sum(durations) / len(durations), tokens
|
|
631
|
+
|
|
632
|
+
base_time, _ = timed(False)
|
|
633
|
+
patch_time, _ = timed(True)
|
|
634
|
+
|
|
635
|
+
table = Table(title=f"Throughput ({runs} runs, {max_new_tokens} max tokens)")
|
|
636
|
+
table.add_column("condition")
|
|
637
|
+
table.add_column("mean seconds", justify="right")
|
|
638
|
+
table.add_column("tokens/sec", justify="right")
|
|
639
|
+
table.add_row("baseline", f"{base_time:.3f}", f"{max_new_tokens / base_time:.1f}")
|
|
640
|
+
table.add_row("patched", f"{patch_time:.3f}", f"{max_new_tokens / patch_time:.1f}")
|
|
641
|
+
console.print(table)
|
|
642
|
+
overhead = (patch_time - base_time) / base_time * 100 if base_time else 0.0
|
|
643
|
+
console.print(f"\noverhead: {overhead:+.1f}%")
|
|
644
|
+
console.print(
|
|
645
|
+
"[dim]Wall-clock over few runs; treat small differences as noise.[/dim]"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# ---------------------------------------------------------------------------
|
|
650
|
+
# compilation
|
|
651
|
+
# ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@app.command()
|
|
655
|
+
def compile(
|
|
656
|
+
source: str = typer.Argument(..., help="Research patch .json, or a .brainpatch to re-export."),
|
|
657
|
+
output: str = typer.Option(..., "--output", "-o"),
|
|
658
|
+
sae: Optional[str] = typer.Option(None, "--sae", help="SAE checkpoint the feature IDs index into."),
|
|
659
|
+
backend: Optional[str] = typer.Option(None, "--backend", help="Export for a backend, e.g. llama.cpp"),
|
|
660
|
+
strength: float = typer.Option(1.0, "--strength", help="Scale baked into a backend export."),
|
|
661
|
+
force: bool = typer.Option(False, "--force"),
|
|
662
|
+
) -> None:
|
|
663
|
+
"""Compile a research patch into a self-contained runtime artifact.
|
|
664
|
+
|
|
665
|
+
Without --backend this produces a portable .brainpatch. With --backend it
|
|
666
|
+
exports a backend-specific representation (currently llama.cpp control
|
|
667
|
+
vectors) from an already-compiled .brainpatch.
|
|
668
|
+
"""
|
|
669
|
+
src = Path(source)
|
|
670
|
+
|
|
671
|
+
if backend:
|
|
672
|
+
normalized = backend.lower().replace(".", "").replace("_", "")
|
|
673
|
+
if normalized not in {"llamacpp", "llama"}:
|
|
674
|
+
_fail(f"no exporter for backend {backend!r}; supported: llama.cpp")
|
|
675
|
+
try:
|
|
676
|
+
from brainpatch.patch.compiler import export_llamacpp_control_vector
|
|
677
|
+
except ModuleNotFoundError as exc:
|
|
678
|
+
_fail(f"export needs extra dependencies: {exc}")
|
|
679
|
+
try:
|
|
680
|
+
written = export_llamacpp_control_vector(src, output, strength=strength)
|
|
681
|
+
except Exception as exc: # noqa: BLE001
|
|
682
|
+
_fail(str(exc))
|
|
683
|
+
console.print(f"[green]exported[/green] {written} ({written.stat().st_size / 1024:.1f} KB)")
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
if sae is None:
|
|
687
|
+
_fail("compiling a research patch needs --sae pointing at its SAE checkpoint")
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
from brainpatch.patch.compiler import compile_from_sae
|
|
691
|
+
from brainpatch.schemas.patch_io import load_patch as load_research_patch
|
|
692
|
+
except ModuleNotFoundError as exc:
|
|
693
|
+
_fail(f"compiling needs the research extra: {exc}")
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
spec = load_research_patch(src)
|
|
697
|
+
written = compile_from_sae(spec, sae, output, overwrite=force)
|
|
698
|
+
except Exception as exc: # noqa: BLE001
|
|
699
|
+
_fail(str(exc))
|
|
700
|
+
|
|
701
|
+
from brainpatch.patch.loader import load_patch, patch_size_report
|
|
702
|
+
|
|
703
|
+
report = patch_size_report(load_patch(written))
|
|
704
|
+
console.print(f"[green]compiled[/green] {written}")
|
|
705
|
+
console.print(f" {report['archive_kb']} KB, {report['num_vectors']} vector(s), "
|
|
706
|
+
f"{report['hidden_size']}-d {report['dtype']}")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ---------------------------------------------------------------------------
|
|
710
|
+
# research subcommands
|
|
711
|
+
# ---------------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
@research_app.command("modal")
|
|
715
|
+
def research_modal(
|
|
716
|
+
function: str = typer.Argument(..., help="Modal function, e.g. gpu_info."),
|
|
717
|
+
extra: Optional[list[str]] = typer.Argument(None),
|
|
718
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
719
|
+
) -> None:
|
|
720
|
+
"""Run a BrainPatch research job on Modal (this repo's research backend)."""
|
|
721
|
+
import shutil
|
|
722
|
+
import subprocess
|
|
723
|
+
|
|
724
|
+
binary = shutil.which("modal")
|
|
725
|
+
if binary is None:
|
|
726
|
+
_fail("the modal CLI is not installed -- pip install 'brainpatch[modal]'")
|
|
727
|
+
args = [binary, "run", f"modal_app/app.py::{function}", *(extra or [])]
|
|
728
|
+
console.print(f"[dim]$ {' '.join(args)}[/dim]")
|
|
729
|
+
if dry_run:
|
|
730
|
+
return
|
|
731
|
+
result = subprocess.run(args, check=False)
|
|
732
|
+
raise typer.Exit(code=result.returncode)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@research_app.command("contrast")
|
|
736
|
+
def research_contrast(
|
|
737
|
+
name: Optional[str] = typer.Argument(None, help="Contrast set name; omit to list."),
|
|
738
|
+
) -> None:
|
|
739
|
+
"""Inspect the synthetic behavioural contrast fixtures."""
|
|
740
|
+
from brainpatch.datasets import list_contrast_sets, load_contrast_set
|
|
741
|
+
|
|
742
|
+
if name is None:
|
|
743
|
+
console.print("available: " + (", ".join(list_contrast_sets()) or "none"))
|
|
744
|
+
return
|
|
745
|
+
try:
|
|
746
|
+
contrast_set = load_contrast_set(name)
|
|
747
|
+
except FileNotFoundError as exc:
|
|
748
|
+
_fail(str(exc))
|
|
749
|
+
console.print(f"[bold]{contrast_set.name}[/bold] -- {len(contrast_set)} examples")
|
|
750
|
+
console.print(contrast_set.description)
|
|
751
|
+
if contrast_set.synthetic:
|
|
752
|
+
console.print("\n[yellow]Synthetic development fixture, not a benchmark.[/yellow]")
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
# helpers
|
|
757
|
+
# ---------------------------------------------------------------------------
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _evidence_markup(level: str) -> str:
|
|
761
|
+
colour = {
|
|
762
|
+
"replicated": "green",
|
|
763
|
+
"controlled_interventional": "green",
|
|
764
|
+
"interventional": "yellow",
|
|
765
|
+
"predictive": "yellow",
|
|
766
|
+
"correlational": "yellow",
|
|
767
|
+
"none": "red",
|
|
768
|
+
}.get(level, "white")
|
|
769
|
+
return f"[{colour}]{level}[/{colour}]"
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _load_patch_arg(ref: str) -> Any:
|
|
773
|
+
from brainpatch.patch.loader import PatchLoadError, load_patch
|
|
774
|
+
from brainpatch.patch.registry import RegistryError, default_registry
|
|
775
|
+
|
|
776
|
+
try:
|
|
777
|
+
path = default_registry().resolve(ref)
|
|
778
|
+
except RegistryError as exc:
|
|
779
|
+
_fail(str(exc))
|
|
780
|
+
try:
|
|
781
|
+
return load_patch(path)
|
|
782
|
+
except PatchLoadError as exc:
|
|
783
|
+
_fail(str(exc))
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _load_model_with_patches(
|
|
787
|
+
model: str,
|
|
788
|
+
patches: list[str],
|
|
789
|
+
backend: str,
|
|
790
|
+
device: str,
|
|
791
|
+
strength: float | None,
|
|
792
|
+
mode: str,
|
|
793
|
+
) -> tuple[Any, list[Any]]:
|
|
794
|
+
from brainpatch.runtime.auto import BackendNotAvailable
|
|
795
|
+
from brainpatch.runtime.model import BrainPatchedModel
|
|
796
|
+
|
|
797
|
+
try:
|
|
798
|
+
model_obj = BrainPatchedModel.from_pretrained(
|
|
799
|
+
model, backend=backend, device=device, compatibility_mode=mode # type: ignore[arg-type]
|
|
800
|
+
)
|
|
801
|
+
except BackendNotAvailable as exc:
|
|
802
|
+
_fail(str(exc))
|
|
803
|
+
except Exception as exc: # noqa: BLE001
|
|
804
|
+
_fail(f"could not load model {model!r}: {exc}")
|
|
805
|
+
|
|
806
|
+
handles = []
|
|
807
|
+
for ref in patches:
|
|
808
|
+
try:
|
|
809
|
+
handles.append(model_obj.install(ref, strength=strength))
|
|
810
|
+
except Exception as exc: # noqa: BLE001
|
|
811
|
+
_fail(f"could not install patch {ref!r}: {exc}")
|
|
812
|
+
return model_obj, handles
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def main() -> None:
|
|
816
|
+
"""Console-script entry point."""
|
|
817
|
+
try:
|
|
818
|
+
app()
|
|
819
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
820
|
+
err_console.print("[yellow]interrupted[/yellow]")
|
|
821
|
+
sys.exit(130)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
if __name__ == "__main__": # pragma: no cover
|
|
825
|
+
main()
|