irregular6612 Claude Sonnet 4.6 commited on
Commit
c8beea5
·
1 Parent(s): fbc6f0f

feat(cp6): CLI compare — aggregate human/LLM traces by (model, difficulty)

Browse files
Files changed (2) hide show
  1. proteus/cli.py +43 -0
  2. tests/cli/test_cli.py +42 -0
proteus/cli.py CHANGED
@@ -4,6 +4,7 @@ Subcommands:
4
  run Run one session and append its trace to a JSONL file.
5
  list-scenarios List the registered scenario names.
6
  replay Print a saved trace's turns, outcome, and metrics.
 
7
 
8
  All commands are network-free except ``run`` with a real provider spec; use
9
  ``--model fake:<name>`` for an offline smoke.
@@ -137,6 +138,39 @@ def _cmd_replay(args: argparse.Namespace) -> int:
137
  return 0
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def build_parser() -> argparse.ArgumentParser:
141
  """Build the top-level argument parser."""
142
  parser = argparse.ArgumentParser(
@@ -194,6 +228,15 @@ def build_parser() -> argparse.ArgumentParser:
194
  replay.add_argument("--fps", type=float, default=4.0, help="replay frames/sec")
195
  replay.set_defaults(func=_cmd_replay)
196
 
 
 
 
 
 
 
 
 
 
197
  return parser
198
 
199
 
 
4
  run Run one session and append its trace to a JSONL file.
5
  list-scenarios List the registered scenario names.
6
  replay Print a saved trace's turns, outcome, and metrics.
7
+ compare Aggregate traces by (model, difficulty) for baseline comparison.
8
 
9
  All commands are network-free except ``run`` with a real provider spec; use
10
  ``--model fake:<name>`` for an offline smoke.
 
138
  return 0
139
 
140
 
141
+ def _cmd_compare(args: argparse.Namespace) -> int:
142
+ import json
143
+
144
+ from proteus.runtime.aggregate import aggregate_traces
145
+
146
+ all_traces = []
147
+ for path in args.trace_files:
148
+ try:
149
+ all_traces.extend(read_traces(path))
150
+ except FileNotFoundError:
151
+ print(f"trace file not found: {path}", file=sys.stderr)
152
+ return 2
153
+ if not all_traces:
154
+ print("no traces to compare", file=sys.stderr)
155
+ return 1
156
+
157
+ groups = aggregate_traces(all_traces)
158
+ for (model, difficulty), info in sorted(groups.items()):
159
+ print(f"=== model={model} difficulty={difficulty} n={info['n']} ===")
160
+ for key, mean in info["metrics"].items():
161
+ print(f" {key}: {mean:.2f}")
162
+
163
+ if args.out:
164
+ serializable = {
165
+ f"{model}|{difficulty}": info
166
+ for (model, difficulty), info in groups.items()
167
+ }
168
+ with open(args.out, "w") as fh:
169
+ json.dump(serializable, fh, indent=2)
170
+ print(f"summary written to {args.out}")
171
+ return 0
172
+
173
+
174
  def build_parser() -> argparse.ArgumentParser:
175
  """Build the top-level argument parser."""
176
  parser = argparse.ArgumentParser(
 
228
  replay.add_argument("--fps", type=float, default=4.0, help="replay frames/sec")
229
  replay.set_defaults(func=_cmd_replay)
230
 
231
+ compare = sub.add_parser(
232
+ "compare", help="aggregate traces by (model, difficulty) for baseline comparison"
233
+ )
234
+ compare.add_argument("trace_files", nargs="+", help="one or more .jsonl trace files")
235
+ compare.add_argument(
236
+ "--out", default=None, help="optional JSON file to write the aggregate summary to"
237
+ )
238
+ compare.set_defaults(func=_cmd_compare)
239
+
240
  return parser
241
 
242
 
tests/cli/test_cli.py CHANGED
@@ -149,3 +149,45 @@ def test_play_handles_stdin_eof(monkeypatch, capsys):
149
  ])
150
  assert rc == 2
151
  assert "stdin" in capsys.readouterr().err.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  ])
150
  assert rc == 2
151
  assert "stdin" in capsys.readouterr().err.lower()
152
+
153
+
154
+ def test_compare_aggregates_traces(tmp_path, capsys):
155
+ out = tmp_path / "runs.jsonl"
156
+ # Two fake-model traces at the same difficulty (model id "demo").
157
+ for seed in (1, 2):
158
+ main([
159
+ "run", "--scenario", "predator_evade", "--model", "fake:demo",
160
+ "--seed", str(seed), "--play-turns", "4", "--no-probe", "--out", str(out),
161
+ ])
162
+ capsys.readouterr() # drain
163
+ rc = main(["compare", str(out)])
164
+ text = capsys.readouterr().out
165
+ assert rc == 0
166
+ assert "demo" in text and "easy" in text
167
+ assert "n=2" in text
168
+ assert "motive_reading_accuracy" in text
169
+
170
+
171
+ def test_compare_writes_summary_json(tmp_path):
172
+ import json
173
+
174
+ out = tmp_path / "runs.jsonl"
175
+ main([
176
+ "run", "--scenario", "predator_evade", "--model", "fake:demo",
177
+ "--seed", "1", "--play-turns", "4", "--no-probe", "--out", str(out),
178
+ ])
179
+ summary = tmp_path / "summary.json"
180
+ rc = main(["compare", str(out), "--out", str(summary)])
181
+ assert rc == 0
182
+ data = json.loads(summary.read_text())
183
+ assert data # non-empty aggregate
184
+ # Pin the documented "model|difficulty" key format + nested shape.
185
+ assert "demo|easy" in data
186
+ assert data["demo|easy"]["n"] == 1
187
+ assert "motive_reading_accuracy" in data["demo|easy"]["metrics"]
188
+
189
+
190
+ def test_compare_missing_file_errors(capsys):
191
+ rc = main(["compare", "/no/such/file.jsonl"])
192
+ assert rc == 2
193
+ assert "not found" in capsys.readouterr().err