2"""Generate model completions and bucket by observed truthful vs deceptive behavior."""
4from __future__
import annotations
7from dataclasses
import dataclass
8from pathlib
import Path
9from typing
import Any, Literal
14HONEST_LABELS = frozenset({
"honest",
"truthful",
"true",
"control",
"baseline"})
15DECEPTIVE_LABELS = frozenset({
"deceptive",
"deception",
"lie",
"lying",
"dishonest",
"false"})
16TEXT_KEYS = (
"instruction",
"prompt",
"text",
"content",
"honest",
"deceptive")
18BehaviorLabel = Literal[
"truthful",
"deceptive",
"ambiguous"]
19VALID_CONDITIONING = frozenset({
"behavior",
"prompt"})
21BEHAVIOR_MIN_SCORE = 0.2
23DEFAULT_MAX_NEW_TOKENS = 64
24DEFAULT_GEN_TEMPERATURE = 0.0
27def _row_text(row: dict[str, Any], *keys: str) -> str:
30 if isinstance(val, str)
and val.strip():
36 """Bundled probes were removed. Callers must pass prompts=."""
37 raise FileNotFoundError(
38 "No default deception probes. Pass prompts= with an honest/deceptive JSON/JSONL path."
43 c = (conditioning
or "behavior").strip().lower()
44 if c
not in VALID_CONDITIONING:
45 raise ValueError(f
"Unknown conditioning {conditioning!r}; supported: behavior, prompt")
49@dataclass(frozen=True)
55 deceptive_reference: str
58@dataclass(frozen=True)
65 behavior: BehaviorLabel
67 deceptive_score: float
73 resolved = resolve_prompts_path(p)
75 raise FileNotFoundError(f
"Prompts file not found: {p}")
80def _load_probe_rows(path: str | Path |
None) -> tuple[list[dict[str, Any]], Path]:
82 text = p.read_text(encoding=
"utf-8").strip()
83 if p.suffix ==
".jsonl":
84 rows = [json.loads(line)
for line
in text.splitlines()
if line.strip()]
86 data = json.loads(text)
87 rows = data
if isinstance(data, list)
else [data]
88 dict_rows = [r
for r
in rows
if isinstance(r, dict)]
95 honest_text: str =
"",
96 deceptive_text: str =
"",
107 "deceptive_reference",
108 "reference_deceptive",
112 if not h_ref
and honest_text:
114 if not d_ref
and deceptive_text:
115 d_ref = deceptive_text
120 """Expand one JSONL row into generation probes with classification references."""
121 row_id = str(row.get(
"id")
or row.get(
"probe_id")
or "probe")
122 prompt =
_row_text(row,
"prompt",
"instruction",
"question")
123 honest_text =
_row_text(row,
"honest",
"instruction_honest",
"true",
"control")
124 deceptive_text =
_row_text(row,
"deceptive",
"instruction_deceptive",
"false",
"lie")
125 h_ref, d_ref =
_references_from_row(row, honest_text=honest_text, deceptive_text=deceptive_text)
127 if prompt
and h_ref
and d_ref:
128 intent = str(row.get(
"intent")
or row.get(
"label")
or "").strip().lower()
or None
129 if intent
and intent
not in HONEST_LABELS | DECEPTIVE_LABELS:
133 if honest_text
and deceptive_text
and h_ref
and d_ref:
135 BehaviorProbe(f
"{row_id}_honest", honest_text,
"honest", h_ref, d_ref),
136 BehaviorProbe(f
"{row_id}_deceptive", deceptive_text,
"deceptive", h_ref, d_ref),
139 label = str(row.get(
"label")
or row.get(
"class")
or row.get(
"condition")
or "").strip().lower()
141 ref =
_row_text(row,
"reference",
"answer",
"target")
142 if body
and label
in HONEST_LABELS
and ref:
144 if body
and label
in DECEPTIVE_LABELS
and ref:
152 probes: list[BehaviorProbe] = []
158 f
"No behavior probes in {p}. Each row needs prompt + honest_reference + "
159 "deceptive_reference, or paired honest/deceptive statements, or labeled rows with reference."
163 "prompts_path": str(p),
164 "n_probes": len(probes),
172 honest_reference: str,
173 deceptive_reference: str,
175 min_score: float = BEHAVIOR_MIN_SCORE,
176 margin: float = BEHAVIOR_MARGIN,
177) -> tuple[BehaviorLabel, float, float]:
178 """Classify a completion by which reference it aligns with (keyword overlap)."""
179 resp = (response
or "").strip()
181 return "ambiguous", 0.0, 0.0
183 sh = keyword_overlap_score(resp, honest_reference)
184 sd = keyword_overlap_score(resp, deceptive_reference)
185 if sh >= min_score
and sh > sd + margin:
186 return "truthful", sh, sd
187 if sd >= min_score
and sd > sh + margin:
188 return "deceptive", sh, sd
189 return "ambiguous", sh, sd
195 formatted = _format_prompt(model, prompt)
196 resp = (response
or "").strip()
198 return formatted + resp
203 probes: list[BehaviorProbe],
205 model: Any |
None =
None,
207 checkpoint_path: str | Path |
None =
None,
208 max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
209 temperature: float = DEFAULT_GEN_TEMPERATURE,
210) -> list[BehaviorSample]:
214 owned_model = model
is None
216 model = load_tl_from_checkpoint(model_id, checkpoint_path)
218 samples: list[BehaviorSample] = []
224 max_new_tokens=max_new_tokens,
225 temperature=temperature,
230 probe.honest_reference,
231 probe.deceptive_reference,
235 probe_id=probe.probe_id,
252 samples: list[BehaviorSample],
253) -> tuple[list[str], list[str], dict[str, Any]]:
254 truthful_texts = [s.encode_text
for s
in samples
if s.behavior ==
"truthful"]
255 deceptive_texts = [s.encode_text
for s
in samples
if s.behavior ==
"deceptive"]
256 ambiguous = [s
for s
in samples
if s.behavior ==
"ambiguous"]
258 intent_honest = sum(1
for s
in samples
if s.intent
in HONEST_LABELS)
259 intent_deceptive = sum(1
for s
in samples
if s.intent
in DECEPTIVE_LABELS)
260 behavior_matches_intent = sum(
263 if s.behavior !=
"ambiguous"
265 (s.intent
in HONEST_LABELS
and s.behavior ==
"truthful")
266 or (s.intent
in DECEPTIVE_LABELS
and s.behavior ==
"deceptive")
270 summary: dict[str, Any] = {
271 "n_generated": len(samples),
272 "n_truthful": len(truthful_texts),
273 "n_deceptive": len(deceptive_texts),
274 "n_ambiguous": len(ambiguous),
275 "n_intent_honest": intent_honest,
276 "n_intent_deceptive": intent_deceptive,
277 "intent_behavior_match": behavior_matches_intent,
280 "probe_id": s.probe_id,
282 "behavior": s.behavior,
283 "honest_score": s.honest_score,
284 "deceptive_score": s.deceptive_score,
285 "response_preview": (s.response[:120] +
"…")
if len(s.response) > 120
else s.response,
290 return truthful_texts, deceptive_texts, summary
294 path: str | Path |
None,
296 model: Any |
None =
None,
298 checkpoint_path: str | Path |
None =
None,
299 max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
300 temperature: float = DEFAULT_GEN_TEMPERATURE,
301) -> tuple[list[str], list[str], dict[str, Any]]:
307 checkpoint_path=checkpoint_path,
308 max_new_tokens=max_new_tokens,
309 temperature=temperature,
312 meta = {**probe_meta, **behavior_summary}
313 return truthful, deceptive, meta
str _row_text(dict[str, Any] row, *str keys)
list[BehaviorProbe] expand_row_to_behavior_probes(dict[str, Any] row)
Path _resolve_deception_fixture()
tuple[list[BehaviorProbe], dict[str, Any]] load_behavior_probes(str|Path|None path)
tuple[list[str], list[str], dict[str, Any]] bucket_by_behavior(list[BehaviorSample] samples)
tuple[BehaviorLabel, float, float] classify_behavior(str response, str honest_reference, str deceptive_reference, *, float min_score=BEHAVIOR_MIN_SCORE, float margin=BEHAVIOR_MARGIN)
str normalize_conditioning(str|None conditioning)
tuple[str, str] _references_from_row(dict[str, Any] row, *, str honest_text="", str deceptive_text="")
tuple[list[str], list[str], dict[str, Any]] collect_behavior_encode_texts(str|Path|None path, str model_id, Any|None model=None, *, str|Path|None checkpoint_path=None, int max_new_tokens=DEFAULT_MAX_NEW_TOKENS, float temperature=DEFAULT_GEN_TEMPERATURE)
list[BehaviorSample] generate_behavior_samples(list[BehaviorProbe] probes, str model_id, Any|None model=None, *, str|Path|None checkpoint_path=None, int max_new_tokens=DEFAULT_MAX_NEW_TOKENS, float temperature=DEFAULT_GEN_TEMPERATURE)
Path _resolve_probe_path(str|Path|None path)
tuple[list[dict[str, Any]], Path] _load_probe_rows(str|Path|None path)
str _build_encode_text(Any model, str prompt, str response)