codeer-cli 0.1.13__py3-none-any.whl → 0.1.15__py3-none-any.whl
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/cli.py +3 -1
- codeer_cli/commands/eval_cmd.py +52 -5
- codeer_cli/commands/history.py +46 -9
- codeer_cli/commands/kb.py +400 -0
- codeer_cli/eval_.py +18 -2
- codeer_cli/histories.py +93 -11
- codeer_cli/kb.py +0 -1
- {codeer_cli-0.1.13.dist-info → codeer_cli-0.1.15.dist-info}/METADATA +86 -6
- {codeer_cli-0.1.13.dist-info → codeer_cli-0.1.15.dist-info}/RECORD +11 -11
- {codeer_cli-0.1.13.dist-info → codeer_cli-0.1.15.dist-info}/WHEEL +0 -0
- {codeer_cli-0.1.13.dist-info → codeer_cli-0.1.15.dist-info}/entry_points.txt +0 -0
codeer_cli/cli.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
codeer check
|
|
4
4
|
codeer agent list|get|apply|diff|versions
|
|
5
5
|
codeer model list
|
|
6
|
-
codeer kb list|files|upload|node-rename|node-delete|faq-list|faq-get|faq-create|faq-update|faq-delete
|
|
6
|
+
codeer kb list|files|export|upload|node-rename|node-delete|faq-list|faq-get|faq-create|faq-update|faq-delete
|
|
7
7
|
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
|
|
8
8
|
codeer history list|get|conversations|negative-feedback|create|send
|
|
9
9
|
"""
|
|
@@ -31,6 +31,8 @@ Safe workflow for coding agents:
|
|
|
31
31
|
codeer agent get <agent-id> --full
|
|
32
32
|
codeer kb list
|
|
33
33
|
codeer kb files --kb-id <kb-id>
|
|
34
|
+
codeer kb export --node-id <file-id> --file guide.md
|
|
35
|
+
codeer kb export --node-id <folder-id> --dir kb-export
|
|
34
36
|
codeer eval list --agent <agent-id>
|
|
35
37
|
codeer eval label-list
|
|
36
38
|
codeer eval case-update --case <case-id> --input "..." --dry-run
|
codeer_cli/commands/eval_cmd.py
CHANGED
|
@@ -128,6 +128,12 @@ def register(subparsers):
|
|
|
128
128
|
g.add_argument("--system-prompt-template", help="Evaluator system prompt template text")
|
|
129
129
|
g.add_argument("--system-prompt-template-file", help="Path to evaluator system prompt template")
|
|
130
130
|
p.add_argument("--description", default=None)
|
|
131
|
+
p.add_argument(
|
|
132
|
+
"--judge-model",
|
|
133
|
+
default=None,
|
|
134
|
+
metavar="MODEL_ID",
|
|
135
|
+
help="Judge LLM model ID (default: system default)",
|
|
136
|
+
)
|
|
131
137
|
p.add_argument("--dry-run", action="store_true",
|
|
132
138
|
help="Validate inputs and print intended mutation without writing server state.")
|
|
133
139
|
p.set_defaults(func=run_evaluator_create)
|
|
@@ -140,6 +146,18 @@ def register(subparsers):
|
|
|
140
146
|
g.add_argument("--system-prompt-template", help="Evaluator system prompt template text")
|
|
141
147
|
g.add_argument("--system-prompt-template-file", help="Path to evaluator system prompt template")
|
|
142
148
|
p.add_argument("--description", default=None)
|
|
149
|
+
g = p.add_mutually_exclusive_group()
|
|
150
|
+
g.add_argument(
|
|
151
|
+
"--judge-model",
|
|
152
|
+
default=None,
|
|
153
|
+
metavar="MODEL_ID",
|
|
154
|
+
help="Set the judge LLM model ID",
|
|
155
|
+
)
|
|
156
|
+
g.add_argument(
|
|
157
|
+
"--clear-judge-model",
|
|
158
|
+
action="store_true",
|
|
159
|
+
help="Clear the evaluator override and use the system default judge model",
|
|
160
|
+
)
|
|
143
161
|
p.add_argument("--dry-run", action="store_true",
|
|
144
162
|
help="Validate inputs and print intended mutation without writing server state.")
|
|
145
163
|
p.set_defaults(func=run_evaluator_update)
|
|
@@ -249,6 +267,7 @@ def _evaluator_summary(evaluator: dict, *, full: bool = False) -> dict:
|
|
|
249
267
|
"id": evaluator.get("id"),
|
|
250
268
|
"name": evaluator.get("name"),
|
|
251
269
|
"description": evaluator.get("description"),
|
|
270
|
+
"judge_llm_model_id": evaluator.get("judge_llm_model_id"),
|
|
252
271
|
"system_prompt_template_chars": len(template),
|
|
253
272
|
"has_tool_steps_placeholder": "{tool_steps}" in template,
|
|
254
273
|
"has_output_placeholder": "{output}" in template,
|
|
@@ -534,6 +553,10 @@ def run_evaluator_create(args, client) -> int:
|
|
|
534
553
|
"workspace_id": workspace_id,
|
|
535
554
|
"name": args.name,
|
|
536
555
|
"description": args.description,
|
|
556
|
+
"judge_model": {
|
|
557
|
+
"action": "set" if args.judge_model is not None else "use_system_default",
|
|
558
|
+
"model_id": args.judge_model,
|
|
559
|
+
},
|
|
537
560
|
"system_prompt_template_chars": len(system_prompt_template or ""),
|
|
538
561
|
"would_write_server_state": True,
|
|
539
562
|
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
@@ -545,6 +568,7 @@ def run_evaluator_create(args, client) -> int:
|
|
|
545
568
|
name=args.name,
|
|
546
569
|
system_prompt_template=system_prompt_template,
|
|
547
570
|
description=args.description,
|
|
571
|
+
judge_llm_model_id=args.judge_model,
|
|
548
572
|
)
|
|
549
573
|
print_json(_evaluator_summary(evaluator, full=True))
|
|
550
574
|
return 0
|
|
@@ -560,13 +584,27 @@ def run_evaluator_update(args, client) -> int:
|
|
|
560
584
|
else:
|
|
561
585
|
system_prompt_template = args.system_prompt_template
|
|
562
586
|
|
|
563
|
-
if
|
|
587
|
+
if (
|
|
588
|
+
args.name is None
|
|
589
|
+
and args.description is None
|
|
590
|
+
and system_prompt_template is None
|
|
591
|
+
and args.judge_model is None
|
|
592
|
+
and not args.clear_judge_model
|
|
593
|
+
):
|
|
564
594
|
log(
|
|
565
595
|
"error: provide at least one of --name, --description, "
|
|
566
|
-
"--system-prompt-template, --system-prompt-template-file"
|
|
596
|
+
"--system-prompt-template, --system-prompt-template-file, "
|
|
597
|
+
"--judge-model, --clear-judge-model"
|
|
567
598
|
)
|
|
568
599
|
return 2
|
|
569
600
|
|
|
601
|
+
if args.clear_judge_model:
|
|
602
|
+
judge_model = {"action": "clear_to_system_default", "model_id": None}
|
|
603
|
+
elif args.judge_model is not None:
|
|
604
|
+
judge_model = {"action": "set", "model_id": args.judge_model}
|
|
605
|
+
else:
|
|
606
|
+
judge_model = {"action": "unchanged"}
|
|
607
|
+
|
|
570
608
|
if args.dry_run:
|
|
571
609
|
print_json({
|
|
572
610
|
"dry_run": True,
|
|
@@ -574,6 +612,7 @@ def run_evaluator_update(args, client) -> int:
|
|
|
574
612
|
"evaluator_id": args.evaluator,
|
|
575
613
|
"name": args.name,
|
|
576
614
|
"description": args.description,
|
|
615
|
+
"judge_model": judge_model,
|
|
577
616
|
"system_prompt_template_chars": (
|
|
578
617
|
len(system_prompt_template) if system_prompt_template is not None else None
|
|
579
618
|
),
|
|
@@ -582,12 +621,20 @@ def run_evaluator_update(args, client) -> int:
|
|
|
582
621
|
})
|
|
583
622
|
return 0
|
|
584
623
|
|
|
624
|
+
evaluator_kwargs: dict[str, Any] = {
|
|
625
|
+
"name": args.name,
|
|
626
|
+
"system_prompt_template": system_prompt_template,
|
|
627
|
+
"description": args.description,
|
|
628
|
+
}
|
|
629
|
+
if args.clear_judge_model:
|
|
630
|
+
evaluator_kwargs["judge_llm_model_id"] = None
|
|
631
|
+
elif args.judge_model is not None:
|
|
632
|
+
evaluator_kwargs["judge_llm_model_id"] = args.judge_model
|
|
633
|
+
|
|
585
634
|
evaluator = eval_mod.update_evaluator(
|
|
586
635
|
client,
|
|
587
636
|
evaluator_id=args.evaluator,
|
|
588
|
-
|
|
589
|
-
system_prompt_template=system_prompt_template,
|
|
590
|
-
description=args.description,
|
|
637
|
+
**evaluator_kwargs,
|
|
591
638
|
)
|
|
592
639
|
print_json(_evaluator_summary(evaluator, full=True))
|
|
593
640
|
return 0
|
codeer_cli/commands/history.py
CHANGED
|
@@ -48,13 +48,17 @@ def register(subparsers):
|
|
|
48
48
|
# codeer history conversations <id>
|
|
49
49
|
p = sub.add_parser(
|
|
50
50
|
"conversations",
|
|
51
|
-
help="Summarize
|
|
51
|
+
help="Summarize persisted History parts. Complete parts/tool payloads require --out.",
|
|
52
52
|
)
|
|
53
53
|
p.add_argument("history_id", type=int)
|
|
54
54
|
p.add_argument("--full", action="store_true",
|
|
55
55
|
help="Require --out and include longer stdout previews; the artifact is always complete.")
|
|
56
56
|
p.add_argument("--out", default=None,
|
|
57
|
-
help="Write every
|
|
57
|
+
help="Write every persisted part allowed by the selected export contract.")
|
|
58
|
+
p.add_argument("--client-visible", action="store_true",
|
|
59
|
+
help="Use the external client-owner Chat V2 contract instead of the management export.")
|
|
60
|
+
p.add_argument("--user", default=None,
|
|
61
|
+
help="external_user_id required with --client-visible.")
|
|
58
62
|
p.set_defaults(func=run_conversations)
|
|
59
63
|
|
|
60
64
|
# codeer history negative-feedback
|
|
@@ -158,10 +162,12 @@ def _history_summary(row: dict, *, full: bool = False) -> dict:
|
|
|
158
162
|
|
|
159
163
|
def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
160
164
|
raw_content = part.get("content")
|
|
161
|
-
|
|
165
|
+
part_kind = part.get("part_kind")
|
|
166
|
+
is_tool_part = part_kind in {"tool-call", "tool-return"}
|
|
167
|
+
if is_tool_part:
|
|
168
|
+
content_value = None
|
|
169
|
+
elif isinstance(raw_content, dict):
|
|
162
170
|
content_value = raw_content.get("content")
|
|
163
|
-
if content_value is None and part.get("part_kind") == "tool-call":
|
|
164
|
-
content_value = raw_content.get("args")
|
|
165
171
|
else:
|
|
166
172
|
content_value = raw_content
|
|
167
173
|
if isinstance(content_value, str):
|
|
@@ -176,7 +182,7 @@ def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
|
176
182
|
"conversation_id": part.get("conversation_id"),
|
|
177
183
|
"conversation_group_id": part.get("conversation_group_id"),
|
|
178
184
|
"sequence": part.get("sequence"),
|
|
179
|
-
"part_kind":
|
|
185
|
+
"part_kind": part_kind,
|
|
180
186
|
"source": part.get("source"),
|
|
181
187
|
"created_at": part.get("created_at"),
|
|
182
188
|
"content_preview": truncate(content, 600 if full else 240),
|
|
@@ -184,6 +190,12 @@ def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
|
184
190
|
"attachment_count": len(part.get("attached_files") or []),
|
|
185
191
|
"feedback_count": len(part.get("feedbacks") or []),
|
|
186
192
|
}
|
|
193
|
+
if is_tool_part and isinstance(raw_content, dict):
|
|
194
|
+
row.update({
|
|
195
|
+
"tool_name": raw_content.get("tool_name"),
|
|
196
|
+
"tool_call_id": raw_content.get("tool_call_id"),
|
|
197
|
+
"outcome": raw_content.get("outcome"),
|
|
198
|
+
})
|
|
187
199
|
if full:
|
|
188
200
|
row["feedbacks"] = [
|
|
189
201
|
{
|
|
@@ -247,24 +259,49 @@ def run_get(args, client) -> int:
|
|
|
247
259
|
|
|
248
260
|
|
|
249
261
|
def run_conversations(args, client) -> int:
|
|
250
|
-
result = chats_mod.list_messages(client, args.history_id)
|
|
251
|
-
parts = result.get("messages") or []
|
|
252
262
|
if args.full and not args.out:
|
|
253
263
|
log("error: full conversation payloads are unbounded; pass --out <path>")
|
|
254
264
|
return 2
|
|
265
|
+
client_visible = bool(getattr(args, "client_visible", False))
|
|
266
|
+
external_user_id = getattr(args, "user", None)
|
|
267
|
+
if client_visible and not external_user_id:
|
|
268
|
+
log("error: --client-visible requires --user <external_user_id>")
|
|
269
|
+
return 2
|
|
270
|
+
if external_user_id and not client_visible:
|
|
271
|
+
log("error: --user is only valid with --client-visible")
|
|
272
|
+
return 2
|
|
273
|
+
|
|
274
|
+
if client_visible:
|
|
275
|
+
result = chats_mod.list_messages(
|
|
276
|
+
client,
|
|
277
|
+
args.history_id,
|
|
278
|
+
external_user_id=external_user_id,
|
|
279
|
+
)
|
|
280
|
+
export_mode = "client-visible"
|
|
281
|
+
else:
|
|
282
|
+
result = hist_mod.list_messages(client, args.history_id)
|
|
283
|
+
export_mode = "management"
|
|
284
|
+
parts = result.get("messages") or []
|
|
255
285
|
write_json(args.out, result)
|
|
256
286
|
group_ids = {
|
|
257
287
|
p.get("conversation_group_id")
|
|
258
288
|
for p in parts
|
|
259
289
|
if p.get("conversation_group_id")
|
|
260
290
|
}
|
|
291
|
+
stdout_limit = 50 if args.full else 20
|
|
292
|
+
shown_parts = parts[:stdout_limit]
|
|
261
293
|
print_json({
|
|
262
294
|
"history_id": args.history_id,
|
|
295
|
+
"export_mode": export_mode,
|
|
296
|
+
"export_contract": result.get("export_contract"),
|
|
297
|
+
"part_revision": result.get("part_revision"),
|
|
263
298
|
"turn_count": len(group_ids),
|
|
264
299
|
"part_count": len(parts),
|
|
300
|
+
"part_summaries_shown": len(shown_parts),
|
|
301
|
+
"part_summaries_truncated": len(parts) > len(shown_parts),
|
|
265
302
|
"wrote_full_detail": bool(args.out),
|
|
266
303
|
"stdout_is_summary": True,
|
|
267
|
-
"parts": [_part_summary(p, i, full=args.full) for i, p in enumerate(
|
|
304
|
+
"parts": [_part_summary(p, i, full=args.full) for i, p in enumerate(shown_parts)],
|
|
268
305
|
})
|
|
269
306
|
return 0
|
|
270
307
|
|
codeer_cli/commands/kb.py
CHANGED
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from .. import kb as kb_mod
|
|
9
|
+
from ..client import AuthError, CodeerError
|
|
9
10
|
from ._util import log, print_json, strip_noisy_fields, truncate, write_json
|
|
10
11
|
|
|
11
12
|
POLL_INTERVAL = 3
|
|
@@ -76,6 +77,28 @@ def register(subparsers):
|
|
|
76
77
|
help="Write stripped full file metadata to this file; stdout stays compact unless --full.")
|
|
77
78
|
p.set_defaults(func=run_files)
|
|
78
79
|
|
|
80
|
+
# codeer kb export
|
|
81
|
+
p = sub.add_parser(
|
|
82
|
+
"export",
|
|
83
|
+
help="Export available KB file snapshot contents as local UTF-8 Markdown files.",
|
|
84
|
+
)
|
|
85
|
+
p.add_argument("--node-id", required=True, help="File, folder, or KB root node UUID to export")
|
|
86
|
+
target = p.add_mutually_exclusive_group(required=True)
|
|
87
|
+
target.add_argument("--file", help="Local output path for single-file export")
|
|
88
|
+
target.add_argument("--dir", help="Local output directory for recursive folder export")
|
|
89
|
+
p.add_argument(
|
|
90
|
+
"--overwrite",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="Overwrite existing export file(s). Without this flag, the export is blocked before writing.",
|
|
93
|
+
)
|
|
94
|
+
p.add_argument("--full", action="store_true", help="Print the full per-file export manifest.")
|
|
95
|
+
p.add_argument(
|
|
96
|
+
"--out",
|
|
97
|
+
default=None,
|
|
98
|
+
help="Write the full export manifest JSON to this file; stdout stays compact unless --full.",
|
|
99
|
+
)
|
|
100
|
+
p.set_defaults(func=run_export)
|
|
101
|
+
|
|
79
102
|
p = sub.add_parser("upload", help="Create/reuse KB and upload files from a directory; run --dry-run first")
|
|
80
103
|
p.add_argument("--dir", required=True, help="Directory containing files to upload")
|
|
81
104
|
p.add_argument("--name", required=True, help="KB display name (idempotent on name)")
|
|
@@ -334,6 +357,383 @@ def run_files(args, client) -> int:
|
|
|
334
357
|
return 0
|
|
335
358
|
|
|
336
359
|
|
|
360
|
+
_WINDOWS_RESERVED_NAMES = {
|
|
361
|
+
"CON",
|
|
362
|
+
"PRN",
|
|
363
|
+
"AUX",
|
|
364
|
+
"NUL",
|
|
365
|
+
*(f"COM{i}" for i in range(1, 10)),
|
|
366
|
+
*(f"LPT{i}" for i in range(1, 10)),
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _safe_export_component(value: str | None, *, fallback: str) -> str:
|
|
371
|
+
"""Return one portable path component without allowing path traversal."""
|
|
372
|
+
raw = (value or "").strip()
|
|
373
|
+
cleaned = "".join(
|
|
374
|
+
"_" if ord(char) < 32 or char in '<>:"/\\|?*' else char
|
|
375
|
+
for char in raw
|
|
376
|
+
).rstrip(" .")
|
|
377
|
+
if not cleaned or cleaned in {".", ".."}:
|
|
378
|
+
cleaned = fallback
|
|
379
|
+
if cleaned.split(".", 1)[0].upper() in _WINDOWS_RESERVED_NAMES:
|
|
380
|
+
cleaned = f"_{cleaned}"
|
|
381
|
+
return cleaned
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _snapshot_filename(name: str | None, *, node_id: str) -> str:
|
|
385
|
+
safe_name = _safe_export_component(name, fallback=f"file-{node_id[:8]}")
|
|
386
|
+
if Path(safe_name).suffix.lower() not in {".md", ".markdown"}:
|
|
387
|
+
safe_name = f"{safe_name}.md"
|
|
388
|
+
return safe_name
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _unique_export_component(component: str, *, node_id: str, used: set[str]) -> str:
|
|
392
|
+
candidate = component
|
|
393
|
+
index = 1
|
|
394
|
+
while candidate.casefold() in used:
|
|
395
|
+
suffix = f"__{node_id[:8]}" if index == 1 else f"__{node_id[:8]}-{index}"
|
|
396
|
+
path = Path(component)
|
|
397
|
+
candidate = f"{path.stem}{suffix}{path.suffix}" if path.suffix else f"{component}{suffix}"
|
|
398
|
+
index += 1
|
|
399
|
+
used.add(candidate.casefold())
|
|
400
|
+
return candidate
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _collect_export_files(
|
|
404
|
+
client,
|
|
405
|
+
*,
|
|
406
|
+
organization_id: str,
|
|
407
|
+
workspace_id: str,
|
|
408
|
+
root_node_id: str,
|
|
409
|
+
) -> tuple[list[dict], list[dict]]:
|
|
410
|
+
"""Walk the KB tree and assign deterministic, collision-safe local paths."""
|
|
411
|
+
pending: list[tuple[str, tuple[str, ...], tuple[str, ...]]] = [(root_node_id, (), ())]
|
|
412
|
+
seen: set[str] = set()
|
|
413
|
+
files: list[dict] = []
|
|
414
|
+
issues: list[dict] = []
|
|
415
|
+
|
|
416
|
+
while pending:
|
|
417
|
+
parent_id, output_parts, source_parts = pending.pop()
|
|
418
|
+
if parent_id in seen:
|
|
419
|
+
issues.append({"node_id": parent_id, "reason": "cycle_or_duplicate_folder"})
|
|
420
|
+
continue
|
|
421
|
+
seen.add(parent_id)
|
|
422
|
+
|
|
423
|
+
nodes = kb_mod.list_nodes(
|
|
424
|
+
client,
|
|
425
|
+
organization_id=organization_id,
|
|
426
|
+
workspace_id=workspace_id,
|
|
427
|
+
parent_id=parent_id,
|
|
428
|
+
)
|
|
429
|
+
nodes = sorted(
|
|
430
|
+
nodes,
|
|
431
|
+
key=lambda node: (
|
|
432
|
+
str(node.get("node_type") or node.get("type") or "").upper() != "FOLDER",
|
|
433
|
+
str(node.get("name") or node.get("original_name") or "").casefold(),
|
|
434
|
+
str(node.get("id") or node.get("node_id") or ""),
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
used_components: set[str] = set()
|
|
438
|
+
child_folders: list[tuple[str, tuple[str, ...], tuple[str, ...]]] = []
|
|
439
|
+
|
|
440
|
+
for node in nodes:
|
|
441
|
+
node_id = str(node.get("id") or node.get("node_id") or "")
|
|
442
|
+
node_type = str(node.get("node_type") or node.get("type") or "").upper()
|
|
443
|
+
node_name = str(node.get("name") or node.get("original_name") or "")
|
|
444
|
+
if not node_id:
|
|
445
|
+
issues.append({"name": node_name, "reason": "missing_node_id"})
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
if node_type == "FOLDER":
|
|
449
|
+
component = _safe_export_component(node_name, fallback=f"folder-{node_id[:8]}")
|
|
450
|
+
component = _unique_export_component(component, node_id=node_id, used=used_components)
|
|
451
|
+
child_folders.append(
|
|
452
|
+
(
|
|
453
|
+
node_id,
|
|
454
|
+
(*output_parts, component),
|
|
455
|
+
(*source_parts, node_name or node_id),
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
if node_type != "FILE":
|
|
461
|
+
issues.append({"node_id": node_id, "name": node_name, "reason": f"unknown_node_type:{node_type}"})
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
filename = _snapshot_filename(node_name, node_id=node_id)
|
|
465
|
+
filename = _unique_export_component(filename, node_id=node_id, used=used_components)
|
|
466
|
+
files.append(
|
|
467
|
+
{
|
|
468
|
+
"node_id": node_id,
|
|
469
|
+
"name": node_name,
|
|
470
|
+
"source_path": "/".join((*source_parts, node_name or node_id)),
|
|
471
|
+
"server_status": str(node.get("status") or "").upper() or None,
|
|
472
|
+
"relative_output_path": str(Path(*output_parts, filename)),
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
pending.extend(reversed(child_folders))
|
|
477
|
+
|
|
478
|
+
files.sort(key=lambda item: item["relative_output_path"].casefold())
|
|
479
|
+
return files, issues
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _compact_export_result(result: dict) -> dict:
|
|
483
|
+
problems = [
|
|
484
|
+
{
|
|
485
|
+
"node_id": item.get("node_id"),
|
|
486
|
+
"source_path": item.get("source_path"),
|
|
487
|
+
"result": item.get("result"),
|
|
488
|
+
"reason": item.get("reason"),
|
|
489
|
+
}
|
|
490
|
+
for item in result.get("files", [])
|
|
491
|
+
if item.get("result") != "exported"
|
|
492
|
+
]
|
|
493
|
+
return {
|
|
494
|
+
key: result.get(key)
|
|
495
|
+
for key in (
|
|
496
|
+
"operation",
|
|
497
|
+
"mode",
|
|
498
|
+
"node_id",
|
|
499
|
+
"directory",
|
|
500
|
+
"output_file",
|
|
501
|
+
"server_status",
|
|
502
|
+
"exported_while_not_ready",
|
|
503
|
+
"complete",
|
|
504
|
+
"file_count",
|
|
505
|
+
"exported_count",
|
|
506
|
+
"non_ready_exported_count",
|
|
507
|
+
"skipped_count",
|
|
508
|
+
"failed_count",
|
|
509
|
+
"issue_count",
|
|
510
|
+
"reason",
|
|
511
|
+
)
|
|
512
|
+
} | {
|
|
513
|
+
"problems": problems[:20],
|
|
514
|
+
"additional_problem_count": max(0, len(problems) - 20),
|
|
515
|
+
"blocked_paths": result.get("blocked_paths", [])[:20],
|
|
516
|
+
"additional_blocked_path_count": max(0, len(result.get("blocked_paths", [])) - 20),
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _print_export_result(args, result: dict) -> None:
|
|
521
|
+
write_json(args.out, result)
|
|
522
|
+
print_json(result if args.full else _compact_export_result(result))
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _read_export_content(
|
|
526
|
+
client,
|
|
527
|
+
*,
|
|
528
|
+
organization_id: str,
|
|
529
|
+
workspace_id: str,
|
|
530
|
+
item: dict,
|
|
531
|
+
) -> str | None:
|
|
532
|
+
try:
|
|
533
|
+
response = kb_mod.read_file_content(
|
|
534
|
+
client,
|
|
535
|
+
organization_id=organization_id,
|
|
536
|
+
workspace_id=workspace_id,
|
|
537
|
+
node_id=item["node_id"],
|
|
538
|
+
)
|
|
539
|
+
except AuthError:
|
|
540
|
+
raise
|
|
541
|
+
except CodeerError as exc:
|
|
542
|
+
if exc.status not in {400, 404}:
|
|
543
|
+
raise
|
|
544
|
+
item.update(result="failed", reason=f"api_error:{exc.status}:{exc.message}")
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
response_status = str(response.get("status") or "").upper()
|
|
548
|
+
response_name = str(response.get("name") or "")
|
|
549
|
+
content = response.get("content")
|
|
550
|
+
item["server_status"] = response_status or item.get("server_status")
|
|
551
|
+
if response_name:
|
|
552
|
+
item["name"] = response_name
|
|
553
|
+
item.setdefault("source_path", response_name or item["node_id"])
|
|
554
|
+
if content is None:
|
|
555
|
+
item.update(
|
|
556
|
+
result="skipped",
|
|
557
|
+
reason=f"content_unavailable:{response_status or 'UNKNOWN'}",
|
|
558
|
+
)
|
|
559
|
+
return None
|
|
560
|
+
if not isinstance(content, str):
|
|
561
|
+
item.update(result="failed", reason="content_is_not_text")
|
|
562
|
+
return None
|
|
563
|
+
return content
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _write_export_content(target: Path, content: str, item: dict) -> None:
|
|
567
|
+
try:
|
|
568
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
569
|
+
target.write_text(content, encoding="utf-8")
|
|
570
|
+
except OSError as exc:
|
|
571
|
+
item.update(result="failed", reason=f"local_write_error:{exc}")
|
|
572
|
+
return
|
|
573
|
+
item.update(
|
|
574
|
+
result="exported",
|
|
575
|
+
bytes_written=len(content.encode("utf-8")),
|
|
576
|
+
exported_while_not_ready=item.get("server_status") != "READY",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _export_counts(files: list[dict]) -> dict:
|
|
581
|
+
exported_count = sum(item.get("result") == "exported" for item in files)
|
|
582
|
+
return {
|
|
583
|
+
"file_count": len(files),
|
|
584
|
+
"exported_count": exported_count,
|
|
585
|
+
"non_ready_exported_count": sum(
|
|
586
|
+
item.get("result") == "exported" and item.get("server_status") != "READY"
|
|
587
|
+
for item in files
|
|
588
|
+
),
|
|
589
|
+
"skipped_count": sum(item.get("result") == "skipped" for item in files),
|
|
590
|
+
"failed_count": sum(item.get("result") == "failed" for item in files),
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _run_file_export(args, client, *, workspace_id: str, organization_id: str) -> int:
|
|
595
|
+
output_file = Path(args.file).expanduser().resolve()
|
|
596
|
+
if args.out and Path(args.out).expanduser().resolve() == output_file:
|
|
597
|
+
log("error: --out manifest path must differ from --file content path")
|
|
598
|
+
return 2
|
|
599
|
+
if output_file.exists() and (output_file.is_dir() or not args.overwrite):
|
|
600
|
+
log("error: single-file export blocked before writing; use --overwrite only if replacement is intended")
|
|
601
|
+
return 2
|
|
602
|
+
|
|
603
|
+
existing_parent = output_file.parent
|
|
604
|
+
while not existing_parent.exists() and existing_parent != existing_parent.parent:
|
|
605
|
+
existing_parent = existing_parent.parent
|
|
606
|
+
if existing_parent.exists() and not existing_parent.is_dir():
|
|
607
|
+
log(f"error: output parent {existing_parent} is not a directory")
|
|
608
|
+
return 2
|
|
609
|
+
|
|
610
|
+
item = {
|
|
611
|
+
"node_id": args.node_id,
|
|
612
|
+
"output_path": str(output_file),
|
|
613
|
+
}
|
|
614
|
+
content = _read_export_content(
|
|
615
|
+
client,
|
|
616
|
+
organization_id=organization_id,
|
|
617
|
+
workspace_id=workspace_id,
|
|
618
|
+
item=item,
|
|
619
|
+
)
|
|
620
|
+
if content is not None:
|
|
621
|
+
_write_export_content(output_file, content, item)
|
|
622
|
+
|
|
623
|
+
counts = _export_counts([item])
|
|
624
|
+
complete = counts["skipped_count"] == 0 and counts["failed_count"] == 0
|
|
625
|
+
result = {
|
|
626
|
+
"operation": "kb_export",
|
|
627
|
+
"mode": "file",
|
|
628
|
+
"node_id": args.node_id,
|
|
629
|
+
"directory": str(output_file.parent),
|
|
630
|
+
"output_file": str(output_file),
|
|
631
|
+
"server_status": item.get("server_status"),
|
|
632
|
+
"exported_while_not_ready": item.get("exported_while_not_ready", False),
|
|
633
|
+
"complete": complete,
|
|
634
|
+
**counts,
|
|
635
|
+
"issue_count": 0,
|
|
636
|
+
"issues": [],
|
|
637
|
+
"files": [item],
|
|
638
|
+
}
|
|
639
|
+
_print_export_result(args, result)
|
|
640
|
+
return 0 if complete else 1
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _run_folder_export(args, client, *, workspace_id: str, organization_id: str) -> int:
|
|
644
|
+
output_dir = Path(args.dir).expanduser().resolve()
|
|
645
|
+
if output_dir.exists() and not output_dir.is_dir():
|
|
646
|
+
log(f"error: --dir {output_dir} exists and is not a directory")
|
|
647
|
+
return 2
|
|
648
|
+
|
|
649
|
+
files, issues = _collect_export_files(
|
|
650
|
+
client,
|
|
651
|
+
organization_id=organization_id,
|
|
652
|
+
workspace_id=workspace_id,
|
|
653
|
+
root_node_id=args.node_id,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
blocked_paths: list[str] = []
|
|
657
|
+
for item in files:
|
|
658
|
+
target = output_dir / item["relative_output_path"]
|
|
659
|
+
if target.exists() and (target.is_dir() or not args.overwrite):
|
|
660
|
+
blocked_paths.append(str(target))
|
|
661
|
+
parent = target.parent
|
|
662
|
+
while parent != output_dir:
|
|
663
|
+
if parent.exists() and not parent.is_dir():
|
|
664
|
+
blocked_paths.append(str(parent))
|
|
665
|
+
break
|
|
666
|
+
parent = parent.parent
|
|
667
|
+
|
|
668
|
+
if blocked_paths:
|
|
669
|
+
result = {
|
|
670
|
+
"operation": "kb_export",
|
|
671
|
+
"mode": "folder",
|
|
672
|
+
"node_id": args.node_id,
|
|
673
|
+
"directory": str(output_dir),
|
|
674
|
+
"output_file": None,
|
|
675
|
+
"complete": False,
|
|
676
|
+
"file_count": len(files),
|
|
677
|
+
"exported_count": 0,
|
|
678
|
+
"non_ready_exported_count": 0,
|
|
679
|
+
"skipped_count": 0,
|
|
680
|
+
"failed_count": len(set(blocked_paths)),
|
|
681
|
+
"issue_count": len(issues),
|
|
682
|
+
"issues": issues,
|
|
683
|
+
"files": [],
|
|
684
|
+
"blocked_paths": sorted(set(blocked_paths)),
|
|
685
|
+
"reason": "existing_paths_require_overwrite_or_conflict_with_directories",
|
|
686
|
+
}
|
|
687
|
+
_print_export_result(args, result)
|
|
688
|
+
log("error: export blocked before writing; use --overwrite only if replacing existing files is intended")
|
|
689
|
+
return 2
|
|
690
|
+
|
|
691
|
+
for item in files:
|
|
692
|
+
target = output_dir / item["relative_output_path"]
|
|
693
|
+
content = _read_export_content(
|
|
694
|
+
client,
|
|
695
|
+
organization_id=organization_id,
|
|
696
|
+
workspace_id=workspace_id,
|
|
697
|
+
item=item,
|
|
698
|
+
)
|
|
699
|
+
if content is not None:
|
|
700
|
+
_write_export_content(target, content, item)
|
|
701
|
+
|
|
702
|
+
counts = _export_counts(files)
|
|
703
|
+
complete = not issues and counts["skipped_count"] == 0 and counts["failed_count"] == 0
|
|
704
|
+
result = {
|
|
705
|
+
"operation": "kb_export",
|
|
706
|
+
"mode": "folder",
|
|
707
|
+
"node_id": args.node_id,
|
|
708
|
+
"directory": str(output_dir),
|
|
709
|
+
"output_file": None,
|
|
710
|
+
"complete": complete,
|
|
711
|
+
**counts,
|
|
712
|
+
"issue_count": len(issues),
|
|
713
|
+
"issues": issues,
|
|
714
|
+
"files": files,
|
|
715
|
+
}
|
|
716
|
+
_print_export_result(args, result)
|
|
717
|
+
return 0 if complete else 1
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def run_export(args, client) -> int:
|
|
721
|
+
workspace_id, organization_id = client.resolve_scope()
|
|
722
|
+
if args.file:
|
|
723
|
+
return _run_file_export(
|
|
724
|
+
args,
|
|
725
|
+
client,
|
|
726
|
+
workspace_id=workspace_id,
|
|
727
|
+
organization_id=organization_id,
|
|
728
|
+
)
|
|
729
|
+
return _run_folder_export(
|
|
730
|
+
args,
|
|
731
|
+
client,
|
|
732
|
+
workspace_id=workspace_id,
|
|
733
|
+
organization_id=organization_id,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
337
737
|
def run_upload(args, client) -> int:
|
|
338
738
|
workspace_id, organization_id = client.resolve_scope()
|
|
339
739
|
kb_dir = Path(args.dir).resolve()
|
codeer_cli/eval_.py
CHANGED
|
@@ -12,6 +12,13 @@ from typing import Any, List, Optional
|
|
|
12
12
|
from .client import CodeerClient
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class _UnsetType:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_UNSET = _UnsetType()
|
|
20
|
+
|
|
21
|
+
|
|
15
22
|
# --- cases ----------------------------------------------------------------
|
|
16
23
|
|
|
17
24
|
def create_case(
|
|
@@ -122,7 +129,7 @@ def get_case_evaluator_infos(client: CodeerClient, *, case_ids: List[str]) -> li
|
|
|
122
129
|
Returns rows shaped like ``{"case_id": str, "evaluators": [...]}``, where
|
|
123
130
|
each evaluator entry is the assigned ``{"evaluator_id", "rubric"}`` pair.
|
|
124
131
|
"""
|
|
125
|
-
return client.post("/eval/case-evaluator-infos:batch", json={"case_ids": case_ids})
|
|
132
|
+
return client.post("/external/eval/case-evaluator-infos:batch", json={"case_ids": case_ids})
|
|
126
133
|
|
|
127
134
|
|
|
128
135
|
def replace_case_evaluator_infos(
|
|
@@ -136,7 +143,10 @@ def replace_case_evaluator_infos(
|
|
|
136
143
|
This is intentionally separate from rubric upsert: replacing removes
|
|
137
144
|
evaluator assignments that are not included in ``evaluators``.
|
|
138
145
|
"""
|
|
139
|
-
return client.put(
|
|
146
|
+
return client.put(
|
|
147
|
+
f"/external/eval/cases/{case_id}/case-evaluator-infos",
|
|
148
|
+
json={"evaluators": evaluators},
|
|
149
|
+
)
|
|
140
150
|
|
|
141
151
|
|
|
142
152
|
# --- case labels -----------------------------------------------------------
|
|
@@ -186,6 +196,7 @@ def create_evaluator(
|
|
|
186
196
|
name: str,
|
|
187
197
|
system_prompt_template: str,
|
|
188
198
|
description: Optional[str] = None,
|
|
199
|
+
judge_llm_model_id: Optional[str] = None,
|
|
189
200
|
) -> dict:
|
|
190
201
|
body: dict[str, Any] = {
|
|
191
202
|
"name": name,
|
|
@@ -193,6 +204,8 @@ def create_evaluator(
|
|
|
193
204
|
}
|
|
194
205
|
if description is not None:
|
|
195
206
|
body["description"] = description
|
|
207
|
+
if judge_llm_model_id is not None:
|
|
208
|
+
body["judge_llm_model_id"] = judge_llm_model_id
|
|
196
209
|
return client.post("/external/eval/evaluators", json=body)
|
|
197
210
|
|
|
198
211
|
|
|
@@ -211,6 +224,7 @@ def update_evaluator(
|
|
|
211
224
|
name: Optional[str] = None,
|
|
212
225
|
system_prompt_template: Optional[str] = None,
|
|
213
226
|
description: Optional[str] = None,
|
|
227
|
+
judge_llm_model_id: str | None | _UnsetType = _UNSET,
|
|
214
228
|
) -> dict:
|
|
215
229
|
body: dict[str, Any] = {}
|
|
216
230
|
if name is not None:
|
|
@@ -219,6 +233,8 @@ def update_evaluator(
|
|
|
219
233
|
body["system_prompt_template"] = system_prompt_template
|
|
220
234
|
if description is not None:
|
|
221
235
|
body["description"] = description
|
|
236
|
+
if judge_llm_model_id is not _UNSET:
|
|
237
|
+
body["judge_llm_model_id"] = judge_llm_model_id
|
|
222
238
|
return client.put(f"/external/eval/evaluators/{evaluator_id}", json=body)
|
|
223
239
|
|
|
224
240
|
|
codeer_cli/histories.py
CHANGED
|
@@ -4,14 +4,15 @@ Use this after an agent has been published and running for a while, to pull
|
|
|
4
4
|
recent traffic, filter by feedback, and feed the failing cases back into the
|
|
5
5
|
evaluation loop.
|
|
6
6
|
|
|
7
|
-
Pagination: ``/histories``
|
|
8
|
-
``
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Pagination: ``/histories`` and the management parts export use ``limit`` +
|
|
8
|
+
``offset`` (NOT ``page`` / ``page_size``). History metadata remains a bounded
|
|
9
|
+
caller-selected page. The parts export follows every server page and rejects a
|
|
10
|
+
cross-page revision change instead of returning a mixed snapshot.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import builtins
|
|
15
16
|
from typing import Any, Iterable, Optional
|
|
16
17
|
|
|
17
18
|
from .client import CodeerClient
|
|
@@ -94,11 +95,9 @@ def list_negative_feedback_turns(
|
|
|
94
95
|
the source channel — usually "system"). Pass the desired sentiment(s)
|
|
95
96
|
in ``feedback_types``.
|
|
96
97
|
|
|
97
|
-
Cost: O(N histories) paginated
|
|
98
|
+
Cost: O(N histories) paginated management parts reads. Filter aggressively via
|
|
98
99
|
``exclude_users`` and ``limit`` before invoking on a busy agent.
|
|
99
100
|
"""
|
|
100
|
-
from . import chats
|
|
101
|
-
|
|
102
101
|
type_set = {t.lower() for t in feedback_types}
|
|
103
102
|
histories = list(
|
|
104
103
|
client,
|
|
@@ -113,10 +112,7 @@ def list_negative_feedback_turns(
|
|
|
113
112
|
hid = h.get("id")
|
|
114
113
|
if hid is None:
|
|
115
114
|
continue
|
|
116
|
-
|
|
117
|
-
parts = (chats.list_messages(client, hid).get("messages") or [])
|
|
118
|
-
except Exception:
|
|
119
|
-
continue
|
|
115
|
+
parts = (list_messages(client, hid).get("messages") or [])
|
|
120
116
|
|
|
121
117
|
turn_indexes: dict[str, int] = {}
|
|
122
118
|
user_messages: dict[str, str] = {}
|
|
@@ -158,6 +154,92 @@ def get(client: CodeerClient, history_id: int) -> dict:
|
|
|
158
154
|
return client.get(f"/external/histories/{history_id}")
|
|
159
155
|
|
|
160
156
|
|
|
157
|
+
def list_messages(
|
|
158
|
+
client: CodeerClient,
|
|
159
|
+
history_id: int,
|
|
160
|
+
*,
|
|
161
|
+
limit: int = 100,
|
|
162
|
+
) -> dict:
|
|
163
|
+
"""Return every persisted diagnostic part through the management export.
|
|
164
|
+
|
|
165
|
+
This route is for workspace editors analyzing a History. It is distinct
|
|
166
|
+
from the external client-owner Chat V2 route and must return the explicit
|
|
167
|
+
``history-parts-v1`` contract. Tool calls and returns are preserved in the
|
|
168
|
+
returned artifact. System prompts and provider raw traces are outside the
|
|
169
|
+
contract.
|
|
170
|
+
"""
|
|
171
|
+
if limit <= 0:
|
|
172
|
+
raise ValueError("limit must be greater than zero")
|
|
173
|
+
|
|
174
|
+
offset = 0
|
|
175
|
+
pages_fetched = 0
|
|
176
|
+
total_records: int | None = None
|
|
177
|
+
part_revision: str | None = None
|
|
178
|
+
result: dict[str, Any] | None = None
|
|
179
|
+
messages: list[dict] = []
|
|
180
|
+
|
|
181
|
+
while True:
|
|
182
|
+
page = client.get(
|
|
183
|
+
f"/external/histories/{history_id}/messages",
|
|
184
|
+
params={"limit": limit, "offset": offset},
|
|
185
|
+
)
|
|
186
|
+
if not isinstance(page, dict):
|
|
187
|
+
raise ValueError("History parts response must be an object")
|
|
188
|
+
if page.get("export_contract") != "history-parts-v1":
|
|
189
|
+
raise ValueError("History parts response is missing export_contract=history-parts-v1")
|
|
190
|
+
|
|
191
|
+
page_messages = page.get("messages")
|
|
192
|
+
page_info = page.get("page")
|
|
193
|
+
revision = page.get("part_revision")
|
|
194
|
+
if not isinstance(page_messages, builtins.list):
|
|
195
|
+
raise ValueError("History parts response must contain a messages list")
|
|
196
|
+
if not isinstance(page_info, dict):
|
|
197
|
+
raise ValueError("History parts response must contain page metadata")
|
|
198
|
+
if not isinstance(revision, str) or not revision:
|
|
199
|
+
raise ValueError("History parts response must contain part_revision")
|
|
200
|
+
|
|
201
|
+
page_offset = page_info.get("offset")
|
|
202
|
+
page_total = page_info.get("total_records")
|
|
203
|
+
page_limit = page_info.get("limit")
|
|
204
|
+
if not all(isinstance(value, int) for value in (page_offset, page_total, page_limit)):
|
|
205
|
+
raise ValueError("History parts page metadata must contain integer limit/offset/total_records")
|
|
206
|
+
if page_offset != offset:
|
|
207
|
+
raise ValueError(f"History parts page offset mismatch: requested {offset}, received {page_offset}")
|
|
208
|
+
if page_total < 0 or page_limit <= 0:
|
|
209
|
+
raise ValueError("History parts page metadata is invalid")
|
|
210
|
+
|
|
211
|
+
if total_records is None:
|
|
212
|
+
total_records = page_total
|
|
213
|
+
elif page_total != total_records:
|
|
214
|
+
raise ValueError("History parts total_records changed while paging; retry the export")
|
|
215
|
+
if part_revision is None:
|
|
216
|
+
part_revision = revision
|
|
217
|
+
elif revision != part_revision:
|
|
218
|
+
raise ValueError("History parts changed while paging; retry the export")
|
|
219
|
+
|
|
220
|
+
if result is None:
|
|
221
|
+
result = dict(page)
|
|
222
|
+
messages.extend(page_messages)
|
|
223
|
+
pages_fetched += 1
|
|
224
|
+
offset += len(page_messages)
|
|
225
|
+
|
|
226
|
+
if offset >= total_records:
|
|
227
|
+
break
|
|
228
|
+
if not page_messages:
|
|
229
|
+
raise ValueError("History parts pagination stopped before total_records was reached")
|
|
230
|
+
|
|
231
|
+
assert result is not None
|
|
232
|
+
assert total_records is not None
|
|
233
|
+
result["messages"] = messages
|
|
234
|
+
result["page"] = {
|
|
235
|
+
"limit": result["page"]["limit"],
|
|
236
|
+
"offset": 0,
|
|
237
|
+
"total_records": total_records,
|
|
238
|
+
}
|
|
239
|
+
result["pages_fetched"] = pages_fetched
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
|
|
161
243
|
def get_conversations(client: CodeerClient, history_id: int) -> list[dict]:
|
|
162
244
|
"""Return legacy V1 conversation rows.
|
|
163
245
|
|
codeer_cli/kb.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.15
|
|
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
|
|
@@ -98,7 +98,8 @@ CODEER_AGENT_ID=<agent-id>
|
|
|
98
98
|
|
|
99
99
|
## Development install
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
Codeer contributors should use an editable install from this checkout, not the
|
|
102
|
+
PyPI package, so the `codeer` command always executes the folder being edited:
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
105
|
cd /path/to/codeer-skills/codeer-cli
|
|
@@ -123,6 +124,34 @@ List the active cloud models without opening the Codeer web app:
|
|
|
123
124
|
codeer model list --type text
|
|
124
125
|
```
|
|
125
126
|
|
|
127
|
+
## Custom evaluator judge models
|
|
128
|
+
|
|
129
|
+
Custom evaluator create/update commands can select a judge LLM model by ID:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
codeer eval evaluator-create \
|
|
133
|
+
--name "Correctness" \
|
|
134
|
+
--system-prompt-template-file evaluator-prompt.txt \
|
|
135
|
+
--judge-model <model-id> \
|
|
136
|
+
--dry-run
|
|
137
|
+
|
|
138
|
+
codeer eval evaluator-update \
|
|
139
|
+
--evaluator <evaluator-id> \
|
|
140
|
+
--judge-model <model-id> \
|
|
141
|
+
--dry-run
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Omit the judge-model flags on update to leave the current setting unchanged.
|
|
145
|
+
Use `--clear-judge-model` to explicitly clear the override and return to the
|
|
146
|
+
system default:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
codeer eval evaluator-update \
|
|
150
|
+
--evaluator <evaluator-id> \
|
|
151
|
+
--clear-judge-model \
|
|
152
|
+
--dry-run
|
|
153
|
+
```
|
|
154
|
+
|
|
126
155
|
## Agent human handoff
|
|
127
156
|
|
|
128
157
|
`codeer agent apply` accepts the same `human_handoff` object as the Agent API.
|
|
@@ -197,10 +226,22 @@ Flags:
|
|
|
197
226
|
for raw eval results, full conversation turns, full rubric matrices, and
|
|
198
227
|
other data that can grow with cases, versions, or turns.
|
|
199
228
|
|
|
200
|
-
`history conversations`
|
|
201
|
-
automatically. Its stdout is
|
|
202
|
-
the
|
|
203
|
-
|
|
229
|
+
`history conversations` defaults to the workspace-editor management export and
|
|
230
|
+
follows all pages automatically. Its stdout is a bounded summary that never
|
|
231
|
+
prints tool arguments or results; the `--out` artifact preserves the complete
|
|
232
|
+
`history-parts-v1` payload, including persisted tool calls/results,
|
|
233
|
+
attachments, feedback, and metadata. System prompts and provider raw traces are
|
|
234
|
+
not part of that export contract, and a missing part does not prove that a tool
|
|
235
|
+
was not executed.
|
|
236
|
+
|
|
237
|
+
Use the external client-owner contract only when that distinction is the point
|
|
238
|
+
of the test:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
codeer history conversations <history-id> \
|
|
242
|
+
--client-visible --user <external-user-id> \
|
|
243
|
+
--out .codeer/current/client-history-<history-id>.json
|
|
244
|
+
```
|
|
204
245
|
|
|
205
246
|
Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
|
|
206
247
|
the coding agent to inspect targeted summaries, IDs, failing cases, or selected
|
|
@@ -228,6 +269,45 @@ paths containing `*` so the shell passes the wildcard to the CLI. Advanced
|
|
|
228
269
|
settings can still be passed through `--config-json`; explicit crawler flags
|
|
229
270
|
override matching JSON keys.
|
|
230
271
|
|
|
272
|
+
## Exporting KB snapshot content
|
|
273
|
+
|
|
274
|
+
Export one file directly from the content endpoint:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
codeer kb export \
|
|
278
|
+
--node-id <file-node-id> \
|
|
279
|
+
--file guide.md
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Or recursively export a folder or an entire KB root:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
codeer kb export \
|
|
286
|
+
--node-id <folder-or-kb-root-node-id> \
|
|
287
|
+
--dir kb-export \
|
|
288
|
+
--out kb-export-manifest.json
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`--file` and `--dir` are mutually exclusive. Single-file mode maps directly to
|
|
292
|
+
the external file-content endpoint and lets the caller choose the exact local
|
|
293
|
+
path. Folder mode recursively lists the node tree, calls that endpoint for each
|
|
294
|
+
file, and writes the extracted snapshot content as UTF-8 Markdown. Existing
|
|
295
|
+
`.md`/`.markdown` names are preserved; other folder-export names receive an
|
|
296
|
+
additional `.md` suffix (for example, `guide.pdf` becomes `guide.pdf.md`) so the
|
|
297
|
+
export is not mistaken for the original binary upload.
|
|
298
|
+
|
|
299
|
+
The command asks the content endpoint for every file regardless of indexing
|
|
300
|
+
status. If the endpoint returns text, it is exported even when the status is
|
|
301
|
+
not `READY`; the full manifest preserves that server status and marks the file
|
|
302
|
+
as `exported_while_not_ready`. If the endpoint returns `content: null`, the file
|
|
303
|
+
is skipped and the command exits non-zero. Existing target files block the
|
|
304
|
+
entire export before any content is written; pass `--overwrite` only when
|
|
305
|
+
replacing those local files is intended.
|
|
306
|
+
|
|
307
|
+
This is a snapshot-content export, not an original-file backup. The server API
|
|
308
|
+
returns processed text and does not return the original PDF, DOCX, or other
|
|
309
|
+
binary bytes through this endpoint.
|
|
310
|
+
|
|
231
311
|
## KB node rename and delete
|
|
232
312
|
|
|
233
313
|
Knowledge Base roots, folders, and files are all KnowledgeNodes. Use
|
|
@@ -2,24 +2,24 @@ codeer_cli/__init__.py,sha256=z3rkUdVCt11c8QE3-YFkG5cFObM6QhPe2yKugA8k3aA,1915
|
|
|
2
2
|
codeer_cli/_validate.py,sha256=lqMwkhsBy1g0o5CINBp1HdBCjMawgUaEsZygjJaAPL0,6943
|
|
3
3
|
codeer_cli/agents.py,sha256=Yt_Rw4c9omsTnXsTe5n_9mD4TpDcx_1WuUkhtKmFY0g,6371
|
|
4
4
|
codeer_cli/chats.py,sha256=Vg1DIu8fT6mPyPpfqLpExKvXhdZEWCyJGPvalQy3BXg,7989
|
|
5
|
-
codeer_cli/cli.py,sha256=
|
|
5
|
+
codeer_cli/cli.py,sha256=PDi0WLC4prkFSbY0Y7619MAdP3uoDzgTWekHrhHprHc,4756
|
|
6
6
|
codeer_cli/client.py,sha256=apUycjK-OvHEquAj58FqOy8FHmHjA6K9dScaYQ4M-VU,13175
|
|
7
7
|
codeer_cli/constants.py,sha256=D1pV3wCoqYybrKGKeoupYjjFWLfaFviKp1yL7oh6Qso,2323
|
|
8
|
-
codeer_cli/eval_.py,sha256=
|
|
9
|
-
codeer_cli/histories.py,sha256=
|
|
10
|
-
codeer_cli/kb.py,sha256
|
|
8
|
+
codeer_cli/eval_.py,sha256=TjDFGCGF19JaUmSaP3PiKIRrBIvYaU-EJS4dC-N7eBI,17868
|
|
9
|
+
codeer_cli/histories.py,sha256=Tom4tM20mls-Jo-q7NXish3zeOyMM5Nkd7MZjHFfy88,9979
|
|
10
|
+
codeer_cli/kb.py,sha256=-HodcGwTvIKgvuRVc5sPFu51si-GlzLEsPSHmnfrVQU,10615
|
|
11
11
|
codeer_cli/models.py,sha256=Ama9fx8hyRMdysiUNA3Ty0sYIYwzFDkOcAZg-id3-vQ,362
|
|
12
12
|
codeer_cli/parse.py,sha256=-txMtIxPfeg_i4McEskVKxo8p4438gKS-sFTI38RoBs,21772
|
|
13
13
|
codeer_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
codeer_cli/commands/_util.py,sha256=X9-9cYgnBYz94hVG4Pe9SsI97DHNv3HE5_7GlPMn93I,1708
|
|
15
15
|
codeer_cli/commands/agent.py,sha256=BjctOF0tflt8yVGzdc6320sWXVUAYrF39tNIiTxtQx4,16319
|
|
16
16
|
codeer_cli/commands/check.py,sha256=lTxolx1mIJ8jldPhJ5FXqie9nbCLVOO-sDPOHTSy1-w,3817
|
|
17
|
-
codeer_cli/commands/eval_cmd.py,sha256=
|
|
18
|
-
codeer_cli/commands/history.py,sha256=
|
|
19
|
-
codeer_cli/commands/kb.py,sha256=
|
|
17
|
+
codeer_cli/commands/eval_cmd.py,sha256=S-ydzWZppPLMCu2i_HOK0OW5ZaoebxcLO--WDYOIuAk,73548
|
|
18
|
+
codeer_cli/commands/history.py,sha256=jUm0ZnIaJc0FV7pwMNdCBuuKPoauhDY2swFqB6_byWU,18800
|
|
19
|
+
codeer_cli/commands/kb.py,sha256=1lwrKTC0NpnAvXZ0u5DPsfx7ZXcHLURyBrqRViv03Yk,42349
|
|
20
20
|
codeer_cli/commands/model.py,sha256=91LDo_YeHi3POZV7KTCkoCcgtK5p87LwrHX1lZgfCps,1698
|
|
21
21
|
codeer_cli/commands/profile.py,sha256=IdlXC_6cqobsfN3JRrAnt-1OgBUsIFneS9QtR4Un6Kc,6521
|
|
22
|
-
codeer_cli-0.1.
|
|
23
|
-
codeer_cli-0.1.
|
|
24
|
-
codeer_cli-0.1.
|
|
25
|
-
codeer_cli-0.1.
|
|
22
|
+
codeer_cli-0.1.15.dist-info/METADATA,sha256=Cwz2jFmIb0GSVClvWt3WqfKm-F_SRvO7W6n67d6t7AY,10673
|
|
23
|
+
codeer_cli-0.1.15.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
24
|
+
codeer_cli-0.1.15.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
|
|
25
|
+
codeer_cli-0.1.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|