mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
feat: multi-agent eval pipeline with tree search, analyst, diversifier
UCB tree search for prompt optimization (arXiv:2603.18620): - EvolutionNode dataclass, UCB1 selection, rolling mean scores - Replaces fragile linear chain with backtracking via tree - Holdout set separation prevents optimizer overfitting - Improvement-based delta feedback to optimizer Multi-agent optimization pipeline: - Analyst agent (phase 1): multi-turn with math/bash tools, identifies semantic failure patterns, computes statistics across test results - Optimizer (phase 2): uses analyst diagnosis to edit developer prompt - Observer: tunes optimizer strategy every 3 iterations - Diversifier: generates paraphrased prompt variants for phrasing robustness, with dedup, delta generation, and JSON caching Failure classification: - 8 failure mode buckets (no_tool_call, wrong_tool, missing_tool, wrong_args, extra_tools, timeout, error, json_dump) - Consistency signals (systematic, flaky, marginal) - Rule-based pre-analysis feeds into analyst as structured input Logging and observability: - Config summary at startup (models, case count, runs) - Per-case diversifier progress with dedup stats - UCB selection reasoning, node score updates, tree growth - Extended TSV: node_score, elapsed_s, prompt_len, iter_tokens, cumul_tokens columns plus 4-decimal precision Infrastructure: - Thread-safe fd-level stdout suppression (os.dup2) - Prompt variants plumbed through parallel execution path - Cached variants auto-detected from tests.json user_prompts field New CLI flags: --explore-constant, --analyst-model/base-url, --diversifier-model/base-url, --diversify N, --save-variants
This commit is contained in:
committed by
Patrick Buckley
parent
c3b0ddeba7
commit
51b5b3ee74
+500
-44
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"description": "turnstone behavior tests — tool selection, sequencing, and multi-step reasoning",
|
||||
"description": "turnstone behavior tests \u2014 tool selection, sequencing, and multi-step reasoning",
|
||||
"defaults": {
|
||||
"n_runs": 5,
|
||||
"max_turns": 15
|
||||
@@ -8,35 +8,121 @@
|
||||
{
|
||||
"id": "read-before-edit",
|
||||
"description": "Must read_file before edit_file on the same path",
|
||||
"user_prompt": "Fix the typo in config.py — change 'recieve' to 'receive'",
|
||||
"user_prompt": "Fix the typo in config.py \u2014 change 'recieve' to 'receive'",
|
||||
"setup": {
|
||||
"files": {
|
||||
"config.py": "# Config module\ndef recieve_data(source):\n \"\"\"Recieve data from source.\"\"\"\n return source.read()\n"
|
||||
}
|
||||
},
|
||||
"expected_actions": [
|
||||
{ "tool": "read_file", "args": { "path": "config.py" } },
|
||||
{ "tool": "edit_file", "args": { "path": "config.py" } }
|
||||
{
|
||||
"tool": "read_file",
|
||||
"args": {
|
||||
"path": "config.py"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tool": "edit_file",
|
||||
"args": {
|
||||
"path": "config.py"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "ordered_subset"
|
||||
"match_mode": "ordered_subset",
|
||||
"user_prompts": [
|
||||
"Fix the typo in config.py \u2014 change 'recieve' to 'receive'",
|
||||
"In config.py, correct the misspelling of 'recieve' to 'receive'",
|
||||
"Please update config.py by replacing 'recieve' with the correct spelling 'receive'",
|
||||
"There's a typo in config.py: 'recieve' should be 'receive'. Please fix it.",
|
||||
"Could you change 'recieve' to 'receive' in config.py?",
|
||||
"Go ahead and fix 'recieve' \u2192 'receive' in config.py",
|
||||
"I need the word 'recieve' corrected to 'receive' in the file config.py",
|
||||
"config.py has a spelling error \u2014 'recieve' needs to be changed to 'receive'",
|
||||
"Kindly rectify the typographical error in config.py, replacing 'recieve' with 'receive'",
|
||||
"Hey, swap 'recieve' for 'receive' in config.py",
|
||||
"The file config.py contains a misspelling: please replace 'recieve' with 'receive'",
|
||||
"Fix the spelling mistake in config.py where 'recieve' is used instead of 'receive'",
|
||||
"Would you mind fixing 'recieve' to 'receive' in config.py?",
|
||||
"Replace the incorrectly spelled 'recieve' with 'receive' in config.py",
|
||||
"In the file config.py, the word 'recieve' is misspelled and should read 'receive' \u2014 please correct it",
|
||||
"Correct 'recieve' to 'receive' in config.py",
|
||||
"config.py: change 'recieve' to 'receive'",
|
||||
"There's a typo \u2014 'recieve' in config.py ought to be 'receive'. Can you fix that?",
|
||||
"I noticed 'recieve' in config.py. It should be 'receive'. Please make the fix.",
|
||||
"Update the spelling of 'recieve' to 'receive' within config.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "write-file-not-bash",
|
||||
"description": "Use write_file for file creation, not bash echo/cat",
|
||||
"user_prompt": "Create a file called hello.py that prints hello world",
|
||||
"expected_actions": [
|
||||
{ "tool": "write_file", "args_pattern": { "path": "hello\\.py" } }
|
||||
{
|
||||
"tool": "write_file",
|
||||
"args_pattern": {
|
||||
"path": "hello\\.py"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Create a file called hello.py that prints hello world",
|
||||
"Make a hello.py file that outputs hello world",
|
||||
"Please write a Python file named hello.py which prints hello world",
|
||||
"I need a file called hello.py that prints hello world",
|
||||
"Could you create hello.py with code that prints hello world?",
|
||||
"Write hello.py \u2014 it should print hello world",
|
||||
"Generate a hello.py file that outputs \"hello world\"",
|
||||
"I'd like you to create a file named hello.py that prints hello world",
|
||||
"Set up a file called hello.py to print hello world",
|
||||
"Kindly produce a hello.py file whose purpose is to print hello world",
|
||||
"hello.py \u2014 create it, should print hello world",
|
||||
"Would you mind creating a hello.py that prints hello world?",
|
||||
"I need you to write a hello.py file that will print hello world",
|
||||
"Create hello.py containing code to print hello world",
|
||||
"Please make a Python file hello.py that prints hello world to the console",
|
||||
"Draft a file named hello.py that outputs hello world when run",
|
||||
"Put together a hello.py script that prints hello world",
|
||||
"Can you create a file hello.py that prints hello world?",
|
||||
"Write a file called hello.py with a print statement for hello world",
|
||||
"I want a hello.py file created that prints hello world"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bash-for-commands",
|
||||
"description": "Use bash for running system commands",
|
||||
"user_prompt": "What Python version is installed?",
|
||||
"expected_actions": [
|
||||
{ "tool": "bash", "args_pattern": { "command": "python" } }
|
||||
{
|
||||
"tool": "bash",
|
||||
"args_pattern": {
|
||||
"command": "python"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"What Python version is installed?",
|
||||
"Check which version of Python is currently installed",
|
||||
"Can you tell me the installed Python version?",
|
||||
"python --version please",
|
||||
"I need to know what version of Python is on this system",
|
||||
"Which Python version do we have?",
|
||||
"Could you look up the Python version that's installed here?",
|
||||
"Determine the currently installed Python version",
|
||||
"What's the Python version on this machine?",
|
||||
"Please check the Python version",
|
||||
"I'm curious about the installed Python version \u2014 can you find out?",
|
||||
"Show me the Python version",
|
||||
"Find out what Python version is available",
|
||||
"What version of Python do we have installed?",
|
||||
"Kindly verify which version of Python is present on the system",
|
||||
"python version?",
|
||||
"I'd like to know the Python version installed on this system",
|
||||
"Would you mind checking what Python version is installed?",
|
||||
"Tell me the Python version",
|
||||
"Report the installed Python version on this machine"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "search-for-patterns",
|
||||
@@ -49,13 +135,40 @@
|
||||
}
|
||||
},
|
||||
"expected_actions": [
|
||||
{ "tool": "search", "args_pattern": { "query": "test_" } }
|
||||
{
|
||||
"tool": "search",
|
||||
"args_pattern": {
|
||||
"query": "test_"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Find all functions that start with 'test_' in the project",
|
||||
"List every function in the project whose name begins with 'test_'",
|
||||
"I need to locate all functions prefixed with 'test_' across the project",
|
||||
"Could you search the project for any functions starting with 'test_'?",
|
||||
"Show me all the test_ prefixed functions in this project",
|
||||
"Hunt down every function that has a 'test_' prefix in the codebase",
|
||||
"I'm looking for all functions named test_* throughout the project",
|
||||
"Search the entire project for functions whose names start with test_",
|
||||
"What functions beginning with 'test_' exist in this project?",
|
||||
"Please identify all functions with the 'test_' prefix in the project files",
|
||||
"Grep the project for all function definitions starting with 'test_'",
|
||||
"Give me a list of all test_ functions defined anywhere in the project",
|
||||
"Can you find every function declaration that starts with 'test_' in the project?",
|
||||
"Scan the project codebase and return all functions prefixed with test_",
|
||||
"I'd like to see all functions in the project that begin with 'test_'",
|
||||
"Go through the project and locate each function starting with 'test_'",
|
||||
"Pull up all function definitions matching the test_ prefix across the project",
|
||||
"Which functions in the project have names that start with 'test_'?",
|
||||
"Enumerate all test_-prefixed function definitions in the project",
|
||||
"Look through the project and find any function whose name starts with test_"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "multi-file-edit",
|
||||
"description": "Read and edit multiple files — must read before editing each, and edit both",
|
||||
"description": "Read and edit multiple files \u2014 must read before editing each, and edit both",
|
||||
"user_prompt": "Change the default port from 8000 to 9000 in both server.py and config.py",
|
||||
"setup": {
|
||||
"files": {
|
||||
@@ -64,12 +177,42 @@
|
||||
}
|
||||
},
|
||||
"expected_actions": [
|
||||
{ "tool": "read_file" },
|
||||
{ "tool": "read_file" },
|
||||
{ "tool": "edit_file" },
|
||||
{ "tool": "edit_file" }
|
||||
{
|
||||
"tool": "read_file"
|
||||
},
|
||||
{
|
||||
"tool": "read_file"
|
||||
},
|
||||
{
|
||||
"tool": "edit_file"
|
||||
},
|
||||
{
|
||||
"tool": "edit_file"
|
||||
}
|
||||
],
|
||||
"match_mode": "ordered_subset"
|
||||
"match_mode": "ordered_subset",
|
||||
"user_prompts": [
|
||||
"Change the default port from 8000 to 9000 in both server.py and config.py",
|
||||
"Update the default port to 9000 instead of 8000 in server.py and config.py",
|
||||
"Could you modify the port number from 8000 to 9000 in both config.py and server.py?",
|
||||
"Please replace port 8000 with 9000 in server.py and config.py",
|
||||
"I need the default port switched from 8000 to 9000 in both server.py and config.py",
|
||||
"In server.py and config.py, the default port should be changed from 8000 to 9000",
|
||||
"Swap out port 8000 for 9000 in config.py and server.py",
|
||||
"Would you mind updating the default port value from 8000 to 9000 across both server.py and config.py?",
|
||||
"The default port in server.py and config.py needs to be 9000 instead of 8000 \u2014 please make that change",
|
||||
"Go ahead and change 8000 to 9000 for the default port in both server.py and config.py",
|
||||
"Kindly adjust the default port configuration from 8000 to 9000 in the files server.py and config.py",
|
||||
"Port change needed: 8000 \u2192 9000 in server.py and config.py",
|
||||
"Both server.py and config.py currently use port 8000 as the default \u2014 update them to use 9000",
|
||||
"Set the default port to 9000 (currently 8000) in server.py and config.py",
|
||||
"I'd like the default port modified from 8000 to 9000 in both config.py and server.py",
|
||||
"Replace the 8000 port default with 9000 in both server.py and config.py please",
|
||||
"Can you switch the default port number to 9000 from 8000 in server.py as well as config.py?",
|
||||
"Make the default port 9000 rather than 8000 in server.py and config.py",
|
||||
"In both config.py and server.py, please update the default port \u2014 it should be 9000 not 8000",
|
||||
"Modify server.py and config.py so the default port is 9000 instead of the current 8000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "search-then-edit",
|
||||
@@ -83,39 +226,159 @@
|
||||
}
|
||||
},
|
||||
"expected_actions": [
|
||||
{ "tool": "search", "args_pattern": { "query": "MAX_RETRIES" } },
|
||||
{ "tool": "read_file" },
|
||||
{ "tool": "edit_file", "args_pattern": { "old_string": "3" } }
|
||||
{
|
||||
"tool": "search",
|
||||
"args_pattern": {
|
||||
"query": "MAX_RETRIES"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tool": "read_file"
|
||||
},
|
||||
{
|
||||
"tool": "edit_file",
|
||||
"args_pattern": {
|
||||
"old_string": "3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "ordered_subset"
|
||||
"match_mode": "ordered_subset",
|
||||
"user_prompts": [
|
||||
"Find where MAX_RETRIES is defined and change it from 3 to 5",
|
||||
"Locate the definition of MAX_RETRIES and update its value from 3 to 5",
|
||||
"Could you search for where MAX_RETRIES is defined and modify it from 3 to 5?",
|
||||
"I need MAX_RETRIES changed from 3 to 5 \u2014 find where it's defined and update it",
|
||||
"Please find the MAX_RETRIES definition and bump it from 3 to 5",
|
||||
"Hunt down MAX_RETRIES in the codebase and change its value from 3 to 5",
|
||||
"Where is MAX_RETRIES set to 3? Change it to 5.",
|
||||
"Search the code for the MAX_RETRIES definition and alter it from 3 to 5",
|
||||
"I'd like you to locate MAX_RETRIES (currently 3) and set it to 5 instead",
|
||||
"Go find MAX_RETRIES and switch it from 3 to 5",
|
||||
"The constant MAX_RETRIES is defined somewhere as 3 \u2014 find it and change it to 5",
|
||||
"Track down the MAX_RETRIES definition and increase it from 3 to 5",
|
||||
"Would you mind finding where MAX_RETRIES is set and updating the value from 3 to 5?",
|
||||
"Grep for MAX_RETRIES, find its definition, and change the value from 3 to 5",
|
||||
"MAX_RETRIES needs to be 5 instead of 3. Find where it's defined and make the change.",
|
||||
"Look up the definition of MAX_RETRIES and replace 3 with 5",
|
||||
"Can you find the spot where MAX_RETRIES is defined as 3 and update it to 5?",
|
||||
"Identify the location of the MAX_RETRIES definition and change its value to 5 from 3",
|
||||
"I need you to search for MAX_RETRIES, which is currently set to 3, and change it to 5",
|
||||
"Find the MAX_RETRIES constant and adjust it \u2014 it should be 5, not 3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bash-git-log",
|
||||
"description": "Use bash for git commands, not other tools",
|
||||
"user_prompt": "Show me the git log for the last 5 commits",
|
||||
"expected_actions": [
|
||||
{ "tool": "bash", "args_pattern": { "command": "git\\s+log" } }
|
||||
{
|
||||
"tool": "bash",
|
||||
"args_pattern": {
|
||||
"command": "git\\s+log"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Show me the git log for the last 5 commits",
|
||||
"Display the 5 most recent git commits",
|
||||
"Can you pull up the git log limited to the last five commits?",
|
||||
"I need to see the git log showing only the previous 5 commits",
|
||||
"git log for the 5 latest commits, please",
|
||||
"Would you mind showing me the last five entries in the git log?",
|
||||
"Print out the most recent 5 commits from the git log",
|
||||
"I'd like to review the git log \u2014 just the last 5 commits",
|
||||
"Show the recent 5 commit history using git log",
|
||||
"Could you display the git commit history for the past five commits?",
|
||||
"Fetch the git log, but only the 5 newest commits",
|
||||
"Let me see the five most recent commits in the git log",
|
||||
"Please retrieve the last 5 entries from git log",
|
||||
"I want to check the git log limited to 5 recent commits",
|
||||
"Run git log and show me just the latest five commits",
|
||||
"Pull up the commit log from git for the last 5 changes",
|
||||
"Mind grabbing the git log? Just the last five commits though",
|
||||
"Give me a view of the 5 most recent git log entries",
|
||||
"Show me the history of the last five commits via git log",
|
||||
"I'd appreciate seeing the git log restricted to the previous 5 commits"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "write-then-run",
|
||||
"description": "Create a script and run it to verify it works",
|
||||
"user_prompt": "Create a Python script called fib.py that prints the first 10 Fibonacci numbers, then run it to verify",
|
||||
"expected_actions": [
|
||||
{ "tool": "write_file", "args_pattern": { "path": "fib\\.py" } },
|
||||
{ "tool": "bash", "args_pattern": { "command": "python" } }
|
||||
{
|
||||
"tool": "write_file",
|
||||
"args_pattern": {
|
||||
"path": "fib\\.py"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tool": "bash",
|
||||
"args_pattern": {
|
||||
"command": "python"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "ordered_subset"
|
||||
"match_mode": "ordered_subset",
|
||||
"user_prompts": [
|
||||
"Create a Python script called fib.py that prints the first 10 Fibonacci numbers, then run it to verify",
|
||||
"Write a Python file named fib.py that outputs the first 10 Fibonacci numbers, and execute it to confirm it works",
|
||||
"Please make fib.py \u2013 a Python script printing the first ten Fibonacci numbers \u2013 then run it to check the output",
|
||||
"I need a Python script fib.py that prints the first 10 Fibonacci numbers. Execute it afterwards to verify correctness.",
|
||||
"Could you create fib.py to display the first 10 Fibonacci numbers in Python, and then run it to make sure it works?",
|
||||
"Draft a script called fib.py in Python that outputs the first ten Fibonacci numbers, then execute it to validate",
|
||||
"Hey, write me a fib.py that prints the first 10 Fibonacci numbers and run it so we can see it works",
|
||||
"Generate a Python program fib.py which prints the initial 10 Fibonacci numbers, and verify by running it",
|
||||
"Kindly produce a Python script named fib.py to print the first 10 Fibonacci numbers, then execute the script to confirm its output",
|
||||
"Make a file fib.py containing Python code to print the first 10 Fibonacci numbers. Then run it to verify.",
|
||||
"I'd like you to write fib.py, a Python script that displays the first ten Fibonacci numbers, and test it by running it",
|
||||
"Put together a Python script fib.py that prints out the first 10 Fibonacci numbers, then go ahead and run it to check",
|
||||
"Create fib.py with Python code for printing the first 10 Fibonacci numbers and execute it to see the results",
|
||||
"Would you mind writing a Python script called fib.py that prints the first 10 Fibonacci numbers? Please run it too to verify.",
|
||||
"Write fib.py in Python to output the first ten Fibonacci numbers. Run the script to ensure it's correct.",
|
||||
"Compose a Python file fib.py that will print the first 10 Fibonacci numbers when executed, and then run it to verify the output",
|
||||
"fib.py \u2014 create this Python script to print the first 10 Fibonacci numbers, and execute it to confirm it's working properly",
|
||||
"Can you set up a Python script named fib.py that prints the first 10 Fibonacci numbers and then run it for verification?",
|
||||
"Please author a Python program in fib.py to display the first ten Fibonacci numbers, followed by running it to check",
|
||||
"Whip up fib.py to print the first 10 Fibonacci numbers using Python, then run it to make sure everything checks out"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "no-bash-for-file-write",
|
||||
"description": "Should NOT use bash (echo/cat/heredoc) to create files — only write_file",
|
||||
"description": "Should NOT use bash (echo/cat/heredoc) to create files \u2014 only write_file",
|
||||
"user_prompt": "Create a new file called README.md with a title and description of this project",
|
||||
"expected_actions": [
|
||||
{ "tool": "write_file", "args_pattern": { "path": "README" } }
|
||||
{
|
||||
"tool": "write_file",
|
||||
"args_pattern": {
|
||||
"path": "README"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Create a new file called README.md with a title and description of this project",
|
||||
"Make a README.md file that includes a project title and description",
|
||||
"I need a README.md created with a title and a brief description of the project",
|
||||
"Please generate a README.md file containing the project's title and description",
|
||||
"Could you set up a README.md with a title and project description?",
|
||||
"Write a README.md that has a title and describes this project",
|
||||
"Go ahead and create README.md \u2014 it should have a title and a description of the project",
|
||||
"I'd like you to produce a new README.md file featuring a project title and description",
|
||||
"Kindly establish a README.md file incorporating both a title and a description for this project",
|
||||
"Spin up a README.md with a project title and description in it",
|
||||
"Draft a README.md file that provides a title and description for the project",
|
||||
"Would you mind creating a README.md that contains a title and project description?",
|
||||
"Set up a new file named README.md and include a title along with a project description",
|
||||
"I want a README.md file \u2014 put a title and a description of the project in it",
|
||||
"Create README.md and populate it with a title and a description of this project",
|
||||
"Add a new README.md to the project with an appropriate title and description",
|
||||
"Can you make a README.md that gives the project a title and description?",
|
||||
"Put together a README.md file with a heading and a description of what this project is about",
|
||||
"Initialize a README.md containing a project title and a short description",
|
||||
"Generate a new file called README.md \u2014 include a title and overview of the project"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "plan-before-refactor",
|
||||
@@ -126,8 +389,34 @@
|
||||
"app.py": "import sqlite3\nfrom flask import Flask, jsonify\n\napp = Flask(__name__)\nDB = 'data.db'\n\ndef get_db():\n return sqlite3.connect(DB)\n\n@app.route('/users')\ndef list_users():\n db = get_db()\n users = db.execute('SELECT * FROM users').fetchall()\n db.close()\n return jsonify(users)\n\n@app.route('/users/<int:uid>')\ndef get_user(uid):\n db = get_db()\n user = db.execute('SELECT * FROM users WHERE id=?', (uid,)).fetchone()\n db.close()\n return jsonify(user)\n\nif __name__ == '__main__':\n app.run(port=8000)\n"
|
||||
}
|
||||
},
|
||||
"expected_actions": [{ "tool": "create_plan" }],
|
||||
"match_mode": "subset"
|
||||
"expected_actions": [
|
||||
{
|
||||
"tool": "create_plan"
|
||||
}
|
||||
],
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"I need to refactor this codebase to separate the database layer from the API layer. Use the plan tool to think through the approach before making any changes.",
|
||||
"Please use the plan tool to think through how to refactor this codebase, separating the database layer from the API layer, before making any changes.",
|
||||
"Before touching any code, use the plan tool to outline an approach for decoupling the database layer from the API layer in this codebase.",
|
||||
"I'd like to split the DB layer and API layer apart in this codebase. Think through the strategy using the plan tool first, then proceed.",
|
||||
"Could you refactor this code to decouple the database and API layers? Start by using the plan tool to reason through the approach before changing anything.",
|
||||
"Hey, I need the database layer pulled apart from the API layer in this codebase. Use the plan tool to map out the approach before diving in.",
|
||||
"The database and API layers in this codebase need to be separated. Please leverage the plan tool to think it through before implementing changes.",
|
||||
"Use the plan tool first to strategize, then refactor this codebase so that the database layer is cleanly separated from the API layer.",
|
||||
"I want the DB layer and API layer in this codebase to be independent. Plan out the refactoring approach using the plan tool before making edits.",
|
||||
"This codebase has its database and API layers tangled together. Use the plan tool to think through a separation strategy before modifying anything.",
|
||||
"Would you mind using the plan tool to devise a refactoring plan for separating the database layer from the API layer in this codebase? Don't change code until you've planned.",
|
||||
"Refactor this code so the DB layer is decoupled from the API layer. But first, use the plan tool to carefully consider the approach.",
|
||||
"I'm looking to have the database concerns separated from the API concerns in this codebase. Think through it with the plan tool before making any modifications.",
|
||||
"Before implementing anything, use the plan tool to plan how to refactor this codebase into separate database and API layers.",
|
||||
"Can you separate the database layer from the API layer here? Please use the plan tool to reason about the approach prior to any code changes.",
|
||||
"Time to decouple the DB and API layers in this codebase. Start with the plan tool to think through the strategy, then make changes.",
|
||||
"I need this codebase restructured so the database layer doesn't mix with the API layer. Use the plan tool to work out the approach first.",
|
||||
"Think through a refactoring plan using the plan tool for separating the database and API layers in this codebase, then execute the changes.",
|
||||
"The goal is to isolate the database layer from the API layer in this codebase. Use the plan tool to figure out the best approach before writing any code.",
|
||||
"Please plan out (using the plan tool) how to cleanly separate the database layer from the API layer in this codebase before making any actual changes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "edit-not-rewrite",
|
||||
@@ -139,10 +428,42 @@
|
||||
}
|
||||
},
|
||||
"expected_actions": [
|
||||
{ "tool": "read_file", "args": { "path": "utils.py" } },
|
||||
{ "tool": "edit_file", "args": { "path": "utils.py" } }
|
||||
{
|
||||
"tool": "read_file",
|
||||
"args": {
|
||||
"path": "utils.py"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tool": "edit_file",
|
||||
"args": {
|
||||
"path": "utils.py"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "ordered_subset"
|
||||
"match_mode": "ordered_subset",
|
||||
"user_prompts": [
|
||||
"Add a docstring to the process_data function in utils.py",
|
||||
"Please add a docstring to the process_data function in utils.py",
|
||||
"Could you write a docstring for process_data in utils.py?",
|
||||
"Insert a docstring into the process_data function found in utils.py",
|
||||
"I need a docstring added to process_data in utils.py",
|
||||
"Put a docstring on the process_data function in utils.py",
|
||||
"The process_data function in utils.py is missing a docstring \u2014 please add one",
|
||||
"Would you mind adding a docstring to process_data in utils.py?",
|
||||
"In utils.py, the process_data function needs a docstring",
|
||||
"Add documentation via a docstring to the process_data function within utils.py",
|
||||
"Go ahead and give process_data in utils.py a proper docstring",
|
||||
"I'd like you to add a docstring to the process_data function in utils.py",
|
||||
"utils.py's process_data function lacks a docstring \u2014 can you add one?",
|
||||
"Write a docstring for the process_data function located in utils.py",
|
||||
"Kindly include a docstring in the process_data function in utils.py",
|
||||
"Can you throw a docstring onto process_data in utils.py?",
|
||||
"The function process_data in utils.py should have a docstring \u2014 please add it",
|
||||
"Give the process_data function in utils.py a docstring",
|
||||
"Hey, add a docstring to process_data over in utils.py",
|
||||
"I require a docstring to be added to the process_data function defined in utils.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bash-run-tests",
|
||||
@@ -154,45 +475,180 @@
|
||||
}
|
||||
},
|
||||
"expected_actions": [
|
||||
{ "tool": "bash", "args_pattern": { "command": "pytest|python.*test" } }
|
||||
{
|
||||
"tool": "bash",
|
||||
"args_pattern": {
|
||||
"command": "pytest|python.*test"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Run the tests",
|
||||
"Execute the test suite",
|
||||
"Please go ahead and run the tests",
|
||||
"Could you run the tests for me?",
|
||||
"I need the tests to be run",
|
||||
"Kick off the tests",
|
||||
"Let's run the tests",
|
||||
"Fire up the tests",
|
||||
"Go ahead and execute the tests",
|
||||
"I'd like you to run the tests",
|
||||
"Run all tests please",
|
||||
"Would you mind running the tests?",
|
||||
"Time to run the tests",
|
||||
"Please execute the tests",
|
||||
"Trigger the test run",
|
||||
"I want to run the tests",
|
||||
"Can you run the tests?",
|
||||
"Launch the tests",
|
||||
"Kindly run the test suite",
|
||||
"Run the tests for me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web-fetch-url",
|
||||
"description": "Use web_fetch when asked to retrieve content from a URL",
|
||||
"user_prompt": "Fetch the contents of https://example.com and summarize what's on the page",
|
||||
"expected_actions": [
|
||||
{ "tool": "web_fetch", "args_pattern": { "url": "example\\.com" } }
|
||||
{
|
||||
"tool": "web_fetch",
|
||||
"args_pattern": {
|
||||
"url": "example\\.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Fetch the contents of https://example.com and summarize what's on the page",
|
||||
"Go to https://example.com and give me a summary of what you find there",
|
||||
"Could you pull up https://example.com and tell me what the page is about?",
|
||||
"Retrieve the content from https://example.com, then provide a summary of it",
|
||||
"I need you to grab https://example.com and summarize its contents for me",
|
||||
"Please access https://example.com and give me an overview of the page",
|
||||
"What's on https://example.com? Fetch it and summarize for me.",
|
||||
"Download the page at https://example.com and provide a brief summary",
|
||||
"I'd like a summary of whatever is at https://example.com \u2014 please fetch it first",
|
||||
"Hit https://example.com and let me know what's there in summary form",
|
||||
"Kindly retrieve the webpage https://example.com and offer a concise summary of its content",
|
||||
"Pull the contents of https://example.com and break down what the page contains",
|
||||
"Can you load https://example.com and give me the gist of it?",
|
||||
"Get https://example.com and tell me what it says",
|
||||
"Grab the page from https://example.com, then summarize what you see",
|
||||
"I'm curious about https://example.com \u2014 fetch it and summarize the contents please",
|
||||
"Would you mind fetching https://example.com and providing a summary of the page?",
|
||||
"Access the content at https://example.com and write a short summary",
|
||||
"Read https://example.com and give me a rundown of what's on that page",
|
||||
"Check out https://example.com for me and summarize what the page contains"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "man-page-lookup",
|
||||
"description": "Use man tool to look up command documentation",
|
||||
"user_prompt": "Look up the man page for tar and tell me what the --xattrs flag does",
|
||||
"expected_actions": [
|
||||
{ "tool": "man", "args_pattern": { "page": "tar" } }
|
||||
{
|
||||
"tool": "man",
|
||||
"args_pattern": {
|
||||
"page": "tar"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Look up the man page for tar and tell me what the --xattrs flag does",
|
||||
"What does the --xattrs flag do in tar? Check the man page for me.",
|
||||
"Could you pull up the man page for tar and explain the --xattrs option?",
|
||||
"I need to know what --xattrs does in tar \u2014 can you check the man page?",
|
||||
"Check tar's man page and let me know the purpose of the --xattrs flag.",
|
||||
"Please consult the tar man page and describe what the --xattrs flag is for.",
|
||||
"Hey, look at the tar man page real quick \u2014 what's --xattrs do?",
|
||||
"I'd like you to read the tar man page and summarize the --xattrs option for me.",
|
||||
"Would you mind checking the man page for tar to find out what --xattrs means?",
|
||||
"Look into the tar manual and explain the --xattrs flag to me.",
|
||||
"What's the --xattrs option in tar? Please reference the man page.",
|
||||
"Refer to the tar man page and tell me about the --xattrs flag.",
|
||||
"Can you check what --xattrs does by looking at tar's man page?",
|
||||
"Pull up the manual for tar and describe the --xattrs flag, please.",
|
||||
"I'm curious about tar's --xattrs flag \u2014 could you look it up in the man page?",
|
||||
"Consult the man page for tar and give me a rundown on --xattrs.",
|
||||
"Open the tar man page and find out what the --xattrs option does.",
|
||||
"From the tar man page, what is the function of --xattrs?",
|
||||
"Mind looking up --xattrs in the tar man page and explaining it?",
|
||||
"Review tar's manual entry and tell me what --xattrs is used for."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "math-calculation",
|
||||
"description": "Use the math tool for precise calculations, not bash or mental math",
|
||||
"user_prompt": "What is 2^64 - 1? Use the math tool to calculate it precisely.",
|
||||
"expected_actions": [
|
||||
{ "tool": "math", "args_pattern": { "code": "2.*64" } }
|
||||
{
|
||||
"tool": "math",
|
||||
"args_pattern": {
|
||||
"code": "2.*64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"What is 2^64 - 1? Use the math tool to calculate it precisely.",
|
||||
"Calculate 2^64 - 1 for me using the math tool, please.",
|
||||
"I need the exact value of 2^64 - 1. Please use the math tool.",
|
||||
"Could you use the math tool to compute 2^64 minus 1 precisely?",
|
||||
"Use the math tool to tell me what 2^64 - 1 equals.",
|
||||
"I'm curious: what's 2^64 - 1? Compute it with the math tool.",
|
||||
"Please precisely determine 2^64 - 1 via the math tool.",
|
||||
"Mind using the math tool to figure out 2^64 - 1 exactly?",
|
||||
"I'd like to know the precise result of 2^64 - 1 \u2014 use the math tool for this.",
|
||||
"Leverage the math tool to give me an exact answer for 2^64 - 1.",
|
||||
"What does 2^64 - 1 evaluate to? Please use the math tool to get the precise answer.",
|
||||
"Kindly employ the math tool to calculate the exact value of 2 to the power of 64, minus 1.",
|
||||
"Hey, what's 2^64 - 1? Hit the math tool for an exact number.",
|
||||
"Using the math tool, compute 2^64 - 1 with full precision.",
|
||||
"I need the math tool to work out 2^64 - 1. What's the result?",
|
||||
"Would you mind calculating 2^64 - 1 precisely with the math tool?",
|
||||
"Give me the exact result of 2^64 - 1, calculated using the math tool.",
|
||||
"Two to the sixty-fourth power minus one \u2014 what is it? Use the math tool to find out.",
|
||||
"Can you determine 2^64 - 1 exactly? Please rely on the math tool for this computation.",
|
||||
"Precisely compute 2**64 - 1 using the math tool and share the result."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web-search-query",
|
||||
"description": "Use web_search for general knowledge lookups, not web_fetch",
|
||||
"user_prompt": "Search the web for the current population of Tokyo",
|
||||
"expected_actions": [
|
||||
{ "tool": "web_search", "args_pattern": { "query": "Tokyo" } }
|
||||
{
|
||||
"tool": "web_search",
|
||||
"args_pattern": {
|
||||
"query": "Tokyo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"match_mode": "subset"
|
||||
"match_mode": "subset",
|
||||
"user_prompts": [
|
||||
"Search the web for the current population of Tokyo",
|
||||
"What's Tokyo's current population? Look it up on the web.",
|
||||
"Could you do a web search to find out how many people currently live in Tokyo?",
|
||||
"Please search online for Tokyo's present-day population.",
|
||||
"I need you to look up the current population of Tokyo on the web.",
|
||||
"Find me Tokyo's current population via a web search.",
|
||||
"Web search: what is the current population of Tokyo?",
|
||||
"I'd like to know Tokyo's current population\u2014can you search the web for that?",
|
||||
"Look up how many people live in Tokyo right now using a web search.",
|
||||
"Do a web search for the population of Tokyo as of now.",
|
||||
"Search the internet to find Tokyo's current population figure.",
|
||||
"Would you mind searching the web to determine Tokyo's present population?",
|
||||
"I'm curious about Tokyo's current population. Please search the web for it.",
|
||||
"Kindly perform a web search regarding the current population of Tokyo.",
|
||||
"Pull up the current population of Tokyo from the web.",
|
||||
"Go ahead and search online for how many people are in Tokyo currently.",
|
||||
"Query the web for Tokyo's population at present.",
|
||||
"Can you find the current population of Tokyo through a web search?",
|
||||
"Use the web to look up what Tokyo's population is right now.",
|
||||
"Search for the latest population count of Tokyo on the web."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+766
-24
@@ -776,6 +776,7 @@ def _run_iteration_parallel(
|
||||
context_window: int,
|
||||
test_timeout: int,
|
||||
parallel: int,
|
||||
prompt_variants: dict[str, list[str]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run all test cases in parallel using ProcessPoolExecutor."""
|
||||
# Build work items for every (case, run) combination
|
||||
@@ -783,14 +784,20 @@ def _run_iteration_parallel(
|
||||
for case in cases:
|
||||
case_id = case["id"]
|
||||
case_n = case.get("n_runs", n_runs)
|
||||
variants = (prompt_variants or {}).get(case_id)
|
||||
for run_idx in range(case_n):
|
||||
# Select prompt variant for this run (cycle through variants)
|
||||
if variants and len(variants) > 1:
|
||||
run_case = {**case, "user_prompt": variants[run_idx % len(variants)]}
|
||||
else:
|
||||
run_case = case
|
||||
work_items.append(
|
||||
{
|
||||
"base_url": base_url,
|
||||
"api_key": api_key,
|
||||
"model": model,
|
||||
"system_prompt": system_prompt,
|
||||
"case": case,
|
||||
"case": run_case,
|
||||
"case_id": case_id,
|
||||
"run_idx": run_idx,
|
||||
"temperature": temperature,
|
||||
@@ -917,6 +924,7 @@ def _run_iteration(
|
||||
parallel: int = 1,
|
||||
base_url: str = "",
|
||||
api_key: str = "",
|
||||
prompt_variants: dict[str, list[str]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run all test cases n_runs times and score them."""
|
||||
if parallel > 1 and base_url:
|
||||
@@ -933,6 +941,7 @@ def _run_iteration(
|
||||
context_window=context_window,
|
||||
test_timeout=test_timeout,
|
||||
parallel=parallel,
|
||||
prompt_variants=prompt_variants,
|
||||
)
|
||||
|
||||
case_results: dict[str, Any] = {}
|
||||
@@ -956,12 +965,25 @@ def _run_iteration(
|
||||
log_prefix = f" [{run_idx + 1}/{case_n}]"
|
||||
run_tokens = 0
|
||||
|
||||
# Select prompt variant for this run (cycle through variants)
|
||||
variants = (prompt_variants or {}).get(case_id)
|
||||
if variants and len(variants) > 1:
|
||||
variant_idx = run_idx % len(variants)
|
||||
run_case = {**case, "user_prompt": variants[variant_idx]}
|
||||
if verbose:
|
||||
_log(
|
||||
f"{log_prefix} variant {variant_idx}: {variants[variant_idx][:80]}...",
|
||||
dim=True,
|
||||
)
|
||||
else:
|
||||
run_case = case
|
||||
|
||||
try:
|
||||
run_result = _run_single_test(
|
||||
client=client,
|
||||
model=model,
|
||||
system_prompt=system_prompt,
|
||||
case=case,
|
||||
case=run_case,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
reasoning_effort=reasoning_effort,
|
||||
@@ -980,6 +1002,8 @@ def _run_iteration(
|
||||
score_result["tool_sequence"] = [t["tool"] for t in run_result["tool_log"]]
|
||||
score_result["tool_args"] = [{t["tool"]: t["args"]} for t in run_result["tool_log"]]
|
||||
score_result["elapsed"] = run_result.get("elapsed", 0)
|
||||
if run_case is not case:
|
||||
score_result["prompt_variant"] = run_case["user_prompt"]
|
||||
run_tokens = sum(run_result.get("usage", {}).values())
|
||||
|
||||
# Detect JSON dumped into final channel (tool call not made)
|
||||
@@ -1092,6 +1116,97 @@ def _run_iteration(
|
||||
# ─── Prompt optimizer ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
DIVERSIFIER_SYSTEM = """\
|
||||
You generate paraphrased variations of user prompts for a tool-use \
|
||||
evaluation harness. Each variation must preserve the EXACT SAME INTENT \
|
||||
— the same task, same expected outcome, same level of specificity — \
|
||||
but use different phrasing, vocabulary, sentence structure, or tone.
|
||||
|
||||
Rules:
|
||||
- Preserve the core action the user is asking for. If the original \
|
||||
says "fix the typo in config.py", all variants must ask to fix a \
|
||||
typo in config.py.
|
||||
- Vary along these dimensions: formality (casual ↔ formal), \
|
||||
directness (imperative ↔ descriptive), verbosity (terse ↔ detailed), \
|
||||
framing (command ↔ question ↔ description of need).
|
||||
- Do NOT change the expected tool behavior. If the original implies \
|
||||
using bash, the variant must also imply bash.
|
||||
- Do NOT add new requirements, constraints, or context not in the \
|
||||
original.
|
||||
- Each variant should be a single message, roughly similar length \
|
||||
to the original.
|
||||
|
||||
Output a JSON array of strings, one per variant. No commentary, \
|
||||
no markdown fences, just the JSON array.\
|
||||
"""
|
||||
|
||||
|
||||
ANALYST_SYSTEM = """\
|
||||
You are a test failure analyst for an LLM tool-use evaluation harness. \
|
||||
You receive test results showing how a coding assistant performed \
|
||||
against expected tool call sequences.
|
||||
|
||||
Your job: identify SEMANTIC PATTERNS across failures and successes — \
|
||||
not just what failed, but WHY, and what the failures have in common.
|
||||
|
||||
You have access to `math` (Python with numpy/scipy/collections) and \
|
||||
`bash` tools. Use them to compute statistics, build confusion matrices, \
|
||||
analyze tool co-occurrence, or quantify patterns — don't just eyeball \
|
||||
the data.
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
### 1. Failure Mode Patterns
|
||||
Look across all failing runs and identify shared root causes:
|
||||
- Does the model treat certain phrasings as conversation vs action?
|
||||
- Are there tool confusion pairs (e.g., always picks write_file over edit_file)?
|
||||
- Do failures cluster around implicit vs explicit instructions?
|
||||
- Are there complexity thresholds where the model breaks down?
|
||||
|
||||
### 2. Success/Failure Contrast
|
||||
Compare passing and failing cases to isolate what makes the difference:
|
||||
- What do passing prompts have that failing ones lack?
|
||||
- Do passing cases use action verbs while failing ones describe goals?
|
||||
- Is there a pattern in prompt length, specificity, or structure?
|
||||
|
||||
### 3. Consistency Signals
|
||||
For each failing case, classify:
|
||||
- Systematic (0% pass): the prompt is fundamentally missing an instruction
|
||||
- Flaky (1-79% pass): the prompt is ambiguous — wording nudge needed
|
||||
- Marginal (80-99%): minor edge case — low priority
|
||||
|
||||
### 4. Actionable Diagnosis
|
||||
For each pattern found, state:
|
||||
- What the model is doing wrong (observed behavior)
|
||||
- Why it's doing it (root cause hypothesis)
|
||||
- What prompt change would fix it (specific, concrete)
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Failure Patterns
|
||||
- [Pattern 1]: [which cases] — [observed behavior] because [root cause]
|
||||
- [Pattern 2]: ...
|
||||
|
||||
## Success/Failure Contrast
|
||||
[What distinguishes passing from failing cases]
|
||||
|
||||
## Consistency
|
||||
- Systematic: [case_ids] — [what's missing]
|
||||
- Flaky: [case_ids] — [what's ambiguous]
|
||||
- Marginal: [case_ids] — [edge case description]
|
||||
|
||||
## Recommended Fixes (priority order)
|
||||
1. [Highest impact fix]: addresses [N] cases — [specific instruction to add/change]
|
||||
2. [Next fix]: ...
|
||||
```
|
||||
|
||||
Be concise. Focus on patterns, not individual case narratives. \
|
||||
The optimizer that reads your output needs actionable signal, \
|
||||
not lengthy explanations.\
|
||||
"""
|
||||
|
||||
|
||||
OPTIMIZER_SYSTEM = """\
|
||||
You are a prompt optimizer. You receive a developer prompt (instructions \
|
||||
for a coding assistant on how to use its tools) and test results \
|
||||
@@ -1117,18 +1232,11 @@ WHAT YOU CANNOT CHANGE:
|
||||
- The overall structure (system prompt for a coding assistant).
|
||||
- Phrasing tied to 100% pass rate cases.
|
||||
|
||||
FAILURE MODE DIAGNOSIS:
|
||||
- If the assistant responded with only text (no tool call) → add a \
|
||||
rule: "ALWAYS call a tool. Never respond with only text."
|
||||
- If write_file was used instead of edit_file for a small change → add: \
|
||||
"Use edit_file for modifying existing files. Only use write_file for \
|
||||
new files."
|
||||
- If create_plan() was not called for a complex task → add: "When asked to \
|
||||
think through a problem, call create_plan(goal='...')."
|
||||
- If the assistant searched for a file before creating it → add: \
|
||||
"When told to create a new file, use write_file directly."
|
||||
- If the tool sequence is correct but arguments are wrong → adjust \
|
||||
the example arguments, not the tool selection logic.
|
||||
FAILURE ANALYSIS: The input includes a "Failure Analysis" section \
|
||||
produced by a separate analyst agent. It identifies semantic patterns \
|
||||
across failures, contrasts them with successes, and recommends specific \
|
||||
fixes in priority order. Use this to guide your edits — address the \
|
||||
highest-priority patterns first.
|
||||
|
||||
STYLE: direct imperative sentences. One instruction per line. \
|
||||
Concrete tool call examples where helpful.
|
||||
@@ -1188,6 +1296,130 @@ Output ONLY the modified optimizer instructions.\
|
||||
"""
|
||||
|
||||
|
||||
def _diversify_prompts(
|
||||
client: Any,
|
||||
model: str,
|
||||
cases: list[dict[str, Any]],
|
||||
n_variants: int,
|
||||
provider: LLMProvider | None = None,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Generate paraphrased prompt variants for each test case.
|
||||
|
||||
Returns {case_id: [variant1, variant2, ...]}. The original
|
||||
user_prompt is always included as the first variant.
|
||||
"""
|
||||
prov = provider or create_provider("openai")
|
||||
result: dict[str, list[str]] = {}
|
||||
|
||||
for ci, case in enumerate(cases):
|
||||
cid = case["id"]
|
||||
original = case["user_prompt"]
|
||||
|
||||
if n_variants <= 1:
|
||||
result[cid] = [original]
|
||||
continue
|
||||
|
||||
# Use cached variants if present in the test case
|
||||
cached = case.get("user_prompts")
|
||||
if cached and isinstance(cached, list) and len(cached) >= n_variants:
|
||||
result[cid] = cached[:n_variants]
|
||||
print(
|
||||
f" {DIM}[{ci + 1}/{len(cases)}] {cid}...{RESET}"
|
||||
f" {CYAN}{len(result[cid])} cached{RESET}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Partial cache: keep existing variants, only generate the delta
|
||||
existing: list[str] = []
|
||||
if cached and isinstance(cached, list):
|
||||
existing = cached
|
||||
|
||||
needed = n_variants - max(len(existing), 1) # original is always slot 0
|
||||
if needed <= 0:
|
||||
result[cid] = (existing or [original])[:n_variants]
|
||||
print(
|
||||
f" {DIM}[{ci + 1}/{len(cases)}] {cid}...{RESET}"
|
||||
f" {CYAN}{len(result[cid])} cached{RESET}"
|
||||
)
|
||||
continue
|
||||
|
||||
print(f" {DIM}[{ci + 1}/{len(cases)}] {cid}...{RESET}", end="", flush=True)
|
||||
# Generate only the missing variants
|
||||
existing_json = json.dumps(existing[1:]) if len(existing) > 1 else "[]"
|
||||
user_content = f"Original prompt: {json.dumps(original)}\n\n"
|
||||
if len(existing) > 1:
|
||||
user_content += f"Existing variations (do NOT repeat these): {existing_json}\n\n"
|
||||
user_content += (
|
||||
f"Generate {needed} new paraphrased variations of the original prompt. "
|
||||
f"Output a JSON array of {needed} strings."
|
||||
)
|
||||
|
||||
try:
|
||||
cr = prov.create_completion(
|
||||
client=client,
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": DIVERSIFIER_SYSTEM},
|
||||
{"role": "user", "content": user_content},
|
||||
],
|
||||
max_tokens=2048,
|
||||
temperature=0.8,
|
||||
reasoning_effort="low",
|
||||
)
|
||||
raw = (cr.content or "").strip()
|
||||
# Strip reasoning tags
|
||||
raw = re.sub(
|
||||
r"<(?:think|reasoning)>.*?</(?:think|reasoning)>",
|
||||
"",
|
||||
raw,
|
||||
flags=re.DOTALL,
|
||||
).strip()
|
||||
# Strip markdown fences
|
||||
fence_match = re.search(r"```[^\n]*\n(.*?)```", raw, re.DOTALL)
|
||||
if fence_match:
|
||||
raw = fence_match.group(1).strip()
|
||||
|
||||
new_variants = json.loads(raw)
|
||||
if isinstance(new_variants, list) and all(isinstance(v, str) for v in new_variants):
|
||||
# Merge existing + new, then deduplicate
|
||||
base = existing if existing else [original]
|
||||
seen: set[str] = {v.strip().lower() for v in base}
|
||||
unique: list[str] = list(base)
|
||||
dupes = 0
|
||||
for v in new_variants[:needed]:
|
||||
key = v.strip().lower()
|
||||
if key in seen:
|
||||
dupes += 1
|
||||
else:
|
||||
seen.add(key)
|
||||
unique.append(v)
|
||||
result[cid] = unique
|
||||
dupe_note = f", {dupes} dupes removed" if dupes else ""
|
||||
short_note = (
|
||||
f" {YELLOW}(< {n_variants} requested, will cycle){RESET}"
|
||||
if len(unique) < n_variants
|
||||
else ""
|
||||
)
|
||||
print(f" {GREEN}{len(unique)} unique{dupe_note}{RESET}{short_note}")
|
||||
else:
|
||||
result[cid] = [original]
|
||||
print(f" {YELLOW}parse error, using original{RESET}")
|
||||
except Exception as e:
|
||||
_log(f"\n Diversifier failed for {cid}: {e}", dim=True)
|
||||
result[cid] = [original]
|
||||
|
||||
# Summary statistics
|
||||
total_variants = sum(len(v) for v in result.values())
|
||||
cases_with_variants = sum(1 for v in result.values() if len(v) > 1)
|
||||
avg_variants = total_variants / len(result) if result else 0
|
||||
print(
|
||||
f" {total_variants} total variants across {cases_with_variants} cases"
|
||||
f" (avg {avg_variants:.1f}/case)"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _observe_and_update_optimizer(
|
||||
client: Any,
|
||||
model: str,
|
||||
@@ -1303,6 +1535,310 @@ def _observe_and_update_optimizer(
|
||||
return result
|
||||
|
||||
|
||||
def _classify_failure(
|
||||
run: dict[str, Any],
|
||||
expected_actions: list[dict[str, Any]],
|
||||
) -> str:
|
||||
"""Classify a failed run into a failure mode bucket."""
|
||||
tools = run.get("tool_sequence", [])
|
||||
if run.get("json_dump"):
|
||||
return "json_dump"
|
||||
if not tools:
|
||||
return "no_tool_call"
|
||||
detail = run.get("detail", "")
|
||||
if "timed out" in detail.lower():
|
||||
return "timeout"
|
||||
if detail.startswith("Error:"):
|
||||
return "error"
|
||||
|
||||
unmatched = run.get("unmatched", [])
|
||||
extra = run.get("extra_tools", [])
|
||||
matched = run.get("matched", [])
|
||||
|
||||
if not unmatched and extra:
|
||||
return "extra_tools"
|
||||
|
||||
# Check for tool substitution — expected one tool, got a different one
|
||||
expected_names = {expected_actions[i]["tool"] for i in unmatched if i < len(expected_actions)}
|
||||
actual_names = set(tools)
|
||||
if expected_names and not expected_names & actual_names:
|
||||
return "wrong_tool"
|
||||
|
||||
# Check if right tools were called but args didn't match
|
||||
if matched and unmatched:
|
||||
unmatched_expected = {
|
||||
expected_actions[i]["tool"] for i in unmatched if i < len(expected_actions)
|
||||
}
|
||||
if unmatched_expected & actual_names:
|
||||
return "wrong_args"
|
||||
|
||||
return "missing_tool"
|
||||
|
||||
|
||||
def _build_failure_analysis(
|
||||
iteration_result: dict[str, Any],
|
||||
test_cases: list[dict[str, Any]],
|
||||
) -> str:
|
||||
"""Build a semantic failure analysis summary across all cases."""
|
||||
# Classify every failed run
|
||||
mode_cases: dict[str, list[str]] = {} # mode -> [case_id, ...]
|
||||
mode_details: dict[str, list[str]] = {} # mode -> [detail strings]
|
||||
consistency: dict[str, str] = {} # case_id -> systematic|flaky|marginal
|
||||
|
||||
for case_id, case_result in iteration_result["cases"].items():
|
||||
case_def = next((c for c in test_cases if c["id"] == case_id), None)
|
||||
expected = case_def.get("expected_actions", []) if case_def else []
|
||||
pr = case_result["pass_rate"]
|
||||
|
||||
if pr == 1.0:
|
||||
continue
|
||||
|
||||
# Consistency classification
|
||||
if pr == 0:
|
||||
consistency[case_id] = "systematic"
|
||||
elif pr < 0.8:
|
||||
consistency[case_id] = "flaky"
|
||||
else:
|
||||
consistency[case_id] = "marginal"
|
||||
|
||||
for run in case_result["runs"]:
|
||||
if run.get("pass"):
|
||||
continue
|
||||
mode = _classify_failure(run, expected)
|
||||
mode_cases.setdefault(mode, [])
|
||||
if case_id not in mode_cases[mode]:
|
||||
mode_cases[mode].append(case_id)
|
||||
|
||||
# Build a detail string for tool substitution cases
|
||||
if mode == "wrong_tool":
|
||||
expected_names = [
|
||||
expected[i]["tool"] for i in run.get("unmatched", []) if i < len(expected)
|
||||
]
|
||||
actual = run.get("tool_sequence", [])
|
||||
detail = f"{case_id}: expected {expected_names}, got {actual}"
|
||||
mode_details.setdefault(mode, []).append(detail)
|
||||
|
||||
if not mode_cases:
|
||||
return ""
|
||||
|
||||
# Build the summary
|
||||
lines: list[str] = []
|
||||
|
||||
# Failure mode summary
|
||||
mode_labels = {
|
||||
"no_tool_call": "Text-only response (no tool call made)",
|
||||
"wrong_tool": "Wrong tool selected",
|
||||
"missing_tool": "Missing required tool in sequence",
|
||||
"wrong_args": "Right tool, wrong arguments",
|
||||
"extra_tools": "Correct sequence but unnecessary extra calls",
|
||||
"timeout": "Timed out",
|
||||
"error": "Tool execution error",
|
||||
"json_dump": "Tool call emitted as JSON text instead of function call",
|
||||
}
|
||||
for mode, cases in sorted(mode_cases.items(), key=lambda x: -len(x[1])):
|
||||
label = mode_labels.get(mode, mode)
|
||||
lines.append(f"- {label}: {', '.join(cases)}")
|
||||
for detail in (mode_details.get(mode, []))[:3]:
|
||||
lines.append(f" {detail}")
|
||||
|
||||
# Consistency summary
|
||||
systematic = [c for c, v in consistency.items() if v == "systematic"]
|
||||
flaky = [c for c, v in consistency.items() if v == "flaky"]
|
||||
marginal = [c for c, v in consistency.items() if v == "marginal"]
|
||||
|
||||
if systematic or flaky or marginal:
|
||||
lines.append("")
|
||||
if systematic:
|
||||
lines.append(
|
||||
f"Systematic failures (always fail, need new instruction): {', '.join(systematic)}"
|
||||
)
|
||||
if flaky:
|
||||
lines.append(
|
||||
f"Flaky failures (sometimes pass, prompt is ambiguous): {', '.join(flaky)}"
|
||||
)
|
||||
if marginal:
|
||||
lines.append(f"Marginal failures (mostly pass, minor edge case): {', '.join(marginal)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
_ANALYST_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "math",
|
||||
"description": (
|
||||
"Execute Python code for analysis. Available: numpy, scipy, "
|
||||
"collections, itertools, math, json, re. Use print() for output. "
|
||||
"Example: print(numpy.mean([0.8, 0.6, 1.0]))"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "Python code to execute."},
|
||||
},
|
||||
"required": ["code"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "bash",
|
||||
"description": "Execute a bash command. Use for jq, awk, sort, uniq, etc.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "Bash command to execute."},
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _exec_analyst_tool(name: str, arguments: str) -> str:
|
||||
"""Execute a tool call from the analyst agent."""
|
||||
try:
|
||||
args = json.loads(arguments)
|
||||
except json.JSONDecodeError:
|
||||
return f"Invalid JSON arguments: {arguments[:200]}"
|
||||
|
||||
if name == "math":
|
||||
from turnstone.core.sandbox import execute_math_sandboxed
|
||||
|
||||
output, is_error = execute_math_sandboxed(args.get("code", ""), timeout=15.0)
|
||||
return output[:4000]
|
||||
elif name == "bash":
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["bash", "-c", args.get("command", "")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
output = proc.stdout + proc.stderr
|
||||
return output[:4000] or "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Command timed out after 15s"
|
||||
return f"Unknown tool: {name}"
|
||||
|
||||
|
||||
def _run_analyst(
|
||||
client: Any,
|
||||
model: str,
|
||||
test_cases: list[dict[str, Any]],
|
||||
iteration_result: dict[str, Any],
|
||||
provider: LLMProvider | None = None,
|
||||
) -> str:
|
||||
"""Run the analyst agent to produce a semantic failure analysis.
|
||||
|
||||
Multi-turn agent with math and bash tools for computing statistics.
|
||||
Phase 1 of the two-phase optimization: analyst diagnoses patterns,
|
||||
then optimizer uses the diagnosis to modify the prompt.
|
||||
"""
|
||||
# Build structured input: per-case results with rule-based pre-analysis
|
||||
case_parts: list[str] = []
|
||||
for case_id, case_result in iteration_result["cases"].items():
|
||||
case_def = next((c for c in test_cases if c["id"] == case_id), None)
|
||||
if not case_def:
|
||||
continue
|
||||
pr = case_result["pass_rate"]
|
||||
status = "PASS" if pr == 1.0 else "WEAK" if pr >= 0.5 else "FAIL"
|
||||
|
||||
part = (
|
||||
f"[{status}] {case_id} (pass_rate={pr:.0%})\n"
|
||||
f" User prompt: {case_def['user_prompt']}\n"
|
||||
f" Expected tools: {json.dumps(case_def.get('expected_actions', []))}\n"
|
||||
f" Actual sequences: "
|
||||
f"{[r.get('tool_sequence', []) for r in case_result['runs']]}"
|
||||
)
|
||||
|
||||
# Add per-run failure classifications
|
||||
expected = case_def.get("expected_actions", [])
|
||||
failed_runs = [r for r in case_result["runs"] if not r.get("pass")]
|
||||
if failed_runs:
|
||||
modes = [_classify_failure(r, expected) for r in failed_runs]
|
||||
part += f"\n Failure modes: {modes}"
|
||||
|
||||
case_parts.append(part)
|
||||
|
||||
# Include rule-based pre-analysis as a starting point
|
||||
rule_analysis = _build_failure_analysis(iteration_result, test_cases)
|
||||
|
||||
user_content = "## Test Results\n" + "\n\n".join(case_parts)
|
||||
if rule_analysis:
|
||||
user_content += f"\n\n## Rule-Based Pre-Analysis\n{rule_analysis}"
|
||||
user_content += (
|
||||
"\n\nAnalyze the semantic patterns across these results. "
|
||||
"Focus on WHY failures happen and what passing cases have in common. "
|
||||
"Use the math or bash tools if you need to compute statistics, "
|
||||
"build confusion matrices, or analyze distributions."
|
||||
)
|
||||
|
||||
prov = provider or create_provider("openai")
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": ANALYST_SYSTEM},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
# Multi-turn loop: let the analyst call tools up to 5 rounds
|
||||
max_turns = 5
|
||||
for _turn in range(max_turns):
|
||||
cr = prov.create_completion(
|
||||
client=client,
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=_ANALYST_TOOLS,
|
||||
max_tokens=4096,
|
||||
temperature=0.3,
|
||||
reasoning_effort="medium",
|
||||
)
|
||||
|
||||
assistant_msg: dict[str, Any] = {
|
||||
"role": "assistant",
|
||||
"content": cr.content or None,
|
||||
}
|
||||
if cr.tool_calls:
|
||||
assistant_msg["tool_calls"] = cr.tool_calls[:5]
|
||||
messages.append(assistant_msg)
|
||||
|
||||
if not cr.tool_calls:
|
||||
break
|
||||
|
||||
# Execute tool calls
|
||||
for tc in assistant_msg["tool_calls"]:
|
||||
func_name = tc["function"]["name"]
|
||||
output = _exec_analyst_tool(func_name, tc["function"]["arguments"])
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tc["id"],
|
||||
"content": output,
|
||||
}
|
||||
)
|
||||
|
||||
# Extract final text response
|
||||
result = ""
|
||||
for msg in reversed(messages):
|
||||
if msg["role"] == "assistant" and msg.get("content"):
|
||||
result = msg["content"]
|
||||
break
|
||||
|
||||
# Strip reasoning tags if present
|
||||
result = re.sub(
|
||||
r"<(?:think|reasoning)>.*?</(?:think|reasoning)>",
|
||||
"",
|
||||
result,
|
||||
flags=re.DOTALL,
|
||||
).strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _propose_prompt_modification(
|
||||
client: Any,
|
||||
model: str,
|
||||
@@ -1313,6 +1849,7 @@ def _propose_prompt_modification(
|
||||
optimizer_system: str = OPTIMIZER_SYSTEM,
|
||||
provider: LLMProvider | None = None,
|
||||
parent_scores: dict[str, float] | None = None,
|
||||
analyst_output: str = "",
|
||||
) -> str:
|
||||
"""Use the model to propose a new prompt based on evaluation results."""
|
||||
# Build summary of results
|
||||
@@ -1364,6 +1901,11 @@ def _propose_prompt_modification(
|
||||
+ "\n\n".join(failing)
|
||||
+ "\n\n"
|
||||
)
|
||||
|
||||
# Failure analysis from analyst agent (phase 1)
|
||||
if analyst_output:
|
||||
user_content += f"## Failure Analysis (from analyst)\n{analyst_output}\n\n"
|
||||
|
||||
user_content += (
|
||||
f"## Score History\n{history_text}\n\n"
|
||||
"Make the MINIMUM change needed to fix failing tests without "
|
||||
@@ -1555,12 +2097,26 @@ def _print_summary_table(iter_result: dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _append_summary_tsv(path: str, iter_result: dict[str, Any], case_ids: list[str]) -> None:
|
||||
def _append_summary_tsv(
|
||||
path: str,
|
||||
iter_result: dict[str, Any],
|
||||
case_ids: list[str],
|
||||
cumulative_tokens: int = 0,
|
||||
node_score: float = 0.0,
|
||||
) -> None:
|
||||
"""Append one row per iteration to a TSV summary file."""
|
||||
write_header = not os.path.exists(path) or os.path.getsize(path) == 0
|
||||
agg = iter_result.get("aggregate", {})
|
||||
per_case = agg.get("per_case_pass_rates", {})
|
||||
|
||||
# Compute iteration elapsed from individual run times
|
||||
iter_elapsed = sum(
|
||||
r.get("elapsed", 0)
|
||||
for cr in iter_result.get("cases", {}).values()
|
||||
for r in cr.get("runs", [])
|
||||
)
|
||||
prompt_len = len(iter_result.get("prompt", ""))
|
||||
|
||||
with open(path, "a") as f:
|
||||
if write_header:
|
||||
cols = [
|
||||
@@ -1571,18 +2127,28 @@ def _append_summary_tsv(path: str, iter_result: dict[str, Any], case_ids: list[s
|
||||
"runs",
|
||||
"json_dumps",
|
||||
"tree_node",
|
||||
"node_score",
|
||||
"elapsed_s",
|
||||
"prompt_len",
|
||||
"iter_tokens",
|
||||
"cumul_tokens",
|
||||
] + [f"case:{cid}" for cid in case_ids]
|
||||
f.write("\t".join(cols) + "\n")
|
||||
|
||||
vals = [
|
||||
str(iter_result.get("iteration", "")),
|
||||
iter_result.get("timestamp", ""),
|
||||
f"{agg.get('overall_pass_rate', 0):.2f}",
|
||||
f"{agg.get('overall_avg_score', 0):.2f}",
|
||||
f"{agg.get('overall_pass_rate', 0):.4f}",
|
||||
f"{agg.get('overall_avg_score', 0):.4f}",
|
||||
str(agg.get("total_runs", 0)),
|
||||
str(agg.get("json_dumps", 0)),
|
||||
str(iter_result.get("tree_node_id", "")),
|
||||
] + [f"{per_case.get(cid, 0):.2f}" for cid in case_ids]
|
||||
f"{node_score:.4f}",
|
||||
f"{iter_elapsed:.1f}",
|
||||
str(prompt_len),
|
||||
str(agg.get("total_tokens", 0)),
|
||||
str(cumulative_tokens),
|
||||
] + [f"{per_case.get(cid, 0):.4f}" for cid in case_ids]
|
||||
f.write("\t".join(vals) + "\n")
|
||||
|
||||
|
||||
@@ -1610,6 +2176,12 @@ def run_optimization(
|
||||
optimizer_model: str | None = None,
|
||||
observer_base_url: str | None = None,
|
||||
observer_model: str | None = None,
|
||||
analyst_base_url: str | None = None,
|
||||
analyst_model: str | None = None,
|
||||
diversifier_base_url: str | None = None,
|
||||
diversifier_model: str | None = None,
|
||||
diversify: int = 0,
|
||||
save_variants: bool = False,
|
||||
explore_constant: float = 1.414,
|
||||
) -> dict[str, Any]:
|
||||
"""Main optimization loop with UCB tree search.
|
||||
@@ -1658,11 +2230,33 @@ def run_optimization(
|
||||
obs_key = os.environ.get(obs_key_env, opt_key)
|
||||
obs_client, obs_provider = _make_client_and_provider(obs_base, obs_key)
|
||||
|
||||
# Log role assignments if any differ from the test model
|
||||
if opt_model != model or opt_base != base_url:
|
||||
_log(f" Optimizer: {opt_model} @ {opt_base}", dim=True)
|
||||
# --- Analyst model (inherits from optimizer if not specified) ---
|
||||
ana_base = analyst_base_url or opt_base
|
||||
ana_model = analyst_model or opt_model
|
||||
ana_key_env = (
|
||||
"ANTHROPIC_API_KEY" if _detect_provider(ana_base) == "anthropic" else "OPENAI_API_KEY"
|
||||
)
|
||||
ana_key = os.environ.get(ana_key_env, opt_key)
|
||||
ana_client, ana_provider = _make_client_and_provider(ana_base, ana_key)
|
||||
|
||||
# --- Diversifier model (inherits from optimizer if not specified) ---
|
||||
div_base = diversifier_base_url or opt_base
|
||||
div_model = diversifier_model or opt_model
|
||||
div_key_env = (
|
||||
"ANTHROPIC_API_KEY" if _detect_provider(div_base) == "anthropic" else "OPENAI_API_KEY"
|
||||
)
|
||||
div_key = os.environ.get(div_key_env, opt_key)
|
||||
div_client, div_provider = _make_client_and_provider(div_base, div_key)
|
||||
|
||||
# Log role assignments
|
||||
_log(f" Test model: {model} @ {base_url}", dim=True)
|
||||
_log(f" Optimizer: {opt_model} @ {opt_base}", dim=True)
|
||||
if obs_model != opt_model or obs_base != opt_base:
|
||||
_log(f" Observer: {obs_model} @ {obs_base}", dim=True)
|
||||
_log(f" Observer: {obs_model} @ {obs_base}", dim=True)
|
||||
if ana_model != opt_model or ana_base != opt_base:
|
||||
_log(f" Analyst: {ana_model} @ {ana_base}", dim=True)
|
||||
if diversify > 0 and (div_model != opt_model or div_base != opt_base):
|
||||
_log(f" Diversifier: {div_model} @ {div_base}", dim=True)
|
||||
|
||||
# Load test cases
|
||||
with open(test_file) as f:
|
||||
@@ -1678,6 +2272,12 @@ def run_optimization(
|
||||
# Precedence: CLI arg (non-None) > tests.json defaults > code default (3)
|
||||
resolved_n_runs: int = n_runs if n_runs is not None else int(defaults.get("n_runs", 3))
|
||||
|
||||
total_runs = sum(c.get("n_runs", resolved_n_runs) for c in cases)
|
||||
print(
|
||||
f" {len(cases)} cases, {resolved_n_runs} runs/case ({total_runs} total), "
|
||||
f"max {max_iterations} iterations"
|
||||
)
|
||||
|
||||
# Holdout split — holdout cases are evaluated but excluded from optimizer feedback
|
||||
holdout_ids: set[str] = {c["id"] for c in cases if c.get("holdout", False)}
|
||||
training_count = len(cases) - len(holdout_ids)
|
||||
@@ -1687,6 +2287,11 @@ def run_optimization(
|
||||
dim=True,
|
||||
)
|
||||
holdout_ids = set()
|
||||
elif holdout_ids:
|
||||
_log(
|
||||
f" Holdout: {len(holdout_ids)} cases ({', '.join(sorted(holdout_ids))})",
|
||||
dim=True,
|
||||
)
|
||||
|
||||
# Get initial prompt
|
||||
if initial_prompt is None:
|
||||
@@ -1731,6 +2336,8 @@ def run_optimization(
|
||||
"optimizer_base_url": opt_base,
|
||||
"observer_model": obs_model,
|
||||
"observer_base_url": obs_base,
|
||||
"analyst_model": ana_model,
|
||||
"analyst_base_url": ana_base,
|
||||
"started": datetime.now().isoformat(),
|
||||
"test_suite": test_file,
|
||||
"n_runs_default": resolved_n_runs,
|
||||
@@ -1744,6 +2351,52 @@ def run_optimization(
|
||||
tsv_path = os.path.splitext(output_file)[0] + ".tsv"
|
||||
case_ids = [c["id"] for c in cases]
|
||||
|
||||
# Diversify prompts — generate paraphrased variants before the loop
|
||||
# Auto-detect: if any case has cached user_prompts, use them even without --diversify
|
||||
prompt_variants: dict[str, list[str]] | None = None
|
||||
if diversify == 0:
|
||||
cached_variants = {
|
||||
c["id"]: c["user_prompts"]
|
||||
for c in cases
|
||||
if isinstance(c.get("user_prompts"), list) and len(c["user_prompts"]) > 1
|
||||
}
|
||||
if cached_variants:
|
||||
prompt_variants = cached_variants
|
||||
total_v = sum(len(v) for v in cached_variants.values())
|
||||
print(
|
||||
f"\n Using cached variants for {len(cached_variants)} cases"
|
||||
f" ({total_v} total prompts)"
|
||||
)
|
||||
|
||||
if diversify > 0:
|
||||
print(f"\nGenerating {diversify} prompt variants per case...")
|
||||
prompt_variants = _diversify_prompts(
|
||||
client=div_client,
|
||||
model=div_model,
|
||||
cases=cases,
|
||||
n_variants=diversify,
|
||||
provider=div_provider,
|
||||
)
|
||||
results["meta"]["diversify"] = diversify
|
||||
results["meta"]["prompt_variants"] = prompt_variants
|
||||
|
||||
# Save variants back to test suite JSON for caching
|
||||
if save_variants:
|
||||
updated = False
|
||||
for case in cases:
|
||||
cid = case["id"]
|
||||
variants = prompt_variants.get(cid, [])
|
||||
if len(variants) > 1 and case.get("user_prompts") != variants:
|
||||
case["user_prompts"] = variants
|
||||
updated = True
|
||||
if updated:
|
||||
suite["cases"] = cases
|
||||
with open(test_file, "w") as f:
|
||||
json.dump(suite, f, indent=2)
|
||||
f.write("\n")
|
||||
print(f" Saved variants to {test_file}")
|
||||
|
||||
cumulative_tokens = 0
|
||||
suite_t0 = time.monotonic()
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
@@ -1760,8 +2413,16 @@ def run_optimization(
|
||||
selected_id = _ucb_select(nodes, explore_constant)
|
||||
selected = nodes[selected_id]
|
||||
|
||||
tree_info = f"node {selected_id}"
|
||||
if selected.visit_count > 0:
|
||||
tree_info += f", score={selected.score:.0%}, visits={selected.visit_count}"
|
||||
else:
|
||||
tree_info += ", unvisited"
|
||||
if len(nodes) > 1:
|
||||
tree_info += f", tree={len(nodes)} nodes"
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Iteration {iteration} (node {selected_id}, visits={selected.visit_count})")
|
||||
print(f"Iteration {iteration} ({tree_info})")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
iter_result = _run_iteration(
|
||||
@@ -1780,6 +2441,7 @@ def run_optimization(
|
||||
parallel=parallel,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
prompt_variants=prompt_variants,
|
||||
)
|
||||
iter_result["iteration"] = iteration
|
||||
iter_result["prompt"] = selected.prompt
|
||||
@@ -1790,6 +2452,7 @@ def run_optimization(
|
||||
|
||||
# Update node score (rolling mean)
|
||||
new_score = _compute_holdout_score(iter_result, holdout_ids)
|
||||
old_score = selected.score
|
||||
if selected.visit_count == 0:
|
||||
selected.score = new_score
|
||||
else:
|
||||
@@ -1797,6 +2460,14 @@ def run_optimization(
|
||||
selected.visit_count + 1
|
||||
)
|
||||
selected.visit_count += 1
|
||||
if selected.visit_count > 1:
|
||||
_log(
|
||||
f" Node {selected_id} score: {old_score:.0%} → {selected.score:.0%}"
|
||||
f" (this eval: {new_score:.0%})",
|
||||
dim=True,
|
||||
)
|
||||
|
||||
cumulative_tokens += iter_result.get("aggregate", {}).get("total_tokens", 0)
|
||||
|
||||
results["iterations"].append(iter_result)
|
||||
|
||||
@@ -1806,7 +2477,13 @@ def run_optimization(
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
_print_summary_table(iter_result)
|
||||
_append_summary_tsv(tsv_path, iter_result, case_ids)
|
||||
_append_summary_tsv(
|
||||
tsv_path,
|
||||
iter_result,
|
||||
case_ids,
|
||||
cumulative_tokens=cumulative_tokens,
|
||||
node_score=selected.score,
|
||||
)
|
||||
|
||||
# Check if all passing
|
||||
agg = iter_result["aggregate"]
|
||||
@@ -1854,6 +2531,28 @@ def run_optimization(
|
||||
holdout_ids,
|
||||
)
|
||||
|
||||
# Phase 1: Analyst diagnoses semantic patterns
|
||||
analyst_output = ""
|
||||
if opt_result["aggregate"].get("overall_pass_rate", 0) < 1.0:
|
||||
print("\nAnalyzing failures...")
|
||||
try:
|
||||
analyst_output = _run_analyst(
|
||||
client=ana_client,
|
||||
model=ana_model,
|
||||
test_cases=opt_cases,
|
||||
iteration_result=opt_result,
|
||||
provider=ana_provider,
|
||||
)
|
||||
if analyst_output:
|
||||
_log(f" Analyst:\n{analyst_output}", dim=True)
|
||||
except Exception as e:
|
||||
_log(f" Analyst failed: {e}", dim=True)
|
||||
|
||||
# Store analyst output before optimizer (survives optimizer failure)
|
||||
if analyst_output:
|
||||
iter_result["analyst"] = analyst_output
|
||||
|
||||
# Phase 2: Optimizer modifies prompt using analyst diagnosis
|
||||
print("\nOptimizing prompt...")
|
||||
try:
|
||||
new_prompt = _propose_prompt_modification(
|
||||
@@ -1866,6 +2565,7 @@ def run_optimization(
|
||||
optimizer_system=current_optimizer_system,
|
||||
provider=opt_provider,
|
||||
parent_scores=parent_scores,
|
||||
analyst_output=analyst_output,
|
||||
)
|
||||
except Exception as e:
|
||||
_log(f" Prompt modification failed: {e}", dim=True)
|
||||
@@ -1889,6 +2589,11 @@ def run_optimization(
|
||||
nodes[next_node_id] = child
|
||||
selected.children.append(next_node_id)
|
||||
iter_result["tree_child_id"] = next_node_id
|
||||
_log(
|
||||
f" Created node {next_node_id} (child of {selected_id},"
|
||||
f" tree={len(nodes)} nodes)",
|
||||
dim=True,
|
||||
)
|
||||
next_node_id += 1
|
||||
|
||||
# Re-write with tree update and diff
|
||||
@@ -1969,6 +2674,37 @@ def main() -> None:
|
||||
default=None,
|
||||
help="Base URL for observer model (default: same as --optimizer-base-url)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--analyst-model",
|
||||
default=None,
|
||||
help="Model for failure analysis (default: same as --optimizer-model)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--analyst-base-url",
|
||||
default=None,
|
||||
help="Base URL for analyst model (default: same as --optimizer-base-url)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diversifier-model",
|
||||
default=None,
|
||||
help="Model for prompt diversification (default: same as --optimizer-model)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diversifier-base-url",
|
||||
default=None,
|
||||
help="Base URL for diversifier model (default: same as --optimizer-base-url)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diversify",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Generate N prompt variants per test case (0=disabled, includes original)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save-variants",
|
||||
action="store_true",
|
||||
help="Save generated variants back to test suite JSON for caching",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt",
|
||||
default=None,
|
||||
@@ -2090,6 +2826,12 @@ def main() -> None:
|
||||
optimizer_model=args.optimizer_model,
|
||||
observer_base_url=args.observer_base_url,
|
||||
observer_model=args.observer_model,
|
||||
analyst_base_url=args.analyst_base_url,
|
||||
analyst_model=args.analyst_model,
|
||||
diversifier_base_url=args.diversifier_base_url,
|
||||
diversifier_model=args.diversifier_model,
|
||||
diversify=args.diversify,
|
||||
save_variants=args.save_variants,
|
||||
explore_constant=args.explore_constant,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user