Spaces:
Sleeping
Sleeping
Commit ·
c8beea5
1
Parent(s): fbc6f0f
feat(cp6): CLI compare — aggregate human/LLM traces by (model, difficulty)
Browse files- proteus/cli.py +43 -0
- 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
|