feat(benchmarks): compare sessions with and without semantic search
Add the setup-no-search arm for Claude, generated filler modules, an optional warm index outside the timing, and a searches count per run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
00a44f50b0
commit
e1493d9758
1 file changed
+48
-3
@@ -74,6 +74,9 @@ def claude_turn(prompt, cwd, arm, model, session, timeout):
|
|||||||
'--permission-mode', 'auto', '--allowedTools', *CLAUDE_TOOLS]
|
'--permission-mode', 'auto', '--allowedTools', *CLAUDE_TOOLS]
|
||||||
if arm == 'baseline':
|
if arm == 'baseline':
|
||||||
command += ['--setting-sources', '', '--strict-mcp-config']
|
command += ['--setting-sources', '', '--strict-mcp-config']
|
||||||
|
if arm == 'setup-no-search':
|
||||||
|
command += ['--disallowedTools', 'Skill(semantic-search)', 'Bash(*semantic-retrieval*)', 'PowerShell(*semantic-retrieval*)',
|
||||||
|
'--append-system-prompt', 'The semantic-search skill and the semantic retrieval CLI are unavailable in this session.']
|
||||||
if session:
|
if session:
|
||||||
command += ['--resume', session]
|
command += ['--resume', session]
|
||||||
events, seconds, timed_out = run_jsonl(command, cwd, clean_env(), timeout)
|
events, seconds, timed_out = run_jsonl(command, cwd, clean_env(), timeout)
|
||||||
@@ -120,6 +123,40 @@ def codex_turn(prompt, cwd, arm, model, session, timeout, baseline_home):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RETRIEVAL = Path.home() / '.my-ai-configuration/semantic-retrieval'
|
||||||
|
FILLER_TOPICS = [
|
||||||
|
('statement', 'download', 'Render a statement download for the export queue.'),
|
||||||
|
('trial', 'banner', 'Show the trial banner text in the marketing header.'),
|
||||||
|
('upload', 'progress', 'Track upload progress for the dashboard widget.'),
|
||||||
|
('network', 'status', 'Describe the network status shown in the footer.'),
|
||||||
|
('invoice', 'label', 'Format the invoice label printed on envelopes.'),
|
||||||
|
('retry', 'hint', 'Return the retry hint shown next to a failed form field.'),
|
||||||
|
('cent', 'display', 'Format a cent amount for the price badge.'),
|
||||||
|
('customer', 'greeting', 'Pick the greeting for a customer newsletter.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_filler(work, count):
|
||||||
|
"""Write deterministic distractor modules that share vocabulary with the task prompts but not their logic."""
|
||||||
|
for number in range(count):
|
||||||
|
noun, verb, doc = FILLER_TOPICS[number % len(FILLER_TOPICS)]
|
||||||
|
name = f'{noun}_{verb}_{number:03d}'
|
||||||
|
path = work / 'app' / 'modules' / f'{name}.py'
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
body = [f'def {name}(value, locale="en"):', f' """{doc}"""', ' text = str(value).strip()',
|
||||||
|
f' return f"{{locale}}:{noun}:{verb}:{{text}}"', '']
|
||||||
|
path.write_text('\n'.join(body), encoding='utf-8')
|
||||||
|
(work / 'app' / 'modules' / '__init__.py').write_text('', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def warm_index(work):
|
||||||
|
"""Build the semantic index before the session, as in a repository that was searched before."""
|
||||||
|
server = RETRIEVAL / 'server.py'
|
||||||
|
if server.is_file():
|
||||||
|
subprocess.run([sys.executable, str(server), 'rebuild', '--root', str(work), '--data-dir', str(RETRIEVAL / 'data'),
|
||||||
|
'--model-cache', str(RETRIEVAL / 'model-cache')], capture_output=True, timeout=1800)
|
||||||
|
|
||||||
|
|
||||||
def check(work, task):
|
def check(work, task):
|
||||||
hidden = work / 'bench_hidden'
|
hidden = work / 'bench_hidden'
|
||||||
shutil.rmtree(hidden, ignore_errors=True)
|
shutil.rmtree(hidden, ignore_errors=True)
|
||||||
@@ -152,8 +189,12 @@ def run_one(job, args):
|
|||||||
with tempfile.TemporaryDirectory(prefix=f'ai-bench-{task.name[:2]}-', ignore_cleanup_errors=True) as directory:
|
with tempfile.TemporaryDirectory(prefix=f'ai-bench-{task.name[:2]}-', ignore_cleanup_errors=True) as directory:
|
||||||
work = Path(directory)
|
work = Path(directory)
|
||||||
shutil.copytree(task / 'repo', work, dirs_exist_ok=True)
|
shutil.copytree(task / 'repo', work, dirs_exist_ok=True)
|
||||||
|
if spec.get('generate', {}).get('filler_modules'):
|
||||||
|
generate_filler(work, spec['generate']['filler_modules'])
|
||||||
for git in (['init', '-q'], ['add', '-A'], ['-c', 'user.name=bench', '-c', 'user.email=bench@example.invalid', 'commit', '-qm', 'fixture']):
|
for git in (['init', '-q'], ['add', '-A'], ['-c', 'user.name=bench', '-c', 'user.email=bench@example.invalid', 'commit', '-qm', 'fixture']):
|
||||||
subprocess.run(['git', '-c', 'core.autocrlf=false', *git], cwd=work, check=True, timeout=60)
|
subprocess.run(['git', '-c', 'core.autocrlf=false', *git], cwd=work, check=True, timeout=60)
|
||||||
|
if args.warm_index and arm == 'setup':
|
||||||
|
warm_index(work)
|
||||||
turns, prompt, session = [], spec['prompt'], None
|
turns, prompt, session = [], spec['prompt'], None
|
||||||
for round_number in range(1 + args.rework):
|
for round_number in range(1 + args.rework):
|
||||||
if client == 'claude':
|
if client == 'claude':
|
||||||
@@ -180,6 +221,8 @@ def run_one(job, args):
|
|||||||
'cost_usd': round(sum(turn['cost_usd'] for turn in turns), 4), 'tool_calls': sum(turn['tool_calls'] for turn in turns),
|
'cost_usd': round(sum(turn['cost_usd'] for turn in turns), 4), 'tool_calls': sum(turn['tool_calls'] for turn in turns),
|
||||||
'agents': sum(turn['agents'] for turn in turns), 'skills': [skill for turn in turns for skill in turn['skills']],
|
'agents': sum(turn['agents'] for turn in turns), 'skills': [skill for turn in turns for skill in turn['skills']],
|
||||||
'commands': [command for turn in turns for command in turn['commands']],
|
'commands': [command for turn in turns for command in turn['commands']],
|
||||||
|
'searches': sum('semantic-retrieval' in command for turn in turns for command in turn['commands'])
|
||||||
|
+ sum(skill == 'semantic-search' for turn in turns for skill in turn['skills']),
|
||||||
'timed_out': any(turn['timed_out'] for turn in turns), 'workflow_kind': spec['workflow'].get('kind'),
|
'timed_out': any(turn['timed_out'] for turn in turns), 'workflow_kind': spec['workflow'].get('kind'),
|
||||||
'workflow_violations': workflow_violations(spec['workflow'], turns), 'check_output': '' if passed else output,
|
'workflow_violations': workflow_violations(spec['workflow'], turns), 'check_output': '' if passed else output,
|
||||||
}
|
}
|
||||||
@@ -209,8 +252,10 @@ def summarize(results):
|
|||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0], formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0], formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
|
||||||
parser.add_argument('--clients', nargs='+', choices=('claude', 'codex'), default=['claude', 'codex'])
|
parser.add_argument('--clients', nargs='+', choices=('claude', 'codex'), default=['claude', 'codex'])
|
||||||
parser.add_argument('--arms', nargs='+', choices=('setup', 'baseline'), default=['setup', 'baseline'])
|
parser.add_argument('--arms', nargs='+', choices=('setup', 'baseline', 'setup-no-search'), default=['setup', 'baseline'],
|
||||||
parser.add_argument('--tasks', nargs='+', help='task directory names (default: all)')
|
help='setup-no-search is the setup with the semantic-search skill and CLI blocked (Claude only)')
|
||||||
|
parser.add_argument('--warm-index', action='store_true', help='build the semantic index before setup-arm sessions, outside the timing')
|
||||||
|
parser.add_argument('--tasks', nargs='+', help='task directory names or patterns such as 11-* (default: all)')
|
||||||
parser.add_argument('--repetitions', type=int, default=3)
|
parser.add_argument('--repetitions', type=int, default=3)
|
||||||
parser.add_argument('--first-repetition', type=int, default=1, help='number of the first repetition, to rerun selected repetitions')
|
parser.add_argument('--first-repetition', type=int, default=1, help='number of the first repetition, to rerun selected repetitions')
|
||||||
parser.add_argument('--rework', type=int, default=1, help='rework turns after a failed check')
|
parser.add_argument('--rework', type=int, default=1, help='rework turns after a failed check')
|
||||||
@@ -241,7 +286,7 @@ def main():
|
|||||||
if 'codex' in args.clients and 'baseline' in args.arms and not (args.codex_baseline_home / 'auth.json').is_file():
|
if 'codex' in args.clients and 'baseline' in args.arms and not (args.codex_baseline_home / 'auth.json').is_file():
|
||||||
parser.error(f'Codex baseline needs a login: run CODEX_HOME="{args.codex_baseline_home}" codex login')
|
parser.error(f'Codex baseline needs a login: run CODEX_HOME="{args.codex_baseline_home}" codex login')
|
||||||
jobs = [(client, arm, task, repetition) for repetition in range(args.first_repetition, args.first_repetition + args.repetitions)
|
jobs = [(client, arm, task, repetition) for repetition in range(args.first_repetition, args.first_repetition + args.repetitions)
|
||||||
for task in tasks for client in args.clients for arm in args.arms]
|
for task in tasks for client in args.clients for arm in args.arms if not (client == 'codex' and arm == 'setup-no-search')]
|
||||||
print(f'{len(jobs)} runs -> {args.out}')
|
print(f'{len(jobs)} runs -> {args.out}')
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
for client, arm, task, repetition in jobs:
|
for client, arm, task, repetition in jobs:
|
||||||
|
|||||||
Reference in new issue
Block a user