codeer-cli 0.1.4__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.4 → codeer_cli-0.1.5}/API_REFERENCE.md +50 -7
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/PKG-INFO +9 -5
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/README.md +8 -4
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/pyproject.toml +1 -1
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/cli.py +3 -1
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/eval_cmd.py +275 -5
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/kb.py +41 -11
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/eval_.py +46 -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.4 → codeer_cli-0.1.5}/uv.lock +1 -1
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/.gitignore +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/__init__.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/_validate.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/agents.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/chats.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/client.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/_util.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/agent.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/history.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/histories.py +0 -0
- {codeer_cli-0.1.4 → codeer_cli-0.1.5}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.4 → 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|case-update|case-delete|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,6 +29,7 @@ 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
|
|
32
33
|
codeer eval case-update --case <case-id> --input "..." --dry-run
|
|
33
34
|
codeer eval evaluators
|
|
34
35
|
codeer agent diff --agent <agent-id> --from-version <n> --to-version <n>
|
|
@@ -37,6 +38,7 @@ Safe workflow for coding agents:
|
|
|
37
38
|
Preview mutations before applying:
|
|
38
39
|
codeer agent apply --payload agent.json --dry-run
|
|
39
40
|
codeer eval case-update --case <case-id> --input "..." --dry-run
|
|
41
|
+
codeer eval label-create --name "routing" --color "#0969da" --dry-run
|
|
40
42
|
codeer eval case-delete --case <case-id> --dry-run
|
|
41
43
|
codeer eval cases-apply --agent <agent-id> --cases eval_cases.json --dry-run
|
|
42
44
|
codeer eval rubrics-apply --rubrics rubrics.json --dry-run
|
|
@@ -46,6 +46,34 @@ 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
|
+
|
|
49
77
|
# codeer eval case-update
|
|
50
78
|
p = sub.add_parser("case-update", help="Update one eval case by UUID; run --dry-run first")
|
|
51
79
|
p.add_argument("--case", required=True, dest="case_id", help="Eval case UUID")
|
|
@@ -66,6 +94,11 @@ def register(subparsers):
|
|
|
66
94
|
g.add_argument("--meta-file", help="Path to new case meta JSON object")
|
|
67
95
|
p.add_argument("--attachment-ids", default=None,
|
|
68
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")
|
|
69
102
|
p.add_argument("--dry-run", action="store_true",
|
|
70
103
|
help="Validate inputs and print intended mutation without writing server state.")
|
|
71
104
|
p.add_argument("--out", default=None)
|
|
@@ -154,6 +187,8 @@ def register(subparsers):
|
|
|
154
187
|
p.add_argument("--agent", required=True)
|
|
155
188
|
p.add_argument("--attachments-dir", default=None, dest="attachments_dir")
|
|
156
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.")
|
|
157
192
|
p.add_argument("--dry-run", action="store_true",
|
|
158
193
|
help="Validate manifest and print intended mutations without writing server state.")
|
|
159
194
|
p.add_argument("--out", default=None)
|
|
@@ -184,6 +219,7 @@ def register(subparsers):
|
|
|
184
219
|
# ---------------------------------------------------------------------------
|
|
185
220
|
|
|
186
221
|
def _case_summary(case: dict, *, full: bool = False) -> dict:
|
|
222
|
+
labels = case.get("labels") or []
|
|
187
223
|
row = {
|
|
188
224
|
"id": case.get("id"),
|
|
189
225
|
"input_preview": truncate(case.get("input") or "", 240 if full else 80),
|
|
@@ -191,6 +227,11 @@ def _case_summary(case: dict, *, full: bool = False) -> dict:
|
|
|
191
227
|
"expected_output_chars": len(case.get("expected_output") or ""),
|
|
192
228
|
"note_preview": truncate(case.get("note") or "", 180 if full else 100),
|
|
193
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
|
+
],
|
|
194
235
|
}
|
|
195
236
|
if full:
|
|
196
237
|
row["created_at"] = case.get("created_at")
|
|
@@ -239,6 +280,116 @@ def run_list(args, client) -> int:
|
|
|
239
280
|
return 0
|
|
240
281
|
|
|
241
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
|
+
|
|
242
393
|
# ---------------------------------------------------------------------------
|
|
243
394
|
# eval case-update / case-delete
|
|
244
395
|
# ---------------------------------------------------------------------------
|
|
@@ -274,16 +425,17 @@ def run_case_update(args, client) -> int:
|
|
|
274
425
|
log(f"error: {e}")
|
|
275
426
|
return 2
|
|
276
427
|
attachment_ids = _ids(args.attachment_ids)
|
|
428
|
+
label_ids = [] if args.clear_labels else _ids(args.label_ids)
|
|
277
429
|
|
|
278
430
|
has_update = any(
|
|
279
431
|
value is not None
|
|
280
|
-
for value in (input_text, expected_output, rubric, note, meta, attachment_ids)
|
|
281
|
-
)
|
|
432
|
+
for value in (input_text, expected_output, rubric, note, meta, attachment_ids, label_ids)
|
|
433
|
+
) or args.clear_labels
|
|
282
434
|
if not has_update:
|
|
283
435
|
log(
|
|
284
436
|
"error: provide at least one of --input, --input-file, --expected-output, "
|
|
285
437
|
"--expected-output-file, --rubric, --rubric-file, --note, --note-file, "
|
|
286
|
-
"--meta-json, --meta-file, --attachment-ids"
|
|
438
|
+
"--meta-json, --meta-file, --attachment-ids, --label-ids, --clear-labels"
|
|
287
439
|
)
|
|
288
440
|
return 2
|
|
289
441
|
|
|
@@ -305,6 +457,7 @@ def run_case_update(args, client) -> int:
|
|
|
305
457
|
"note_chars": len(note) if note is not None else None,
|
|
306
458
|
"meta": meta,
|
|
307
459
|
"attachment_ids": attachment_ids,
|
|
460
|
+
"label_ids": label_ids,
|
|
308
461
|
},
|
|
309
462
|
"would_write_server_state": True,
|
|
310
463
|
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
@@ -320,6 +473,7 @@ def run_case_update(args, client) -> int:
|
|
|
320
473
|
expected_output=expected_output,
|
|
321
474
|
rubric=rubric,
|
|
322
475
|
attachment_ids=attachment_ids,
|
|
476
|
+
label_ids=label_ids,
|
|
323
477
|
meta=meta,
|
|
324
478
|
note=note,
|
|
325
479
|
)
|
|
@@ -941,6 +1095,50 @@ def _upload_attachment(client: CodeerClient, *, file_path: Path, workspace_id: s
|
|
|
941
1095
|
return uuid
|
|
942
1096
|
|
|
943
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
|
+
|
|
944
1142
|
def run_cases_apply(args, client) -> int:
|
|
945
1143
|
payload = json.loads(Path(args.cases).read_text())
|
|
946
1144
|
cases = payload.get("cases") or []
|
|
@@ -961,6 +1159,53 @@ def run_cases_apply(args, client) -> int:
|
|
|
961
1159
|
return 2
|
|
962
1160
|
|
|
963
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))
|
|
964
1209
|
|
|
965
1210
|
existing_by_input: dict[str, dict] = {}
|
|
966
1211
|
if not args.allow_duplicates:
|
|
@@ -984,6 +1229,11 @@ def run_cases_apply(args, client) -> int:
|
|
|
984
1229
|
return 2
|
|
985
1230
|
|
|
986
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
|
|
987
1237
|
attachment_ids: list[str] = []
|
|
988
1238
|
for fname in case.get("attachment_files") or []:
|
|
989
1239
|
fp = (attach_dir / fname).resolve() if attach_dir else None
|
|
@@ -1012,16 +1262,26 @@ def run_cases_apply(args, client) -> int:
|
|
|
1012
1262
|
or attachment_ids
|
|
1013
1263
|
or case.get("meta") is not None
|
|
1014
1264
|
or case.get("note") is not None
|
|
1265
|
+
or case_label_ids is not None
|
|
1015
1266
|
),
|
|
1267
|
+
"labels": case_label_names,
|
|
1268
|
+
"label_ids": case_label_ids,
|
|
1016
1269
|
"rubric_count": len(rubrics),
|
|
1017
1270
|
})
|
|
1018
1271
|
continue
|
|
1019
1272
|
log(f"reusing existing case: {label} ({case_id[:8]})")
|
|
1020
|
-
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
|
+
):
|
|
1021
1280
|
eval_mod.update_case(
|
|
1022
1281
|
client, case_id,
|
|
1023
1282
|
expected_output=case.get("expected_output"),
|
|
1024
1283
|
attachment_ids=attachment_ids or None,
|
|
1284
|
+
label_ids=case_label_ids,
|
|
1025
1285
|
meta=case.get("meta"),
|
|
1026
1286
|
note=case.get("note"),
|
|
1027
1287
|
)
|
|
@@ -1040,6 +1300,8 @@ def run_cases_apply(args, client) -> int:
|
|
|
1040
1300
|
"input_chars": len(case.get("input") or ""),
|
|
1041
1301
|
"expected_output_chars": len(case.get("expected_output") or ""),
|
|
1042
1302
|
"attachment_count": len(attachment_ids),
|
|
1303
|
+
"labels": case_label_names,
|
|
1304
|
+
"label_ids": case_label_ids,
|
|
1043
1305
|
"rubric_count": len(rubrics),
|
|
1044
1306
|
})
|
|
1045
1307
|
continue
|
|
@@ -1049,6 +1311,7 @@ def run_cases_apply(args, client) -> int:
|
|
|
1049
1311
|
client, agent_id=args.agent, input=case["input"],
|
|
1050
1312
|
expected_output=case.get("expected_output"),
|
|
1051
1313
|
attachment_ids=attachment_ids or None,
|
|
1314
|
+
label_ids=case_label_ids,
|
|
1052
1315
|
rubrics_by_evaluator=rubrics, meta=case.get("meta"),
|
|
1053
1316
|
note=case.get("note"),
|
|
1054
1317
|
)
|
|
@@ -1056,12 +1319,19 @@ def run_cases_apply(args, client) -> int:
|
|
|
1056
1319
|
labels.append(label)
|
|
1057
1320
|
created.append({"case_id": result["id"], "label": label})
|
|
1058
1321
|
|
|
1059
|
-
out = {
|
|
1322
|
+
out = {
|
|
1323
|
+
"case_ids": case_ids,
|
|
1324
|
+
"labels": labels,
|
|
1325
|
+
"created": created,
|
|
1326
|
+
"reused": reused,
|
|
1327
|
+
"created_case_labels": created_labels,
|
|
1328
|
+
}
|
|
1060
1329
|
if args.dry_run:
|
|
1061
1330
|
out.update({
|
|
1062
1331
|
"dry_run": True,
|
|
1063
1332
|
"operation": "cases_apply",
|
|
1064
1333
|
"agent_id": args.agent,
|
|
1334
|
+
"would_create_case_labels": would_create_labels,
|
|
1065
1335
|
"updates": dry_run_updates,
|
|
1066
1336
|
"would_write_server_state": True,
|
|
1067
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:
|
|
@@ -87,6 +93,44 @@ def delete_case(client: CodeerClient, case_id: str) -> dict:
|
|
|
87
93
|
return client.delete(f"/external/eval/cases/{case_id}")
|
|
88
94
|
|
|
89
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
|
+
|
|
90
134
|
|
|
91
135
|
# --- evaluators -----------------------------------------------------------
|
|
92
136
|
|
|
@@ -397,6 +441,7 @@ def create_case_with_rubrics(
|
|
|
397
441
|
rubrics_by_evaluator: dict[str, str],
|
|
398
442
|
expected_output: Optional[str] = None,
|
|
399
443
|
attachment_ids: Optional[List[str]] = None,
|
|
444
|
+
label_ids: Optional[List[str]] = None,
|
|
400
445
|
meta: Optional[dict] = None,
|
|
401
446
|
note: Optional[str] = None,
|
|
402
447
|
) -> dict:
|
|
@@ -416,6 +461,7 @@ def create_case_with_rubrics(
|
|
|
416
461
|
input=input,
|
|
417
462
|
expected_output=expected_output,
|
|
418
463
|
attachment_ids=attachment_ids,
|
|
464
|
+
label_ids=label_ids,
|
|
419
465
|
meta=meta,
|
|
420
466
|
note=note,
|
|
421
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
|