codeer-cli 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/API_REFERENCE.md +29 -25
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/PKG-INFO +2 -2
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/README.md +1 -1
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/pyproject.toml +1 -1
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/eval_cmd.py +151 -19
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/eval_.py +51 -8
- codeer_cli-0.1.7/tests/test_eval_pairs.py +125 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/uv.lock +1 -1
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/.gitignore +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/__init__.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/_validate.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/agents.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/chats.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/cli.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/client.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/_util.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/agent.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/history.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/kb.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/histories.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/src/codeer_cli/parse.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/tests/test_eval_labels.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/tests/test_kb_nodes.py +0 -0
- {codeer_cli-0.1.6 → codeer_cli-0.1.7}/tests/test_kb_ranges.py +0 -0
|
@@ -164,9 +164,11 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
|
|
|
164
164
|
| `GET /eval/evaluators?wid=<ws>` | List evaluators |
|
|
165
165
|
| `PUT /eval/evaluators/{id}` | Update |
|
|
166
166
|
| `DELETE /eval/evaluators/{id}` | Delete |
|
|
167
|
-
| `POST /eval/
|
|
167
|
+
| `POST /eval/case-evaluator-infos:batch` | Read assigned evaluators/rubrics for cases |
|
|
168
|
+
| `PUT /eval/cases/{case_id}/case-evaluator-infos` | Replace assigned evaluators/rubrics for one case |
|
|
169
|
+
| `POST /eval/trigger` | Run explicit assigned `case_evaluator_pairs` pinned to `agent_history_id` |
|
|
168
170
|
| `POST /eval/stop` | Cancel running case+evaluator combo |
|
|
169
|
-
| `POST /eval/rubric` | Set/override the rubric for one (case, evaluator)
|
|
171
|
+
| `POST /eval/rubric` | Set/override the rubric for one (case, evaluator); also creates assignment |
|
|
170
172
|
| `POST /eval/rubrics/batch` | **Read** rubrics for a batch of (case, evaluator) pairs |
|
|
171
173
|
|
|
172
174
|
Eval case labels are workspace-scoped reusable objects. The case create/update
|
|
@@ -220,17 +222,17 @@ normalized `tool_calls`, `tool_calls_summary`, and `tool_total_duration_ms`;
|
|
|
220
222
|
in `eval_table_full.json`. Per-tool time is computed from `start_at/end_at`.
|
|
221
223
|
|
|
222
224
|
**`evaluator_id` is singular — one call returns results for one evaluator
|
|
223
|
-
only.**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
only.** Cases are also bound to specific evaluator assignments. To see the
|
|
226
|
+
full picture for a case, first read the case/evaluator assignments, then call
|
|
227
|
+
results once per assigned evaluator. `codeer eval run` and
|
|
228
|
+
`codeer eval rubrics` handle this automatically; if calling the API directly,
|
|
229
|
+
prefer `eval_mod.get_case_evaluator_infos(case_ids=[...])` as the source of
|
|
230
|
+
truth for which pairs should run.
|
|
231
|
+
|
|
232
|
+
Regression workflow (apply prompt change → re-run all assigned pairs → spot
|
|
233
|
+
side effects): `codeer eval run --agent <agent_id>` runs the latest
|
|
234
|
+
AgentHistory by default. For the common "many cases, one tester" flow, use
|
|
235
|
+
`codeer eval run --agent <agent_id> --cases <ids> --evaluator <evaluator_id>`.
|
|
234
236
|
|
|
235
237
|
## Stage 7 — Publish
|
|
236
238
|
|
|
@@ -305,10 +307,12 @@ analytics/column name, `question` is the user-facing prompt.
|
|
|
305
307
|
### 3. The `Standard` shown in Test Suite is a per-(case, evaluator) rubric
|
|
306
308
|
|
|
307
309
|
`POST /eval/cases` has a `rubric` field — this is NOT what the Test Suite's
|
|
308
|
-
`Standard` column reads. That column is populated by `
|
|
309
|
-
on `(evaluation_case_id, evaluator_id)`.
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
`Standard` column reads. That column is populated by `CaseEvaluatorInfo` keyed
|
|
311
|
+
on `(evaluation_case_id, evaluator_id)`. The same row is also the assignment
|
|
312
|
+
that makes the pair eligible to run. Set it explicitly for each
|
|
313
|
+
(case, evaluator) pair, or use the eval helpers in
|
|
314
|
+
`codeer-cli/src/codeer_cli/eval_.py` which create assignments and rubrics
|
|
315
|
+
together.
|
|
312
316
|
|
|
313
317
|
To **read** rubrics back, use `POST /eval/rubrics/batch` with
|
|
314
318
|
`{case_ids: [...], evaluator_id}` — it returns the raw rubric strings out of
|
|
@@ -477,22 +481,22 @@ If the user's source tree has deeper nesting, flatten it first with the
|
|
|
477
481
|
`products/a.md` using `/` U+FF0F as separator) so `list_kb_files` regex
|
|
478
482
|
can still recover structure. That skill's docs explain the full flow.
|
|
479
483
|
|
|
480
|
-
### 13. Eval results and rubrics are **per
|
|
484
|
+
### 13. Eval results and rubrics are **per assigned case/evaluator pair**
|
|
481
485
|
|
|
482
486
|
Both `POST /eval/results/batch` and `POST /eval/rubrics/batch` take a
|
|
483
487
|
**singular** `evaluator_id`, not a list. Each call returns data for one
|
|
484
|
-
evaluator only. A
|
|
485
|
-
|
|
486
|
-
passing — while the other evaluator (e.g. Style/Tone) scored it 0.3.
|
|
488
|
+
evaluator only. A case only runs with evaluators it is assigned to through
|
|
489
|
+
`CaseEvaluatorInfo`.
|
|
487
490
|
|
|
488
491
|
When pulling eval data manually, always:
|
|
489
|
-
1.
|
|
490
|
-
2. Call `get_results()` or `get_rubrics_batch()` once
|
|
491
|
-
3. Join the results by `case_id
|
|
492
|
+
1. Read assignments with `eval_mod.get_case_evaluator_infos(case_ids=[...])`.
|
|
493
|
+
2. Call `get_results()` or `get_rubrics_batch()` once per assigned evaluator.
|
|
494
|
+
3. Join the results by `(case_id, evaluator_id)`.
|
|
492
495
|
|
|
493
496
|
The CLI commands (`codeer eval run`, `codeer eval rubrics`,
|
|
494
497
|
`codeer eval rubrics-apply`) and the `get_case_rubrics()` helper all handle
|
|
495
|
-
this iteration automatically.
|
|
498
|
+
this iteration automatically. Use `codeer eval rubrics --all-pairs` only when
|
|
499
|
+
you intentionally want the old full matrix scan.
|
|
496
500
|
|
|
497
501
|
---
|
|
498
502
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Command line tools for managing Codeer agents over the Codeer API.
|
|
5
5
|
Project-URL: Homepage, https://www.codeer.ai
|
|
6
6
|
Author: Codeer.AI
|
|
@@ -143,7 +143,7 @@ Use this pattern during agent lifecycle work:
|
|
|
143
143
|
```bash
|
|
144
144
|
codeer agent list
|
|
145
145
|
codeer history list --agent <agent-id> --limit 50
|
|
146
|
-
codeer eval run --agent <agent-id> --
|
|
146
|
+
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Flags:
|
|
@@ -125,7 +125,7 @@ Use this pattern during agent lifecycle work:
|
|
|
125
125
|
```bash
|
|
126
126
|
codeer agent list
|
|
127
127
|
codeer history list --agent <agent-id> --limit 50
|
|
128
|
-
codeer eval run --agent <agent-id> --
|
|
128
|
+
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
Flags:
|
|
@@ -154,7 +154,9 @@ def register(subparsers):
|
|
|
154
154
|
g.add_argument("--latest", action="store_true",
|
|
155
155
|
help="Auto-select the newest AgentHistory (default)")
|
|
156
156
|
p.add_argument("--cases", default=None, help="Comma-separated case UUIDs (default: all)")
|
|
157
|
-
p.
|
|
157
|
+
g = p.add_mutually_exclusive_group()
|
|
158
|
+
g.add_argument("--evaluator", default=None, help="Evaluator UUID; common path for running many cases with one tester")
|
|
159
|
+
g.add_argument("--evaluators", default=None, help="Comma-separated evaluator UUIDs")
|
|
158
160
|
p.add_argument("--poll-timeout", type=int, default=POLL_TIMEOUT)
|
|
159
161
|
p.add_argument("--full", action="store_true",
|
|
160
162
|
help="Use longer previews in stdout. Raw outputs/tool calls still require --out.")
|
|
@@ -195,10 +197,12 @@ def register(subparsers):
|
|
|
195
197
|
p.set_defaults(func=run_cases_apply)
|
|
196
198
|
|
|
197
199
|
# codeer eval rubrics
|
|
198
|
-
p = sub.add_parser("rubrics", help="Read per-(case, evaluator) rubrics")
|
|
200
|
+
p = sub.add_parser("rubrics", help="Read assigned per-(case, evaluator) rubrics")
|
|
199
201
|
p.add_argument("--agent", required=True)
|
|
200
202
|
p.add_argument("--evaluators", default=None, help="Comma-separated evaluator UUIDs")
|
|
201
203
|
p.add_argument("--cases", default=None, help="Comma-separated case UUIDs")
|
|
204
|
+
p.add_argument("--all-pairs", action="store_true",
|
|
205
|
+
help="With omitted --evaluators, scan every workspace evaluator instead of assigned pairs only.")
|
|
202
206
|
p.add_argument("--full", action="store_true",
|
|
203
207
|
help="Print complete rubric text. Default prints matrix summaries/previews.")
|
|
204
208
|
p.add_argument("--out", default=None,
|
|
@@ -599,6 +603,56 @@ def run_evaluator_update(args, client) -> int:
|
|
|
599
603
|
# eval run
|
|
600
604
|
# ---------------------------------------------------------------------------
|
|
601
605
|
|
|
606
|
+
def _assigned_evaluators_by_case(info_rows: list[dict]) -> dict[str, dict[str, dict]]:
|
|
607
|
+
out: dict[str, dict[str, dict]] = {}
|
|
608
|
+
for row in info_rows:
|
|
609
|
+
case_id = row.get("case_id")
|
|
610
|
+
if not case_id:
|
|
611
|
+
continue
|
|
612
|
+
out[str(case_id)] = {
|
|
613
|
+
str(info.get("evaluator_id")): info
|
|
614
|
+
for info in (row.get("evaluators") or [])
|
|
615
|
+
if info.get("evaluator_id")
|
|
616
|
+
}
|
|
617
|
+
return out
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _planned_eval_pairs(
|
|
621
|
+
*,
|
|
622
|
+
case_ids: list[str],
|
|
623
|
+
assigned_by_case: dict[str, dict[str, dict]],
|
|
624
|
+
requested_evaluator_ids: list[str] | None,
|
|
625
|
+
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
|
626
|
+
pairs: list[dict[str, str]] = []
|
|
627
|
+
skipped: list[dict[str, str]] = []
|
|
628
|
+
|
|
629
|
+
if requested_evaluator_ids:
|
|
630
|
+
for case_id in case_ids:
|
|
631
|
+
assigned = assigned_by_case.get(case_id, {})
|
|
632
|
+
for evaluator_id in requested_evaluator_ids:
|
|
633
|
+
if evaluator_id in assigned:
|
|
634
|
+
pairs.append({"case_id": case_id, "evaluator_id": evaluator_id})
|
|
635
|
+
else:
|
|
636
|
+
skipped.append({
|
|
637
|
+
"case_id": case_id,
|
|
638
|
+
"evaluator_id": evaluator_id,
|
|
639
|
+
"reason": "not_assigned",
|
|
640
|
+
})
|
|
641
|
+
return pairs, skipped
|
|
642
|
+
|
|
643
|
+
for case_id in case_ids:
|
|
644
|
+
for evaluator_id in assigned_by_case.get(case_id, {}):
|
|
645
|
+
pairs.append({"case_id": case_id, "evaluator_id": evaluator_id})
|
|
646
|
+
return pairs, skipped
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _group_case_ids_by_evaluator(pairs: list[dict[str, str]]) -> dict[str, list[str]]:
|
|
650
|
+
grouped: dict[str, list[str]] = defaultdict(list)
|
|
651
|
+
for pair in pairs:
|
|
652
|
+
grouped[pair["evaluator_id"]].append(pair["case_id"])
|
|
653
|
+
return dict(grouped)
|
|
654
|
+
|
|
655
|
+
|
|
602
656
|
def run_run(args, client) -> int:
|
|
603
657
|
workspace_id, _ = client.resolve_scope()
|
|
604
658
|
if args.latest or not args.history:
|
|
@@ -625,41 +679,76 @@ def run_run(args, client) -> int:
|
|
|
625
679
|
log("error: no cases to run")
|
|
626
680
|
return 2
|
|
627
681
|
|
|
628
|
-
evaluator_ids = _ids(args.evaluators) or []
|
|
629
|
-
|
|
630
|
-
|
|
682
|
+
evaluator_ids = [args.evaluator] if args.evaluator else (_ids(args.evaluators) or [])
|
|
683
|
+
requested_evaluator_ids = evaluator_ids or None
|
|
684
|
+
|
|
685
|
+
assignment_rows = eval_mod.get_case_evaluator_infos(client, case_ids=case_ids)
|
|
686
|
+
assigned_by_case = _assigned_evaluators_by_case(assignment_rows)
|
|
687
|
+
pairs, skipped_unassigned = _planned_eval_pairs(
|
|
688
|
+
case_ids=case_ids,
|
|
689
|
+
assigned_by_case=assigned_by_case,
|
|
690
|
+
requested_evaluator_ids=requested_evaluator_ids,
|
|
691
|
+
)
|
|
692
|
+
if not pairs:
|
|
693
|
+
if skipped_unassigned:
|
|
694
|
+
log("error: none of the requested case/evaluator pairs are assigned")
|
|
695
|
+
else:
|
|
696
|
+
log("error: no assigned case/evaluator pairs to run")
|
|
697
|
+
print_json({
|
|
698
|
+
"agent_id": args.agent,
|
|
699
|
+
"history_id": args.history,
|
|
700
|
+
"requested_case_count": len(case_ids),
|
|
701
|
+
"requested_evaluator_count": len(evaluator_ids),
|
|
702
|
+
"triggered_pair_count": 0,
|
|
703
|
+
"skipped_unassigned": skipped_unassigned,
|
|
704
|
+
})
|
|
631
705
|
return 2
|
|
706
|
+
|
|
707
|
+
evaluator_ids = _dedupe_preserve_order([pair["evaluator_id"] for pair in pairs])
|
|
632
708
|
evaluators = [eval_mod.get_evaluator(client, eid) for eid in evaluator_ids]
|
|
633
709
|
|
|
634
710
|
case_label_by_id = {c["id"]: truncate(c.get("input") or "", 60) for c in case_objs}
|
|
635
711
|
evaluator_name_by_id = {e["id"]: e.get("name", e["id"]) for e in evaluators}
|
|
636
712
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
713
|
+
if skipped_unassigned:
|
|
714
|
+
log(f"skipping {len(skipped_unassigned)} unassigned requested pairs")
|
|
715
|
+
log(f"triggering: {len(pairs)} assigned case/evaluator pairs on history {args.history}")
|
|
716
|
+
trigger_response = eval_mod.trigger_pairs(
|
|
717
|
+
client,
|
|
718
|
+
case_evaluator_pairs=pairs,
|
|
719
|
+
agent_history_id=args.history,
|
|
720
|
+
)
|
|
640
721
|
|
|
641
722
|
deadline = time.time() + args.poll_timeout
|
|
642
723
|
results_by_eval: dict[str, list[dict]] = {}
|
|
724
|
+
case_ids_by_evaluator = _group_case_ids_by_evaluator(pairs)
|
|
725
|
+
target_pair_keys = {(pair["case_id"], pair["evaluator_id"]) for pair in pairs}
|
|
643
726
|
while time.time() < deadline:
|
|
644
727
|
results_by_eval = {}
|
|
645
|
-
|
|
646
|
-
total = len(
|
|
647
|
-
for ev_id in
|
|
728
|
+
done_pairs: set[tuple[str, str]] = set()
|
|
729
|
+
total = len(pairs)
|
|
730
|
+
for ev_id, ev_case_ids in case_ids_by_evaluator.items():
|
|
648
731
|
rows = eval_mod.get_results(
|
|
649
|
-
client, case_ids=
|
|
732
|
+
client, case_ids=ev_case_ids, evaluator_id=ev_id,
|
|
650
733
|
agent_history_id=args.history, workspace_id=workspace_id,
|
|
651
734
|
include_output=True, include_reasoning_steps=True,
|
|
652
735
|
)
|
|
653
736
|
results_by_eval[ev_id] = rows
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
737
|
+
for row in rows:
|
|
738
|
+
key = (row.get("case_id") or row.get("evaluation_case_id"), ev_id)
|
|
739
|
+
if key in target_pair_keys and row.get("score") is not None:
|
|
740
|
+
done_pairs.add(key)
|
|
741
|
+
log(f" progress: {len(done_pairs)}/{total}")
|
|
742
|
+
if len(done_pairs) >= total:
|
|
657
743
|
break
|
|
658
744
|
time.sleep(POLL_INTERVAL)
|
|
659
745
|
|
|
660
746
|
flat: list[dict] = []
|
|
661
747
|
for ev_id, rows in results_by_eval.items():
|
|
662
748
|
for r in rows:
|
|
749
|
+
row_case_id = r.get("case_id") or r.get("evaluation_case_id")
|
|
750
|
+
if (row_case_id, ev_id) not in target_pair_keys:
|
|
751
|
+
continue
|
|
663
752
|
result_summary = parse_eval_result(r)
|
|
664
753
|
tool_calls = parse_eval_tool_calls(r)
|
|
665
754
|
total_tool_duration_ms = sum(
|
|
@@ -684,7 +773,15 @@ def run_run(args, client) -> int:
|
|
|
684
773
|
"raw_result": r,
|
|
685
774
|
})
|
|
686
775
|
|
|
687
|
-
|
|
776
|
+
scored_pair_keys = {
|
|
777
|
+
(r.get("case_id"), r.get("evaluator_id"))
|
|
778
|
+
for r in flat
|
|
779
|
+
if r.get("score") is not None
|
|
780
|
+
}
|
|
781
|
+
all_perfect = (
|
|
782
|
+
len(scored_pair_keys) == len(target_pair_keys)
|
|
783
|
+
and all((r.get("score") or 0.0) >= 1.0 for r in flat)
|
|
784
|
+
)
|
|
688
785
|
log("\n" + "=" * 80)
|
|
689
786
|
log(f"RESULTS agent={args.agent} history={args.history}")
|
|
690
787
|
log("=" * 80)
|
|
@@ -726,15 +823,25 @@ def run_run(args, client) -> int:
|
|
|
726
823
|
out = {
|
|
727
824
|
"agent_id": args.agent,
|
|
728
825
|
"history_id": args.history,
|
|
826
|
+
"requested_case_count": len(case_ids),
|
|
827
|
+
"requested_evaluator_count": len(requested_evaluator_ids or evaluator_ids),
|
|
828
|
+
"triggered_pair_count": len(pairs),
|
|
829
|
+
"scored_pair_count": len(scored_pair_keys),
|
|
830
|
+
"skipped_unassigned_count": len(skipped_unassigned),
|
|
729
831
|
"all_perfect": all_perfect,
|
|
730
832
|
"result_count": len(result_summaries),
|
|
731
833
|
"non_perfect_count": len(non_perfect),
|
|
732
834
|
"wrote_full_detail": bool(args.out),
|
|
835
|
+
"trigger_response": trigger_response,
|
|
836
|
+
"skipped_unassigned": skipped_unassigned,
|
|
733
837
|
"results": result_summaries,
|
|
734
838
|
}
|
|
735
839
|
full_out = {
|
|
736
840
|
"agent_id": args.agent,
|
|
737
841
|
"history_id": args.history,
|
|
842
|
+
"triggered_pairs": pairs,
|
|
843
|
+
"trigger_response": trigger_response,
|
|
844
|
+
"skipped_unassigned": skipped_unassigned,
|
|
738
845
|
"all_perfect": all_perfect,
|
|
739
846
|
"results": flat,
|
|
740
847
|
}
|
|
@@ -1359,18 +1466,29 @@ def run_rubrics(args, client) -> int:
|
|
|
1359
1466
|
log("error: no cases for this agent")
|
|
1360
1467
|
return 2
|
|
1361
1468
|
|
|
1469
|
+
assignment_rows = eval_mod.get_case_evaluator_infos(client, case_ids=case_ids)
|
|
1470
|
+
assigned_by_case = _assigned_evaluators_by_case(assignment_rows)
|
|
1471
|
+
|
|
1362
1472
|
if args.evaluators:
|
|
1363
1473
|
evaluator_ids = _ids(args.evaluators) or []
|
|
1364
1474
|
evaluators = [eval_mod.get_evaluator(client, eid) for eid in evaluator_ids]
|
|
1365
|
-
|
|
1475
|
+
elif args.all_pairs:
|
|
1366
1476
|
evaluators = eval_mod.list_evaluators(client, workspace_id)
|
|
1367
1477
|
evaluator_ids = [e["id"] for e in evaluators]
|
|
1478
|
+
else:
|
|
1479
|
+
evaluator_ids = _dedupe_preserve_order([
|
|
1480
|
+
evaluator_id
|
|
1481
|
+
for case_id in case_ids
|
|
1482
|
+
for evaluator_id in assigned_by_case.get(case_id, {})
|
|
1483
|
+
])
|
|
1484
|
+
evaluators = [eval_mod.get_evaluator(client, eid) for eid in evaluator_ids]
|
|
1368
1485
|
evaluator_name = {e["id"]: e.get("name", e["id"]) for e in evaluators}
|
|
1369
1486
|
if not evaluator_ids:
|
|
1370
|
-
log("error: no evaluators
|
|
1487
|
+
log("error: no assigned evaluators for these cases")
|
|
1371
1488
|
return 2
|
|
1372
1489
|
|
|
1373
|
-
|
|
1490
|
+
mode = "all requested pairs" if args.evaluators or args.all_pairs else "assigned pairs"
|
|
1491
|
+
log(f"reading {mode}: {len(case_ids)} cases, {len(evaluator_ids)} evaluators...")
|
|
1374
1492
|
|
|
1375
1493
|
rubrics = eval_mod.get_case_rubrics(
|
|
1376
1494
|
client, agent_id=args.agent, workspace_id=workspace_id,
|
|
@@ -1383,8 +1501,14 @@ def run_rubrics(args, client) -> int:
|
|
|
1383
1501
|
log(f"CASE {cid}")
|
|
1384
1502
|
log(f" input: {truncate(case_input.get(cid, ''), 120)}")
|
|
1385
1503
|
for ev_id in evaluator_ids:
|
|
1504
|
+
is_assigned = ev_id in assigned_by_case.get(cid, {})
|
|
1505
|
+
if not is_assigned and not (args.evaluators or args.all_pairs):
|
|
1506
|
+
continue
|
|
1386
1507
|
ev_name = evaluator_name.get(ev_id, ev_id)
|
|
1387
1508
|
rubric_text = (rubrics.get(cid) or {}).get(ev_id, "")
|
|
1509
|
+
if not is_assigned:
|
|
1510
|
+
log(f" [{ev_name}] (not assigned)")
|
|
1511
|
+
continue
|
|
1388
1512
|
if not rubric_text:
|
|
1389
1513
|
log(f" [{ev_name}] (rubric not set)")
|
|
1390
1514
|
else:
|
|
@@ -1396,9 +1520,13 @@ def run_rubrics(args, client) -> int:
|
|
|
1396
1520
|
for cid in case_ids:
|
|
1397
1521
|
rubrics_summary = {}
|
|
1398
1522
|
for ev_id in evaluator_ids:
|
|
1523
|
+
is_assigned = ev_id in assigned_by_case.get(cid, {})
|
|
1524
|
+
if not is_assigned and not (args.evaluators or args.all_pairs):
|
|
1525
|
+
continue
|
|
1399
1526
|
rubric_text = (rubrics.get(cid) or {}).get(ev_id, "")
|
|
1400
1527
|
rubrics_summary[ev_id] = {
|
|
1401
1528
|
"evaluator_name": evaluator_name.get(ev_id, ev_id),
|
|
1529
|
+
"is_assigned": is_assigned,
|
|
1402
1530
|
"is_set": bool(rubric_text),
|
|
1403
1531
|
"chars": len(rubric_text),
|
|
1404
1532
|
"preview": truncate(rubric_text, 240),
|
|
@@ -1412,6 +1540,7 @@ def run_rubrics(args, client) -> int:
|
|
|
1412
1540
|
out = {
|
|
1413
1541
|
"agent_id": args.agent,
|
|
1414
1542
|
"workspace_id": workspace_id,
|
|
1543
|
+
"mode": mode,
|
|
1415
1544
|
"evaluators": [{"id": e["id"], "name": e.get("name")} for e in evaluators],
|
|
1416
1545
|
"cases": [
|
|
1417
1546
|
{
|
|
@@ -1420,7 +1549,9 @@ def run_rubrics(args, client) -> int:
|
|
|
1420
1549
|
"rubrics_by_evaluator": {
|
|
1421
1550
|
ev_id: (rubrics.get(cid) or {}).get(ev_id)
|
|
1422
1551
|
for ev_id in evaluator_ids
|
|
1552
|
+
if ev_id in assigned_by_case.get(cid, {}) or args.evaluators or args.all_pairs
|
|
1423
1553
|
},
|
|
1554
|
+
"assigned_evaluator_ids": list(assigned_by_case.get(cid, {})),
|
|
1424
1555
|
}
|
|
1425
1556
|
for cid in case_ids
|
|
1426
1557
|
],
|
|
@@ -1432,6 +1563,7 @@ def run_rubrics(args, client) -> int:
|
|
|
1432
1563
|
print_json({
|
|
1433
1564
|
"agent_id": args.agent,
|
|
1434
1565
|
"workspace_id": workspace_id,
|
|
1566
|
+
"mode": mode,
|
|
1435
1567
|
"evaluator_count": len(evaluators),
|
|
1436
1568
|
"case_count": len(case_ids),
|
|
1437
1569
|
"wrote_full_detail": bool(args.out),
|
|
@@ -23,6 +23,7 @@ def create_case(
|
|
|
23
23
|
rubric: Optional[str] = None,
|
|
24
24
|
attachment_ids: Optional[List[str]] = None,
|
|
25
25
|
label_ids: Optional[List[str]] = None,
|
|
26
|
+
evaluators: Optional[List[dict[str, Any]]] = None,
|
|
26
27
|
meta: Optional[dict] = None,
|
|
27
28
|
note: Optional[str] = None,
|
|
28
29
|
) -> dict:
|
|
@@ -44,6 +45,8 @@ def create_case(
|
|
|
44
45
|
body["attachment_ids"] = attachment_ids
|
|
45
46
|
if label_ids is not None:
|
|
46
47
|
body["label_ids"] = label_ids
|
|
48
|
+
if evaluators is not None:
|
|
49
|
+
body["evaluators"] = evaluators
|
|
47
50
|
if meta:
|
|
48
51
|
body["meta"] = meta
|
|
49
52
|
if note is not None:
|
|
@@ -93,6 +96,31 @@ def delete_case(client: CodeerClient, case_id: str) -> dict:
|
|
|
93
96
|
return client.delete(f"/external/eval/cases/{case_id}")
|
|
94
97
|
|
|
95
98
|
|
|
99
|
+
# --- case/evaluator assignments ------------------------------------------
|
|
100
|
+
|
|
101
|
+
def get_case_evaluator_infos(client: CodeerClient, *, case_ids: List[str]) -> list[dict]:
|
|
102
|
+
"""Read assigned evaluator metadata for each case.
|
|
103
|
+
|
|
104
|
+
Returns rows shaped like ``{"case_id": str, "evaluators": [...]}``, where
|
|
105
|
+
each evaluator entry is the assigned ``{"evaluator_id", "rubric"}`` pair.
|
|
106
|
+
"""
|
|
107
|
+
return client.post("/eval/case-evaluator-infos:batch", json={"case_ids": case_ids})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def replace_case_evaluator_infos(
|
|
111
|
+
client: CodeerClient,
|
|
112
|
+
*,
|
|
113
|
+
case_id: str,
|
|
114
|
+
evaluators: list[dict[str, Any]],
|
|
115
|
+
) -> dict:
|
|
116
|
+
"""Replace a case's assigned evaluators.
|
|
117
|
+
|
|
118
|
+
This is intentionally separate from rubric upsert: replacing removes
|
|
119
|
+
evaluator assignments that are not included in ``evaluators``.
|
|
120
|
+
"""
|
|
121
|
+
return client.put(f"/eval/cases/{case_id}/case-evaluator-infos", json={"evaluators": evaluators})
|
|
122
|
+
|
|
123
|
+
|
|
96
124
|
# --- case labels -----------------------------------------------------------
|
|
97
125
|
|
|
98
126
|
def list_case_labels(client: CodeerClient, *, workspace_id: str) -> list[dict]:
|
|
@@ -202,6 +230,22 @@ def trigger(
|
|
|
202
230
|
return client.post("/external/eval/runs", json=body)
|
|
203
231
|
|
|
204
232
|
|
|
233
|
+
def trigger_pairs(
|
|
234
|
+
client: CodeerClient,
|
|
235
|
+
*,
|
|
236
|
+
case_evaluator_pairs: list[dict[str, str]],
|
|
237
|
+
agent_history_id: str,
|
|
238
|
+
) -> dict:
|
|
239
|
+
"""Kick off evaluation for explicit assigned case/evaluator pairs."""
|
|
240
|
+
return client.post(
|
|
241
|
+
"/eval/trigger",
|
|
242
|
+
json={
|
|
243
|
+
"case_evaluator_pairs": case_evaluator_pairs,
|
|
244
|
+
"agent_history_id": agent_history_id,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
205
249
|
def stop(client: CodeerClient, *, case_id: str, evaluator_id: str) -> Any:
|
|
206
250
|
return client.post("/external/eval/runs:stop", json={"case_id": case_id, "evaluator_id": evaluator_id})
|
|
207
251
|
|
|
@@ -451,9 +495,9 @@ def create_case_with_rubrics(
|
|
|
451
495
|
is filled in for every evaluator it will be judged by.
|
|
452
496
|
|
|
453
497
|
``rubrics_by_evaluator`` maps ``evaluator_id → rubric_text``. Each entry
|
|
454
|
-
becomes a
|
|
455
|
-
|
|
456
|
-
|
|
498
|
+
becomes a case/evaluator assignment on create. Use different rubric wording
|
|
499
|
+
per evaluator when the evaluators judge different aspects (e.g. Style/Tone
|
|
500
|
+
vs Content Compliance).
|
|
457
501
|
"""
|
|
458
502
|
case = create_case(
|
|
459
503
|
client,
|
|
@@ -462,12 +506,11 @@ def create_case_with_rubrics(
|
|
|
462
506
|
expected_output=expected_output,
|
|
463
507
|
attachment_ids=attachment_ids,
|
|
464
508
|
label_ids=label_ids,
|
|
509
|
+
evaluators=[
|
|
510
|
+
{"evaluator_id": ev_id, "rubric": rubric}
|
|
511
|
+
for ev_id, rubric in rubrics_by_evaluator.items()
|
|
512
|
+
],
|
|
465
513
|
meta=meta,
|
|
466
514
|
note=note,
|
|
467
515
|
)
|
|
468
|
-
set_rubric_bulk(
|
|
469
|
-
client,
|
|
470
|
-
evaluation_case_id=case["id"],
|
|
471
|
-
rubrics_by_evaluator=rubrics_by_evaluator,
|
|
472
|
-
)
|
|
473
516
|
return case
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from codeer_cli import eval_ as eval_mod
|
|
6
|
+
from codeer_cli.commands.eval_cmd import _assigned_evaluators_by_case, _planned_eval_pairs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeClient:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.calls: list[tuple[str, str, dict]] = []
|
|
12
|
+
|
|
13
|
+
def post(self, path: str, **kwargs):
|
|
14
|
+
self.calls.append(("POST", path, kwargs))
|
|
15
|
+
if path == "/external/eval/cases":
|
|
16
|
+
return {"id": "case-1"}
|
|
17
|
+
return {"ok": True}
|
|
18
|
+
|
|
19
|
+
def put(self, path: str, **kwargs):
|
|
20
|
+
self.calls.append(("PUT", path, kwargs))
|
|
21
|
+
return {"ok": True}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EvalPairClientTests(unittest.TestCase):
|
|
25
|
+
def test_assignment_helper_paths(self) -> None:
|
|
26
|
+
client = FakeClient()
|
|
27
|
+
|
|
28
|
+
eval_mod.get_case_evaluator_infos(client, case_ids=["case-1"]) # type: ignore[arg-type]
|
|
29
|
+
eval_mod.replace_case_evaluator_infos( # type: ignore[arg-type]
|
|
30
|
+
client,
|
|
31
|
+
case_id="case-1",
|
|
32
|
+
evaluators=[{"evaluator_id": "eval-1", "rubric": "Must pass"}],
|
|
33
|
+
)
|
|
34
|
+
eval_mod.trigger_pairs( # type: ignore[arg-type]
|
|
35
|
+
client,
|
|
36
|
+
case_evaluator_pairs=[{"case_id": "case-1", "evaluator_id": "eval-1"}],
|
|
37
|
+
agent_history_id="hist-1",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self.assertEqual(client.calls[0][0:2], ("POST", "/eval/case-evaluator-infos:batch"))
|
|
41
|
+
self.assertEqual(client.calls[0][2]["json"], {"case_ids": ["case-1"]})
|
|
42
|
+
self.assertEqual(client.calls[1][0:2], ("PUT", "/eval/cases/case-1/case-evaluator-infos"))
|
|
43
|
+
self.assertEqual(
|
|
44
|
+
client.calls[1][2]["json"],
|
|
45
|
+
{"evaluators": [{"evaluator_id": "eval-1", "rubric": "Must pass"}]},
|
|
46
|
+
)
|
|
47
|
+
self.assertEqual(client.calls[2][0:2], ("POST", "/eval/trigger"))
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
client.calls[2][2]["json"],
|
|
50
|
+
{
|
|
51
|
+
"case_evaluator_pairs": [{"case_id": "case-1", "evaluator_id": "eval-1"}],
|
|
52
|
+
"agent_history_id": "hist-1",
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def test_create_case_with_rubrics_sends_atomic_evaluator_assignments(self) -> None:
|
|
57
|
+
client = FakeClient()
|
|
58
|
+
|
|
59
|
+
eval_mod.create_case_with_rubrics( # type: ignore[arg-type]
|
|
60
|
+
client,
|
|
61
|
+
agent_id="agent-1",
|
|
62
|
+
input="Question",
|
|
63
|
+
rubrics_by_evaluator={"eval-1": "Must pass", "eval-2": "Must cite"},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.assertEqual(len(client.calls), 1)
|
|
67
|
+
self.assertEqual(client.calls[0][0:2], ("POST", "/external/eval/cases"))
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
client.calls[0][2]["json"]["evaluators"],
|
|
70
|
+
[
|
|
71
|
+
{"evaluator_id": "eval-1", "rubric": "Must pass"},
|
|
72
|
+
{"evaluator_id": "eval-2", "rubric": "Must cite"},
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EvalPairPlannerTests(unittest.TestCase):
|
|
78
|
+
def test_plans_only_assigned_pairs_for_specific_evaluator(self) -> None:
|
|
79
|
+
rows = [
|
|
80
|
+
{"case_id": "case-1", "evaluators": [{"evaluator_id": "eval-1", "rubric": "A"}]},
|
|
81
|
+
{"case_id": "case-2", "evaluators": [{"evaluator_id": "eval-2", "rubric": "B"}]},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
pairs, skipped = _planned_eval_pairs(
|
|
85
|
+
case_ids=["case-1", "case-2"],
|
|
86
|
+
assigned_by_case=_assigned_evaluators_by_case(rows),
|
|
87
|
+
requested_evaluator_ids=["eval-1"],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self.assertEqual(pairs, [{"case_id": "case-1", "evaluator_id": "eval-1"}])
|
|
91
|
+
self.assertEqual(
|
|
92
|
+
skipped,
|
|
93
|
+
[{"case_id": "case-2", "evaluator_id": "eval-1", "reason": "not_assigned"}],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def test_omitted_evaluator_runs_all_assigned_pairs(self) -> None:
|
|
97
|
+
rows = [
|
|
98
|
+
{
|
|
99
|
+
"case_id": "case-1",
|
|
100
|
+
"evaluators": [
|
|
101
|
+
{"evaluator_id": "eval-1", "rubric": "A"},
|
|
102
|
+
{"evaluator_id": "eval-2", "rubric": "B"},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{"case_id": "case-2", "evaluators": []},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
pairs, skipped = _planned_eval_pairs(
|
|
109
|
+
case_ids=["case-1", "case-2"],
|
|
110
|
+
assigned_by_case=_assigned_evaluators_by_case(rows),
|
|
111
|
+
requested_evaluator_ids=None,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self.assertEqual(
|
|
115
|
+
pairs,
|
|
116
|
+
[
|
|
117
|
+
{"case_id": "case-1", "evaluator_id": "eval-1"},
|
|
118
|
+
{"case_id": "case-1", "evaluator_id": "eval-2"},
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
self.assertEqual(skipped, [])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|