codeer-cli 0.1.3__tar.gz → 0.1.5__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.3 → codeer_cli-0.1.5}/API_REFERENCE.md +50 -7
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/PKG-INFO +9 -5
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/README.md +8 -4
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/pyproject.toml +1 -1
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/cli.py +6 -1
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/eval_cmd.py +418 -2
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/kb.py +41 -11
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/eval_.py +50 -0
- codeer_cli-0.1.5/tests/test_eval_labels.py +101 -0
- codeer_cli-0.1.5/tests/test_kb_ranges.py +35 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/uv.lock +1 -1
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/.gitignore +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/__init__.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/_validate.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/agents.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/chats.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/client.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/_util.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/agent.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/history.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/histories.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.3 → codeer_cli-0.1.5}/src/codeer_cli/parse.py +0 -0
|
@@ -81,23 +81,46 @@ Base path: `/external/context-object-faqs`
|
|
|
81
81
|
Create body:
|
|
82
82
|
|
|
83
83
|
```json
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
"context_object_id": 123,
|
|
86
|
+
"question": "How do I reset billing?",
|
|
87
|
+
"ranges": [
|
|
88
|
+
{
|
|
89
|
+
"start_line": 12,
|
|
90
|
+
"start_column": 0,
|
|
91
|
+
"end_line": 12,
|
|
92
|
+
"end_column": 42
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
85
96
|
```
|
|
86
97
|
|
|
87
98
|
Update body accepts any of these fields:
|
|
88
99
|
|
|
89
100
|
```json
|
|
90
|
-
{
|
|
101
|
+
{
|
|
102
|
+
"context_object_id": 456,
|
|
103
|
+
"question": "How do I update billing?",
|
|
104
|
+
"ranges": [
|
|
105
|
+
{
|
|
106
|
+
"start_line": 20,
|
|
107
|
+
"start_column": 0,
|
|
108
|
+
"end_line": 20,
|
|
109
|
+
"end_column": 39
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
91
113
|
```
|
|
92
114
|
|
|
93
115
|
`context_object_id` is the KB file's `snapshot_object_id` from the KB node
|
|
94
116
|
listing. `ranges` is optional; use it when the FAQ route should reserve chunks
|
|
95
|
-
overlapping a stable
|
|
96
|
-
the
|
|
117
|
+
overlapping a stable passage inside that file. Include both line and column
|
|
118
|
+
positions so the Codeer UI can map the range onto rendered Markdown. The compact
|
|
119
|
+
CLI output includes the target id:
|
|
97
120
|
|
|
98
121
|
```bash
|
|
99
122
|
codeer kb files --kb-id <kb-id>
|
|
100
|
-
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:
|
|
123
|
+
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:0-12:42 --dry-run
|
|
101
124
|
```
|
|
102
125
|
|
|
103
126
|
## Stage 3 — Live Test on a specific version
|
|
@@ -126,10 +149,14 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
|
|
|
126
149
|
|
|
127
150
|
| Method & path | Purpose |
|
|
128
151
|
| --- | --- |
|
|
129
|
-
| `
|
|
152
|
+
| `GET /eval/workspaces/{workspace_id}/case-labels` | List reusable eval case labels |
|
|
153
|
+
| `POST /eval/workspaces/{workspace_id}/case-labels` | Create reusable eval case label (`name`, `color?`) |
|
|
154
|
+
| `PUT /eval/case-labels/{label_id}` | Update eval case label name/color |
|
|
155
|
+
| `DELETE /eval/case-labels/{label_id}` | Delete eval case label and clear associations |
|
|
156
|
+
| `POST /eval/cases` | Create case (`input`, `expected_output?`, `rubric?`, `label_ids?`); rubric = user-docs "Standard" |
|
|
130
157
|
| `GET /eval/agents/{agent_id}/cases` | List cases for an agent |
|
|
131
158
|
| `GET /eval/cases/{case_id}` | Read one |
|
|
132
|
-
| `PUT /eval/cases/{case_id}` | Update |
|
|
159
|
+
| `PUT /eval/cases/{case_id}` | Update, including replacing labels via `label_ids` |
|
|
133
160
|
| `DELETE /eval/cases/{case_id}` | Delete |
|
|
134
161
|
| `POST /eval/cases/upload-csv` | Bulk import |
|
|
135
162
|
| `POST /eval/cases/bulk` | Bulk delete |
|
|
@@ -142,6 +169,22 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
|
|
|
142
169
|
| `POST /eval/rubric` | Set/override the rubric for one (case, evaluator) — write-only |
|
|
143
170
|
| `POST /eval/rubrics/batch` | **Read** rubrics for a batch of (case, evaluator) pairs |
|
|
144
171
|
|
|
172
|
+
Eval case labels are workspace-scoped reusable objects. The case create/update
|
|
173
|
+
payload uses `label_ids` (stringified label IDs), not freeform label names:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"agent_id": "<agent_uuid>",
|
|
178
|
+
"input": "How do I return an item?",
|
|
179
|
+
"expected_output": "Explain the return policy.",
|
|
180
|
+
"label_ids": ["12", "13"]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Send `label_ids: []` on update to clear all labels from a case. The
|
|
185
|
+
`codeer eval cases-apply` manifest can resolve label names through a separate
|
|
186
|
+
`labels` array; the legacy `label` field remains a local review/display label.
|
|
187
|
+
|
|
145
188
|
## Stage 6 — Diagnose + update
|
|
146
189
|
|
|
147
190
|
| Method & path | Purpose |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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
|
|
@@ -184,15 +184,19 @@ override matching JSON keys.
|
|
|
184
184
|
|
|
185
185
|
Use Context Object FAQ entries to route high-value questions to a canonical KB
|
|
186
186
|
file when semantic retrieval misses the right source. The FAQ target is a KB
|
|
187
|
-
file's `snapshot_object_id`, shown by `codeer kb files`. Add `--range
|
|
188
|
-
|
|
189
|
-
line
|
|
187
|
+
file's `snapshot_object_id`, shown by `codeer kb files`. Add `--range` when the
|
|
188
|
+
route should reserve a stable passage inside that file. Ranges must include both
|
|
189
|
+
line and column positions so the Codeer UI can map them onto rendered Markdown.
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
192
|
codeer kb files --kb-id <kb-id>
|
|
193
193
|
codeer kb faq-list --context-object-id <snapshot-object-id>
|
|
194
|
-
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:
|
|
194
|
+
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:0-12:42 --dry-run
|
|
195
|
+
codeer kb faq-update <faq-id> --range 12:0-12:42 --dry-run
|
|
195
196
|
```
|
|
196
197
|
|
|
198
|
+
`--range` accepts `START_LINE:START_COLUMN-END_LINE:END_COLUMN`; repeat it to
|
|
199
|
+
reserve multiple passages.
|
|
200
|
+
|
|
197
201
|
After reviewing the dry-run output, rerun the create/update/delete command
|
|
198
202
|
without `--dry-run` to apply it.
|
|
@@ -166,15 +166,19 @@ override matching JSON keys.
|
|
|
166
166
|
|
|
167
167
|
Use Context Object FAQ entries to route high-value questions to a canonical KB
|
|
168
168
|
file when semantic retrieval misses the right source. The FAQ target is a KB
|
|
169
|
-
file's `snapshot_object_id`, shown by `codeer kb files`. Add `--range
|
|
170
|
-
|
|
171
|
-
line
|
|
169
|
+
file's `snapshot_object_id`, shown by `codeer kb files`. Add `--range` when the
|
|
170
|
+
route should reserve a stable passage inside that file. Ranges must include both
|
|
171
|
+
line and column positions so the Codeer UI can map them onto rendered Markdown.
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
174
|
codeer kb files --kb-id <kb-id>
|
|
175
175
|
codeer kb faq-list --context-object-id <snapshot-object-id>
|
|
176
|
-
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:
|
|
176
|
+
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:0-12:42 --dry-run
|
|
177
|
+
codeer kb faq-update <faq-id> --range 12:0-12:42 --dry-run
|
|
177
178
|
```
|
|
178
179
|
|
|
180
|
+
`--range` accepts `START_LINE:START_COLUMN-END_LINE:END_COLUMN`; repeat it to
|
|
181
|
+
reserve multiple passages.
|
|
182
|
+
|
|
179
183
|
After reviewing the dry-run output, rerun the create/update/delete command
|
|
180
184
|
without `--dry-run` to apply it.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
codeer check
|
|
4
4
|
codeer agent list|get|apply|diff|versions
|
|
5
5
|
codeer kb list|files|upload|faq-list|faq-get|faq-create|faq-update|faq-delete
|
|
6
|
-
codeer eval list|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
|
|
6
|
+
codeer eval list|label-list|label-create|label-update|label-delete|case-update|case-delete|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
|
|
7
7
|
codeer history list|get|conversations|negative-feedback
|
|
8
8
|
"""
|
|
9
9
|
|
|
@@ -29,12 +29,17 @@ Safe workflow for coding agents:
|
|
|
29
29
|
codeer agent get <agent-id> --full
|
|
30
30
|
codeer kb list
|
|
31
31
|
codeer eval list --agent <agent-id>
|
|
32
|
+
codeer eval label-list
|
|
33
|
+
codeer eval case-update --case <case-id> --input "..." --dry-run
|
|
32
34
|
codeer eval evaluators
|
|
33
35
|
codeer agent diff --agent <agent-id> --from-version <n> --to-version <n>
|
|
34
36
|
codeer eval reconcile --agent <agent-id> --manifest .codeer/eval_cases.json
|
|
35
37
|
|
|
36
38
|
Preview mutations before applying:
|
|
37
39
|
codeer agent apply --payload agent.json --dry-run
|
|
40
|
+
codeer eval case-update --case <case-id> --input "..." --dry-run
|
|
41
|
+
codeer eval label-create --name "routing" --color "#0969da" --dry-run
|
|
42
|
+
codeer eval case-delete --case <case-id> --dry-run
|
|
38
43
|
codeer eval cases-apply --agent <agent-id> --cases eval_cases.json --dry-run
|
|
39
44
|
codeer eval rubrics-apply --rubrics rubrics.json --dry-run
|
|
40
45
|
codeer kb upload --dir kb --name "Product KB" --dry-run
|
|
@@ -46,6 +46,72 @@ def register(subparsers):
|
|
|
46
46
|
help="Write stripped full case payloads to this file; stdout stays compact unless --full.")
|
|
47
47
|
p.set_defaults(func=run_list)
|
|
48
48
|
|
|
49
|
+
# codeer eval label-list/create/update/delete
|
|
50
|
+
p = sub.add_parser("label-list", help="List eval case labels in the workspace")
|
|
51
|
+
p.add_argument("--workspace", default=None, help="Workspace UUID (default: active API-key workspace)")
|
|
52
|
+
p.add_argument("--out", default=None)
|
|
53
|
+
p.set_defaults(func=run_label_list)
|
|
54
|
+
|
|
55
|
+
p = sub.add_parser("label-create", help="Create an eval case label; run --dry-run first")
|
|
56
|
+
p.add_argument("--name", required=True)
|
|
57
|
+
p.add_argument("--color", default=None, help="Hex color like #0969da (default: server default)")
|
|
58
|
+
p.add_argument("--workspace", default=None, help="Workspace UUID (default: active API-key workspace)")
|
|
59
|
+
p.add_argument("--dry-run", action="store_true")
|
|
60
|
+
p.add_argument("--out", default=None)
|
|
61
|
+
p.set_defaults(func=run_label_create)
|
|
62
|
+
|
|
63
|
+
p = sub.add_parser("label-update", help="Update an eval case label; run --dry-run first")
|
|
64
|
+
p.add_argument("--label", required=True, dest="label_id", help="Eval case label ID")
|
|
65
|
+
p.add_argument("--name", default=None)
|
|
66
|
+
p.add_argument("--color", default=None, help="Hex color like #0969da")
|
|
67
|
+
p.add_argument("--dry-run", action="store_true")
|
|
68
|
+
p.add_argument("--out", default=None)
|
|
69
|
+
p.set_defaults(func=run_label_update)
|
|
70
|
+
|
|
71
|
+
p = sub.add_parser("label-delete", help="Delete an eval case label; run --dry-run first")
|
|
72
|
+
p.add_argument("--label", required=True, dest="label_id", help="Eval case label ID")
|
|
73
|
+
p.add_argument("--dry-run", action="store_true")
|
|
74
|
+
p.add_argument("--out", default=None)
|
|
75
|
+
p.set_defaults(func=run_label_delete)
|
|
76
|
+
|
|
77
|
+
# codeer eval case-update
|
|
78
|
+
p = sub.add_parser("case-update", help="Update one eval case by UUID; run --dry-run first")
|
|
79
|
+
p.add_argument("--case", required=True, dest="case_id", help="Eval case UUID")
|
|
80
|
+
g = p.add_mutually_exclusive_group()
|
|
81
|
+
g.add_argument("--input", help="New eval case input text")
|
|
82
|
+
g.add_argument("--input-file", help="Path to new eval case input text")
|
|
83
|
+
g = p.add_mutually_exclusive_group()
|
|
84
|
+
g.add_argument("--expected-output", help="New expected_output text")
|
|
85
|
+
g.add_argument("--expected-output-file", help="Path to new expected_output text")
|
|
86
|
+
g = p.add_mutually_exclusive_group()
|
|
87
|
+
g.add_argument("--rubric", help="New case-level rubric text")
|
|
88
|
+
g.add_argument("--rubric-file", help="Path to new case-level rubric text")
|
|
89
|
+
g = p.add_mutually_exclusive_group()
|
|
90
|
+
g.add_argument("--note", help="New case note text")
|
|
91
|
+
g.add_argument("--note-file", help="Path to new case note text")
|
|
92
|
+
g = p.add_mutually_exclusive_group()
|
|
93
|
+
g.add_argument("--meta-json", help="New case meta JSON object")
|
|
94
|
+
g.add_argument("--meta-file", help="Path to new case meta JSON object")
|
|
95
|
+
p.add_argument("--attachment-ids", default=None,
|
|
96
|
+
help="Comma-separated file UUIDs to set as the case attachments")
|
|
97
|
+
g = p.add_mutually_exclusive_group()
|
|
98
|
+
g.add_argument("--label-ids", default=None,
|
|
99
|
+
help="Comma-separated eval case label IDs to set on the case")
|
|
100
|
+
g.add_argument("--clear-labels", action="store_true",
|
|
101
|
+
help="Remove all labels from the case")
|
|
102
|
+
p.add_argument("--dry-run", action="store_true",
|
|
103
|
+
help="Validate inputs and print intended mutation without writing server state.")
|
|
104
|
+
p.add_argument("--out", default=None)
|
|
105
|
+
p.set_defaults(func=run_case_update)
|
|
106
|
+
|
|
107
|
+
# codeer eval case-delete
|
|
108
|
+
p = sub.add_parser("case-delete", help="Delete one eval case by UUID; run --dry-run first")
|
|
109
|
+
p.add_argument("--case", required=True, dest="case_id", help="Eval case UUID")
|
|
110
|
+
p.add_argument("--dry-run", action="store_true",
|
|
111
|
+
help="Print intended deletion without writing server state.")
|
|
112
|
+
p.add_argument("--out", default=None)
|
|
113
|
+
p.set_defaults(func=run_case_delete)
|
|
114
|
+
|
|
49
115
|
# codeer eval evaluators
|
|
50
116
|
p = sub.add_parser(
|
|
51
117
|
"evaluators",
|
|
@@ -121,6 +187,8 @@ def register(subparsers):
|
|
|
121
187
|
p.add_argument("--agent", required=True)
|
|
122
188
|
p.add_argument("--attachments-dir", default=None, dest="attachments_dir")
|
|
123
189
|
p.add_argument("--allow-duplicates", action="store_true")
|
|
190
|
+
p.add_argument("--create-labels", action="store_true",
|
|
191
|
+
help="Create missing labels referenced by manifest case labels.")
|
|
124
192
|
p.add_argument("--dry-run", action="store_true",
|
|
125
193
|
help="Validate manifest and print intended mutations without writing server state.")
|
|
126
194
|
p.add_argument("--out", default=None)
|
|
@@ -151,6 +219,7 @@ def register(subparsers):
|
|
|
151
219
|
# ---------------------------------------------------------------------------
|
|
152
220
|
|
|
153
221
|
def _case_summary(case: dict, *, full: bool = False) -> dict:
|
|
222
|
+
labels = case.get("labels") or []
|
|
154
223
|
row = {
|
|
155
224
|
"id": case.get("id"),
|
|
156
225
|
"input_preview": truncate(case.get("input") or "", 240 if full else 80),
|
|
@@ -158,6 +227,11 @@ def _case_summary(case: dict, *, full: bool = False) -> dict:
|
|
|
158
227
|
"expected_output_chars": len(case.get("expected_output") or ""),
|
|
159
228
|
"note_preview": truncate(case.get("note") or "", 180 if full else 100),
|
|
160
229
|
"attachment_count": len(case.get("attachments") or case.get("attachment_ids") or []),
|
|
230
|
+
"labels": [
|
|
231
|
+
{"id": label.get("id"), "name": label.get("name"), "color": label.get("color")}
|
|
232
|
+
for label in labels
|
|
233
|
+
if isinstance(label, dict)
|
|
234
|
+
],
|
|
161
235
|
}
|
|
162
236
|
if full:
|
|
163
237
|
row["created_at"] = case.get("created_at")
|
|
@@ -206,6 +280,232 @@ def run_list(args, client) -> int:
|
|
|
206
280
|
return 0
|
|
207
281
|
|
|
208
282
|
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# eval case labels
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def _workspace_arg_or_default(client, workspace_id: str | None) -> str:
|
|
288
|
+
if workspace_id:
|
|
289
|
+
return workspace_id
|
|
290
|
+
ws, _ = client.resolve_scope()
|
|
291
|
+
return ws
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _label_summary(label: dict) -> dict:
|
|
295
|
+
return {
|
|
296
|
+
"id": label.get("id"),
|
|
297
|
+
"name": label.get("name"),
|
|
298
|
+
"color": label.get("color"),
|
|
299
|
+
"workspace_id": label.get("workspace_id"),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def run_label_list(args, client) -> int:
|
|
304
|
+
workspace_id = _workspace_arg_or_default(client, args.workspace)
|
|
305
|
+
labels = eval_mod.list_case_labels(client, workspace_id=workspace_id)
|
|
306
|
+
out = {
|
|
307
|
+
"workspace_id": workspace_id,
|
|
308
|
+
"label_count": len(labels),
|
|
309
|
+
"labels": [_label_summary(label) for label in labels],
|
|
310
|
+
}
|
|
311
|
+
print_json(out)
|
|
312
|
+
write_json(args.out, out)
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def run_label_create(args, client) -> int:
|
|
317
|
+
workspace_id = _workspace_arg_or_default(client, args.workspace)
|
|
318
|
+
if args.dry_run:
|
|
319
|
+
out = {
|
|
320
|
+
"dry_run": True,
|
|
321
|
+
"operation": "label_create",
|
|
322
|
+
"method": "POST",
|
|
323
|
+
"path": f"/eval/workspaces/{workspace_id}/case-labels",
|
|
324
|
+
"workspace_id": workspace_id,
|
|
325
|
+
"name": args.name,
|
|
326
|
+
"color": args.color,
|
|
327
|
+
"would_write_server_state": True,
|
|
328
|
+
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
329
|
+
}
|
|
330
|
+
print_json(out)
|
|
331
|
+
write_json(args.out, out)
|
|
332
|
+
return 0
|
|
333
|
+
|
|
334
|
+
label = eval_mod.create_case_label(
|
|
335
|
+
client, workspace_id=workspace_id, name=args.name, color=args.color
|
|
336
|
+
)
|
|
337
|
+
out = _label_summary(strip_noisy_fields(label))
|
|
338
|
+
print_json(out)
|
|
339
|
+
write_json(args.out, out)
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def run_label_update(args, client) -> int:
|
|
344
|
+
if args.name is None and args.color is None:
|
|
345
|
+
log("error: provide --name and/or --color")
|
|
346
|
+
return 2
|
|
347
|
+
|
|
348
|
+
if args.dry_run:
|
|
349
|
+
out = {
|
|
350
|
+
"dry_run": True,
|
|
351
|
+
"operation": "label_update",
|
|
352
|
+
"method": "PUT",
|
|
353
|
+
"path": f"/eval/case-labels/{args.label_id}",
|
|
354
|
+
"label_id": args.label_id,
|
|
355
|
+
"updates": {"name": args.name, "color": args.color},
|
|
356
|
+
"would_write_server_state": True,
|
|
357
|
+
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
358
|
+
}
|
|
359
|
+
print_json(out)
|
|
360
|
+
write_json(args.out, out)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
label = eval_mod.update_case_label(
|
|
364
|
+
client, label_id=args.label_id, name=args.name, color=args.color
|
|
365
|
+
)
|
|
366
|
+
out = _label_summary(strip_noisy_fields(label))
|
|
367
|
+
print_json(out)
|
|
368
|
+
write_json(args.out, out)
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def run_label_delete(args, client) -> int:
|
|
373
|
+
if args.dry_run:
|
|
374
|
+
out = {
|
|
375
|
+
"dry_run": True,
|
|
376
|
+
"operation": "label_delete",
|
|
377
|
+
"method": "DELETE",
|
|
378
|
+
"path": f"/eval/case-labels/{args.label_id}",
|
|
379
|
+
"label_id": args.label_id,
|
|
380
|
+
"would_write_server_state": True,
|
|
381
|
+
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
382
|
+
}
|
|
383
|
+
print_json(out)
|
|
384
|
+
write_json(args.out, out)
|
|
385
|
+
return 0
|
|
386
|
+
|
|
387
|
+
deleted = strip_noisy_fields(eval_mod.delete_case_label(client, label_id=args.label_id))
|
|
388
|
+
print_json(deleted)
|
|
389
|
+
write_json(args.out, deleted)
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
# eval case-update / case-delete
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
def _read_text_arg(value: str | None, file_path: str | None) -> str | None:
|
|
398
|
+
if file_path is not None:
|
|
399
|
+
return Path(file_path).read_text()
|
|
400
|
+
return value
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _read_meta_arg(value: str | None, file_path: str | None) -> dict | None:
|
|
404
|
+
if file_path is not None:
|
|
405
|
+
raw = Path(file_path).read_text()
|
|
406
|
+
elif value is not None:
|
|
407
|
+
raw = value
|
|
408
|
+
else:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
meta = json.loads(raw)
|
|
412
|
+
if not isinstance(meta, dict):
|
|
413
|
+
raise ValueError("case meta must be a JSON object")
|
|
414
|
+
return meta
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def run_case_update(args, client) -> int:
|
|
418
|
+
try:
|
|
419
|
+
input_text = _read_text_arg(args.input, args.input_file)
|
|
420
|
+
expected_output = _read_text_arg(args.expected_output, args.expected_output_file)
|
|
421
|
+
rubric = _read_text_arg(args.rubric, args.rubric_file)
|
|
422
|
+
note = _read_text_arg(args.note, args.note_file)
|
|
423
|
+
meta = _read_meta_arg(args.meta_json, args.meta_file)
|
|
424
|
+
except (OSError, json.JSONDecodeError, ValueError) as e:
|
|
425
|
+
log(f"error: {e}")
|
|
426
|
+
return 2
|
|
427
|
+
attachment_ids = _ids(args.attachment_ids)
|
|
428
|
+
label_ids = [] if args.clear_labels else _ids(args.label_ids)
|
|
429
|
+
|
|
430
|
+
has_update = any(
|
|
431
|
+
value is not None
|
|
432
|
+
for value in (input_text, expected_output, rubric, note, meta, attachment_ids, label_ids)
|
|
433
|
+
) or args.clear_labels
|
|
434
|
+
if not has_update:
|
|
435
|
+
log(
|
|
436
|
+
"error: provide at least one of --input, --input-file, --expected-output, "
|
|
437
|
+
"--expected-output-file, --rubric, --rubric-file, --note, --note-file, "
|
|
438
|
+
"--meta-json, --meta-file, --attachment-ids, --label-ids, --clear-labels"
|
|
439
|
+
)
|
|
440
|
+
return 2
|
|
441
|
+
|
|
442
|
+
if args.dry_run:
|
|
443
|
+
current = strip_noisy_fields(eval_mod.get_case(client, args.case_id))
|
|
444
|
+
out = {
|
|
445
|
+
"dry_run": True,
|
|
446
|
+
"operation": "case_update",
|
|
447
|
+
"method": "PUT",
|
|
448
|
+
"path": f"/external/eval/cases/{args.case_id}",
|
|
449
|
+
"case_id": args.case_id,
|
|
450
|
+
"current": _case_summary(current, full=True),
|
|
451
|
+
"updates": {
|
|
452
|
+
"input_chars": len(input_text) if input_text is not None else None,
|
|
453
|
+
"expected_output_chars": (
|
|
454
|
+
len(expected_output) if expected_output is not None else None
|
|
455
|
+
),
|
|
456
|
+
"rubric_chars": len(rubric) if rubric is not None else None,
|
|
457
|
+
"note_chars": len(note) if note is not None else None,
|
|
458
|
+
"meta": meta,
|
|
459
|
+
"attachment_ids": attachment_ids,
|
|
460
|
+
"label_ids": label_ids,
|
|
461
|
+
},
|
|
462
|
+
"would_write_server_state": True,
|
|
463
|
+
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
464
|
+
}
|
|
465
|
+
print_json(out)
|
|
466
|
+
write_json(args.out, out)
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
updated = eval_mod.update_case(
|
|
470
|
+
client,
|
|
471
|
+
args.case_id,
|
|
472
|
+
input=input_text,
|
|
473
|
+
expected_output=expected_output,
|
|
474
|
+
rubric=rubric,
|
|
475
|
+
attachment_ids=attachment_ids,
|
|
476
|
+
label_ids=label_ids,
|
|
477
|
+
meta=meta,
|
|
478
|
+
note=note,
|
|
479
|
+
)
|
|
480
|
+
out = strip_noisy_fields(updated)
|
|
481
|
+
print_json(out)
|
|
482
|
+
write_json(args.out, out)
|
|
483
|
+
return 0
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def run_case_delete(args, client) -> int:
|
|
487
|
+
if args.dry_run:
|
|
488
|
+
current = strip_noisy_fields(eval_mod.get_case(client, args.case_id))
|
|
489
|
+
out = {
|
|
490
|
+
"dry_run": True,
|
|
491
|
+
"operation": "case_delete",
|
|
492
|
+
"method": "DELETE",
|
|
493
|
+
"path": f"/external/eval/cases/{args.case_id}",
|
|
494
|
+
"case_id": args.case_id,
|
|
495
|
+
"current": _case_summary(current, full=True),
|
|
496
|
+
"would_write_server_state": True,
|
|
497
|
+
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
498
|
+
}
|
|
499
|
+
print_json(out)
|
|
500
|
+
write_json(args.out, out)
|
|
501
|
+
return 0
|
|
502
|
+
|
|
503
|
+
deleted = strip_noisy_fields(eval_mod.delete_case(client, args.case_id))
|
|
504
|
+
print_json(deleted)
|
|
505
|
+
write_json(args.out, deleted)
|
|
506
|
+
return 0
|
|
507
|
+
|
|
508
|
+
|
|
209
509
|
# ---------------------------------------------------------------------------
|
|
210
510
|
# eval evaluators
|
|
211
511
|
# ---------------------------------------------------------------------------
|
|
@@ -795,6 +1095,50 @@ def _upload_attachment(client: CodeerClient, *, file_path: Path, workspace_id: s
|
|
|
795
1095
|
return uuid
|
|
796
1096
|
|
|
797
1097
|
|
|
1098
|
+
def _manifest_label_names(case: dict) -> list[str]:
|
|
1099
|
+
raw = case.get("labels")
|
|
1100
|
+
if raw is None:
|
|
1101
|
+
return []
|
|
1102
|
+
if not isinstance(raw, list) or not all(isinstance(item, str) for item in raw):
|
|
1103
|
+
raise ValueError(f"case '{case.get('label')}' labels must be a list of label names")
|
|
1104
|
+
return [item.strip() for item in raw if item.strip()]
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _manifest_label_ids(case: dict) -> list[str] | None:
|
|
1108
|
+
raw = case.get("label_ids")
|
|
1109
|
+
if raw is None:
|
|
1110
|
+
return None
|
|
1111
|
+
if not isinstance(raw, list) or not all(isinstance(item, str) for item in raw):
|
|
1112
|
+
raise ValueError(f"case '{case.get('label')}' label_ids must be a list of label ID strings")
|
|
1113
|
+
return [item.strip() for item in raw if item.strip()]
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _dedupe_preserve_order(items: list[str]) -> list[str]:
|
|
1117
|
+
seen: set[str] = set()
|
|
1118
|
+
out: list[str] = []
|
|
1119
|
+
for item in items:
|
|
1120
|
+
if item in seen:
|
|
1121
|
+
continue
|
|
1122
|
+
seen.add(item)
|
|
1123
|
+
out.append(item)
|
|
1124
|
+
return out
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def _resolve_case_label_ids(case: dict, labels_by_name: dict[str, dict]) -> tuple[list[str] | None, list[str]]:
|
|
1128
|
+
explicit_ids = _manifest_label_ids(case)
|
|
1129
|
+
label_names = _manifest_label_names(case)
|
|
1130
|
+
if explicit_ids is None and not label_names:
|
|
1131
|
+
return None, []
|
|
1132
|
+
|
|
1133
|
+
resolved_ids = list(explicit_ids or [])
|
|
1134
|
+
for name in label_names:
|
|
1135
|
+
label = labels_by_name.get(name.casefold())
|
|
1136
|
+
if label is None:
|
|
1137
|
+
raise ValueError(f"case '{case.get('label')}' references unknown label '{name}'")
|
|
1138
|
+
resolved_ids.append(str(label["id"]))
|
|
1139
|
+
return _dedupe_preserve_order(resolved_ids), label_names
|
|
1140
|
+
|
|
1141
|
+
|
|
798
1142
|
def run_cases_apply(args, client) -> int:
|
|
799
1143
|
payload = json.loads(Path(args.cases).read_text())
|
|
800
1144
|
cases = payload.get("cases") or []
|
|
@@ -815,6 +1159,53 @@ def run_cases_apply(args, client) -> int:
|
|
|
815
1159
|
return 2
|
|
816
1160
|
|
|
817
1161
|
workspace_id, _ = client.resolve_scope()
|
|
1162
|
+
try:
|
|
1163
|
+
manifest_label_names = sorted({
|
|
1164
|
+
name
|
|
1165
|
+
for case in cases
|
|
1166
|
+
for name in _manifest_label_names(case)
|
|
1167
|
+
}, key=str.casefold)
|
|
1168
|
+
for case in cases:
|
|
1169
|
+
_manifest_label_ids(case)
|
|
1170
|
+
except ValueError as e:
|
|
1171
|
+
log(f"error: {e}")
|
|
1172
|
+
return 2
|
|
1173
|
+
|
|
1174
|
+
labels_by_name: dict[str, dict] = {}
|
|
1175
|
+
created_labels: list[dict] = []
|
|
1176
|
+
would_create_labels: list[str] = []
|
|
1177
|
+
if manifest_label_names:
|
|
1178
|
+
labels_by_name = {
|
|
1179
|
+
(label.get("name") or "").casefold(): label
|
|
1180
|
+
for label in eval_mod.list_case_labels(client, workspace_id=workspace_id)
|
|
1181
|
+
if label.get("name")
|
|
1182
|
+
}
|
|
1183
|
+
missing_label_names = [
|
|
1184
|
+
name for name in manifest_label_names
|
|
1185
|
+
if name.casefold() not in labels_by_name
|
|
1186
|
+
]
|
|
1187
|
+
if missing_label_names and not args.create_labels:
|
|
1188
|
+
log(
|
|
1189
|
+
"error: manifest references missing labels: "
|
|
1190
|
+
+ ", ".join(missing_label_names)
|
|
1191
|
+
+ ". Create them first with `codeer eval label-create`, "
|
|
1192
|
+
+ "or rerun cases-apply with --create-labels."
|
|
1193
|
+
)
|
|
1194
|
+
return 2
|
|
1195
|
+
if args.dry_run:
|
|
1196
|
+
would_create_labels = missing_label_names
|
|
1197
|
+
for name in missing_label_names:
|
|
1198
|
+
labels_by_name[name.casefold()] = {
|
|
1199
|
+
"id": f"(new:{name})",
|
|
1200
|
+
"name": name,
|
|
1201
|
+
"color": "#0969da",
|
|
1202
|
+
}
|
|
1203
|
+
else:
|
|
1204
|
+
for name in missing_label_names:
|
|
1205
|
+
log(f"creating label: {name}")
|
|
1206
|
+
label = eval_mod.create_case_label(client, workspace_id=workspace_id, name=name)
|
|
1207
|
+
labels_by_name[name.casefold()] = label
|
|
1208
|
+
created_labels.append(_label_summary(label))
|
|
818
1209
|
|
|
819
1210
|
existing_by_input: dict[str, dict] = {}
|
|
820
1211
|
if not args.allow_duplicates:
|
|
@@ -838,6 +1229,11 @@ def run_cases_apply(args, client) -> int:
|
|
|
838
1229
|
return 2
|
|
839
1230
|
|
|
840
1231
|
label = case.get("label", "(unlabeled)")
|
|
1232
|
+
try:
|
|
1233
|
+
case_label_ids, case_label_names = _resolve_case_label_ids(case, labels_by_name)
|
|
1234
|
+
except ValueError as e:
|
|
1235
|
+
log(f"error: {e}")
|
|
1236
|
+
return 2
|
|
841
1237
|
attachment_ids: list[str] = []
|
|
842
1238
|
for fname in case.get("attachment_files") or []:
|
|
843
1239
|
fp = (attach_dir / fname).resolve() if attach_dir else None
|
|
@@ -866,16 +1262,26 @@ def run_cases_apply(args, client) -> int:
|
|
|
866
1262
|
or attachment_ids
|
|
867
1263
|
or case.get("meta") is not None
|
|
868
1264
|
or case.get("note") is not None
|
|
1265
|
+
or case_label_ids is not None
|
|
869
1266
|
),
|
|
1267
|
+
"labels": case_label_names,
|
|
1268
|
+
"label_ids": case_label_ids,
|
|
870
1269
|
"rubric_count": len(rubrics),
|
|
871
1270
|
})
|
|
872
1271
|
continue
|
|
873
1272
|
log(f"reusing existing case: {label} ({case_id[:8]})")
|
|
874
|
-
if
|
|
1273
|
+
if (
|
|
1274
|
+
case.get("expected_output") is not None
|
|
1275
|
+
or attachment_ids
|
|
1276
|
+
or case.get("meta") is not None
|
|
1277
|
+
or case.get("note") is not None
|
|
1278
|
+
or case_label_ids is not None
|
|
1279
|
+
):
|
|
875
1280
|
eval_mod.update_case(
|
|
876
1281
|
client, case_id,
|
|
877
1282
|
expected_output=case.get("expected_output"),
|
|
878
1283
|
attachment_ids=attachment_ids or None,
|
|
1284
|
+
label_ids=case_label_ids,
|
|
879
1285
|
meta=case.get("meta"),
|
|
880
1286
|
note=case.get("note"),
|
|
881
1287
|
)
|
|
@@ -894,6 +1300,8 @@ def run_cases_apply(args, client) -> int:
|
|
|
894
1300
|
"input_chars": len(case.get("input") or ""),
|
|
895
1301
|
"expected_output_chars": len(case.get("expected_output") or ""),
|
|
896
1302
|
"attachment_count": len(attachment_ids),
|
|
1303
|
+
"labels": case_label_names,
|
|
1304
|
+
"label_ids": case_label_ids,
|
|
897
1305
|
"rubric_count": len(rubrics),
|
|
898
1306
|
})
|
|
899
1307
|
continue
|
|
@@ -903,6 +1311,7 @@ def run_cases_apply(args, client) -> int:
|
|
|
903
1311
|
client, agent_id=args.agent, input=case["input"],
|
|
904
1312
|
expected_output=case.get("expected_output"),
|
|
905
1313
|
attachment_ids=attachment_ids or None,
|
|
1314
|
+
label_ids=case_label_ids,
|
|
906
1315
|
rubrics_by_evaluator=rubrics, meta=case.get("meta"),
|
|
907
1316
|
note=case.get("note"),
|
|
908
1317
|
)
|
|
@@ -910,12 +1319,19 @@ def run_cases_apply(args, client) -> int:
|
|
|
910
1319
|
labels.append(label)
|
|
911
1320
|
created.append({"case_id": result["id"], "label": label})
|
|
912
1321
|
|
|
913
|
-
out = {
|
|
1322
|
+
out = {
|
|
1323
|
+
"case_ids": case_ids,
|
|
1324
|
+
"labels": labels,
|
|
1325
|
+
"created": created,
|
|
1326
|
+
"reused": reused,
|
|
1327
|
+
"created_case_labels": created_labels,
|
|
1328
|
+
}
|
|
914
1329
|
if args.dry_run:
|
|
915
1330
|
out.update({
|
|
916
1331
|
"dry_run": True,
|
|
917
1332
|
"operation": "cases_apply",
|
|
918
1333
|
"agent_id": args.agent,
|
|
1334
|
+
"would_create_case_labels": would_create_labels,
|
|
919
1335
|
"updates": dry_run_updates,
|
|
920
1336
|
"would_write_server_state": True,
|
|
921
1337
|
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
@@ -107,8 +107,7 @@ def register(subparsers):
|
|
|
107
107
|
p.add_argument("--context-object-id", type=int, required=True,
|
|
108
108
|
help="KB file snapshot_object_id from `codeer kb files`")
|
|
109
109
|
p.add_argument("--question", required=True)
|
|
110
|
-
p
|
|
111
|
-
help="Reserve matching chunks that overlap START_LINE:END_LINE; repeatable")
|
|
110
|
+
_add_faq_range_args(p)
|
|
112
111
|
p.add_argument("--dry-run", action="store_true",
|
|
113
112
|
help="Print intended request without writing server state.")
|
|
114
113
|
p.add_argument("--out", default=None, help="Write result JSON to this file too")
|
|
@@ -119,8 +118,7 @@ def register(subparsers):
|
|
|
119
118
|
p.add_argument("--context-object-id", type=int, default=None,
|
|
120
119
|
help="Move FAQ to a different KB file snapshot_object_id")
|
|
121
120
|
p.add_argument("--question", default=None)
|
|
122
|
-
p
|
|
123
|
-
help="Replace reserved ranges with START_LINE:END_LINE; repeatable")
|
|
121
|
+
_add_faq_range_args(p, verb="Replace")
|
|
124
122
|
p.add_argument("--dry-run", action="store_true",
|
|
125
123
|
help="Print intended request without writing server state.")
|
|
126
124
|
p.add_argument("--out", default=None, help="Write result JSON to this file too")
|
|
@@ -217,18 +215,50 @@ def _dry_run(path: str | None, result: dict) -> int:
|
|
|
217
215
|
return 0
|
|
218
216
|
|
|
219
217
|
|
|
218
|
+
def _add_faq_range_args(parser, *, verb: str = "Reserve") -> None:
|
|
219
|
+
parser.add_argument(
|
|
220
|
+
"--range",
|
|
221
|
+
dest="ranges",
|
|
222
|
+
action="append",
|
|
223
|
+
type=_parse_faq_range,
|
|
224
|
+
default=None,
|
|
225
|
+
help=(
|
|
226
|
+
f"{verb} matching passages as "
|
|
227
|
+
"START_LINE:START_COLUMN-END_LINE:END_COLUMN; repeatable"
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
220
232
|
def _parse_faq_range(value: str) -> dict[str, int]:
|
|
221
233
|
try:
|
|
222
|
-
start_raw, end_raw = value.split("
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
start_raw, end_raw = value.split("-", 1)
|
|
235
|
+
start_line_raw, start_column_raw = start_raw.split(":", 1)
|
|
236
|
+
end_line_raw, end_column_raw = end_raw.split(":", 1)
|
|
237
|
+
faq_range = {
|
|
238
|
+
"start_line": int(start_line_raw),
|
|
239
|
+
"start_column": int(start_column_raw),
|
|
240
|
+
"end_line": int(end_line_raw),
|
|
241
|
+
"end_column": int(end_column_raw),
|
|
242
|
+
}
|
|
225
243
|
except ValueError as exc:
|
|
226
|
-
raise argparse.ArgumentTypeError(
|
|
244
|
+
raise argparse.ArgumentTypeError(
|
|
245
|
+
"expected START_LINE:START_COLUMN-END_LINE:END_COLUMN"
|
|
246
|
+
) from exc
|
|
247
|
+
_validate_faq_range_position(faq_range)
|
|
248
|
+
return faq_range
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _validate_faq_range_position(faq_range: dict[str, int]) -> None:
|
|
252
|
+
start_line = faq_range["start_line"]
|
|
253
|
+
end_line = faq_range["end_line"]
|
|
254
|
+
start_column = faq_range["start_column"]
|
|
255
|
+
end_column = faq_range["end_column"]
|
|
227
256
|
if start_line < 1 or end_line < 1:
|
|
228
257
|
raise argparse.ArgumentTypeError("line numbers must be >= 1")
|
|
229
|
-
if
|
|
230
|
-
raise argparse.ArgumentTypeError("
|
|
231
|
-
|
|
258
|
+
if start_column < 0 or end_column < 0:
|
|
259
|
+
raise argparse.ArgumentTypeError("column numbers must be >= 0")
|
|
260
|
+
if (end_line, end_column) < (start_line, start_column):
|
|
261
|
+
raise argparse.ArgumentTypeError("end position must be >= start position")
|
|
232
262
|
|
|
233
263
|
|
|
234
264
|
def _parse_config_json(config_json: str | None) -> dict | None:
|
|
@@ -22,6 +22,7 @@ def create_case(
|
|
|
22
22
|
expected_output: Optional[str] = None,
|
|
23
23
|
rubric: Optional[str] = None,
|
|
24
24
|
attachment_ids: Optional[List[str]] = None,
|
|
25
|
+
label_ids: Optional[List[str]] = None,
|
|
25
26
|
meta: Optional[dict] = None,
|
|
26
27
|
note: Optional[str] = None,
|
|
27
28
|
) -> dict:
|
|
@@ -41,6 +42,8 @@ def create_case(
|
|
|
41
42
|
body["rubric"] = rubric
|
|
42
43
|
if attachment_ids:
|
|
43
44
|
body["attachment_ids"] = attachment_ids
|
|
45
|
+
if label_ids is not None:
|
|
46
|
+
body["label_ids"] = label_ids
|
|
44
47
|
if meta:
|
|
45
48
|
body["meta"] = meta
|
|
46
49
|
if note is not None:
|
|
@@ -64,6 +67,7 @@ def update_case(
|
|
|
64
67
|
expected_output: Optional[str] = None,
|
|
65
68
|
rubric: Optional[str] = None,
|
|
66
69
|
attachment_ids: Optional[List[str]] = None,
|
|
70
|
+
label_ids: Optional[List[str]] = None,
|
|
67
71
|
meta: Optional[dict] = None,
|
|
68
72
|
note: Optional[str] = None,
|
|
69
73
|
) -> dict:
|
|
@@ -76,6 +80,8 @@ def update_case(
|
|
|
76
80
|
body["rubric"] = rubric
|
|
77
81
|
if attachment_ids is not None:
|
|
78
82
|
body["attachment_ids"] = attachment_ids
|
|
83
|
+
if label_ids is not None:
|
|
84
|
+
body["label_ids"] = label_ids
|
|
79
85
|
if meta is not None:
|
|
80
86
|
body["meta"] = meta
|
|
81
87
|
if note is not None:
|
|
@@ -83,6 +89,48 @@ def update_case(
|
|
|
83
89
|
return client.put(f"/external/eval/cases/{case_id}", json=body)
|
|
84
90
|
|
|
85
91
|
|
|
92
|
+
def delete_case(client: CodeerClient, case_id: str) -> dict:
|
|
93
|
+
return client.delete(f"/external/eval/cases/{case_id}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- case labels -----------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def list_case_labels(client: CodeerClient, *, workspace_id: str) -> list[dict]:
|
|
99
|
+
return client.get(f"/eval/workspaces/{workspace_id}/case-labels")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_case_label(
|
|
103
|
+
client: CodeerClient,
|
|
104
|
+
*,
|
|
105
|
+
workspace_id: str,
|
|
106
|
+
name: str,
|
|
107
|
+
color: Optional[str] = None,
|
|
108
|
+
) -> dict:
|
|
109
|
+
body: dict[str, Any] = {"name": name}
|
|
110
|
+
if color is not None:
|
|
111
|
+
body["color"] = color
|
|
112
|
+
return client.post(f"/eval/workspaces/{workspace_id}/case-labels", json=body)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def update_case_label(
|
|
116
|
+
client: CodeerClient,
|
|
117
|
+
*,
|
|
118
|
+
label_id: str,
|
|
119
|
+
name: Optional[str] = None,
|
|
120
|
+
color: Optional[str] = None,
|
|
121
|
+
) -> dict:
|
|
122
|
+
body: dict[str, Any] = {}
|
|
123
|
+
if name is not None:
|
|
124
|
+
body["name"] = name
|
|
125
|
+
if color is not None:
|
|
126
|
+
body["color"] = color
|
|
127
|
+
return client.put(f"/eval/case-labels/{label_id}", json=body)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def delete_case_label(client: CodeerClient, *, label_id: str) -> dict:
|
|
131
|
+
return client.delete(f"/eval/case-labels/{label_id}")
|
|
132
|
+
|
|
133
|
+
|
|
86
134
|
|
|
87
135
|
# --- evaluators -----------------------------------------------------------
|
|
88
136
|
|
|
@@ -393,6 +441,7 @@ def create_case_with_rubrics(
|
|
|
393
441
|
rubrics_by_evaluator: dict[str, str],
|
|
394
442
|
expected_output: Optional[str] = None,
|
|
395
443
|
attachment_ids: Optional[List[str]] = None,
|
|
444
|
+
label_ids: Optional[List[str]] = None,
|
|
396
445
|
meta: Optional[dict] = None,
|
|
397
446
|
note: Optional[str] = None,
|
|
398
447
|
) -> dict:
|
|
@@ -412,6 +461,7 @@ def create_case_with_rubrics(
|
|
|
412
461
|
input=input,
|
|
413
462
|
expected_output=expected_output,
|
|
414
463
|
attachment_ids=attachment_ids,
|
|
464
|
+
label_ids=label_ids,
|
|
415
465
|
meta=meta,
|
|
416
466
|
note=note,
|
|
417
467
|
)
|
|
@@ -0,0 +1,101 @@
|
|
|
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 _resolve_case_label_ids
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeClient:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.calls: list[tuple[str, str, dict]] = []
|
|
12
|
+
|
|
13
|
+
def get(self, path: str):
|
|
14
|
+
self.calls.append(("GET", path, {}))
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
def post(self, path: str, **kwargs):
|
|
18
|
+
self.calls.append(("POST", path, kwargs))
|
|
19
|
+
return {"id": "created"}
|
|
20
|
+
|
|
21
|
+
def put(self, path: str, **kwargs):
|
|
22
|
+
self.calls.append(("PUT", path, kwargs))
|
|
23
|
+
return {"id": "updated"}
|
|
24
|
+
|
|
25
|
+
def delete(self, path: str, **kwargs):
|
|
26
|
+
self.calls.append(("DELETE", path, kwargs))
|
|
27
|
+
return {"ok": True}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EvalLabelClientTests(unittest.TestCase):
|
|
31
|
+
def test_create_case_sends_label_ids(self) -> None:
|
|
32
|
+
client = FakeClient()
|
|
33
|
+
|
|
34
|
+
eval_mod.create_case(
|
|
35
|
+
client, # type: ignore[arg-type]
|
|
36
|
+
agent_id="agent-1",
|
|
37
|
+
input="How much?",
|
|
38
|
+
label_ids=["10", "11"],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.assertEqual(client.calls[0][0], "POST")
|
|
42
|
+
self.assertEqual(client.calls[0][1], "/external/eval/cases")
|
|
43
|
+
self.assertEqual(client.calls[0][2]["json"]["label_ids"], ["10", "11"])
|
|
44
|
+
|
|
45
|
+
def test_update_case_sends_empty_label_ids_to_clear(self) -> None:
|
|
46
|
+
client = FakeClient()
|
|
47
|
+
|
|
48
|
+
eval_mod.update_case(
|
|
49
|
+
client, # type: ignore[arg-type]
|
|
50
|
+
"case-1",
|
|
51
|
+
label_ids=[],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self.assertEqual(client.calls[0][0], "PUT")
|
|
55
|
+
self.assertEqual(client.calls[0][1], "/external/eval/cases/case-1")
|
|
56
|
+
self.assertEqual(client.calls[0][2]["json"]["label_ids"], [])
|
|
57
|
+
|
|
58
|
+
def test_case_label_crud_paths(self) -> None:
|
|
59
|
+
client = FakeClient()
|
|
60
|
+
|
|
61
|
+
eval_mod.list_case_labels(client, workspace_id="ws-1") # type: ignore[arg-type]
|
|
62
|
+
eval_mod.create_case_label(client, workspace_id="ws-1", name="Routing") # type: ignore[arg-type]
|
|
63
|
+
eval_mod.update_case_label(client, label_id="5", color="#0969da") # type: ignore[arg-type]
|
|
64
|
+
eval_mod.delete_case_label(client, label_id="5") # type: ignore[arg-type]
|
|
65
|
+
|
|
66
|
+
self.assertEqual(client.calls[0], ("GET", "/eval/workspaces/ws-1/case-labels", {}))
|
|
67
|
+
self.assertEqual(client.calls[1][0:2], ("POST", "/eval/workspaces/ws-1/case-labels"))
|
|
68
|
+
self.assertEqual(client.calls[1][2]["json"], {"name": "Routing"})
|
|
69
|
+
self.assertEqual(client.calls[2][0:2], ("PUT", "/eval/case-labels/5"))
|
|
70
|
+
self.assertEqual(client.calls[2][2]["json"], {"color": "#0969da"})
|
|
71
|
+
self.assertEqual(client.calls[3][0:2], ("DELETE", "/eval/case-labels/5"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class EvalLabelManifestTests(unittest.TestCase):
|
|
75
|
+
def test_resolve_case_label_ids_from_ids_and_names(self) -> None:
|
|
76
|
+
label_ids, label_names = _resolve_case_label_ids(
|
|
77
|
+
{"label_ids": ["7"], "labels": ["Routing", "Billing"]},
|
|
78
|
+
{
|
|
79
|
+
"routing": {"id": "8", "name": "Routing"},
|
|
80
|
+
"billing": {"id": "9", "name": "Billing"},
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.assertEqual(label_ids, ["7", "8", "9"])
|
|
85
|
+
self.assertEqual(label_names, ["Routing", "Billing"])
|
|
86
|
+
|
|
87
|
+
def test_resolve_case_label_ids_dedupes_preserving_order(self) -> None:
|
|
88
|
+
label_ids, _ = _resolve_case_label_ids(
|
|
89
|
+
{"label_ids": ["7", "8"], "labels": ["Routing"]},
|
|
90
|
+
{"routing": {"id": "8", "name": "Routing"}},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.assertEqual(label_ids, ["7", "8"])
|
|
94
|
+
|
|
95
|
+
def test_unknown_label_name_is_error(self) -> None:
|
|
96
|
+
with self.assertRaises(ValueError):
|
|
97
|
+
_resolve_case_label_ids({"labels": ["Missing"]}, {})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
unittest.main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from codeer_cli.commands.kb import _parse_faq_range
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FaqRangeTests(unittest.TestCase):
|
|
10
|
+
def test_parse_column_range(self) -> None:
|
|
11
|
+
self.assertEqual(
|
|
12
|
+
_parse_faq_range("47:2-48:9"),
|
|
13
|
+
{
|
|
14
|
+
"start_line": 47,
|
|
15
|
+
"start_column": 2,
|
|
16
|
+
"end_line": 48,
|
|
17
|
+
"end_column": 9,
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def test_rejects_single_line_range(self) -> None:
|
|
22
|
+
with self.assertRaises(argparse.ArgumentTypeError):
|
|
23
|
+
_parse_faq_range("47")
|
|
24
|
+
|
|
25
|
+
def test_rejects_legacy_line_range(self) -> None:
|
|
26
|
+
with self.assertRaises(argparse.ArgumentTypeError):
|
|
27
|
+
_parse_faq_range("47:48")
|
|
28
|
+
|
|
29
|
+
def test_rejects_reversed_column_range(self) -> None:
|
|
30
|
+
with self.assertRaises(argparse.ArgumentTypeError):
|
|
31
|
+
_parse_faq_range("48:1-47:1")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
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
|