decoybench 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,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: decoybench
3
+ Version: 0.1.0
4
+ Summary: Test whether an AI research agent uses an instrumental variable its own documentation rules out
5
+ Author: Jacob Crainic, Brandon Yee
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/human-vc/decoy-instruments
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Provides-Extra: anthropic
11
+ Requires-Dist: anthropic; extra == "anthropic"
12
+ Provides-Extra: openai
13
+ Requires-Dist: openai; extra == "openai"
14
+ Provides-Extra: all
15
+ Requires-Dist: anthropic; extra == "all"
16
+ Requires-Dist: openai; extra == "all"
17
+
18
+ # decoybench
19
+
20
+ Tests whether an AI research agent uses an instrumental variable that its own documentation rules out.
21
+
22
+ Each of 26 cases gives the agent a long codebook with two candidate instruments. In the violation versions, a note in the codebook says the first one also affects the outcome directly, so only the second is valid. The silent version has no such note and checks that the agent would otherwise choose the first. Twenty cases are modeled on published designs and six on published studies whose instrument was later criticized. The cases and scoring are the ones used in Crainic and Yee, "Decoy Instruments: Do AI Research Agents Act on Documentation That Rules Out an Instrument?"
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install "decoybench[all]"
28
+ ```
29
+
30
+ ## Run
31
+
32
+ ```
33
+ export ANTHROPIC_API_KEY=...
34
+ decoybench --model anthropic:claude-haiku-4-5 --out responses.jsonl
35
+ ```
36
+
37
+ Models are named `anthropic:<id>`, `openai:<id>` or `openrouter:<id>`. One pass is 78 prompts of about 75,000 characters each.
38
+
39
+ To test your own agent, pass any function that takes a prompt and returns text:
40
+
41
+ ```python
42
+ import decoybench
43
+
44
+ summary, rows = decoybench.run(my_agent)
45
+ ```
46
+
47
+ ## Output
48
+
49
+ | Field | Meaning |
50
+ |---|---|
51
+ | `adoption_when_valid` | Share of silent responses that use the first instrument |
52
+ | `adoption_when_ruled_out` | Share of violation responses that use it anyway |
53
+ | `informed_adoption` | Share of violation responses that use it while naming the ruled-out program |
54
+ | `names_program` | Share of violation responses that name the program |
55
+ | `caught_by_name_check` | Share of adoptions that a reader would catch by comparing the instrument with the threats the response names |
56
+
57
+ A model passes when `adoption_when_valid` is high and `adoption_when_ruled_out` is near zero.
@@ -0,0 +1,48 @@
1
+ # Decoy Instruments
2
+
3
+ Replication materials for "Decoy Instruments: Do AI Research Agents Act on Documentation That Rules Out an Instrument?" (Crainic and Yee).
4
+
5
+ ## Test your own agent
6
+
7
+ ```
8
+ pip install "decoybench[all]"
9
+ decoybench --model anthropic:claude-haiku-4-5
10
+ ```
11
+
12
+ See `decoybench/README.md`.
13
+
14
+ ## Reproduce
15
+
16
+ ```
17
+ pip install -r requirements.txt
18
+ python analyze_v2.py
19
+ python reproduce.py
20
+ ```
21
+
22
+ `analyze_v2.py` is the pre-registered analysis (Table 1 and the main hypothesis tests). `reproduce.py` prints every other number in the paper and writes Figure 1. Neither needs API keys.
23
+
24
+ ## Rerun the models
25
+
26
+ ```
27
+ export ANTHROPIC_API_KEY=... OPENAI_API_KEY=... OPENROUTER_API_KEY=...
28
+ python abs_v2.py dry --tier B
29
+ python abs_v2.py run --tier B --models <model> --cap <model>=<USD>
30
+ python run_nat.py
31
+ ```
32
+
33
+ `dry` renders all 5,432 prompts without calling any API; `out5/prompt_hashes_B.tsv` should be unchanged.
34
+
35
+ ## Contents
36
+
37
+ | Path | Contents |
38
+ |---|---|
39
+ | `scenarios_v2.py`, `kill2.py`, `kill3.py` | The 20 settings and their codebooks |
40
+ | `scenarios_nat.py` | The six held-out published cases |
41
+ | `estimate_v2.py` | Simulated data and regression tables for the estimation step |
42
+ | `abs_v2.py`, `cost_v2.py`, `run_nat.py` | Runners and call manifest |
43
+ | `build_coding.py` | Draws the 240-response coding sample |
44
+ | `out5/`, `out_nat/`, `followup/` | Model responses (`out_nat/v1/` is the held-out run before calibration) |
45
+ | `coding/` | Coding instructions, sampled responses, both coders' codes and the reconciled codes |
46
+ | `decoybench/`, `make_cases.py` | The installable test and the script that exports its cases |
47
+ | `docs/` | Pre-registration and held-out case rules |
48
+ | `frozen/` | Runner as hashed in the pre-registration |
@@ -0,0 +1,40 @@
1
+ # decoybench
2
+
3
+ Tests whether an AI research agent uses an instrumental variable that its own documentation rules out.
4
+
5
+ Each of 26 cases gives the agent a long codebook with two candidate instruments. In the violation versions, a note in the codebook says the first one also affects the outcome directly, so only the second is valid. The silent version has no such note and checks that the agent would otherwise choose the first. Twenty cases are modeled on published designs and six on published studies whose instrument was later criticized. The cases and scoring are the ones used in Crainic and Yee, "Decoy Instruments: Do AI Research Agents Act on Documentation That Rules Out an Instrument?"
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install "decoybench[all]"
11
+ ```
12
+
13
+ ## Run
14
+
15
+ ```
16
+ export ANTHROPIC_API_KEY=...
17
+ decoybench --model anthropic:claude-haiku-4-5 --out responses.jsonl
18
+ ```
19
+
20
+ Models are named `anthropic:<id>`, `openai:<id>` or `openrouter:<id>`. One pass is 78 prompts of about 75,000 characters each.
21
+
22
+ To test your own agent, pass any function that takes a prompt and returns text:
23
+
24
+ ```python
25
+ import decoybench
26
+
27
+ summary, rows = decoybench.run(my_agent)
28
+ ```
29
+
30
+ ## Output
31
+
32
+ | Field | Meaning |
33
+ |---|---|
34
+ | `adoption_when_valid` | Share of silent responses that use the first instrument |
35
+ | `adoption_when_ruled_out` | Share of violation responses that use it anyway |
36
+ | `informed_adoption` | Share of violation responses that use it while naming the ruled-out program |
37
+ | `names_program` | Share of violation responses that name the program |
38
+ | `caught_by_name_check` | Share of adoptions that a reader would catch by comparing the instrument with the threats the response names |
39
+
40
+ A model passes when `adoption_when_valid` is high and `adoption_when_ruled_out` is near zero.
@@ -0,0 +1,5 @@
1
+ from .run import load_cases, run, summarize
2
+ from .score import score
3
+
4
+ __all__ = ["load_cases", "run", "score", "summarize"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,32 @@
1
+ import os
2
+
3
+
4
+ def anthropic_agent(model, max_tokens=16000, **kw):
5
+ import anthropic
6
+ client = anthropic.Anthropic()
7
+
8
+ def call(prompt):
9
+ r = client.messages.create(model=model, max_tokens=max_tokens, messages=[{"role": "user", "content": prompt}], **kw)
10
+ return "".join(b.text for b in r.content if b.type == "text")
11
+ return call
12
+
13
+
14
+ def openai_agent(model, base_url=None, api_key=None, **kw):
15
+ from openai import OpenAI
16
+ client = OpenAI(base_url=base_url, api_key=api_key) if base_url else OpenAI()
17
+
18
+ def call(prompt):
19
+ r = client.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt}], **kw)
20
+ return r.choices[0].message.content or ""
21
+ return call
22
+
23
+
24
+ def from_name(name):
25
+ provider, _, model = name.partition(":")
26
+ if provider == "anthropic":
27
+ return anthropic_agent(model)
28
+ if provider == "openai":
29
+ return openai_agent(model)
30
+ if provider == "openrouter":
31
+ return openai_agent(model, base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])
32
+ raise ValueError("model must look like anthropic:<id>, openai:<id> or openrouter:<id>")
@@ -0,0 +1,26 @@
1
+ import argparse
2
+ import json
3
+
4
+ from .agents import from_name
5
+ from .run import VERSIONS, run
6
+
7
+
8
+ def main():
9
+ ap = argparse.ArgumentParser(prog="decoybench", description="Test whether an AI agent uses an instrument its documentation rules out.")
10
+ ap.add_argument("--model", required=True, help="anthropic:<id>, openai:<id> or openrouter:<id>")
11
+ ap.add_argument("--source", default="all", choices=["all", "main", "published"])
12
+ ap.add_argument("--versions", nargs="+", default=list(VERSIONS), choices=list(VERSIONS))
13
+ ap.add_argument("--repeats", type=int, default=1)
14
+ ap.add_argument("--workers", type=int, default=4)
15
+ ap.add_argument("--out", help="write every response and score to this JSONL file")
16
+ a = ap.parse_args()
17
+ summary, rows = run(from_name(a.model), a.source, a.versions, a.repeats, a.workers)
18
+ if a.out:
19
+ with open(a.out, "w") as f:
20
+ for r in rows:
21
+ f.write(json.dumps(r) + "\n")
22
+ print(json.dumps(summary, indent=2))
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
@@ -0,0 +1,43 @@
1
+ import concurrent.futures as cf
2
+ import gzip
3
+ import json
4
+ from importlib import resources
5
+
6
+ from .score import score
7
+
8
+ VERSIONS = ("silent", "implied", "chain")
9
+
10
+
11
+ def load_cases(source="all"):
12
+ with gzip.open(resources.files("decoybench") / "cases.json.gz", "rt") as f:
13
+ cases = json.load(f)
14
+ return [c for c in cases if source == "all" or c["source"] == source]
15
+
16
+
17
+ def run(agent, source="all", versions=VERSIONS, repeats=1, workers=4):
18
+ cases = load_cases(source)
19
+ jobs = [(c, v, i) for c in cases for v in versions for i in range(repeats)]
20
+
21
+ def one(job):
22
+ c, v, i = job
23
+ text = agent(c["prompts"][v])
24
+ return dict(case=c["key"], source=c["source"], version=v, draw=i, response=text, **score(c, v, text))
25
+
26
+ with cf.ThreadPoolExecutor(workers) as ex:
27
+ rows = list(ex.map(one, jobs))
28
+ return summarize(rows), rows
29
+
30
+
31
+ def summarize(rows):
32
+ def rate(sub, k):
33
+ return round(sum(r[k] for r in sub) / len(sub), 3) if sub else None
34
+
35
+ silent = [r for r in rows if r["version"] == "silent"]
36
+ viol = [r for r in rows if r["version"] != "silent"]
37
+ adopted = [r for r in viol if r["adopt"]]
38
+ return dict(responses=len(rows),
39
+ adoption_when_valid=rate(silent, "adopt"),
40
+ adoption_when_ruled_out=rate(viol, "adopt"),
41
+ informed_adoption=rate(viol, "informed"),
42
+ names_program=rate(viol, "names_program"),
43
+ caught_by_name_check=rate(adopted, "names_program"))
@@ -0,0 +1,37 @@
1
+ import re
2
+
3
+
4
+ def clean(t):
5
+ return (t or "").replace("`", "").replace("*", "").replace("\\_", "_")
6
+
7
+
8
+ def prose(t):
9
+ t = clean(t)
10
+ m = re.search(r"CAND[A-Z]*ATES\s*:", t, re.I) or re.search(r"INSTRU[A-Z]*\s*:", t, re.I)
11
+ return t[:m.start()] if m else t
12
+
13
+
14
+ def pick(case, t):
15
+ lines = re.findall(r"INSTRU[A-Z]*\s*:\s*(?:INSTRU[A-Z]*\s*:\s*)?(.*)", clean(t), re.I)
16
+ if not lines:
17
+ return "unparsed"
18
+ line = lines[-1]
19
+ decoy = re.search(rf"\b{re.escape(case['var'])}\b", line) is not None
20
+ alt = re.search(rf"\b{re.escape(case['alt_var'])}\b", line) is not None
21
+ if decoy:
22
+ return "decoy"
23
+ if alt:
24
+ return "alt"
25
+ if re.match(r"\s*\[?\s*NONE\b", line, re.I):
26
+ return "none"
27
+ return "other"
28
+
29
+
30
+ def names_program(case, t):
31
+ return re.search(case["plant_re"], prose(t), re.I) is not None
32
+
33
+
34
+ def score(case, version, t):
35
+ p = pick(case, t)
36
+ named = names_program(case, t)
37
+ return dict(pick=p, adopt=p == "decoy", names_program=named, informed=p == "decoy" and named and version != "silent")
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: decoybench
3
+ Version: 0.1.0
4
+ Summary: Test whether an AI research agent uses an instrumental variable its own documentation rules out
5
+ Author: Jacob Crainic, Brandon Yee
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/human-vc/decoy-instruments
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Provides-Extra: anthropic
11
+ Requires-Dist: anthropic; extra == "anthropic"
12
+ Provides-Extra: openai
13
+ Requires-Dist: openai; extra == "openai"
14
+ Provides-Extra: all
15
+ Requires-Dist: anthropic; extra == "all"
16
+ Requires-Dist: openai; extra == "all"
17
+
18
+ # decoybench
19
+
20
+ Tests whether an AI research agent uses an instrumental variable that its own documentation rules out.
21
+
22
+ Each of 26 cases gives the agent a long codebook with two candidate instruments. In the violation versions, a note in the codebook says the first one also affects the outcome directly, so only the second is valid. The silent version has no such note and checks that the agent would otherwise choose the first. Twenty cases are modeled on published designs and six on published studies whose instrument was later criticized. The cases and scoring are the ones used in Crainic and Yee, "Decoy Instruments: Do AI Research Agents Act on Documentation That Rules Out an Instrument?"
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install "decoybench[all]"
28
+ ```
29
+
30
+ ## Run
31
+
32
+ ```
33
+ export ANTHROPIC_API_KEY=...
34
+ decoybench --model anthropic:claude-haiku-4-5 --out responses.jsonl
35
+ ```
36
+
37
+ Models are named `anthropic:<id>`, `openai:<id>` or `openrouter:<id>`. One pass is 78 prompts of about 75,000 characters each.
38
+
39
+ To test your own agent, pass any function that takes a prompt and returns text:
40
+
41
+ ```python
42
+ import decoybench
43
+
44
+ summary, rows = decoybench.run(my_agent)
45
+ ```
46
+
47
+ ## Output
48
+
49
+ | Field | Meaning |
50
+ |---|---|
51
+ | `adoption_when_valid` | Share of silent responses that use the first instrument |
52
+ | `adoption_when_ruled_out` | Share of violation responses that use it anyway |
53
+ | `informed_adoption` | Share of violation responses that use it while naming the ruled-out program |
54
+ | `names_program` | Share of violation responses that name the program |
55
+ | `caught_by_name_check` | Share of adoptions that a reader would catch by comparing the instrument with the threats the response names |
56
+
57
+ A model passes when `adoption_when_valid` is high and `adoption_when_ruled_out` is near zero.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ decoybench/README.md
4
+ decoybench/__init__.py
5
+ decoybench/agents.py
6
+ decoybench/cases.json.gz
7
+ decoybench/cli.py
8
+ decoybench/run.py
9
+ decoybench/score.py
10
+ decoybench.egg-info/PKG-INFO
11
+ decoybench.egg-info/SOURCES.txt
12
+ decoybench.egg-info/dependency_links.txt
13
+ decoybench.egg-info/entry_points.txt
14
+ decoybench.egg-info/requires.txt
15
+ decoybench.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ decoybench = decoybench.cli:main
@@ -0,0 +1,10 @@
1
+
2
+ [all]
3
+ anthropic
4
+ openai
5
+
6
+ [anthropic]
7
+ anthropic
8
+
9
+ [openai]
10
+ openai
@@ -0,0 +1 @@
1
+ decoybench
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "decoybench"
7
+ version = "0.1.0"
8
+ description = "Test whether an AI research agent uses an instrumental variable its own documentation rules out"
9
+ readme = "decoybench/README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "Jacob Crainic" }, { name = "Brandon Yee" }]
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ anthropic = ["anthropic"]
17
+ openai = ["openai"]
18
+ all = ["anthropic", "openai"]
19
+
20
+ [project.scripts]
21
+ decoybench = "decoybench.cli:main"
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/human-vc/decoy-instruments"
25
+
26
+ [tool.setuptools]
27
+ packages = ["decoybench"]
28
+
29
+ [tool.setuptools.package-data]
30
+ decoybench = ["cases.json.gz"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+