Reorganise the repository around three layout rules - #2
Conversation
Snapshot of the uncommitted working tree before the repository layout change, so the reorganisation lands as a reviewable move-only diff rather than being tangled with unrelated source edits. Covers the orchestrator surface (team_evaluation, summariser, train_orchestrator, orchestration/orchestrator), the shared answer evaluator, and the heterogeneous-agent runner script. No behaviour is reviewed or corrected here; correctness fixes follow separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tree had accumulated executable Python at the root, generated output in seven places, and ignore rules that made a required input uncommittable. Establish three rules and move everything to match: Python under src/, shell runners under scripts/, generated output under results/. Moves: K_star_analysis/*.py -> src/analysis/k_star/ K_star_analysis/*.sh -> scripts/k_star/ overlap-viz.py -> src/analysis/overlap_viz.py chosen-agents.py -> src/analysis/agent_selection_sweep.py orchestrator_test.py -> src/analysis/orchestrator_pool_sweep.py personas.json -> configs/ Untitled.ipynb -> notebooks/exploration.ipynb viz/, csv/, task_plots/, root PNGs, root JSON result dumps, overall-performance.pdf -> results/ The three moved analysis scripts read and wrote cwd-relative paths, which only worked when they sat at the root. They now resolve their inputs and outputs from __file__, so they can be invoked from anywhere. The two K* shell runners derived their paths from their own location; they now compute a repo root and point at src/analysis/k_star/, with output under results/k_star/ instead of a directory beside the script. orchestrator_test.py is not a test. It is a standalone Azure probe carrying a second, divergent copy of the orchestrator system prompt, and a test runner would have collected it by name and executed its module-level API calls. Renamed to say what it is. .gitignore matched *.csv, *.json and *.jsonl across the whole repository. The consequence was not cosmetic: HuggingFace save_to_disk writes dataset_info.json and state.json beside the arrow file, so data/tagged_dataset/ - the input every orchestrator run reads - could not be committed and existed on one machine only. The same patterns hid configs/personas.json and .vscode/settings.json. Rules are now scoped to the directories that actually hold output, and the tagged dataset is committed. Also corrects .DS_STORE to .DS_Store and adds env/ explicitly, which until now escaped git only because venv writes its own .gitignore. .vscode/settings.json pointed python.analysis.extraPaths at ./src/orchestation, missing the r, so editor import resolution had been silently broken. Points at ./src, which the new layout makes correct. README gains a section at the top covering what the repository holds, how to set it up, and the three-stage orchestration pipeline, whose stages were previously discoverable only by reading the source. The stale project structure tree and the K_star_analysis invocation paths are updated. The orchestrator section carries a status note: stage 3 is not yet ready for experiments. Left alone deliberately: out/ and out-baseline/ hold 182 MB of live debate-pipeline results and remain the --out_dir default, so they keep their location; the .ipynb_checkpoints directories under src/ contain autosaves that differ from the live files, including a 896-line model_utils_copy with no counterpart, and are not mine to delete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
| temperature = config.get('temperature', getattr(current_agent, 'temperature', 1.0)) if config else getattr(current_agent, 'temperature', 1.0) | ||
| top_p = config.get('top_p', getattr(current_agent, 'top_p', 0.9)) if config else getattr(current_agent, 'top_p', 0.9) | ||
| max_new_tokens = config.get('max_new_tokens', getattr(current_agent, 'max_new_tokens', 512)) if config else getattr(current_agent, 'max_new_tokens', 512) | ||
| max_new_tokens = config.get('max_new_tokens', getattr(current_agent, 'max_new_tokens', 2048)) if config else getattr(current_agent, 'max_new_tokens', 512) |
There was a problem hiding this comment.
why are there two different max_new_tokens numbers here? they should be the same and the default should be put as a global constant to remove the magic number
There was a problem hiding this comment.
Fixed. The two were 2048 (when a persona config was present) and 512 (when not), so an agent's token budget depended on whether it had a persona at all. Both now come from MAX_NEW_TOKENS in the new src/defaults.py, which matches the --max_new_tokens argparse default get_agents was already using.
| @@ -34,7 +34,7 @@ def _run_one(current_agent, msg, config=None): | |||
| # Get generation parameters: prefer config, fall back to agent defaults | |||
| temperature = config.get('temperature', getattr(current_agent, 'temperature', 1.0)) if config else getattr(current_agent, 'temperature', 1.0) | |||
There was a problem hiding this comment.
magic numbers here too on the temperature and top_p
There was a problem hiding this comment.
Fixed — TEMPERATURE and TOP_P in src/defaults.py. Worth noting these had drifted too: engine fell back to 1.0, but get_agents used 0 for the same parameter, so the effective temperature depended on which code path set it. Both now use the constant.
Per-persona temperature/top_p values inside the persona definitions are left as literals — those are persona data rather than defaults.
| max_tokens=max_new_tokens, | ||
| temperature=temperature, | ||
| top_p=top_p, | ||
| extra_body={"thinking_token_budget": 1024}, |
There was a problem hiding this comment.
more magic numbers, should do a pass, if possible it should be added as an argument in the parseargs with a default
There was a problem hiding this comment.
Done, and promoted to a flag. thinking_token_budget was hard-coded in three places with two different values (1024 in the agent engine, 2048 in the orchestrator and summariser). It is now --thinking_token_budget on main.py, tag_questions.py and train_orchestrator.py, reaching agents as an attribute set by get_agents and the orchestrator via its constructor. Precedence follows the other generation params: persona config → agent attribute → default.
| model=orchestrator.model_name, | ||
| messages=messages, | ||
| max_tokens=4096, | ||
| max_tokens=8192, |
There was a problem hiding this comment.
more magic numbers to clean up
There was a problem hiding this comment.
Fixed — SUMMARISER_MAX_TOKENS, SUMMARISER_TEMPERATURE and THINKING_TOKEN_BUDGET in src/defaults.py.
| fallback.append("```json\n" + json.dumps(summary_items, indent=2, default=str) + "\n```") | ||
| md_content = "\n\n".join(fallback) | ||
| print(f"Summariser call failed: {e}") | ||
| # fallback = [] |
There was a problem hiding this comment.
let's just delete this fallback printing and do a retry on the summariser (up to a number of times before actually failing)
There was a problem hiding this comment.
Done — retries up to SUMMARISER_MAX_ATTEMPTS (3) and raises if all fail.
Worth flagging what the fallback was actually doing, because it was worse than dead code. md_content was stringified before the is None guard, so str(None) produced the four-character string "None", the guard could never fire, and the atomic write then replaced the scoreboard with it. A single transient API failure wiped the accumulated per-tag performance history — the file the orchestrator reads back as its only memory. On failure the previous version is now left untouched, and the temp file is unlinked rather than leaked. The unconditional dump of the whole prompt on every iteration is gone too.
| print("=" * 60) | ||
| print(f"{agent_responses}\n") | ||
| print("=" * 60) | ||
| # print("\n" + "=" * 60) |
There was a problem hiding this comment.
print this but only on a debug flag being active
There was a problem hiding this comment.
Done — real print behind args.debug, one agent per block rather than a single dict repr.
There was a problem hiding this comment.
is this used for anything? if there's parts of the code that checks for personas based on dataset they should refer to this instead of hard-coded
There was a problem hiding this comment.
Yes — src/analysis/overlap_viz.py reads it as the canonical persona set per task, to measure how far the LLM-selected teams overlap with it.
You are right that the mapping is duplicated: _build_enhanced_personas in model_utils.py branches on args.data and hard-codes the same dataset → persona grouping. I checked whether this file can just become the source of truth, and it is close but not quite — three things block it, and each one changes experimental results, so I have not made the swap in this PR.
1. Six of seven datasets match exactly. gsm8k, pro_medicine, formal_logic, humaneval, arc, winogrande — identical name sets.
2. truthfulqa lists 10 names here, but the code only ever uses 5. The extra five are the second elif args.data in ['truthfulqa'] branch at model_utils.py:1195, which is unreachable — the branch at :1156 catches it first. So this file records an intent the code has never executed. Which set of five is the one the published TruthfulQA numbers used?
3. piqa is missing from this file but has a persona set in the code.
4. Elimination_Specialist is a genuine name collision. It is defined twice with different content — the arc version (style: elimination_science, top_p: 0.84) and the winogrande version (style: elimination, top_p: 0.9, a longer prompt). The flat bank in _build_chosen_personas keeps only one of them, so --chosen_personas Elimination_Specialist on winogrande silently gets the arc persona today. Unifying means renaming one, which changes the persona name recorded in every output file.
Everything else in the two persona banks is identical — I diffed all 44 definitions field by field and that collision is the only difference.
So the unification is worth doing and mostly mechanical, but it needs your call on (2) and (4). Happy to take it as the next PR once you say which TruthfulQA set is correct and what the two Elimination_Specialist personas should be called.
There was a problem hiding this comment.
All three resolved, and configs/personas.json now matches the code exactly for all seven datasets (verified name-by-name).
TruthfulQA → 10. The two elif args.data in ['truthfulqa'] branches are merged into one set. Their union was already exactly the 10 you had listed here, so the file was right and the code was short by five — those five had never executed, since the first branch always caught it.
Elimination_Specialist renamed. Based on the prompts: the ARC one is scientific option elimination ("find one clear flaw in each wrong option… prefer the one consistent with general scientific consensus"), the WinoGrande one is pronoun resolution ("for each candidate, list what must be true for it to be the pronoun referent"). It turned out the flat bank in _build_chosen_personas had already resolved this by calling the WinoGrande one Elimination_Based_Solver — byte-identical prompt — so I made _build_enhanced_personas match rather than invent a third name. ARC keeps Elimination_Specialist. This also fixes the silent substitution: --chosen_personas Elimination_Specialist on WinoGrande was getting the ARC persona.
PIQA is Physical Interaction QA — a physical-commonsense benchmark (which of two ways of achieving a goal actually works), the sort of thing the five personas there describe: simulating action sequences, mechanical stability, material interaction. It was never wired up, though: no src/data/piqa.py, no branch in data_utils.load_data, no case in main.py's evaluator dispatch. --data piqa could not run, so those personas were unreachable. I removed the branch. Nothing is lost — all five are still in the flat bank and reachable by name through --chosen_personas, and they are in git history if you ever add the loader.
With the file and the code now in agreement, the structural unification (deriving the per-dataset names from this file instead of duplicating them) is unblocked. One thing to know before doing it: only 5 of 45 personas in the flat bank carry nvidia_persona, and _add_nvidia_personas indexes that key unconditionally — so --persona_prompt currently only works for GSM8K and raises KeyError on every other dataset. That wants fixing as part of the same change.
There was a problem hiding this comment.
i think this fits in the src dir, we want to have all our experimental scripts in one place at the top level, and analysis can contain the scripts for looking at data/visualisation
There was a problem hiding this comment.
Moved. src/analysis/ now holds only what reads results and draws plots (k_star/, overlap_viz.py); both sweeps moved up to the top of src/.
Both also turned out to execute on import: importing agent_selection_sweep spawned its entire job queue through ThreadPoolExecutor (I hit this while verifying the move — the subprocesses died immediately because python on PATH has no torch, but it did fire), and orchestrator_pool_sweep built an OpenAI client at module scope, so it raised on import without credentials and --help did not work. Both are now behind a main guard with the client built after argument parsing.
orchestrator_pool_sweep was also writing its chosen-agents JSON to the working directory; it now writes to results/agent-selection/.
… sweeps Named constants for generation parameters ----------------------------------------- engine() disagreed with itself about max_new_tokens: 2048 when a persona config was present, 512 when it was not, so an agent's token budget depended on whether it had been given a persona. Both are now MAX_NEW_TOKENS, matching the --max_new_tokens argparse default that get_agents already used. temperature and top_p carried literal fallbacks in four places, and get_agents used 0 where engine used 1.0. All of them now come from src/defaults.py, which is the single source for MAX_NEW_TOKENS, TEMPERATURE, TOP_P and THINKING_TOKEN_BUDGET, plus the orchestrator and summariser call parameters. Per-persona temperature and top_p values are deliberately left as literals: they are persona data, not defaults. thinking_token_budget becomes a flag ------------------------------------ It was hard-coded three times with two different values (1024 in the agent engine, 2048 in the orchestrator and summariser). It is now --thinking_token_budget on main.py, tag_questions.py and train_orchestrator.py, flowing to agents as an attribute set by get_agents and to the orchestrator through its constructor. Precedence matches the other generation parameters: persona config, then agent attribute, then default. Summariser retries instead of destroying the scoreboard ------------------------------------------------------- The commented-out fallback block is gone. The call now retries up to SUMMARISER_MAX_ATTEMPTS times and raises if every attempt fails. This also removes a data-loss bug. md_content was stringified before the `is None` guard, so `str(None)` produced the four-character string "None", the guard could never fire, and a single transient API failure replaced the accumulated per-tag performance history with "None" - the file the orchestrator reads back as its only memory. On failure the previous version is now left untouched, and the temp file is cleaned up rather than leaked. The unconditional dump of the full prompt on every iteration is dropped. Agent responses print behind the debug flag ------------------------------------------- The commented-out block in team_evaluation becomes a real print gated on args.debug, one agent per block rather than a single dict repr. Experiment entry points move to the top of src/ ----------------------------------------------- src/analysis/ now holds only what reads results and draws plots - k_star/ and overlap_viz.py. The two sweeps that launch runs move up: src/analysis/agent_selection_sweep.py -> src/ src/analysis/orchestrator_pool_sweep.py -> src/ Both executed on import: importing agent_selection_sweep spawned its whole job queue through ThreadPoolExecutor, and orchestrator_pool_sweep built an OpenAI client at module scope, so it raised on import without credentials and --help could not run. Both are now behind a main guard, with the client built after argument parsing. orchestrator_pool_sweep also wrote its chosen-agents JSON to the working directory; it now writes to results/agent-selection/ like everything else that produces those files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| parser.add_argument("--model_name", type=str, default="gpt-4.1") | ||
| parser.add_argument("--num_agents", type=int, default=4) | ||
| parser.add_argument("--agent_detail", choices=["name", "single", "full"]) | ||
|
|
There was a problem hiding this comment.
we should put settings like temperature and max tokens here as arguments if relevant and same in any argparse.
There was a problem hiding this comment.
Added --temperature, --top_p, --max_completion_tokens and --repeats, and passed all four to the call. The temperature and top_p were not being set at all before, so the sweep was running at whatever the endpoint defaults to.
Same pass applied to agent_selection_sweep.py, which was pinning --max_new_tokens 1024, --num_agents 4, --data_size 100, --solver vote, --debate_rounds 0 and max_workers=10 directly into each main.py command line — all flags now. It also launched "python" rather than sys.executable, which is why its subprocesses died silently for me earlier; fixed.
| agent_list=agent_descs | ||
| )} | ||
| ], | ||
| max_completion_tokens=1024 |
There was a problem hiding this comment.
magic number, put in arguments
There was a problem hiding this comment.
Done — --max_completion_tokens, default 1024 from POOL_SWEEP_MAX_COMPLETION_TOKENS.
| output_fname = os.path.join(SELECTIONS_DIR, f"{model_name}-agents={num_agents}-{agent_detail}") | ||
|
|
||
| for task_name in tasks.keys(): | ||
| for _ in range(10): |
There was a problem hiding this comment.
this 10 should also be an argument
There was a problem hiding this comment.
Done — --repeats, default 10 from POOL_SWEEP_REPEATS.
| response = orchestrator.client.chat.completions.create( | ||
| model=orchestrator.model_name, | ||
| messages=messages, | ||
| max_tokens=SUMMARISER_MAX_TOKENS, |
There was a problem hiding this comment.
we should be use args instead of global defaults, the global defaults should be the default if no arg is passed in the argparse
There was a problem hiding this comment.
Understood, and applied throughout — src/defaults.py now exists only to supply argparse defaults, and its docstring says so.
save_evaluation_summary_with_llm takes max_tokens, temperature, max_attempts and thinking_token_budget as parameters (falling back to the orchestrator's own settings when not passed), and train_orchestrator supplies them from --summariser_max_tokens, --summariser_temperature, --summariser_max_attempts and --thinking_token_budget. OrchestratorAgent likewise takes temperature and top_p alongside max_tokens. Neither module imports a constant any more.
| if str(ROOT) not in sys.path: | ||
| sys.path.insert(0, str(ROOT)) | ||
|
|
||
| from defaults import MAX_NEW_TOKENS, THINKING_TOKEN_BUDGET |
There was a problem hiding this comment.
like this, this is good, do this instead of directly using the defaults constants in the code
There was a problem hiding this comment.
Applied as the rule everywhere. train_orchestrator now has three groups of generation flags — the agents in the selected team, the selection call, and the scoreboard rewrite — plus --iterations to replace the hard-coded 10.
The agent-facing group closes a real gap rather than just tidying: team_evaluation builds its namespace by deep-copying these args, and it had no max_new_tokens/temperature/top_p on it, so the selected team ran on whatever get_agents fell back to no matter what you configured.
I also added argparse to tag_dataset.py and analysis/overlap_viz.py, which had none at all — input paths, the tag-frequency threshold and the model/description selectors were literals in the module body.
…cument every script
Defaults belong to argparse, not to call sites
----------------------------------------------
src/defaults.py now exists only to supply argparse defaults. Modules read
the value from args or from a function parameter, so passing a flag
actually changes behaviour; importing a constant and using it inline meant
the flag silently did nothing.
summariser max_tokens, temperature, max_attempts and
thinking_token_budget become parameters, defaulting to
the orchestrator's own settings when not given.
OrchestratorAgent temperature and top_p join max_tokens and
thinking_token_budget as constructor arguments.
train_orchestrator gains --iterations (replacing a hard-coded 10) and
three groups of generation flags - one for the agents
in the selected team, one for the selection call, one
for the scoreboard rewrite - all threaded through.
The agent-facing flags also close a gap: team_evaluation
builds its namespace from these args, so until now the
selected team ran on whatever get_agents fell back to.
orchestrator_pool_sweep gains --repeats, --max_completion_tokens,
--temperature and --top_p.
agent_selection_sweep gains the run settings it was pinning into each
main.py command line, and now launches sys.executable
rather than whatever "python" resolves to.
main, tag_questions --temperature and --top_p defaults now come from
defaults.py rather than repeating the literals.
tag_dataset.py and overlap_viz.py had no argparse at all - input paths, the
tag-frequency threshold and the model/description selectors were literals
in the module body. Both now take flags, and tag_dataset is behind a main
guard like the other scripts.
configs/personas.json is now accurate
-------------------------------------
It was a hand-maintained file that had drifted from the code. All seven
datasets now match the per-dataset sets in _build_enhanced_personas exactly,
which is checked in the commit that follows this work.
truthfulqa The two `elif args.data in ['truthfulqa']` branches are merged
into one set of 10. The second branch was unreachable, so five
of those personas had never run. Their union is exactly what
personas.json already listed.
Elimination_Specialist was defined twice with different content: ARC's
scientific option elimination and WinoGrande's pronoun
resolution. The flat bank in _build_chosen_personas had already
resolved this by naming the WinoGrande one
Elimination_Based_Solver, so _build_enhanced_personas now
matches that. Before this, --chosen_personas Elimination_Specialist
on WinoGrande silently got the ARC persona.
piqa Removed. PIQA is a physical-commonsense benchmark, but there is
no loader for it - no src/data/piqa.py, no branch in
data_utils.load_data, no case in main.py's evaluator dispatch -
so --data piqa could never run and the five personas were
unreachable config. They remain available by name through
--chosen_personas, and in git history.
README documents every runnable script
--------------------------------------
A section per entry point: what it does, an example invocation, what it
writes, and its flags with defaults and purpose. Covers main.py, the three
orchestration stages, both selection sweeps, the three K* scripts,
overlap_viz, and the shell runners.
This replaces Installation, Quick Start, Running Large-Scale Experiments and
Key Arguments, which between them documented the same flags three times and
had gone stale. The known gaps are stated where they matter rather than
buried: --solver is ignored, --output_path is never written, and the selected
agents ignore --api_base_url.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
scripts/add.sh looks non-runnable after this change. GPU_LIST, GPU_COUNT, MAX_JOBS_PER_GPU, MAX_PARALLEL_JOBS, and the local GPU_ID / PHYS_GPU_ID assignments are commented out, but the scheduler and run_experiment() still dereference them.
With set -u, the script should fail on these unset variables before the experiments can run. Could the GPU definitions and local assignments be restored, or the remaining GPU scheduling / CUDA_VISIBLE_DEVICES logic be removed consistently?
Since this PR is described as an organisation-only change, this currently introduces a functional regression in the batch runner.
Repository organisation only. No behaviour changes to either pipeline — the one code change is path resolution in three analysis scripts that had to move.
Three rules
src/scripts/results/Reference inputs go in
configs/. The notebook goes innotebooks/.What moved
The three moved analysis scripts read and wrote cwd-relative paths, so they only worked from the root. They now resolve paths from
__file__and can be invoked from anywhere. The K* shell runners derived everything from their own location; they now compute a repo root, point atsrc/analysis/k_star/, and write toresults/k_star/.orchestrator_test.pyis not a test — it is a standalone Azure probe carrying a second, divergent copy of the orchestrator system prompt, andpytestwould have collected it by name and executed its module-level API calls. Renamed to say what it does.The
.gitignorefix matters more than it looksThe old rules matched
*.csv,*.jsonand*.jsonlacross the whole repository. HuggingFacesave_to_diskwritesdataset_info.jsonandstate.jsonbeside the arrow file, so:The same patterns also hid
configs/personas.jsonand.vscode/settings.jsonfrom git entirely. Rules are now scoped to the directories that actually hold output (out/,out-baseline/,results/,**/history/), and the tagged dataset is committed in this PR (699 questions, 370 KB).Also in the ignore file:
.DS_STOREcorrected to.DS_Store, andenv/added explicitly — until now the 1.3 GB venv escaped git only becausevenvwrites its own.gitignoreinside itself.Other fixes
.vscode/settings.jsonpointedpython.analysis.extraPathsat./src/orchestation— missing the r. Editor import resolution had been silently broken. Now./src.K_star_analysis/invocation paths are updated.Verified
--helpworks onmain.py,tag_questions.py,train_orchestrator.py.src/analysis/overlap_viz.pyruns end to end and writes intoresults/overlap/{csv,viz,png}— nothing leaks back to the root.data/tagged_datasetloads: 699 rows.git check-ignoreconfirms the dataset andconfigs/personas.jsonare no longer ignored, while all output directories still are.Left alone deliberately
out/andout-baseline/hold 182 MB of live debate-pipeline results and remain the--out_dirdefault, so they keep their location.results/is the convention going forward..ipynb_checkpoints/undersrc/contains autosaves that differ from the live files, including an 896-linemodel_utils_copy-checkpoint.pywith no live counterpart. Not mine to delete — worth a look before you remove them.notebooks/exploration.ipynbis committed with its outputs (812 KB). Easy to drop from the commit if you would rather it stayed untracked.Commits
Checkpoint in-progress orchestrator work— snapshots the 7 modified files that were uncommitted, so the reorganisation reads as a move-only diff rather than being tangled with unrelated source edits. No behaviour reviewed or corrected.Reorganise the repository around three layout rules— this PR.🤖 Generated with Claude Code