Metadata-Version: 2.4
Name: domainsteer
Version: 0.2.0
Summary: DomainSteer: Contrastive Activation Addition for scientific domains
License-Expression: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: torch>=2.0.0
Requires-Dist: transformers>=4.35.0
Requires-Dist: accelerate>=0.25.0
Requires-Dist: numpy>=1.24.0
Requires-Dist: tqdm>=4.65.0
Provides-Extra: generation
Requires-Dist: anthropic>=0.40.0; extra == "generation"
Requires-Dist: openai>=1.10.0; extra == "generation"
Provides-Extra: eval
Requires-Dist: sentence-transformers>=3.0.0; extra == "eval"
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: ruff>=0.4.0; extra == "dev"
Dynamic: license-file

# Domainsteer

Steer any causal LLM toward **user-specified domain expertise** via calibrated
activation steering — derive a contrastive direction per domain, add it to the
residual stream, and dial the strength.

Contrastive pairs for **144 scientific domains** ship with the package, so
building a steering vector needs no API key — as does the **2,160-item
polysemy-trap benchmark** the vectors are evaluated on.

```bash
pip install -e .
```

```python
from domainsteer import DomainSteerer

steerer = DomainSteerer(
    model_name="meta-llama/Llama-3.1-8B-Instruct",
    domain="Industrial biotechnology",
)
steerer.build(layer=14)
steerer.generate("What is a strain?", alpha=0.25)
steerer.compare("What does a payload deliver??",
                alpha=0.2)
```

Steered, "strain" is a microbial variant; unsteered, it is a pulled muscle. That
is the effect the bundled pairs encode — one domain sense against the everyday
one.

## The bundled pairs

| Model | Domains | Pairs |
|---|---|---|
| `meta-llama/Llama-3.1-8B-Instruct` | 144 | 4,320 |
| `meta-llama/Llama-3.2-3B-Instruct` | 144 | 4,320 |

```python
from domainsteer import list_bundled_models, bundled_manifest, load_bundled_pairs

list_bundled_models()
bundled_manifest("meta-llama/Llama-3.1-8B-Instruct")["contrast"]

# cluster id, directory name, or cluster name all resolve
load_bundled_pairs("meta-llama/Llama-3.1-8B-Instruct", "3106")
load_bundled_pairs("meta-llama/Llama-3.1-8B-Instruct", "Industrial biotechnology")
```

Pairs are **model-specific** — each model wrote both sides of its own, and only
the question stems are shared between the two sets. Ask for the model you are
steering; you get `None` if it ships none, and pairs are then generated through
the API as before.

`DomainSteerer` resolves pairs in this order:

1. cached pairs under `cache_dir/<domain-slug>/pairs.jsonl`
2. the bundled pairs, for a matching (model, domain)
3. API generation via `PairGenerator` (needs `OPENAI_API_KEY`)

Opt out with `DomainSteerer(..., use_bundled_pairs=False)` or regenerate with
`build(force_pairs=True)`. Full details, including the contrast the bundled sets
carry, are in [domainsteer/data/README.md](domainsteer/data/README.md).

## The benchmark

The polysemy-trap exam ships too: **144 domains x 15 items = 2,160** bare
questions whose overloaded term has to be read in the domain sense.

```python
from domainsteer import load_benchmark, iter_benchmark

item = load_benchmark("3106")["items"][0]
item["question"]        # 'What can a fingerprint distinguish?'
item["gold"]            # 'product identity'      <- the domain reading
item["baseline_trap"]   # "a person's identity"   <- where an unsteered model lands
item["gold_responses"]  # one reference answer, the one scored against

sum(len(d["items"]) for d in iter_benchmark())    # 2160
```

`gold_responses` holds **exactly one** frozen reference sentence per item.
Every arm is scored against that same sentence.

## Pipeline

```
domain → bundled (or generated) pairs → ΔH → v → residual add
```

1. **Domain registry** — [domain_clusters.json](domainsteer/data/domain_clusters.json)
   names every steerable domain and subdivides each into **exactly ten
   concepts**.
2. **Contrastive pairs** — both personas answer the same **unnamed questions**.
   Shipped as package data for the two models above.
3. **Vector extraction** — mean-pooled assistant hidden states at each
   middle-third layer become one unit vector per layer (`diff_means` default;
   `rfm` optional).
4. **Steering** — a forward hook adds a norm-relative shift
   `h' = h + α‖h‖v` at one decoder block. Pass a raw `alpha` (typical 0.10–0.40).

`build(layer=…)` is the installed-package path. NLI layer pick and the
`expertise` dial live in [`extras/`](extras/README.md) (`build()` with no
`layer`); they are **not** in the wheel and are **not** the paper
measurement (the paper reports the full layer × strength grid).

## Layout

```
domainsteer/                                the installable package
  clusters.py  pairs.py  em_traps.py        registry, contrastive pairs, trap exam
  extract.py  directions.py  steering.py    hidden states, directions, the hook
  steerer.py  self_gold.py                  DomainSteerer API, self-gold contrast
  data/                                     package data → data/README.md
    domain_clusters.json                      144 domains × 10 concepts
    pairs/<model-slug>/*.jsonl                144 × 30 pairs, per model
    benchmark/*.json                          144 × 15 polysemy traps
extras/                                     not installed (research checkout only)
  run_model_sweep.py                        four-arm layer × strength grid
  calibrate.py  judge.py  llm_judge.py      NLI/pairwise calibration and judges
  benchmark.py  trap.py  self_gold_sim.py   older exams and hybrid scoring
  concepts.py                               concept-file I/O
  iclr2027/                                 ICLR 2027 paper sources
tests/                                      CPU-only tests → tests/README.md
```

## Tests

```bash
pip install -e ".[dev]"
pytest -q
```

CPU-only; nothing here downloads a model. Research scripts, the paper, and
NLI calibration live in [`extras/`](extras/README.md) and are not part of
`pip install`.

## Optional extras

| Extra | For |
|---|---|
| `generation` | Pair and gold-item generation, LLM judge (`anthropic`, `openai`) |
| `eval` | Cosine scoring against reference answers (`sentence-transformers`) |
| `dev` | `pytest`, `ruff` |

## License

MIT — see [LICENSE](LICENSE).
