codeer-cli 0.1.14__py3-none-any.whl → 0.1.16__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/_http_contracts.py +109 -0
- codeer_cli/_validate.py +8 -2
- codeer_cli/agents.py +1 -1
- codeer_cli/cli.py +8 -2
- codeer_cli/client.py +4 -3
- codeer_cli/commands/_util.py +7 -2
- codeer_cli/commands/agent.py +31 -10
- codeer_cli/commands/history.py +142 -9
- codeer_cli/commands/kb.py +400 -0
- codeer_cli/eval_.py +5 -2
- codeer_cli/histories.py +193 -12
- codeer_cli/kb.py +0 -1
- codeer_cli-0.1.16.dist-info/METADATA +479 -0
- codeer_cli-0.1.16.dist-info/RECORD +26 -0
- {codeer_cli-0.1.14.dist-info → codeer_cli-0.1.16.dist-info}/WHEEL +1 -1
- codeer_cli-0.1.14.dist-info/METADATA +0 -292
- codeer_cli-0.1.14.dist-info/RECORD +0 -25
- {codeer_cli-0.1.14.dist-info → codeer_cli-0.1.16.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""HTTP input *configuration* checks; value conversion belongs to the server.
|
|
2
|
+
|
|
3
|
+
Key generation follows collect_http_request_agent_placeholders in the backend's
|
|
4
|
+
http_request/execution.py. Keep its traversal and collision rules in sync.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_AGENT_PLACEHOLDER_RE = re.compile(r"\{\{\s*agent\[(.*?)\]\s*\}\}")
|
|
14
|
+
_CONTRACT_FIELDS = {
|
|
15
|
+
"type": ("string", "number", "integer", "boolean", "object", "array"),
|
|
16
|
+
"format": ("text", "json"),
|
|
17
|
+
"json_type": ("any", "object", "array"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _contract_defaults(contract: Any, key: str) -> dict[str, str]:
|
|
22
|
+
prefix = f"input_contracts[{key!r}]"
|
|
23
|
+
if not isinstance(contract, dict):
|
|
24
|
+
raise ValueError(f"{prefix} must be an object.")
|
|
25
|
+
if any(field not in _CONTRACT_FIELDS for field in contract):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"{prefix} accepts only type, format, json_type (snake_case, not jsonType)."
|
|
28
|
+
)
|
|
29
|
+
resolved = {}
|
|
30
|
+
for field, choices in _CONTRACT_FIELDS.items():
|
|
31
|
+
value = contract.get(field, choices[0])
|
|
32
|
+
if not isinstance(value, str) or value not in choices:
|
|
33
|
+
raise ValueError(f"{prefix}.{field} must be one of: {', '.join(choices)}.")
|
|
34
|
+
resolved[field] = value
|
|
35
|
+
if resolved["format"] == "json" and resolved["type"] != "string":
|
|
36
|
+
raise ValueError(f"{prefix}: format=json is only available for type=string.")
|
|
37
|
+
if resolved["format"] != "json" and resolved["json_type"] != "any":
|
|
38
|
+
raise ValueError(f"{prefix}: json_type requires format=json unless it is any.")
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_http_input_contracts(body: Any) -> list[dict[str, Any]]:
|
|
43
|
+
"""Validate without mutation and return value-free input format summaries.
|
|
44
|
+
|
|
45
|
+
An omitted body is supplied as {} by callers; template may be null/absent.
|
|
46
|
+
Unconfigured placeholders retain string/text/any, including mixed text.
|
|
47
|
+
Unknown body fields follow the backend's existing ignore policy, except the
|
|
48
|
+
frontend alias inputContracts: reject it rather than silently losing intent.
|
|
49
|
+
Contract entries themselves are strict and reject all unknown fields.
|
|
50
|
+
"""
|
|
51
|
+
if not isinstance(body, dict):
|
|
52
|
+
raise ValueError("body must be an object; omit it for no body.")
|
|
53
|
+
if "inputContracts" in body:
|
|
54
|
+
raise ValueError("use input_contracts (snake_case), not inputContracts.")
|
|
55
|
+
contracts = body.get("input_contracts", {})
|
|
56
|
+
if not isinstance(contracts, dict) or any(
|
|
57
|
+
not isinstance(key, str) for key in contracts
|
|
58
|
+
):
|
|
59
|
+
raise ValueError("input_contracts must be an object with string keys.")
|
|
60
|
+
resolved = {
|
|
61
|
+
key: _contract_defaults(contract, key) for key, contract in contracts.items()
|
|
62
|
+
}
|
|
63
|
+
summaries: list[dict[str, Any]] = []
|
|
64
|
+
used_keys: set[str] = set()
|
|
65
|
+
|
|
66
|
+
def walk(value: Any, path: str) -> None:
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
for key, item in value.items():
|
|
69
|
+
walk(item, f"{path}.{key}" if path else key)
|
|
70
|
+
return
|
|
71
|
+
if isinstance(value, list):
|
|
72
|
+
for index, item in enumerate(value):
|
|
73
|
+
walk(item, f"{path}[{index}]")
|
|
74
|
+
return
|
|
75
|
+
if not isinstance(value, str):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
matches = list(_AGENT_PLACEHOLDER_RE.finditer(value))
|
|
79
|
+
for index, match in enumerate(matches):
|
|
80
|
+
if not match.group(1).strip():
|
|
81
|
+
raise ValueError("Agent placeholder instruction cannot be empty.")
|
|
82
|
+
base = (
|
|
83
|
+
re.sub(r"[^a-zA-Z0-9]+", "_", path or "body").strip("_").lower()
|
|
84
|
+
or "body"
|
|
85
|
+
)
|
|
86
|
+
if len(matches) > 1:
|
|
87
|
+
base = f"{base}_{index + 1}"
|
|
88
|
+
key = base
|
|
89
|
+
suffix = 2
|
|
90
|
+
while key in used_keys:
|
|
91
|
+
key = f"{base}_{suffix}"
|
|
92
|
+
suffix += 1
|
|
93
|
+
used_keys.add(key)
|
|
94
|
+
contract = resolved.get(key) or _contract_defaults({}, key)
|
|
95
|
+
if (contract["type"] != "string" or contract["format"] == "json") and (
|
|
96
|
+
len(matches) != 1 or match.group(0) != value
|
|
97
|
+
):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Input {key!r}: typed and JSON-text inputs must occupy the entire template value."
|
|
100
|
+
)
|
|
101
|
+
summaries.append({"key": key, **contract, "configured": key in contracts})
|
|
102
|
+
|
|
103
|
+
walk(body.get("template"), "")
|
|
104
|
+
if set(contracts) - used_keys:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"input_contracts contains keys that do not match current placeholders; "
|
|
107
|
+
"remove stale contracts or regenerate keys after changing template paths/order."
|
|
108
|
+
)
|
|
109
|
+
return summaries
|
codeer_cli/_validate.py
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
These checks exist because the backend's form-schema validator is lenient
|
|
4
4
|
(``extra="allow"``) and silently accepts unknown ``type`` strings, which then
|
|
5
5
|
render as blank fields in the web builder. Catching the common mistakes here
|
|
6
|
-
gives actionable errors before the
|
|
6
|
+
gives actionable errors before the PATCH/POST round-trip. HTTP input contracts
|
|
7
|
+
also need strict configuration checks before a draft is saved.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
12
|
from typing import Any, Iterable
|
|
12
13
|
|
|
14
|
+
from ._http_contracts import validate_http_input_contracts
|
|
13
15
|
from .constants import (
|
|
14
16
|
FORM_FIELD_TYPES,
|
|
15
17
|
MAX_CALL_AGENT_TOOLS,
|
|
@@ -121,10 +123,14 @@ def _validate_single_tool(tool: dict[str, Any], index: int) -> None:
|
|
|
121
123
|
raise ToolValidationError(f"{prefix}: call_agent tool requires agent_id.")
|
|
122
124
|
elif tool_type == "http_request":
|
|
123
125
|
cfg = tool.get("http_request")
|
|
124
|
-
if not cfg or not cfg.get("method") or not cfg.get("url_template"):
|
|
126
|
+
if not isinstance(cfg, dict) or not cfg.get("method") or not cfg.get("url_template"):
|
|
125
127
|
raise ToolValidationError(
|
|
126
128
|
f"{prefix}: http_request tool requires http_request.method and http_request.url_template."
|
|
127
129
|
)
|
|
130
|
+
try:
|
|
131
|
+
validate_http_input_contracts(cfg.get("body", {}))
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
raise ToolValidationError(f"{prefix}.http_request.body: {exc}") from None
|
|
128
134
|
|
|
129
135
|
|
|
130
136
|
def _validate_form_schema(schema: Any, prefix: str) -> None:
|
codeer_cli/agents.py
CHANGED
|
@@ -66,7 +66,7 @@ def update(
|
|
|
66
66
|
attachment_ids: Optional[List[str]] = None,
|
|
67
67
|
human_handoff: Optional[dict[str, Any]] = None,
|
|
68
68
|
) -> dict:
|
|
69
|
-
"""
|
|
69
|
+
"""PATCH replaces settings and creates a draft; unified_tools is the full list."""
|
|
70
70
|
validated_tools = validate_unified_tools(unified_tools)
|
|
71
71
|
validated_handoff = validate_human_handoff(human_handoff)
|
|
72
72
|
body: dict[str, Any] = {
|
codeer_cli/cli.py
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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
|
-
codeer history list|get|conversations|negative-feedback|create|send
|
|
8
|
+
codeer history list|get|conversations|ai-drafts|negative-feedback|create|send
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
|
|
17
17
|
from .client import AuthError, CodeerClient, CodeerError
|
|
18
18
|
from .commands import check
|
|
19
|
+
from .histories import HistoryExportError
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -31,6 +32,8 @@ Safe workflow for coding agents:
|
|
|
31
32
|
codeer agent get <agent-id> --full
|
|
32
33
|
codeer kb list
|
|
33
34
|
codeer kb files --kb-id <kb-id>
|
|
35
|
+
codeer kb export --node-id <file-id> --file guide.md
|
|
36
|
+
codeer kb export --node-id <folder-id> --dir kb-export
|
|
34
37
|
codeer eval list --agent <agent-id>
|
|
35
38
|
codeer eval label-list
|
|
36
39
|
codeer eval case-update --case <case-id> --input "..." --dry-run
|
|
@@ -125,6 +128,9 @@ Use --out <path> for large raw artifacts; stdout defaults to compact summaries.
|
|
|
125
128
|
|
|
126
129
|
try:
|
|
127
130
|
return args.func(args, client)
|
|
131
|
+
except HistoryExportError as e:
|
|
132
|
+
print(f"error: {e}", file=sys.stderr)
|
|
133
|
+
return 1
|
|
128
134
|
except AuthError as e:
|
|
129
135
|
print(f"auth: {e}", file=sys.stderr)
|
|
130
136
|
return 3
|
codeer_cli/client.py
CHANGED
|
@@ -161,6 +161,7 @@ class CodeerClient:
|
|
|
161
161
|
files: Any = None,
|
|
162
162
|
data: Any = None,
|
|
163
163
|
timeout: Optional[float] = None,
|
|
164
|
+
unwrap: bool = True,
|
|
164
165
|
) -> Any:
|
|
165
166
|
url = _api_url(path, api_version=api_version)
|
|
166
167
|
request_kwargs: dict[str, Any] = {}
|
|
@@ -186,7 +187,7 @@ class CodeerClient:
|
|
|
186
187
|
) from exc
|
|
187
188
|
except httpx.RequestError as exc:
|
|
188
189
|
raise self._transport_error(method_upper, path, exc) from exc
|
|
189
|
-
return self._parse(r)
|
|
190
|
+
return self._parse(r, unwrap=unwrap)
|
|
190
191
|
|
|
191
192
|
def get(self, path: str, **kwargs: Any) -> Any:
|
|
192
193
|
return self.request("GET", path, **kwargs)
|
|
@@ -290,7 +291,7 @@ class CodeerClient:
|
|
|
290
291
|
},
|
|
291
292
|
)
|
|
292
293
|
|
|
293
|
-
def _parse(self, r: httpx.Response) -> Any:
|
|
294
|
+
def _parse(self, r: httpx.Response, *, unwrap: bool = True) -> Any:
|
|
294
295
|
text = r.text
|
|
295
296
|
try:
|
|
296
297
|
payload = r.json() if text else None
|
|
@@ -306,7 +307,7 @@ class CodeerClient:
|
|
|
306
307
|
if isinstance(payload, dict) and "error_code" in payload and "data" in payload:
|
|
307
308
|
if payload.get("error_code") not in (0, None):
|
|
308
309
|
raise CodeerError(r.status_code, payload.get("message") or "error", payload)
|
|
309
|
-
return payload["data"]
|
|
310
|
+
return payload["data"] if unwrap else payload
|
|
310
311
|
return payload
|
|
311
312
|
|
|
312
313
|
def _raise_for_error(self, status: int, payload: Any) -> None:
|
codeer_cli/commands/_util.py
CHANGED
|
@@ -34,7 +34,12 @@ NOISY_KEYS = {
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def strip_noisy_fields(value: Any) -> Any:
|
|
37
|
-
"""
|
|
37
|
+
"""Simplify metadata on one API resource (or a list of resources).
|
|
38
|
+
|
|
39
|
+
Callers pass unwrapped resource records, not response envelopes. Nested
|
|
40
|
+
dictionaries are opaque content: templates, schemas, tool results and other
|
|
41
|
+
user data can legally contain any of the metadata names. Do not recurse.
|
|
42
|
+
"""
|
|
38
43
|
if isinstance(value, list):
|
|
39
44
|
return [strip_noisy_fields(item) for item in value]
|
|
40
45
|
if not isinstance(value, dict):
|
|
@@ -51,7 +56,7 @@ def strip_noisy_fields(value: Any) -> Any:
|
|
|
51
56
|
if item.get(k) is not None
|
|
52
57
|
}
|
|
53
58
|
continue
|
|
54
|
-
out[key] =
|
|
59
|
+
out[key] = item
|
|
55
60
|
return out
|
|
56
61
|
|
|
57
62
|
|
codeer_cli/commands/agent.py
CHANGED
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
from .. import agents as agents_mod
|
|
9
|
+
from .._http_contracts import validate_http_input_contracts
|
|
9
10
|
from .._validate import validate_human_handoff, validate_unified_tools
|
|
10
11
|
from ..client import CodeerClient
|
|
11
12
|
from ._util import log, print_json, strip_noisy_fields, truncate, write_json
|
|
@@ -32,6 +33,7 @@ def register(subparsers):
|
|
|
32
33
|
help="Read one agent. Defaults to summary; use --full for prompt/tool detail or --out for an artifact.",
|
|
33
34
|
)
|
|
34
35
|
p.add_argument("agent_id")
|
|
36
|
+
p.add_argument("--history", default=None, help="Read this exact AgentHistory UUID instead of the editable agent.")
|
|
35
37
|
p.add_argument("--full", action="store_true",
|
|
36
38
|
help="Print stripped full agent config, including system_prompt and tools.")
|
|
37
39
|
p.add_argument("--out", default=None,
|
|
@@ -41,8 +43,8 @@ def register(subparsers):
|
|
|
41
43
|
# codeer agent apply --payload agent.json
|
|
42
44
|
p = sub.add_parser("apply", help="Create or update agent from JSON payload; run --dry-run first")
|
|
43
45
|
p.add_argument("--payload", required=True, help="Path to agent payload JSON")
|
|
44
|
-
p.add_argument("--agent-id", default=None, help="If set,
|
|
45
|
-
p.add_argument("--note", default="", help="version_note for
|
|
46
|
+
p.add_argument("--agent-id", default=None, help="If set, PATCH (update). Else POST (create).")
|
|
47
|
+
p.add_argument("--note", default="", help="version_note for PATCH")
|
|
46
48
|
p.add_argument("--dry-run", action="store_true",
|
|
47
49
|
help="Validate payload and print intended mutation without writing server state.")
|
|
48
50
|
p.add_argument("--out", default=None, help="Write result JSON to this file too")
|
|
@@ -61,13 +63,13 @@ def register(subparsers):
|
|
|
61
63
|
# codeer agent versions --agent <id>
|
|
62
64
|
p = sub.add_parser(
|
|
63
65
|
"versions",
|
|
64
|
-
help="List version
|
|
66
|
+
help="List version metadata; use agent get --history <id> for a stored snapshot.",
|
|
65
67
|
)
|
|
66
68
|
p.add_argument("--agent", required=True)
|
|
67
69
|
p.add_argument("--full", action="store_true",
|
|
68
|
-
help="Add
|
|
70
|
+
help="Add prompt/tool size metadata when included by the server.")
|
|
69
71
|
p.add_argument("--out", default=None,
|
|
70
|
-
help="Write
|
|
72
|
+
help="Write the full version listing (metadata, not snapshots); stdout stays compact.")
|
|
71
73
|
p.set_defaults(func=run_versions)
|
|
72
74
|
|
|
73
75
|
p = sub.add_parser("impact", help="Check downstream agents affected by this agent")
|
|
@@ -130,6 +132,8 @@ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
|
|
|
130
132
|
"human_handoff_enabled": bool(human_handoff.get("enabled")),
|
|
131
133
|
"system_prompt_chars": len(agent.get("system_prompt") or ""),
|
|
132
134
|
}
|
|
135
|
+
if "version_number" in agent:
|
|
136
|
+
row.update({key: agent.get(key) for key in ("agent_id", "version_number", "status")})
|
|
133
137
|
if full:
|
|
134
138
|
row["description"] = agent.get("description") or ""
|
|
135
139
|
row["use_search"] = agent.get("use_search")
|
|
@@ -150,7 +154,11 @@ def run_list(args, client) -> int:
|
|
|
150
154
|
|
|
151
155
|
|
|
152
156
|
def run_get(args, client) -> int:
|
|
153
|
-
|
|
157
|
+
history_id = getattr(args, "history", None)
|
|
158
|
+
result = (
|
|
159
|
+
agents_mod.get_version(client, args.agent_id, history_id)
|
|
160
|
+
if history_id else agents_mod.get(client, args.agent_id)
|
|
161
|
+
)
|
|
154
162
|
full_result = strip_noisy_fields(result)
|
|
155
163
|
write_json(args.out, full_result)
|
|
156
164
|
print_json(full_result if args.full else _agent_summary(result))
|
|
@@ -183,6 +191,15 @@ def run_apply(args, client) -> int:
|
|
|
183
191
|
"name": body.get("name"),
|
|
184
192
|
"system_prompt_chars": len(body.get("system_prompt") or ""),
|
|
185
193
|
"tool_count": len(validated_tools),
|
|
194
|
+
"http_inputs": [
|
|
195
|
+
{
|
|
196
|
+
"tool_index": index,
|
|
197
|
+
"body_inputs_used": tool["http_request"]["method"] not in ("GET", "HEAD"),
|
|
198
|
+
"inputs": validate_http_input_contracts(tool["http_request"].get("body", {})),
|
|
199
|
+
}
|
|
200
|
+
for index, tool in enumerate(validated_tools)
|
|
201
|
+
if tool.get("type") == "http_request"
|
|
202
|
+
],
|
|
186
203
|
"use_search": body.get("use_search", False),
|
|
187
204
|
"llm_model": body.get("llm_model"),
|
|
188
205
|
"llm_model_settings_provided": llm_model_settings_provided,
|
|
@@ -223,7 +240,7 @@ def run_apply(args, client) -> int:
|
|
|
223
240
|
**model_settings_kwargs,
|
|
224
241
|
)
|
|
225
242
|
agent_id = args.agent_id
|
|
226
|
-
log(f"
|
|
243
|
+
log(f"PATCH /agents/{agent_id} ok")
|
|
227
244
|
else:
|
|
228
245
|
if not body.get("workspace_id"):
|
|
229
246
|
body["workspace_id"] = client.resolve_scope()[0]
|
|
@@ -274,8 +291,10 @@ def run_versions(args, client) -> int:
|
|
|
274
291
|
"created_at": v.get("created_at"),
|
|
275
292
|
}
|
|
276
293
|
if args.full:
|
|
277
|
-
|
|
278
|
-
|
|
294
|
+
if "system_prompt" in v:
|
|
295
|
+
row["system_prompt_chars"] = len(v.get("system_prompt") or "")
|
|
296
|
+
if "unified_tools" in v or "tools" in v:
|
|
297
|
+
row["tool_count"] = len(v.get("unified_tools") or v.get("tools") or [])
|
|
279
298
|
rows.append(row)
|
|
280
299
|
print_json(rows)
|
|
281
300
|
return 0
|
|
@@ -371,7 +390,9 @@ def _summarize_tool(t: dict) -> str:
|
|
|
371
390
|
"knowledge_node_ids", "domain", "agent_id",
|
|
372
391
|
"custom_form_schema", "http_request")
|
|
373
392
|
safe = {k: t.get(k) for k in keep_keys if t.get(k) is not None}
|
|
374
|
-
|
|
393
|
+
# HTTP placeholder keys depend on template insertion order. Sorting would
|
|
394
|
+
# hide a reordering that changes which input receives a collision suffix.
|
|
395
|
+
return json.dumps(safe, ensure_ascii=False, indent=2, sort_keys=t.get("type") != "http_request")
|
|
375
396
|
|
|
376
397
|
|
|
377
398
|
def run_diff(args, client) -> int:
|
codeer_cli/commands/history.py
CHANGED
|
@@ -23,6 +23,8 @@ def register(subparsers):
|
|
|
23
23
|
p.add_argument("--agent", default=None)
|
|
24
24
|
p.add_argument("--user", default=None, help="Filter by external_user_id")
|
|
25
25
|
p.add_argument("--feedback", default=None, help="positive / negative / any")
|
|
26
|
+
p.add_argument("--has-ai-drafts", action="store_true",
|
|
27
|
+
help="Only list histories with at least one AI Draft.")
|
|
26
28
|
p.add_argument("--exclude-users", default=None,
|
|
27
29
|
help="Comma-separated external_user_ids to exclude")
|
|
28
30
|
p.add_argument("--version", type=int, default=None,
|
|
@@ -48,15 +50,31 @@ def register(subparsers):
|
|
|
48
50
|
# codeer history conversations <id>
|
|
49
51
|
p = sub.add_parser(
|
|
50
52
|
"conversations",
|
|
51
|
-
help="Summarize
|
|
53
|
+
help="Summarize persisted History parts. Complete parts/tool payloads require --out.",
|
|
52
54
|
)
|
|
53
55
|
p.add_argument("history_id", type=int)
|
|
54
56
|
p.add_argument("--full", action="store_true",
|
|
55
57
|
help="Require --out and include longer stdout previews; the artifact is always complete.")
|
|
56
58
|
p.add_argument("--out", default=None,
|
|
57
|
-
help="Write every
|
|
59
|
+
help="Write every persisted part allowed by the selected export contract.")
|
|
60
|
+
p.add_argument("--client-visible", action="store_true",
|
|
61
|
+
help="Use the external client-owner Chat V2 contract instead of the management export.")
|
|
62
|
+
p.add_argument("--user", default=None,
|
|
63
|
+
help="external_user_id required with --client-visible.")
|
|
58
64
|
p.set_defaults(func=run_conversations)
|
|
59
65
|
|
|
66
|
+
# codeer history ai-drafts <id>
|
|
67
|
+
p = sub.add_parser(
|
|
68
|
+
"ai-drafts",
|
|
69
|
+
help="Export every paginated AI Draft lifecycle record returned for one History.",
|
|
70
|
+
)
|
|
71
|
+
p.add_argument("history_id", type=int)
|
|
72
|
+
p.add_argument("--full", action="store_true",
|
|
73
|
+
help="Require --out and opt into sensitive content previews on stdout.")
|
|
74
|
+
p.add_argument("--out", default=None,
|
|
75
|
+
help="Write every AI Draft record, refinement signal, outcome, tool activity, and delivery.")
|
|
76
|
+
p.set_defaults(func=run_ai_drafts)
|
|
77
|
+
|
|
60
78
|
# codeer history negative-feedback
|
|
61
79
|
p = sub.add_parser("negative-feedback", help="Surface assistant turns with negative feedback")
|
|
62
80
|
p.add_argument("--agent", required=True)
|
|
@@ -144,6 +162,15 @@ def _history_summary(row: dict, *, full: bool = False) -> dict:
|
|
|
144
162
|
"feedback_counts": _feedback_counts(row),
|
|
145
163
|
"snippet_preview": truncate(row.get("snippet") or "", 240),
|
|
146
164
|
}
|
|
165
|
+
for key in (
|
|
166
|
+
"ai_draft_count",
|
|
167
|
+
"dismissed_draft_count",
|
|
168
|
+
"regenerated_draft_count",
|
|
169
|
+
"applied_draft_count",
|
|
170
|
+
"sent_from_ai_draft_count",
|
|
171
|
+
):
|
|
172
|
+
if key in row:
|
|
173
|
+
out[key] = row.get(key)
|
|
147
174
|
if full:
|
|
148
175
|
out["share_type"] = row.get("share_type")
|
|
149
176
|
meta = row.get("meta") or {}
|
|
@@ -158,10 +185,12 @@ def _history_summary(row: dict, *, full: bool = False) -> dict:
|
|
|
158
185
|
|
|
159
186
|
def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
160
187
|
raw_content = part.get("content")
|
|
161
|
-
|
|
188
|
+
part_kind = part.get("part_kind")
|
|
189
|
+
is_tool_part = part_kind in {"tool-call", "tool-return"}
|
|
190
|
+
if is_tool_part:
|
|
191
|
+
content_value = None
|
|
192
|
+
elif isinstance(raw_content, dict):
|
|
162
193
|
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
194
|
else:
|
|
166
195
|
content_value = raw_content
|
|
167
196
|
if isinstance(content_value, str):
|
|
@@ -176,7 +205,7 @@ def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
|
176
205
|
"conversation_id": part.get("conversation_id"),
|
|
177
206
|
"conversation_group_id": part.get("conversation_group_id"),
|
|
178
207
|
"sequence": part.get("sequence"),
|
|
179
|
-
"part_kind":
|
|
208
|
+
"part_kind": part_kind,
|
|
180
209
|
"source": part.get("source"),
|
|
181
210
|
"created_at": part.get("created_at"),
|
|
182
211
|
"content_preview": truncate(content, 600 if full else 240),
|
|
@@ -184,6 +213,15 @@ def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
|
184
213
|
"attachment_count": len(part.get("attached_files") or []),
|
|
185
214
|
"feedback_count": len(part.get("feedbacks") or []),
|
|
186
215
|
}
|
|
216
|
+
if is_tool_part:
|
|
217
|
+
# Exact tool payloads belong in the artifact, never terminal previews.
|
|
218
|
+
row.pop("content_preview", None)
|
|
219
|
+
if is_tool_part and isinstance(raw_content, dict):
|
|
220
|
+
row.update({
|
|
221
|
+
"tool_name": raw_content.get("tool_name"),
|
|
222
|
+
"tool_call_id": raw_content.get("tool_call_id"),
|
|
223
|
+
"outcome": raw_content.get("outcome"),
|
|
224
|
+
})
|
|
187
225
|
if full:
|
|
188
226
|
row["feedbacks"] = [
|
|
189
227
|
{
|
|
@@ -212,6 +250,7 @@ def run_list(args, client) -> int:
|
|
|
212
250
|
organization_id=organization_id,
|
|
213
251
|
external_user_id=args.user,
|
|
214
252
|
feedback_filter=args.feedback,
|
|
253
|
+
has_ai_drafts=True if getattr(args, "has_ai_drafts", False) else None,
|
|
215
254
|
exclude_users=exclude,
|
|
216
255
|
limit=args.limit,
|
|
217
256
|
offset=args.offset,
|
|
@@ -247,24 +286,118 @@ def run_get(args, client) -> int:
|
|
|
247
286
|
|
|
248
287
|
|
|
249
288
|
def run_conversations(args, client) -> int:
|
|
250
|
-
result = chats_mod.list_messages(client, args.history_id)
|
|
251
|
-
parts = result.get("messages") or []
|
|
252
289
|
if args.full and not args.out:
|
|
253
290
|
log("error: full conversation payloads are unbounded; pass --out <path>")
|
|
254
291
|
return 2
|
|
292
|
+
client_visible = bool(getattr(args, "client_visible", False))
|
|
293
|
+
external_user_id = getattr(args, "user", None)
|
|
294
|
+
if client_visible and not external_user_id:
|
|
295
|
+
log("error: --client-visible requires --user <external_user_id>")
|
|
296
|
+
return 2
|
|
297
|
+
if external_user_id and not client_visible:
|
|
298
|
+
log("error: --user is only valid with --client-visible")
|
|
299
|
+
return 2
|
|
300
|
+
|
|
301
|
+
if client_visible:
|
|
302
|
+
result = chats_mod.list_messages(
|
|
303
|
+
client,
|
|
304
|
+
args.history_id,
|
|
305
|
+
external_user_id=external_user_id,
|
|
306
|
+
)
|
|
307
|
+
export_mode = "client-visible"
|
|
308
|
+
else:
|
|
309
|
+
result = hist_mod.list_messages(client, args.history_id)
|
|
310
|
+
export_mode = "management"
|
|
311
|
+
parts = result.get("messages") or []
|
|
255
312
|
write_json(args.out, result)
|
|
256
313
|
group_ids = {
|
|
257
314
|
p.get("conversation_group_id")
|
|
258
315
|
for p in parts
|
|
259
316
|
if p.get("conversation_group_id")
|
|
260
317
|
}
|
|
318
|
+
stdout_limit = 50 if args.full else 20
|
|
319
|
+
shown_parts = parts[:stdout_limit]
|
|
261
320
|
print_json({
|
|
262
321
|
"history_id": args.history_id,
|
|
322
|
+
"export_mode": export_mode,
|
|
323
|
+
"export_contract": result.get("export_contract"),
|
|
324
|
+
"part_revision": result.get("part_revision"),
|
|
263
325
|
"turn_count": len(group_ids),
|
|
264
326
|
"part_count": len(parts),
|
|
327
|
+
"part_summaries_shown": len(shown_parts),
|
|
328
|
+
"part_summaries_truncated": len(parts) > len(shown_parts),
|
|
329
|
+
"wrote_full_detail": bool(args.out),
|
|
330
|
+
"stdout_is_summary": True,
|
|
331
|
+
"read_contract": result.get("export_contract") or "client-visible-chat-v2",
|
|
332
|
+
"provider_raw_trace": result.get("provider_raw_trace", "not_included"),
|
|
333
|
+
"missing_parts_do_not_prove_non_execution": True,
|
|
334
|
+
"omitted_part_summaries": len(parts) - len(shown_parts),
|
|
335
|
+
"parts": [_part_summary(p, i, full=args.full) for i, p in enumerate(shown_parts)],
|
|
336
|
+
})
|
|
337
|
+
return 0
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _ai_draft_summary(draft: dict, *, full: bool = False) -> dict:
|
|
341
|
+
delivery = draft.get("delivery") or {}
|
|
342
|
+
row = {
|
|
343
|
+
"id": draft.get("id"),
|
|
344
|
+
"history_id": draft.get("history_id"),
|
|
345
|
+
"thread_key": draft.get("thread_key"),
|
|
346
|
+
"context_through_sequence": draft.get("context_through_sequence"),
|
|
347
|
+
"outcome": draft.get("outcome"),
|
|
348
|
+
"result_type": draft.get("result_type"),
|
|
349
|
+
"refinement_source_draft_id": draft.get("refinement_source_draft_id"),
|
|
350
|
+
"has_generation_instruction": bool(draft.get("generation_instruction")),
|
|
351
|
+
"dismiss_reason": draft.get("dismiss_reason"),
|
|
352
|
+
"has_dismiss_feedback": bool(draft.get("dismiss_feedback")),
|
|
353
|
+
"has_generated_content": bool(draft.get("content")),
|
|
354
|
+
"has_actual_content": bool(delivery.get("actual_content")),
|
|
355
|
+
"delivery_status": delivery.get("status"),
|
|
356
|
+
"tool_activity_count": len(draft.get("tool_activities") or []),
|
|
357
|
+
"has_proposed_actions": draft.get("proposed_actions") is not None,
|
|
358
|
+
"created_at": draft.get("created_at"),
|
|
359
|
+
}
|
|
360
|
+
if full:
|
|
361
|
+
row.update({
|
|
362
|
+
"generation_instruction_preview": truncate(draft.get("generation_instruction") or "", 600),
|
|
363
|
+
"dismiss_feedback_preview": truncate(draft.get("dismiss_feedback") or "", 600),
|
|
364
|
+
"content_preview": truncate(draft.get("content") or "", 600),
|
|
365
|
+
"actual_content_preview": truncate(delivery.get("actual_content") or "", 600),
|
|
366
|
+
})
|
|
367
|
+
return row
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def run_ai_drafts(args, client) -> int:
|
|
371
|
+
if args.full and not args.out:
|
|
372
|
+
log("error: AI Draft content previews require --out <path>")
|
|
373
|
+
return 2
|
|
374
|
+
|
|
375
|
+
result = hist_mod.list_ai_drafts(client, args.history_id)
|
|
376
|
+
drafts = result["drafts"]
|
|
377
|
+
write_json(args.out, result)
|
|
378
|
+
stdout_limit = 50 if args.full else 20
|
|
379
|
+
shown_drafts = drafts[:stdout_limit]
|
|
380
|
+
outcome_counts: dict[str, int] = {}
|
|
381
|
+
for draft in drafts:
|
|
382
|
+
outcome = str(draft.get("outcome") or "unknown")
|
|
383
|
+
outcome_counts[outcome] = outcome_counts.get(outcome, 0) + 1
|
|
384
|
+
print_json({
|
|
385
|
+
"history_id": args.history_id,
|
|
386
|
+
"draft_count": len(drafts),
|
|
387
|
+
"outcome_counts": outcome_counts,
|
|
388
|
+
"regenerated_draft_count": sum(
|
|
389
|
+
1 for draft in drafts if draft.get("refinement_source_draft_id") is not None
|
|
390
|
+
),
|
|
391
|
+
"sent_from_ai_draft_count": sum(
|
|
392
|
+
1 for draft in drafts if (draft.get("delivery") or {}).get("status") == "sent"
|
|
393
|
+
),
|
|
394
|
+
"draft_summaries_shown": len(shown_drafts),
|
|
395
|
+
"draft_summaries_truncated": len(drafts) > len(shown_drafts),
|
|
396
|
+
"pages_fetched": result["pages_fetched"],
|
|
397
|
+
"snapshot_consistency": result.get("snapshot_consistency", "best-effort"),
|
|
265
398
|
"wrote_full_detail": bool(args.out),
|
|
266
399
|
"stdout_is_summary": True,
|
|
267
|
-
"
|
|
400
|
+
"drafts": [_ai_draft_summary(draft, full=args.full) for draft in shown_drafts],
|
|
268
401
|
})
|
|
269
402
|
return 0
|
|
270
403
|
|