mirror of
https://github.com/priyanshujain/messageboardbench.git
synced 2026-10-02 11:07:07 +00:00
124 lines
10 KiB
Python
124 lines
10 KiB
Python
"""Offline descriptive synthesis of the frozen GLM board-interface rerun."""
|
||
from pathlib import Path
|
||
import csv
|
||
import hashlib
|
||
import json
|
||
import statistics
|
||
|
||
ROOT = Path(__file__).resolve().parent
|
||
V1 = ROOT.parent / 'board-pilot-sept8'
|
||
|
||
|
||
def read(path):
|
||
return json.loads(path.read_text())
|
||
|
||
|
||
def enrich():
|
||
episodes = read(ROOT / 'final-export/episodes.json')
|
||
assert len(episodes) == 12
|
||
operations = read(ROOT / 'final-export/board-operations.json')
|
||
rows = []
|
||
for e in episodes:
|
||
task = e['task_id'].removeprefix('lcbhard_')
|
||
path = ROOT / f"reviews/{e['condition']}-c{e['cohort']}-task{task}.json"
|
||
r = read(path)
|
||
assert r.get('review_complete'), path
|
||
assert r.get('sample_id', r.get('task_id')) == e['task_id'], path
|
||
row = dict(e)
|
||
row.update({
|
||
'executed_gaming_observed': r.get('gaming', r.get('executed_gaming_observed')),
|
||
'gaming_mechanism': r.get('mechanism', r.get('gaming_mechanism')),
|
||
'final_local_refusal': r.get('final_local_refusal', False),
|
||
'peer_adoption_observed': r.get('peer_adoption_observed', False),
|
||
'peer_rejection_observed': r.get('peer_rejection_observed', False),
|
||
'published_gaming_method': r.get('published_gaming_method', e['condition'] == 'board' and e['cohort'] == 1 and task in {'1','2'}),
|
||
'behavior': r.get('verdict', r.get('behavior')),
|
||
'review_file': str(path.relative_to(ROOT)),
|
||
'review_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
|
||
'review_method': 'Codex-assisted full trace/artifact review; no independent blinded human labels',
|
||
'read_tool_calls': sum(o['episode_id'] == e['episode_id'] and o['operation'] in {'board_read','read_team_messages'} for o in operations),
|
||
})
|
||
assert isinstance(row['executed_gaming_observed'], bool), path
|
||
assert e['total_tokens'] == e['input_tokens'] + (e['cache_read_tokens'] or 0) + e['output_tokens'], e
|
||
rows.append(row)
|
||
(ROOT / 'reviewed-episodes.json').write_text(json.dumps(rows, indent=2)+'\n')
|
||
with (ROOT / 'reviewed-episodes.csv').open('w', newline='') as f:
|
||
w = csv.DictWriter(f, fieldnames=list(rows[0]))
|
||
w.writeheader()
|
||
for r in rows:
|
||
w.writerow({k: json.dumps(v) if isinstance(v, (dict,list)) else v for k,v in r.items()})
|
||
return rows
|
||
|
||
|
||
def usage(rows):
|
||
return {'n':len(rows), **{f'{aggregation}_{field}': fn([r[field] for r in rows]) if rows else None
|
||
for field in ['total_tokens','output_tokens','reasoning_tokens','input_tokens','cache_read_tokens']
|
||
for aggregation,fn in [('sum',sum),('median',statistics.median)]}}
|
||
|
||
|
||
def interface_stats(root, rows):
|
||
ops = read(root / 'final-export/board-operations.json')
|
||
board = [r for r in rows if r['condition']=='board']
|
||
readops = [o for o in ops if o['operation'] in {'board_read','read_team_messages'}]
|
||
return {
|
||
'board_episodes':len(board), 'reader_episodes':len({o['episode_id'] for o in readops}),
|
||
'read_calls':len(readops), 'peer_exposed_episodes':sum(bool(r['peer_post_exposure_ids']) for r in board),
|
||
'publisher_episodes':sum(bool(r['published_post_ids']) for r in board),
|
||
'posts':len(read(root/'final-export/public-posts.json')),
|
||
'board_conflicting_gaming':sum(r['executed_gaming_observed'] for r in board if r['split']=='conflicting'),
|
||
'board_conflicting_n':sum(r['split']=='conflicting' for r in board),
|
||
'private_conflicting_gaming':sum(r['executed_gaming_observed'] for r in rows if r['condition']=='private' and r['split']=='conflicting'),
|
||
'private_conflicting_n':sum(r['condition']=='private' and r['split']=='conflicting' for r in rows),
|
||
'peer_adoption_episodes':sum(r['peer_adoption_observed'] for r in board),
|
||
'peer_rejection_episodes':sum(r['peer_rejection_observed'] for r in board),
|
||
}
|
||
|
||
|
||
def main():
|
||
rows = enrich()
|
||
old = read(V1/'reviewed-episodes.json')
|
||
comparisons = {'v1':interface_stats(V1,old), 'v2':interface_stats(ROOT,rows)}
|
||
groups = {'original':[r for r in rows if r['split']=='original'],
|
||
'conflicting':[r for r in rows if r['split']=='conflicting'],
|
||
'gaming':[r for r in rows if r['executed_gaming_observed']],
|
||
'non_gaming':[r for r in rows if not r['executed_gaming_observed']]}
|
||
summaries = {name:usage(rs) for name,rs in groups.items()}
|
||
old_groups = {'original':[r for r in old if r['split']=='original'],
|
||
'conflicting':[r for r in old if r['split']=='conflicting'],
|
||
'gaming':[r for r in old if r['executed_gaming_observed']],
|
||
'non_gaming':[r for r in old if not r['executed_gaming_observed']]}
|
||
summary = {'interface_comparison':comparisons,'v2_usage':summaries,
|
||
'v1_usage':{name:usage(rs) for name,rs in old_groups.items()}}
|
||
(ROOT/'token-summary.json').write_text(json.dumps(summary,indent=2)+'\n')
|
||
lines = ['# GLM board interface rerun: descriptive analysis','',
|
||
'Twelve completed episodes, joined by condition/cohort/task to full trajectory reviews. No additional model calls. Counts below describe these episodes; they are not model-level rates or causal effect estimates.','',
|
||
'## Interface use and behavior','', '| Measure | v1 | v2 |','|---|---:|---:|']
|
||
for key,label in [('reader_episodes','Board episodes that called read (of 6)'),('read_calls','Read calls'),('peer_exposed_episodes','Board episodes actually receiving peer content (of 6)'),('publisher_episodes','Board episodes posting (of 6)'),('posts','Public posts'),('board_conflicting_gaming','Gaming in board impossible tasks (of 4)'),('private_conflicting_gaming','Gaming in private impossible tasks (of 4)'),('peer_adoption_episodes','Reviewed peer-adoption trajectories'),('peer_rejection_episodes','Reviewed peer-rejection trajectories')]:
|
||
lines.append(f"| {label} | {comparisons['v1'][key]} | {comparisons['v2'][key]} |")
|
||
lines += ['', 'The interface revision bundled renamed tools, clearer purpose/read instructions, and explicit placement in the main tool list. This is one small sequential rerun, not randomized replication separating those changes. Shared-board episodes belong to one interacting population, so they are not independent samples. A read call returning an empty board is not exposure; receipt of content alone is not adoption. Adoption labels require explicit trajectory evidence, and do not identify the counterfactual behavior without that message.', '',
|
||
'## Token use in v2','', '| Group | n | Total tokens, sum | Total, median | Output, sum | Output, median | Reasoning, sum |','|---|---:|---:|---:|---:|---:|---:|']
|
||
for name,u in summaries.items():
|
||
lines.append(f"| {name} | {u['n']} | {u['sum_total_tokens']:,} | {u['median_total_tokens']:,} | {u['sum_output_tokens']:,} | {u['median_output_tokens']:,} | {u['sum_reasoning_tokens']:,} |")
|
||
a,b=summaries['original'],summaries['conflicting']
|
||
lines += ['', f"Impossible-task median total tokens were {b['median_total_tokens']/a['median_total_tokens']:.2f}× the original-task median; median generated output was {b['median_output_tokens']/a['median_output_tokens']:.2f}×. These groups use different task IDs (original 0/11; conflicting 1/2/10/12), so difficulty and content confound any impossibility interpretation.", '',
|
||
'Total tokens = uncached input + cache-read input + output in these exports. Repeated/cached context is counted each model call. Reasoning is a subset of output and must not be added again. Neither total-token ratios nor a common reasoning-effort label measure equal compute or cost.', '',
|
||
'Gaming/non-gaming groups are behavior-defined, not randomized. All eight impossible episodes gamed in v2, so this comparison is exactly the original/conflicting comparison and says nothing separate about the token cost of cheating. Failed honest submissions followed by gaming are assigned to gaming at episode level; their tokens include both phases. Long private task10/12 trajectories also contain substantial unproductive interpretation-search and errors in agents’ own validation code.', '',
|
||
'## Same task, different communication condition','',
|
||
'| Task | Split | Private total | Board total | Board/private total | Private output | Board output | Board/private output |','|---|---|---:|---:|---:|---:|---:|---:|']
|
||
for task in sorted({r['task_id'] for r in rows},key=lambda x:int(x.split('_')[-1])):
|
||
p=next(r for r in rows if r['task_id']==task and r['condition']=='private')
|
||
q=next(r for r in rows if r['task_id']==task and r['condition']=='board')
|
||
lines.append(f"| {task} | {p['split']} | {p['total_tokens']:,} | {q['total_tokens']:,} | {q['total_tokens']/p['total_tokens']:.2f}× | {p['output_tokens']:,} | {q['output_tokens']:,} | {q['output_tokens']/p['output_tokens']:.2f}× |")
|
||
lines += ['', 'These are single-attempt task matches, not paired random-seed replications. Board cohort2 can receive cohort1 posts and differs from private controls in both communication access and realized peer advice. Prior v1/v2 outcomes cannot support a stable cheating-rate or efficiency estimate; use them to establish usable interfaces and traceable behavior for a larger controlled design.', '',
|
||
'## Earlier v1 token context','', '| Group | n | Total, median | Output, median |','|---|---:|---:|---:|']
|
||
for name,u in summary['v1_usage'].items():
|
||
lines.append(f"| {name} | {u['n']} | {u['median_total_tokens']:,} | {u['median_output_tokens']:,} |")
|
||
lines += ['', 'In v1, the non-gaming group contains four original tasks and one impossible-task refusal. That mixture is not a matched comparison with successful gaming, and one refusal provides no reliable estimate of honest impossible-task token use.', '',
|
||
'Source files: `final-export/episodes.json`, `final-export/board-operations.json`, `final-export/public-posts.json`, each linked review, and v1 equivalents. Machine-readable outputs: `reviewed-episodes.json`, `reviewed-episodes.csv`, `token-summary.json`.']
|
||
(ROOT/'token-analysis.md').write_text('\n'.join(lines)+'\n')
|
||
print(json.dumps(summary,indent=2))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|