brainscope 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kateřina Fajmanová
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,380 @@
1
+ Metadata-Version: 2.4
2
+ Name: brainscope
3
+ Version: 0.1.0
4
+ Summary: Watch your model think while your app talks to it
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/moudrkat/brainscope
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastapi
11
+ Requires-Dist: uvicorn[standard]
12
+ Requires-Dist: transformers
13
+ Requires-Dist: torch
14
+ Requires-Dist: accelerate
15
+ Requires-Dist: bitsandbytes
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: httpx; extra == "test"
19
+ Requires-Dist: playwright; extra == "test"
20
+ Dynamic: license-file
21
+
22
+ # brainscope
23
+
24
+ [![CI](https://github.com/moudrkat/brainscope/actions/workflows/ci.yml/badge.svg)](https://github.com/moudrkat/brainscope/actions/workflows/ci.yml)
25
+
26
+ **Watch your model think while your app talks to it.**
27
+
28
+ ## ⚡ Run in 30 s (laptop, no GPU)
29
+
30
+ ```bash
31
+ pip install -e . && brainscope --model tiny # 0.5B model, CPU is fine
32
+ ```
33
+
34
+ Open the page it launches, type in the built-in chat box, and watch each word
35
+ surface through the layers *before* the model writes it. No app to wire up,
36
+ no card required. ([full quickstart ↓](#quickstart))
37
+
38
+ An OpenAI-compatible chat server over any Hugging Face causal LM with a live
39
+ view into the residual stream. What it does:
40
+
41
+ - **Point your own app at it and watch real traffic** — no code changes:
42
+ aim your OpenAI `base_url` at brainscope and every generation your app makes
43
+ streams per-token, per-layer activity into the browser — logit lens,
44
+ attention, and where each word's prediction settled.
45
+ - **Steer behaviour live** — extract a direction from contrast pairs and
46
+ drive it from a slider, per request, or by a tag-matched policy. Speaks
47
+ [hotwire-vllm](https://github.com/moudrkat/hotwire-vllm)'s steering spec
48
+ (`{"id", "layer", "scale", "decode_only"}`, incl. the `vllm_xargs` wire
49
+ format) — **calibrate here, deploy there**: `python -m
50
+ brainscope.export_hotwire` ships vectors with a regime passport, and
51
+ `POST /replay` A/Bs any conversation under a spec, showing per-layer
52
+ cosine and which words the vector suppressed
53
+ ([docs](docs/steering.md)).
54
+ - **Watch words surface before they're written** — a
55
+ [J-lens](#j-lens-reading-ahead-of-the-output) (Jacobian lens, Anthropic
56
+ 2026) readout next to the logit lens: words represented and pushed toward
57
+ future output before — or without — being emitted. Type a word to turn it
58
+ into a steering vector and nudge what the model is disposed to say.
59
+ - **Inspect reasoning traces** — every generation can be persisted, replayed
60
+ token by token, and analyzed: when did the answer emerge inside the
61
+ `<think>` block, and which lens saw it first?
62
+ - **Audit baked personas** — serve a model that carries a baked-in covert
63
+ advocate and the per-layer cosines expose it, token by token
64
+ ([details ↓](#auditing-baked-personas)).
65
+
66
+ ```mermaid
67
+ flowchart LR
68
+ app["Your app<br/>(any OpenAI client)"] <-->|"base_url → /v1"| bs["brainscope<br/>server"]
69
+ bs <--> model["HF model<br/>(transformers)"]
70
+ model -. "residual stream" .-> ui["Browser viz 👁"]
71
+ ```
72
+
73
+ > Built at [Lifeheck](https://www.lifeheck.com/) while evaluating local models for a
74
+ > Czech agentic assistant - thanks to the whole team for the playground. 💛
75
+
76
+ ![brainscope demo](docs/demo.gif)
77
+
78
+ **Docs:** [Steering](docs/steering.md) · [Auditing baked personas](docs/auditing.md) · [J-lens](docs/jlens.md) · [Reasoning traces](docs/traces.md)
79
+
80
+ ## Quickstart
81
+
82
+ ```bash
83
+ git clone https://github.com/moudrkat/brainscope && cd brainscope
84
+ pip install -e . # needs Python 3.11+
85
+ brainscope --model tiny # 0.5B, runs on CPU - good first try
86
+ # → your app: http://<host>:8010/v1 (chat completions, incl. tool calls)
87
+ # → your eyes: http://<host>:8010 (opens automatically)
88
+ ```
89
+
90
+ No app handy? The viz page has a built-in chat box - type and watch.
91
+
92
+ Or skip Python entirely and run the Docker image:
93
+
94
+ ```bash
95
+ docker run -p 127.0.0.1:8010:8010 -v ~/.cache/huggingface:/root/.cache/huggingface \
96
+ ghcr.io/moudrkat/brainscope:cpu
97
+ ```
98
+
99
+ Anything after the image name goes to the brainscope CLI. With an NVIDIA GPU
100
+ (and nvidia-container-toolkit) use the `:cuda` tag and bigger models:
101
+
102
+ ```bash
103
+ docker run --gpus all -p 127.0.0.1:8010:8010 -v ~/.cache/huggingface:/root/.cache/huggingface \
104
+ ghcr.io/moudrkat/brainscope:cuda --model qwen3-4b
105
+ ```
106
+
107
+ The cache mount keeps downloaded model weights on your disk, so they survive
108
+ container restarts. The `127.0.0.1:` binding keeps the port private to your
109
+ machine — brainscope has no auth, and Docker port mappings bypass ufw-style
110
+ firewalls, so only drop it (`-p 8010:8010`) on a network you trust.
111
+
112
+ `--model` takes any Hugging Face model id, plus presets: `tiny`
113
+ (Qwen2.5-0.5B, CPU-friendly), `qwen3-4b`, `qwen3-8b`, `qwen3.5-9b`,
114
+ `gemma-e4b`. Bigger models fit a 16 GB card with `--quantize 8bit`.
115
+ Pointing your app at it is one line - wherever it builds its OpenAI client:
116
+
117
+ ```python
118
+ client = OpenAI(base_url="http://localhost:8010/v1", api_key="unused")
119
+ ```
120
+
121
+ ![The whole instrument: the model architecture on the left, the logit-lens grid open on the right, the generated answer below](docs/img/ui-overview.png)
122
+
123
+ ## What am I looking at?
124
+
125
+ Left: the model itself - the prompt enters at the bottom, one **clickable row
126
+ per decoder layer**, the next word exits at the top (lm_head). On the right,
127
+ the instruments:
128
+
129
+ - **activity over time** - one column per generated token, one row per
130
+ layer, color = how loudly that layer works relative to its own average.
131
+ - **neurons** (click any cell of the heatmap) - the number un-collapsed:
132
+ every residual channel behind that (token, layer) cell as a bipolar bar
133
+ strip, with per-generation z-scores flagging the channels that are unusual
134
+ *now* rather than just always loud. Hover a bar for channel, value and z;
135
+ "follow live" tracks the newest token while generating.
136
+ - **attention** - for the clicked layer: what each answer token looks back
137
+ at; **heads** splits the newest token per attention head.
138
+ - **logit lens** (click lm_head) - every layer's next-token readout: watch
139
+ the answer crystallize with depth. Hover a cell for the top-5 candidates,
140
+ click to pin the tooltip. With `--tlens` it reads through a **tuned lens**
141
+ (Belrose et al. 2023, trained per-layer corrections), which removes the
142
+ raw lens's mid-stack noise — same grid, honest depths.
143
+ - **J-lens** (with `--jlens`) - the same grid, but reading what each layer
144
+ is disposed to make the model say *later* - words visible before they are
145
+ emitted, see [below](#j-lens-reading-ahead-of-the-output).
146
+ - **traces** (with `--traces`) - stored generations: replay any of them with
147
+ a scrubber and chart when the answer emerged inside the think block.
148
+ - **the answer text is an instrument too** - each word is tinted by the
149
+ layer where its prediction settled (clean = early, amber = late, red =
150
+ never before lm_head); hovering shows what the model almost said instead.
151
+
152
+ The ◉ capture button pauses the instruments when you just want fast
153
+ generation; ● record exports a WebM, PNG saves a snapshot. In the header,
154
+ ⏻ switches steering on and off without losing the strength and layer
155
+ settings - instant A/B.
156
+
157
+ An example of what the lens view can catch:
158
+
159
+ ![Logit lens: the meaning decodes mid-stack in English and Chinese; the Czech surface form assembles only in the last few layers](docs/img/lens-concept-before-language.png)
160
+
161
+ *Qwen3-4B writing the Czech word "zážitkům" (experiences). Mid-stack readouts
162
+ decode the meaning - in English and Chinese - while the Czech surface form
163
+ assembles only in the last few layers: the geometry of multilingual
164
+ representations, studied properly in Wendler et al. 2024 (arXiv:2402.10588).
165
+ Readouts are a raw logit lens, so mid-stack tokens are approximate.*
166
+
167
+ ## Tuned lens (trusting the middle of the stack)
168
+
169
+ The raw logit lens reads every layer through the final head, which the model
170
+ never trained the middle layers for — mid-stack readouts are suggestive, not
171
+ reliable. A **tuned lens** (Belrose et al. 2023, arXiv:2303.08112) fixes
172
+ that with one trained affine correction per layer. Fit one with the
173
+ [`tuned-lens`](https://github.com/AlignmentResearch/tuned-lens) package
174
+ (hours, once per model), then point brainscope at it:
175
+
176
+ ```bash
177
+ pip install tuned-lens
178
+ python -m tuned_lens train --model.name Qwen/Qwen3-4B-Instruct-2507 \
179
+ --data.name wikitext --data.config_name wikitext-103-raw-v1 \
180
+ --output lenses/qwen3-4b-tuned
181
+ brainscope --model qwen3-4b --tlens lenses/qwen3-4b-tuned/params.pt
182
+ ```
183
+
184
+ The lens tab relabels itself "tuned lens" and everything downstream — the
185
+ per-word settle depths in the answer text, the recorded trace readouts —
186
+ reads through the translators. One extra `hidden × hidden` matmul per layer
187
+ per token.
188
+
189
+ ## J-lens (reading ahead of the output)
190
+
191
+ The logit lens reads what each layer would say *now*; the **J-lens**
192
+ (Jacobian lens — Anthropic,
193
+ [*A global workspace in language models*](https://www.anthropic.com/research/global-workspace),
194
+ 2026) reads what it is disposed to make the model say **later**: words
195
+ visible in the activations before any of them are emitted — violet cells in
196
+ the grid are words that really arrive later in the answer. Type a word and
197
+ it becomes a steering vector: nudge what the model is disposed to say, with
198
+ the same panel as the readout.
199
+
200
+ ```bash
201
+ brainscope-jlens fit --model qwen3-4b --prompts wikitext --out lenses/qwen3-4b.pt
202
+ brainscope --model qwen3-4b --jlens lenses/qwen3-4b.pt --traces traces/
203
+ ```
204
+
205
+ Fit once per model (minutes on a GPU, reproducible:
206
+ [examples/fit_jlens.sh](examples/fit_jlens.sh)); the readout is cheap and
207
+ switchable live.
208
+
209
+ ![J-lens: while writing "The capital of France is…", violet cells across earlier columns show France, is, Paris — words that really arrive later in the answer](docs/img/jlens-reads-ahead.png)
210
+
211
+ *Qwen3-4B writing "The capital of France is Paris." — violet cells are words
212
+ the J-lens read out before they were emitted: while the model is writing
213
+ " of", the readout already shows " France" and " Paris", the words that
214
+ complete the sentence (the "saw coming" line under each column sums this
215
+ up). Verify any of it against the trace with
216
+ [examples/audit_jlens_hits.py](examples/audit_jlens_hits.py).*
217
+
218
+ > **→ [docs/jlens.md](docs/jlens.md)** - method, fitting, health checks,
219
+ > steering × J-lens, the experimental A-lens, limitations, licensing.
220
+
221
+ ## Reasoning traces
222
+
223
+ With `--traces DIR` every generation is persisted and replayable token by
224
+ token — the `<think>` block segmented out, both lens columns per step, and
225
+ an **answer-emergence chart**: for the token that opens the final answer
226
+ (or any word you click), its probability at every reasoning step, under
227
+ each lens. When did the answer settle, and which lens saw it first?
228
+
229
+ ![Trace replay with the answer-emergence chart: an early green J-lens bump while the amber logit lens is still flat](docs/img/answer-emergence.png)
230
+
231
+ *Replaying a trace, tracking " Paris". The leftmost green bump is the
232
+ point: while the model is still writing " of", the J-lens already reads
233
+ " Paris" at p ≈ 0.4 — the logit lens sits at ~0.0002 there and only spikes
234
+ once the word is actually being written. Later peaks coincide by nature:
235
+ whenever the word is being emitted, both lenses see it.*
236
+
237
+ > **→ [docs/traces.md](docs/traces.md)** - replay, the emergence chart and
238
+ > its honest limits, the API, and storage costs.
239
+
240
+ ## Steering
241
+
242
+ Extract a direction from contrast pairs and drive it live - activation
243
+ addition (Turner et al., arXiv:2308.10248) on real traffic: from a slider,
244
+ per request, or by a tag-matched policy so the app stays steering-agnostic.
245
+ **Steer your app's own requests, not just the built-in chat box** - the
246
+ same per-request `extra_body` scopes a vector to one call and leaves every
247
+ other agent on the server untouched.
248
+
249
+ ```mermaid
250
+ flowchart LR
251
+ pairs["contrast pairs<br/>+ / − completions"] -->|"extract<br/>mean-diff / PCA"| vec["steering vector<br/>+ layer &amp; strength"]
252
+ vec -->|"add mid-stack"| out["shifted<br/>behaviour"]
253
+ ```
254
+
255
+ The best source of vectors is the sister repo
256
+ [hidden-directions](https://github.com/moudrkat/hidden-directions): 40
257
+ pre-verified directions to load and use as-is, plus the pipeline to extract
258
+ more and the references behind the method - often the better place to start
259
+ than a blank `pairs.jsonl` (see [Auditing baked personas](#auditing-baked-personas)).
260
+
261
+ Steering also goes agent-to-agent: the `/capture` endpoint reads one
262
+ agent's residual-stream state so another can be steered by it — that
263
+ experiment lives in [steeropathy](https://github.com/moudrkat/steeropathy)
264
+ (agents steering agents), with brainscope as the window into what it does
265
+ to the model.
266
+
267
+ Extraction quality decides everything and over-steering quietly breaks the
268
+ model, so before steering anything real read the full guide:
269
+
270
+ > **→ [docs/steering.md](docs/steering.md)** - the two extractors, the live
271
+ > API (`/steer`, per-request, policies), a real case study, and the lessons we
272
+ > learned the hard way.
273
+
274
+ ## Auditing baked personas
275
+
276
+ A 9 KB weights patch - one MLP bias - can turn a model into a covert advocate
277
+ (a flat-earther, a sycophant) with **no runtime steering active**. brainscope
278
+ is the live half of that audit: serve the baked model, load the persona
279
+ catalogue from
280
+ [hidden-directions](https://github.com/moudrkat/hidden-directions), and watch
281
+ `v_pref_flat_earth` light up token by token from the baked layer on. The same
282
+ 40-direction catalogue doubles as the pre-verified vector library for
283
+ [steering](#steering).
284
+
285
+ > **→ [docs/auditing.md](docs/auditing.md)** - the `--bake` walkthrough, the
286
+ > dictionary, and stacking vectors to re-create a bake recipe live.
287
+
288
+ ## Will it work with my app?
289
+
290
+ Works when your app talks the **OpenAI chat-completions API** with
291
+ **non-streaming** responses (`stream: true` not supported yet); tool calls
292
+ are parsed in hermes/qwen, gemma-fenced and plain-JSON formats, and
293
+ `tool_choice: "required"` (or a named function) is enforced by seeding the
294
+ generation with the opening of a tool call in the model's own format.
295
+
296
+ Honest limitations: generation runs on plain `transformers` - tens of tokens
297
+ per second, one request at a time, no auth, context bounded by VRAM. Why not
298
+ vLLM? vLLM is a black box by design - per-layer states are consumed the
299
+ moment they're produced; `transformers` exposes them for every architecture
300
+ with one flag. That's the trade: brainscope is slower, but it sees
301
+ everything. It's a lab instrument for development - run it next to
302
+ production, not instead of it.
303
+
304
+ ## Where this sits in the lab
305
+
306
+ ```mermaid
307
+ flowchart LR
308
+ hd["🧭 hidden-directions<br/>behavior → vector"]
309
+ bs(["🧠 brainscope<br/>watch the model think"])
310
+ hw["🔥 hotwire-vllm<br/>steering in production"]
311
+ st["🕹️ steeropathy<br/>agents talk via activations"]
312
+ tm["⚖️ in-two-minds<br/>agent hesitating between tools"]
313
+ sm["🧪 steering-mechanics<br/>how steering actually works"]
314
+
315
+ hd -->|vectors| bs
316
+ hd -->|vector + passport| hw
317
+ bs --> st
318
+ bs --> tm
319
+ bs -->|causal replay| sm
320
+ hw -.->|vector under study| sm
321
+
322
+ click hd "https://github.com/moudrkat/hidden-directions"
323
+ click bs "https://github.com/moudrkat/brainscope"
324
+ click hw "https://github.com/moudrkat/hotwire-vllm"
325
+ click st "https://github.com/moudrkat/steeropathy"
326
+ click tm "https://github.com/moudrkat/in-two-minds"
327
+ click sm "https://github.com/moudrkat/steering-mechanics"
328
+
329
+ classDef dim fill:#f6f8fa,stroke:#d0d7de,color:#57606a;
330
+ classDef here fill:#8957e5,stroke:#6e40c9,color:#ffffff;
331
+ class hd,bs,hw,st,tm,sm dim;
332
+ class bs here;
333
+ ```
334
+
335
+ *Highlighted = this repo. The full lab map (with the two other repos' stories) lives on [moudrkat](https://github.com/moudrkat).*
336
+
337
+ brainscope is the middle of a three-repo stack; each piece also runs alone:
338
+
339
+ - **[hidden-directions](https://github.com/moudrkat/hidden-directions)** —
340
+ the direction catalogue: extract, bake, and audit steering directions,
341
+ per model. Where the vectors come from.
342
+ - **brainscope** *(you are here)* — the instrument: hosts the model,
343
+ captures activations, steers at runtime, reads the J-lens, keeps traces.
344
+ - **[steeropathy](https://github.com/moudrkat/steeropathy)** — the lab on
345
+ top: agents that communicate through activations and J-space instead of
346
+ text, brainscope as their only channel.
347
+
348
+ ## Standing on shoulders
349
+
350
+ The instruments implement or adapt published techniques - the credit belongs
351
+ with the originals:
352
+
353
+ - **Logit lens** - nostalgebraist, *interpreting GPT: the logit lens*
354
+ (LessWrong, 2020); the cleaned-up successor is the tuned lens, Belrose
355
+ et al. (arXiv:2303.08112).
356
+ - **Jacobian lens / J-space** - Gurnee, Sofroniew et al. (Anthropic),
357
+ *Verbalizable Representations Form a Global Workspace in Language Models*
358
+ ([Transformer Circuits Thread, 2026](https://transformer-circuits.pub/2026/workspace/index.html);
359
+ announcement: [*A global workspace in language models*](https://www.anthropic.com/research/global-workspace));
360
+ reference implementation
361
+ [anthropics/jacobian-lens](https://github.com/anthropics/jacobian-lens)
362
+ (Apache-2.0). brainscope's `jlens.py` is an independent reimplementation
363
+ from the paper (full citation + BibTeX in [docs/jlens.md](docs/jlens.md#citing));
364
+ the A-lens ("answer lens") variant is a brainscope experiment on top of
365
+ their estimator, not part of the published work.
366
+ - **Workspace decomposition** - the sparse nonnegative recipe is from the
367
+ same Gurnee et al. paper; the pursuit algorithm family is Blumensath &
368
+ Davies, *Gradient Pursuits* (IEEE Trans. Signal Processing, 2008).
369
+ Experimental — see [docs/jlens.md](docs/jlens.md#workspace-decomposition-j-space-experimental).
370
+ - **Concept-before-language** - Wendler et al., *Do Llamas Work in English?*
371
+ (arXiv:2402.10588).
372
+ - **Activation steering** - Turner et al., Zou et al., Rimsky et al.; cited in
373
+ full in [docs/steering.md](docs/steering.md#references).
374
+ - **Attention aggregation** - the "sources" view averages attention across
375
+ layers; the principled cross-layer flow is Abnar & Zuidema
376
+ (arXiv:2005.00928).
377
+
378
+ ## License
379
+
380
+ MIT © Kateřina Fajmanová