falsepass 0.3.1__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.
falsepass/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """falsepass — probe RL environment verifiers for false passes and false fails.
2
+
3
+ The verifier is the system under test. falsepass feeds it completions whose
4
+ correctness is known in advance (the reference answer, an empty string, a
5
+ well-formatted wrong answer, ...) and reports every case where the reward
6
+ disagrees with what a correct grader would return. No model rollouts.
7
+ """
8
+
9
+ from falsepass.adapter import Adapter
10
+ from falsepass.model import Completion, Example, Expect, ProbeResult, Score
11
+ from falsepass import hygiene, probes, report # noqa: E402,F401
12
+
13
+ __all__ = ["Adapter", "Completion", "Example", "Expect", "ProbeResult", "Score"]
14
+ __version__ = "0.3.1"
@@ -0,0 +1,29 @@
1
+ """Sums of two numbers, answered in <answer> tags.
2
+
3
+ The grader has one planted defect for trying falsepass: it pays when the reference
4
+ appears anywhere in the answer (`answer in reply`) instead of comparing the two, so a
5
+ reply that lists several numbers is paid whenever one of them is right.
6
+ """
7
+
8
+ import random
9
+
10
+ import verifiers as vf
11
+ from datasets import Dataset
12
+
13
+ PROMPT = "Add the two numbers. Reply with the sum only, inside <answer></answer> tags."
14
+
15
+
16
+ def load_environment(n: int = 200, seed: int = 0) -> vf.Environment:
17
+ rng = random.Random(seed)
18
+ rows = []
19
+ for _ in range(n):
20
+ a, b = rng.randint(10, 999), rng.randint(10, 999)
21
+ rows.append({"question": f"{a} + {b}", "answer": str(a + b)})
22
+ parser = vf.XMLParser(["answer"], answer_field="answer")
23
+
24
+ def correct(completion, answer, **kwargs) -> float:
25
+ reply = parser.parse_answer(completion) or ""
26
+ return 1.0 if answer in reply else 0.0 # planted defect: should be reply.strip() == answer
27
+
28
+ rubric = vf.Rubric(funcs=[correct], weights=[1.0], parser=parser)
29
+ return vf.SingleTurnEnv(dataset=Dataset.from_list(rows), system_prompt=PROMPT, parser=parser, rubric=rubric)
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "falsepass-toy-env"
3
+ version = "0.1.0"
4
+ description = "A tiny arithmetic environment with one planted grading defect, for trying falsepass."
5
+ requires-python = ">=3.11"
6
+ dependencies = ["verifiers>=0.3.1", "datasets>=3.0.0"]
7
+
8
+ [build-system]
9
+ requires = ["hatchling"]
10
+ build-backend = "hatchling.build"
11
+
12
+ [tool.hatch.build]
13
+ include = ["falsepass_toy_env.py"]
falsepass/adapter.py ADDED
@@ -0,0 +1,61 @@
1
+ """Per-environment adapter: the only env-specific code a new audit needs.
2
+
3
+ Subclass `Adapter` in `audits/<env>/adapter.py` and expose it as `ADAPTER`.
4
+ The generic probes only ever talk to the environment through this interface.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Awaitable, Callable
10
+ from typing import TYPE_CHECKING
11
+
12
+ from falsepass.model import Completion, Example, ProbeResult
13
+
14
+ if TYPE_CHECKING:
15
+ from falsepass.scorers import Scorer
16
+
17
+ ExtraProbe = Callable[["Adapter", "Scorer", Example], Awaitable[list[ProbeResult]]]
18
+
19
+
20
+ class Adapter:
21
+ name: str = ""
22
+ max_reward: float = 1.0
23
+ # Reward a known-bad completion may receive before it counts as a false pass.
24
+ # Leave at 0 unless the env grades by similarity and partial credit is by design.
25
+ bad_tolerance: float = 0.0
26
+ has_judge: bool = False # enables P8 (judge injection)
27
+
28
+ # --- loading -----------------------------------------------------------------
29
+ def load(self, n: int, seed: int) -> tuple["Scorer", list[Example]]:
30
+ """Return the scorer and `n` examples, sampled deterministically from `seed`."""
31
+ raise NotImplementedError
32
+
33
+ def hygiene_examples(self, limit: int) -> list[Example]:
34
+ """Examples for the dataset checks (P12); defaults to none."""
35
+ return []
36
+
37
+ # --- how this env wants answers written ----------------------------------------
38
+ def render(self, answer: str, example: Example) -> Completion:
39
+ """Write `answer` the way the env's prompt asks for it."""
40
+ raise NotImplementedError
41
+
42
+ def wrong_answer(self, example: Example) -> str:
43
+ """An answer that is definitely not correct for `example`."""
44
+ raise NotImplementedError
45
+
46
+ def shotgun_answers(self, example: Example) -> list[str]:
47
+ """Candidate answers to spray (P5); must include the correct one."""
48
+ return [self.wrong_answer(example), example.answer]
49
+
50
+ def equivalent_forms(self, example: Example) -> dict[str, Completion]:
51
+ """Semantically identical ways to give the correct answer (P2)."""
52
+ return {}
53
+
54
+ def with_reference(self, example: Example, reference: str) -> Example | None:
55
+ """A copy of `example` whose gold answer is `reference` (for rewardlint's
56
+ corpus). Return None when references cannot be swapped in this env."""
57
+ return None
58
+
59
+ # --- env-specific checks (T1–T5 and anything the static review suggests) --------
60
+ extra_probes: list[ExtraProbe] = [] # run on the first `--few` examples
61
+ per_example_probes: list[ExtraProbe] = [] # cheap ones: run on every sampled example
falsepass/auto.py ADDED
@@ -0,0 +1,394 @@
1
+ """Adapter inference: audit an environment without writing an adapter.
2
+
3
+ A hand-written adapter answers three questions. `AutoAdapter` answers them itself:
4
+
5
+ 1. *How does this env want an answer written?* Candidate formats are collected from the
6
+ env's parser and prompt text, the reference answer is scored in each, and the format
7
+ that the env's own reward code pays best is chosen. The verifier tells us its format.
8
+ 2. *What is a wrong answer?* Another label when answers form a small closed set, a
9
+ shifted value when numeric, otherwise the reference of a different example.
10
+ 3. *What are harmless variations?* Generic ones only: surrounding whitespace, a trailing
11
+ sentence, punctuation and padding inside the answer slot.
12
+
13
+ Works for single-turn environments that carry a reference answer (verifiers v0 rubric
14
+ envs and v1 tasksets). Multi-turn, tool-use and judge-only environments still need a
15
+ hand-written adapter; `load` says so instead of guessing.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import itertools
21
+ import random
22
+ import re
23
+ from collections.abc import Callable
24
+ from typing import Any
25
+
26
+ from falsepass.adapter import Adapter
27
+ from falsepass.model import Example
28
+ from falsepass.scorers import Scorer, V0RolloutScorer, V0RubricScorer, V1TaskScorer
29
+
30
+ REASONING = (
31
+ "Reading the problem carefully, the relevant facts are identified first and each "
32
+ "condition is checked in turn. The intermediate steps are worked through one at a "
33
+ "time, alternatives are compared against the constraints given, and the result is "
34
+ "verified once more before the final answer is stated below."
35
+ )
36
+ MAX_ROWS = 2000
37
+ CLOSED_SET_MAX = 12
38
+ INPUT_WRAPPER_MIN = 200 # characters between <tag> and </tag>
39
+ Format = Callable[[str], str]
40
+
41
+
42
+ def candidate_formats(prompt_text: str, parser: Any = None) -> dict[str, Format]:
43
+ """Ways to write an answer, most specific first. Insertion order is the tie-break."""
44
+ found: dict[str, Format] = {}
45
+ wants_think = "<think>" in prompt_text or "think" in (getattr(parser, "fields", None) or [])
46
+ tags = [t for t in dict.fromkeys(re.findall(r"<([A-Za-z_][\w-]*)>", prompt_text))
47
+ if t != "think" and not wraps_input(prompt_text, t)]
48
+ answer_field = getattr(parser, "answer_field", None)
49
+ if isinstance(answer_field, str) and answer_field not in tags:
50
+ tags.insert(0, answer_field)
51
+
52
+ def add(name: str, body: Format) -> None:
53
+ if wants_think:
54
+ found[f"think+{name}"] = lambda a, body=body: f"<think>\n{REASONING}\n</think>\n{body(a)}"
55
+ found[f"reasoning+{name}"] = lambda a, body=body: f"{REASONING}\n{body(a)}"
56
+ found[name] = body
57
+
58
+ for tag in tags[:4]:
59
+ add(f"<{tag}>", lambda a, tag=tag: f"<{tag}>{a}</{tag}>")
60
+ if "boxed" in prompt_text:
61
+ add("boxed", lambda a: f"\\boxed{{{a}}}")
62
+ if "####" in prompt_text:
63
+ add("####", lambda a: f"#### {a}")
64
+ if re.search(r"\[[A-Za-z]\w*\]", prompt_text): # games that take moves as [move]
65
+ add("[brackets]", lambda a: f"[{a}]")
66
+ for tag in tags[:2]:
67
+ add(f"<{tag}>[brackets]", lambda a, tag=tag: f"<{tag}>[{a}]</{tag}>")
68
+ if "```" in prompt_text:
69
+ add("fenced", lambda a: f"```\n{a}\n```")
70
+ if "ANSWER:" in prompt_text: # Inspect's templates ask for "ANSWER: $ANSWER"
71
+ add("ANSWER:", lambda a: f"ANSWER: {a}")
72
+ # Nothing below was asked for by the prompt: the plain answer goes first, so that it wins a tie.
73
+ found["bare"] = lambda a: a
74
+ add("boxed", lambda a: f"\\boxed{{{a}}}")
75
+ add("Answer:", lambda a: f"Answer: {a}")
76
+ return found
77
+
78
+
79
+ def wraps_input(prompt_text: str, tag: str) -> bool:
80
+ """A tag the prompt uses to enclose material it hands over (a document, a conversation),
81
+ as opposed to one it shows as the place for the answer."""
82
+ enclosed = re.search(rf"<{re.escape(tag)}>(.*?)</{re.escape(tag)}>", prompt_text, re.S)
83
+ return bool(enclosed) and len(enclosed.group(1)) > INPUT_WRAPPER_MIN
84
+
85
+
86
+ def own_markup(answer: str) -> bool:
87
+ """The reference already carries an answer wrapper of its own."""
88
+ return bool(re.search(r"<[A-Za-z_][\w-]*>|\\boxed|####", answer))
89
+
90
+
91
+ def is_number(text: str) -> bool:
92
+ return bool(re.fullmatch(r"-?\$?\d[\d,]*(\.\d+)?", text.strip()))
93
+
94
+
95
+ class ContainerTask(TypeError):
96
+ """A v1 task that needs a container. Raised so the caller does not fall back to v0 loading."""
97
+
98
+
99
+ class AutoAdapter(Adapter):
100
+ def __init__(self, env: dict[str, Any]) -> None:
101
+ self.name = env["id"]
102
+ self.framework = env.get("framework", "verifiers")
103
+ self.task_spec = env.get("task", "")
104
+ self.answers = env.get("answers")
105
+ self.answer_free = False # no reference answers (HUD without an answers file): answer-free probes only
106
+ self.module = env.get("module") or env.get("package") or self.task_spec
107
+ self.load_args = env.get("load_args", {})
108
+ self.multi_turn = False
109
+ self.tool_mode = False # a tool-use environment: played with policies, no answer format to infer
110
+ self.open_ended = False
111
+ self.loader: dict[str, Any] = {}
112
+ self.per_example_probes = [other_task_reference]
113
+ self.format_name, self.format = "bare", (lambda a: a)
114
+ self.inferred: dict[str, Any] = {}
115
+
116
+ # --- loading -------------------------------------------------------------------
117
+ def load(self, n: int, seed: int) -> tuple[Scorer, list[Example]]:
118
+ if self.framework == "inspect":
119
+ return self._load_inspect(n, seed)
120
+ if self.framework == "hud":
121
+ return self._load_hud(n, seed)
122
+ try:
123
+ scorer, self.pool, self.hint = self._load_v1()
124
+ except ContainerTask as needs_box:
125
+ raise RuntimeError(str(needs_box)) from needs_box
126
+ except Exception as v1_error:
127
+ try:
128
+ scorer, self.pool, self.hint = self._load_v0()
129
+ except Exception as v0_error:
130
+ raise RuntimeError(
131
+ f"could not load {self.module!r} as a v1 taskset ({v1_error}) or a v0 environment ({v0_error})"
132
+ ) from v0_error
133
+ self._classify_pool()
134
+ return scorer, random.Random(seed).sample(self.pool, min(n, len(self.pool)))
135
+
136
+ def _classify_pool(self) -> None:
137
+ if not (self.tool_mode or self.answer_free):
138
+ self.pool = [e for e in self.pool if e.answer.strip()]
139
+ if not self.pool:
140
+ raise RuntimeError("no reference answers found: this environment needs a hand-written adapter")
141
+ lengths = sorted(len(e.answer) for e in self.pool)
142
+ # Open-ended tasks (write code, explain, document): the reference is a passage, not a value.
143
+ self.open_ended = lengths[len(lengths) // 2] > 60 or sum("\n" in e.answer for e in self.pool) > len(self.pool) / 2
144
+ self.labels = sorted({e.answer for e in self.pool})
145
+ self.closed = len(self.labels) <= CLOSED_SET_MAX and len(self.pool) > 2 * len(self.labels)
146
+
147
+ def _load_hud(self, n: int, seed: int) -> tuple[Scorer, list[Example]]:
148
+ """A HUD task template: its own grader scores the reply (hud_support.py)."""
149
+ from falsepass import hud_support
150
+
151
+ scorer, self.pool = hud_support.load(self.task_spec, MAX_ROWS, self.answers)
152
+ self.answer_free = not any(e.answer.strip() for e in self.pool)
153
+ self._classify_pool()
154
+ sampled = random.Random(seed).sample(self.pool, min(n, len(self.pool)))
155
+ self.hint = (sampled[0].prompt, None)
156
+ self.loader = {"kind": "hud", "task": self.task_spec, "answers": self.answers}
157
+ return scorer, sampled
158
+
159
+ def _load_inspect(self, n: int, seed: int) -> tuple[Scorer, list[Example]]:
160
+ """An Inspect AI task: its solver chain and scorers grade the completion (inspect_support.py)."""
161
+ from falsepass import inspect_support
162
+
163
+ scorer, self.pool = inspect_support.load(self.task_spec, self.load_args, MAX_ROWS)
164
+ self._classify_pool()
165
+ sampled = random.Random(seed).sample(self.pool, min(n, len(self.pool)))
166
+ for example in sampled: # what the model would see: the task's templates applied
167
+ example.prompt = scorer.rendered_prompt(example)
168
+ self.hint = (sampled[0].prompt, None)
169
+ self.loader = {"kind": "inspect", "task": self.task_spec, "load_args": self.load_args}
170
+ return scorer, sampled
171
+
172
+ def _load_v0(self) -> tuple[Scorer, list[Example], tuple[str, Any]]:
173
+ import verifiers as vf
174
+
175
+ env = vf.load_environment(self.module, **self.load_args)
176
+ if not hasattr(env, "rubric"):
177
+ raise TypeError("not a rubric environment")
178
+ lineage = {cls.__name__ for cls in type(env).__mro__}
179
+ needs_infrastructure = lineage & {"SandboxEnv", "PythonEnv", "CliAgentEnv", "MCPEnv", "BrowserEnv", "HarborEnv", "EnvGroup"}
180
+ if needs_infrastructure:
181
+ raise TypeError(f"{type(env).__name__} uses a sandbox or an outside service ({sorted(needs_infrastructure)}): write an adapter")
182
+ self.multi_turn = "SingleTurnEnv" not in lineage and "MultiTurnEnv" in lineage
183
+ # Tools that run in this process: the answer is given through tool calls, so there is
184
+ # no text format to infer. Such an environment is played with policies (probe T6).
185
+ self.tool_mode = bool(lineage & {"ToolEnv", "StatefulToolEnv"})
186
+ rows = None
187
+ for getter in ("get_eval_dataset", "get_dataset"):
188
+ try:
189
+ rows = getattr(env, getter)()
190
+ if rows is not None and len(rows):
191
+ break
192
+ except Exception:
193
+ continue
194
+ if rows is None:
195
+ raise TypeError("environment exposes no dataset")
196
+ rows = rows.select(range(min(len(rows), MAX_ROWS)))
197
+ pool = [
198
+ Example(f"row-{i}", self._last_user(row["prompt"]), str(row.get("answer") or ""), row)
199
+ for i, row in enumerate(rows)
200
+ ]
201
+ system = getattr(env, "system_prompt", None) or ""
202
+ first = " ".join(str(m.get("content") or "") for m in rows[0]["prompt"])
203
+ scorer = V0RolloutScorer(env) if self.multi_turn else V0RubricScorer(env)
204
+ self.loader = {"kind": "v0-rollout" if self.multi_turn else "v0", "module": self.module,
205
+ "load_args": self.load_args, "getter": getter} # fmt: skip
206
+ return scorer, pool, (f"{system}\n{first}", getattr(env, "parser", None))
207
+
208
+ def _load_v1(self) -> tuple[Scorer, list[Example], tuple[str, Any]]:
209
+ import verifiers.v1 as vf
210
+ from verifiers.v1.utils.loaders import taskset_class
211
+
212
+ taskset = taskset_class(self.module)(vf.taskset_config_type(self.module)(id=self.module, **self.load_args))
213
+ tasks = list(itertools.islice(iter(taskset.load()), MAX_ROWS))
214
+ if tasks and type(tasks[0]).NEEDS_CONTAINER:
215
+ # Graded on what is left in a box, not on text: text probes would all score 0
216
+ # and say nothing. See falsepass/container.py.
217
+ raise ContainerTask(
218
+ f"{type(tasks[0]).__name__} is graded on files left in a container: run `falsepass container "
219
+ "<env>` (needs Docker), or write submissions with falsepass.container.score_submission"
220
+ )
221
+ from falsepass.scorers import scripted_user_turns
222
+
223
+ if tasks and not tasks[0].data.prompt_text and scripted_user_turns(tasks[0].data):
224
+ raise ContainerTask( # not a container, but equally not something to fall back to v0 for
225
+ f"{type(tasks[0]).__name__} has no prompt: its env plays a scripted user over several turns, and the "
226
+ "answer for each turn cannot be inferred. Write an adapter whose render() returns the conversation; "
227
+ "V1TaskScorer opens the trace with the first scripted turn (write an adapter.py with the scripted turns)"
228
+ )
229
+ pool = [
230
+ Example(f"task-{i}", t.data.prompt_text, str(getattr(t.data, "answer", "") or ""), t)
231
+ for i, t in enumerate(tasks)
232
+ ]
233
+ self.loader = {"kind": "v1", "module": self.module, "load_args": self.load_args}
234
+ system = getattr(tasks[0].data, "system_prompt", None) or ""
235
+ return V1TaskScorer(tasks[0]), pool, (f"{system}\n{tasks[0].data.prompt_text}", None)
236
+
237
+ @staticmethod
238
+ def _last_user(messages: Any) -> str:
239
+ if isinstance(messages, str):
240
+ return messages
241
+ users = [m for m in messages if m.get("role") == "user"]
242
+ return str((users or messages)[-1].get("content") or "")
243
+
244
+ def hygiene_examples(self, limit: int) -> list[Example]:
245
+ return self.pool[:limit]
246
+
247
+ # --- format inference ------------------------------------------------------------
248
+ async def prepare(self, scorer: Scorer, examples: list[Example], k: int = 5) -> None:
249
+ """Score the reference in every candidate format and keep the best-paid one."""
250
+ if self.answer_free:
251
+ self.inferred = {
252
+ "validity": "no control: the tasks carry no reference answer (HUD keeps it inside the grader), so only "
253
+ "answer-free probes ran (P3, P4, A1, P10, P11) and the maximum reward is assumed to be 1.0. "
254
+ "Give reference answers with `falsepass init ... --answers file.json` to run every probe",
255
+ "format": "plain reply", "reference_reward_by_format": {}, "examples_tried": 0, "answers": "not available",
256
+ "turns": "single answer, graded by the task template", "note": "",
257
+ } # fmt: skip
258
+ return
259
+ if self.tool_mode:
260
+ self.inferred = {
261
+ "validity": "no control: the reference cannot be replayed without a policy that solves the task, so the "
262
+ "maximum reward is assumed to be 1.0 and only the tool-policy probes (T5, T6) were run",
263
+ "format": "tool calls", "reference_reward_by_format": {}, "examples_tried": 0, "answers": "given through tool calls",
264
+ "turns": "multi-turn with tools: generic policies call the environment's own tools", "note": "",
265
+ } # fmt: skip
266
+ return
267
+ trial = examples[:k]
268
+ table: dict[str, float] = {}
269
+ for name, fmt in candidate_formats(*self.hint).items():
270
+ rewards = [(await scorer.score(ex, fmt(ex.answer))).reward or 0.0 for ex in trial]
271
+ table[name] = sum(rewards) / len(rewards)
272
+ best = max(table, key=lambda name: table[name]) # max() keeps the first of equals
273
+ self.format_name, self.format = best, candidate_formats(*self.hint)[best]
274
+ if table[best] <= 0:
275
+ raise RuntimeError(
276
+ f"no candidate format earned any reward for the reference answer ({table}): "
277
+ "write an adapter with `falsepass init --manual`"
278
+ )
279
+ self.max_reward = round(table[best], 6) if abs(table[best] - 1.0) > 1e-9 else 1.0
280
+ # Control: an audit built on the reference is only meaningful if the reward tells
281
+ # the reference apart from saying nothing.
282
+ empty = [(await scorer.score(ex, "")).reward or 0.0 for ex in trial]
283
+ blank = sum(empty) / len(empty)
284
+ # Separated when saying nothing earns less than half of what the reference earns
285
+ # and the gap is material. (An absolute threshold wrongly rejected environments
286
+ # whose own reference answers score only 0.4.)
287
+ separated = blank < 0.5 * table[best] and table[best] - blank >= 0.2
288
+ if not separated:
289
+ validity = (
290
+ f"inconclusive: the reference scored {table[best]:.3f} and an empty completion "
291
+ f"{blank:.3f}. Either this reward does not depend on the `answer` field "
292
+ "(a game, a simulation, a judge), or it pays regardless of the answer. Automatic mode "
293
+ "cannot tell which; the probe results below are not evidence either way"
294
+ )
295
+ else:
296
+ validity = "ok"
297
+ notes = []
298
+ if abs(table[best] - 1.0) > 1e-9:
299
+ notes.append(f"the reference did not score exactly 1.0; the best format averaged {table[best]:.3f} and is used as the maximum")
300
+ if sum(own_markup(ex.answer) for ex in trial) > len(trial) / 2:
301
+ # Seen in curvebench: wrapping such a reference again, or a wrong value next to it,
302
+ # leaves the grader reading the reference's own tags.
303
+ notes.append("the reference answers carry answer markup of their own, so probes that place a wrong "
304
+ "answer in the format may not present a competing answer: check P5, P6 and P9 rows by hand")
305
+ self.inferred = {
306
+ "validity": validity,
307
+ "format": best,
308
+ "reference_reward_by_format": table,
309
+ "examples_tried": len(trial),
310
+ "answers": "closed set: " + ", ".join(self.labels) if self.closed
311
+ else "open-ended passages" if self.open_ended else "open",
312
+ "turns": "multi-turn: the answer is given at every turn through the env's own rollout loop"
313
+ if self.multi_turn else "single-turn",
314
+ "note": "; ".join(notes),
315
+ }
316
+
317
+ # --- the three adapter questions -----------------------------------------------
318
+ def render(self, answer: str, example: Example) -> str:
319
+ return self.format(answer)
320
+
321
+ def other_reference(self, example: Example) -> str | None:
322
+ """The reference answer of a different task, as far from this one's as the pool allows."""
323
+ answer = example.answer.strip().lower()
324
+ for other in self.pool:
325
+ if other.key != example.key and other.answer.strip().lower() != answer:
326
+ return other.answer
327
+ return None
328
+
329
+ def wrong_answer(self, example: Example) -> str:
330
+ answer = example.answer.strip()
331
+ if self.open_ended:
332
+ # A passage that answers nothing. Another task's reference is probed separately
333
+ # (X1): for open-ended tasks that measures task-independence, not wrongness.
334
+ return "I am not sure how to approach this, so I will not give an answer."
335
+ if self.closed:
336
+ return self.labels[(self.labels.index(example.answer) + 1) % len(self.labels)]
337
+ if is_number(answer):
338
+ value = float(answer.replace(",", "").replace("$", ""))
339
+ return f"{value + 7:g}"
340
+ for other in self.pool:
341
+ if other.answer.strip().lower() != answer.lower():
342
+ return other.answer
343
+ return "none of the above"
344
+
345
+ def shotgun_answers(self, example: Example) -> list[str]:
346
+ if self.closed:
347
+ return list(self.labels)
348
+ if is_number(example.answer):
349
+ value = float(example.answer.replace(",", "").replace("$", ""))
350
+ return [f"{value + d:g}" for d in (-3, -2, -1, 1, 2, 3)] + [example.answer]
351
+ return [self.wrong_answer(example), example.answer]
352
+
353
+ def equivalent_forms(self, example: Example) -> dict[str, str]:
354
+ a, correct = example.answer, self.format(example.answer)
355
+ forms = {
356
+ "surrounding_whitespace": f"\n\n {correct} \n\n",
357
+ "sentence_after": f"{correct}\nThat is my final answer.",
358
+ "crlf_line_endings": correct.replace("\n", "\r\n"),
359
+ "padding_inside_slot": self.format(f" {a} "),
360
+ "trailing_period_inside_slot": self.format(f"{a}."),
361
+ }
362
+ if a.isalpha() and a.swapcase() != a:
363
+ forms["answer_case_swapped"] = self.format(a.swapcase())
364
+ return forms
365
+
366
+ def with_reference(self, example: Example, reference: str) -> Example | None:
367
+ if not is_number(example.answer):
368
+ return None
369
+ if self.framework == "inspect":
370
+ from falsepass import inspect_support
371
+
372
+ return inspect_support.with_reference(example, reference)
373
+ raw = example.raw
374
+ if hasattr(raw, "data"): # v1 task
375
+ data = raw.data.model_copy(update={"answer": reference})
376
+ return Example(example.key, example.prompt, reference, type(raw)(data, raw.config))
377
+ row = dict(raw)
378
+ row["answer"] = reference
379
+ return Example(example.key, example.prompt, reference, row)
380
+
381
+
382
+ async def other_task_reference(adapter: AutoAdapter, scorer: Scorer, ex: Example) -> list:
383
+ """X1 — submit the reference answer of a different task. For value answers this is
384
+ simply a wrong answer. For open-ended tasks it asks a different question: does the
385
+ reward depend on which task was set, or only on what a good answer looks like?"""
386
+ from falsepass.model import Expect
387
+ from falsepass.probes import targeted
388
+
389
+ other = adapter.other_reference(ex)
390
+ if other is None:
391
+ return []
392
+ return await targeted("X1", "task_independence", {
393
+ "other_task_reference": (Expect.ZERO, adapter.format(other)),
394
+ }, adapter, scorer, ex, answer_free=("other_task_reference",)) # fmt: skip