codeer-cli 0.1.11__tar.gz → 0.1.13__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.11 → codeer_cli-0.1.13}/API_REFERENCE.md +7 -4
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/PKG-INFO +8 -2
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/README.md +6 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/pyproject.toml +1 -1
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/agents.py +6 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/chats.py +44 -8
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/agent.py +15 -1
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/history.py +43 -19
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/histories.py +40 -18
- codeer_cli-0.1.13/tests/test_agent_model_settings.py +230 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_chats_v2.py +53 -0
- codeer_cli-0.1.13/tests/test_history_read.py +132 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/uv.lock +1 -1
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/.gitignore +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/__init__.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/_validate.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/cli.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/client.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/_util.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/eval_cmd.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/kb.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/model.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/eval_.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/models.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/src/codeer_cli/parse.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_agent_handoff.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_client_transport.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_eval_labels.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_eval_pairs.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_history_send.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_kb_nodes.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_kb_ranges.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_models.py +0 -0
- {codeer_cli-0.1.11 → codeer_cli-0.1.13}/tests/test_util.py +0 -0
|
@@ -27,6 +27,9 @@ need a default agent.
|
|
|
27
27
|
- `/histories` uses **`limit` + `offset`** (NOT `page` / `page_size`).
|
|
28
28
|
Default in `histories.list()` is `limit=500`. Backend hard-cap may be
|
|
29
29
|
lower — check the response length.
|
|
30
|
+
- `/api/v2/chats/{id}/messages` also uses `limit` + `offset`.
|
|
31
|
+
`chats.list_messages()` follows pages until exhaustion; its `limit` argument
|
|
32
|
+
is a page size, not a total-result cap.
|
|
30
33
|
- `/agents/{id}/histories`, `/eval/agents/{id}/cases`, `/eval/evaluators` all
|
|
31
34
|
return everything in one shot today (no pagination).
|
|
32
35
|
- `order_by` defaults to `"desc"` (most recent first) on endpoints that
|
|
@@ -275,7 +278,7 @@ the public CLI.
|
|
|
275
278
|
| `GET /api/v2/chats/{id}/messages` | Read persisted Chat V2 conversation parts |
|
|
276
279
|
| `GET /histories?agent_id=X&feedback_filter=improve_feedback&external_user_id=…` | List conversations with filters |
|
|
277
280
|
| `GET /histories/{id}` | Read one history's metadata |
|
|
278
|
-
| `GET /histories/{id}/conversations` |
|
|
281
|
+
| `GET /histories/{id}/conversations` | Legacy compact conversation rows; not complete tool I/O |
|
|
279
282
|
| `POST /histories/{hid}/conversations/{cid}/feedbacks` | Leave freeform improvement feedback |
|
|
280
283
|
| `POST /histories/{hid}/conversations/{cid}/score` | Numeric score |
|
|
281
284
|
|
|
@@ -467,7 +470,7 @@ For bulk creation, `codeer eval cases-apply --attachments-dir <dir>` reads
|
|
|
467
470
|
each case's `attachment_files: ["x.jpg"]` array, uploads, and attaches in one
|
|
468
471
|
pass. Workspace scope is inferred from the API-key virtual user profile.
|
|
469
472
|
|
|
470
|
-
### 11. Tool args + outputs are
|
|
473
|
+
### 11. Tool args + outputs are not available in legacy V1 history reads
|
|
471
474
|
|
|
472
475
|
Legacy V1 `History` conversation rows have only three roles
|
|
473
476
|
(`OpenAIChatRole = system | user | assistant`) — there is no `tool` role row.
|
|
@@ -492,8 +495,8 @@ Chat V2 improves this contract: structured SSE emits tool calls and returns as
|
|
|
492
495
|
`response.part.created` / `response.part.completed`, and
|
|
493
496
|
`GET /api/v2/chats/{id}/messages` reads the persisted conversation parts.
|
|
494
497
|
Capture the SSE artifact with `--out` when exact event order matters; use the
|
|
495
|
-
V2 message read for persisted after-the-fact tool I/O. The legacy
|
|
496
|
-
read remains useful for compact turn-level
|
|
498
|
+
paginated V2 message read for persisted after-the-fact tool I/O. The legacy V1
|
|
499
|
+
history read remains useful for compact turn-level compatibility only.
|
|
497
500
|
|
|
498
501
|
### 10. A KB has exactly ONE level of folders — no nesting
|
|
499
502
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
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
|
|
@@ -172,6 +172,7 @@ Use this pattern during agent lifecycle work:
|
|
|
172
172
|
```bash
|
|
173
173
|
codeer agent list
|
|
174
174
|
codeer history list --agent <agent-id> --limit 50
|
|
175
|
+
codeer history conversations <history-id> --out .codeer/current/history-<history-id>.json
|
|
175
176
|
codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
|
|
176
177
|
codeer history send <history-id> --message "Use the recommended options" --timeout 240
|
|
177
178
|
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
@@ -196,6 +197,11 @@ Flags:
|
|
|
196
197
|
for raw eval results, full conversation turns, full rubric matrices, and
|
|
197
198
|
other data that can grow with cases, versions, or turns.
|
|
198
199
|
|
|
200
|
+
`history conversations` reads Chat V2 parts and follows all pages
|
|
201
|
+
automatically. Its stdout is still a bounded summary; the `--out` artifact is
|
|
202
|
+
the complete client-visible history, including tool calls/results,
|
|
203
|
+
attachments, interactions, feedback, and passthrough metadata.
|
|
204
|
+
|
|
199
205
|
Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
|
|
200
206
|
the coding agent to inspect targeted summaries, IDs, failing cases, or selected
|
|
201
207
|
snippets from the saved file.
|
|
@@ -154,6 +154,7 @@ Use this pattern during agent lifecycle work:
|
|
|
154
154
|
```bash
|
|
155
155
|
codeer agent list
|
|
156
156
|
codeer history list --agent <agent-id> --limit 50
|
|
157
|
+
codeer history conversations <history-id> --out .codeer/current/history-<history-id>.json
|
|
157
158
|
codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
|
|
158
159
|
codeer history send <history-id> --message "Use the recommended options" --timeout 240
|
|
159
160
|
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
@@ -178,6 +179,11 @@ Flags:
|
|
|
178
179
|
for raw eval results, full conversation turns, full rubric matrices, and
|
|
179
180
|
other data that can grow with cases, versions, or turns.
|
|
180
181
|
|
|
182
|
+
`history conversations` reads Chat V2 parts and follows all pages
|
|
183
|
+
automatically. Its stdout is still a bounded summary; the `--out` artifact is
|
|
184
|
+
the complete client-visible history, including tool calls/results,
|
|
185
|
+
attachments, interactions, feedback, and passthrough metadata.
|
|
186
|
+
|
|
181
187
|
Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
|
|
182
188
|
the coding agent to inspect targeted summaries, IDs, failing cases, or selected
|
|
183
189
|
snippets from the saved file.
|
|
@@ -20,6 +20,7 @@ def create(
|
|
|
20
20
|
unified_tools: Optional[List[dict]] = None,
|
|
21
21
|
use_search: bool = False,
|
|
22
22
|
llm_model: Optional[str] = None,
|
|
23
|
+
llm_model_settings: Optional[dict[str, dict[str, Any]]] = None,
|
|
23
24
|
description: Optional[str] = None,
|
|
24
25
|
suggested_questions: Optional[List[str]] = None,
|
|
25
26
|
primary_object_ids: Optional[List[int]] = None,
|
|
@@ -41,6 +42,8 @@ def create(
|
|
|
41
42
|
body["description"] = description
|
|
42
43
|
if llm_model is not None:
|
|
43
44
|
body["llm_model"] = llm_model
|
|
45
|
+
if llm_model_settings is not None:
|
|
46
|
+
body["llm_model_settings"] = llm_model_settings
|
|
44
47
|
if validated_handoff is not None:
|
|
45
48
|
body["human_handoff"] = validated_handoff
|
|
46
49
|
return client.post("/external/agents", json=body)
|
|
@@ -57,6 +60,7 @@ def update(
|
|
|
57
60
|
version_note: str = "",
|
|
58
61
|
description: Optional[str] = None,
|
|
59
62
|
llm_model: Optional[str] = None,
|
|
63
|
+
llm_model_settings: Optional[dict[str, dict[str, Any]]] = None,
|
|
60
64
|
suggested_questions: Optional[List[str]] = None,
|
|
61
65
|
primary_object_ids: Optional[List[int]] = None,
|
|
62
66
|
attachment_ids: Optional[List[str]] = None,
|
|
@@ -79,6 +83,8 @@ def update(
|
|
|
79
83
|
body["description"] = description
|
|
80
84
|
if llm_model is not None:
|
|
81
85
|
body["llm_model"] = llm_model
|
|
86
|
+
if llm_model_settings is not None:
|
|
87
|
+
body["llm_model_settings"] = llm_model_settings
|
|
82
88
|
if validated_handoff is not None:
|
|
83
89
|
body["human_handoff"] = validated_handoff
|
|
84
90
|
return client.patch(f"/external/agents/{agent_id}", json=body)
|
|
@@ -186,14 +186,50 @@ def list_messages(
|
|
|
186
186
|
external_user_id: Optional[str] = None,
|
|
187
187
|
limit: int = 500,
|
|
188
188
|
) -> dict:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
"""Return every client-visible Chat V2 part for a persisted chat.
|
|
190
|
+
|
|
191
|
+
``limit`` is the per-request page size, not a cap on the returned history.
|
|
192
|
+
The HTTP client unwraps Codeer's response envelope (and therefore its
|
|
193
|
+
pagination object), so exhaustion is detected from the number of messages
|
|
194
|
+
returned by each page. A final empty request is possible when the total is
|
|
195
|
+
an exact multiple of ``limit``.
|
|
196
|
+
"""
|
|
197
|
+
if limit <= 0:
|
|
198
|
+
raise ValueError("limit must be greater than zero")
|
|
199
|
+
|
|
200
|
+
offset = 0
|
|
201
|
+
result: dict[str, Any] | None = None
|
|
202
|
+
messages: list[dict] = []
|
|
203
|
+
while True:
|
|
204
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
205
|
+
if external_user_id is not None:
|
|
206
|
+
params["external_user_id"] = external_user_id
|
|
207
|
+
page = client.get(
|
|
208
|
+
f"/chats/{chat_id}/messages",
|
|
209
|
+
api_version="v2",
|
|
210
|
+
params=params,
|
|
211
|
+
)
|
|
212
|
+
if not isinstance(page, dict):
|
|
213
|
+
raise ValueError("Chat V2 messages response must be an object")
|
|
214
|
+
|
|
215
|
+
page_messages = page.get("messages") or []
|
|
216
|
+
if not isinstance(page_messages, list):
|
|
217
|
+
raise ValueError("Chat V2 messages response must contain a messages list")
|
|
218
|
+
if result is None:
|
|
219
|
+
result = dict(page)
|
|
220
|
+
else:
|
|
221
|
+
for key, value in page.items():
|
|
222
|
+
if key != "messages" and value is not None:
|
|
223
|
+
result[key] = value
|
|
224
|
+
messages.extend(page_messages)
|
|
225
|
+
|
|
226
|
+
if len(page_messages) < limit:
|
|
227
|
+
break
|
|
228
|
+
offset += len(page_messages)
|
|
229
|
+
|
|
230
|
+
assert result is not None
|
|
231
|
+
result["messages"] = messages
|
|
232
|
+
return result
|
|
197
233
|
|
|
198
234
|
|
|
199
235
|
def list_chats(client: CodeerClient) -> list[dict]:
|
|
@@ -89,7 +89,9 @@ def register(subparsers):
|
|
|
89
89
|
def _tool_summary(tools: list[dict] | None) -> list[dict]:
|
|
90
90
|
out = []
|
|
91
91
|
for t in tools or []:
|
|
92
|
-
form = t.get("custom_form_schema")
|
|
92
|
+
form = t.get("custom_form_schema")
|
|
93
|
+
if not isinstance(form, dict):
|
|
94
|
+
form = {}
|
|
93
95
|
out.append({
|
|
94
96
|
"id": t.get("id"),
|
|
95
97
|
"type": t.get("type"),
|
|
@@ -169,6 +171,8 @@ def run_apply(args, client) -> int:
|
|
|
169
171
|
log(f"error: invalid agent payload: {exc}")
|
|
170
172
|
return 2
|
|
171
173
|
|
|
174
|
+
llm_model_settings_provided = "llm_model_settings" in body
|
|
175
|
+
|
|
172
176
|
if args.dry_run:
|
|
173
177
|
operation = "update" if args.agent_id else "create"
|
|
174
178
|
result = {
|
|
@@ -181,6 +185,8 @@ def run_apply(args, client) -> int:
|
|
|
181
185
|
"tool_count": len(validated_tools),
|
|
182
186
|
"use_search": body.get("use_search", False),
|
|
183
187
|
"llm_model": body.get("llm_model"),
|
|
188
|
+
"llm_model_settings_provided": llm_model_settings_provided,
|
|
189
|
+
"llm_model_settings": body.get("llm_model_settings"),
|
|
184
190
|
"human_handoff": {
|
|
185
191
|
"enabled": bool((validated_handoff or {}).get("enabled")),
|
|
186
192
|
"idle_timeout_minutes": (validated_handoff or {}).get("idle_timeout_minutes"),
|
|
@@ -193,6 +199,12 @@ def run_apply(args, client) -> int:
|
|
|
193
199
|
print_json(result)
|
|
194
200
|
return 0
|
|
195
201
|
|
|
202
|
+
model_settings_kwargs = (
|
|
203
|
+
{"llm_model_settings": body["llm_model_settings"]}
|
|
204
|
+
if llm_model_settings_provided
|
|
205
|
+
else {}
|
|
206
|
+
)
|
|
207
|
+
|
|
196
208
|
if args.agent_id:
|
|
197
209
|
body.pop("workspace_id", None)
|
|
198
210
|
agents_mod.update(
|
|
@@ -208,6 +220,7 @@ def run_apply(args, client) -> int:
|
|
|
208
220
|
primary_object_ids=body.get("primary_object_ids") or [],
|
|
209
221
|
attachment_ids=body.get("attachment_ids") or [],
|
|
210
222
|
human_handoff=validated_handoff,
|
|
223
|
+
**model_settings_kwargs,
|
|
211
224
|
)
|
|
212
225
|
agent_id = args.agent_id
|
|
213
226
|
log(f"PUT /agents/{agent_id} ok")
|
|
@@ -227,6 +240,7 @@ def run_apply(args, client) -> int:
|
|
|
227
240
|
primary_object_ids=body.get("primary_object_ids") or [],
|
|
228
241
|
attachment_ids=body.get("attachment_ids") or [],
|
|
229
242
|
human_handoff=validated_handoff,
|
|
243
|
+
**model_settings_kwargs,
|
|
230
244
|
)
|
|
231
245
|
agent_id = agent["id"]
|
|
232
246
|
log(f"POST /agents ok, id={agent_id}")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import math
|
|
4
5
|
import os
|
|
5
6
|
|
|
@@ -47,13 +48,13 @@ def register(subparsers):
|
|
|
47
48
|
# codeer history conversations <id>
|
|
48
49
|
p = sub.add_parser(
|
|
49
50
|
"conversations",
|
|
50
|
-
help="Summarize all
|
|
51
|
+
help="Summarize all Chat V2 parts in a history. Complete parts/tool payloads require --out.",
|
|
51
52
|
)
|
|
52
53
|
p.add_argument("history_id", type=int)
|
|
53
54
|
p.add_argument("--full", action="store_true",
|
|
54
|
-
help="Require --out and
|
|
55
|
+
help="Require --out and include longer stdout previews; the artifact is always complete.")
|
|
55
56
|
p.add_argument("--out", default=None,
|
|
56
|
-
help="Write
|
|
57
|
+
help="Write every unmodified client-visible Chat V2 part to this file.")
|
|
57
58
|
p.set_defaults(func=run_conversations)
|
|
58
59
|
|
|
59
60
|
# codeer history negative-feedback
|
|
@@ -155,18 +156,33 @@ def _history_summary(row: dict, *, full: bool = False) -> dict:
|
|
|
155
156
|
return out
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
def
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
160
|
+
raw_content = part.get("content")
|
|
161
|
+
if isinstance(raw_content, dict):
|
|
162
|
+
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
|
+
else:
|
|
166
|
+
content_value = raw_content
|
|
167
|
+
if isinstance(content_value, str):
|
|
168
|
+
content = content_value
|
|
169
|
+
elif content_value is None:
|
|
170
|
+
content = ""
|
|
171
|
+
else:
|
|
172
|
+
content = json.dumps(content_value, ensure_ascii=False, default=str)
|
|
161
173
|
row = {
|
|
162
|
-
"
|
|
163
|
-
"id":
|
|
164
|
-
"
|
|
165
|
-
"
|
|
174
|
+
"part_idx": idx,
|
|
175
|
+
"id": part.get("id"),
|
|
176
|
+
"conversation_id": part.get("conversation_id"),
|
|
177
|
+
"conversation_group_id": part.get("conversation_group_id"),
|
|
178
|
+
"sequence": part.get("sequence"),
|
|
179
|
+
"part_kind": part.get("part_kind"),
|
|
180
|
+
"source": part.get("source"),
|
|
181
|
+
"created_at": part.get("created_at"),
|
|
166
182
|
"content_preview": truncate(content, 600 if full else 240),
|
|
167
183
|
"content_chars": len(content),
|
|
168
|
-
"
|
|
169
|
-
"feedback_count": len(
|
|
184
|
+
"attachment_count": len(part.get("attached_files") or []),
|
|
185
|
+
"feedback_count": len(part.get("feedbacks") or []),
|
|
170
186
|
}
|
|
171
187
|
if full:
|
|
172
188
|
row["feedbacks"] = [
|
|
@@ -175,7 +191,7 @@ def _turn_summary(turn: dict, idx: int, *, full: bool = False) -> dict:
|
|
|
175
191
|
"tag": fb.get("tag"),
|
|
176
192
|
"content_preview": truncate(fb.get("content") or "", 240),
|
|
177
193
|
}
|
|
178
|
-
for fb in (
|
|
194
|
+
for fb in (part.get("feedbacks") or [])
|
|
179
195
|
]
|
|
180
196
|
return row
|
|
181
197
|
|
|
@@ -231,16 +247,24 @@ def run_get(args, client) -> int:
|
|
|
231
247
|
|
|
232
248
|
|
|
233
249
|
def run_conversations(args, client) -> int:
|
|
234
|
-
result =
|
|
250
|
+
result = chats_mod.list_messages(client, args.history_id)
|
|
251
|
+
parts = result.get("messages") or []
|
|
235
252
|
if args.full and not args.out:
|
|
236
253
|
log("error: full conversation payloads are unbounded; pass --out <path>")
|
|
237
254
|
return 2
|
|
238
|
-
write_json(args.out,
|
|
255
|
+
write_json(args.out, result)
|
|
256
|
+
group_ids = {
|
|
257
|
+
p.get("conversation_group_id")
|
|
258
|
+
for p in parts
|
|
259
|
+
if p.get("conversation_group_id")
|
|
260
|
+
}
|
|
239
261
|
print_json({
|
|
240
262
|
"history_id": args.history_id,
|
|
241
|
-
"turn_count": len(
|
|
263
|
+
"turn_count": len(group_ids),
|
|
264
|
+
"part_count": len(parts),
|
|
242
265
|
"wrote_full_detail": bool(args.out),
|
|
243
|
-
"
|
|
266
|
+
"stdout_is_summary": True,
|
|
267
|
+
"parts": [_part_summary(p, i, full=args.full) for i, p in enumerate(parts)],
|
|
244
268
|
})
|
|
245
269
|
return 0
|
|
246
270
|
|
|
@@ -355,7 +379,7 @@ def run_create(args, client) -> int:
|
|
|
355
379
|
"messages": message_results,
|
|
356
380
|
"conversation_parts": conversation_parts,
|
|
357
381
|
}
|
|
358
|
-
write_json(args.out,
|
|
382
|
+
write_json(args.out, out)
|
|
359
383
|
print_json({
|
|
360
384
|
"agent_id": agent_id,
|
|
361
385
|
"history_id": history_id,
|
|
@@ -418,7 +442,7 @@ def run_send(args, client) -> int:
|
|
|
418
442
|
"messages": message_results,
|
|
419
443
|
"conversation_parts": conversation_parts,
|
|
420
444
|
}
|
|
421
|
-
write_json(args.out,
|
|
445
|
+
write_json(args.out, out)
|
|
422
446
|
print_json({
|
|
423
447
|
"agent_id": agent_id,
|
|
424
448
|
"history_id": args.history_id,
|
|
@@ -94,11 +94,10 @@ def list_negative_feedback_turns(
|
|
|
94
94
|
the source channel — usually "system"). Pass the desired sentiment(s)
|
|
95
95
|
in ``feedback_types``.
|
|
96
96
|
|
|
97
|
-
Cost: O(N histories)
|
|
98
|
-
|
|
99
|
-
before invoking on a busy agent.
|
|
97
|
+
Cost: O(N histories) paginated Chat V2 reads. Filter aggressively via
|
|
98
|
+
``exclude_users`` and ``limit`` before invoking on a busy agent.
|
|
100
99
|
"""
|
|
101
|
-
from .
|
|
100
|
+
from . import chats
|
|
102
101
|
|
|
103
102
|
type_set = {t.lower() for t in feedback_types}
|
|
104
103
|
histories = list(
|
|
@@ -115,33 +114,42 @@ def list_negative_feedback_turns(
|
|
|
115
114
|
if hid is None:
|
|
116
115
|
continue
|
|
117
116
|
try:
|
|
118
|
-
|
|
117
|
+
parts = (chats.list_messages(client, hid).get("messages") or [])
|
|
119
118
|
except Exception:
|
|
120
119
|
continue
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
|
|
121
|
+
turn_indexes: dict[str, int] = {}
|
|
122
|
+
user_messages: dict[str, str] = {}
|
|
123
|
+
next_turn_idx = 0
|
|
124
|
+
for part_idx, part in enumerate(parts):
|
|
125
|
+
group_id = str(part.get("conversation_group_id") or "")
|
|
126
|
+
part_kind = part.get("part_kind") or ""
|
|
127
|
+
if group_id and group_id not in turn_indexes:
|
|
128
|
+
turn_indexes[group_id] = next_turn_idx
|
|
129
|
+
next_turn_idx += 1
|
|
130
|
+
if part_kind == "user-prompt":
|
|
131
|
+
user_messages[group_id] = _part_text(part)
|
|
132
|
+
continue
|
|
133
|
+
if part_kind != "text":
|
|
123
134
|
continue
|
|
124
|
-
fbs =
|
|
135
|
+
fbs = part.get("feedbacks") or []
|
|
125
136
|
for fb in fbs:
|
|
126
137
|
ftype = (fb.get("type") or "").lower()
|
|
127
138
|
if ftype not in type_set:
|
|
128
139
|
continue
|
|
129
|
-
# Find the most recent user turn before this assistant turn.
|
|
130
|
-
prior_user = ""
|
|
131
|
-
for j in range(i - 1, -1, -1):
|
|
132
|
-
if (convs[j].get("role") or "") == "user":
|
|
133
|
-
prior_user = (convs[j].get("content") or "")[:user_excerpt_chars]
|
|
134
|
-
break
|
|
135
140
|
out.append({
|
|
136
141
|
"history_id": hid,
|
|
137
142
|
"history_title": h.get("name") or h.get("title") or "",
|
|
138
143
|
"external_user_id": h.get("external_user_id") or "",
|
|
139
144
|
"created_at": h.get("created_at"),
|
|
140
|
-
"turn_idx":
|
|
145
|
+
"turn_idx": turn_indexes.get(group_id),
|
|
146
|
+
"part_idx": part_idx,
|
|
147
|
+
"conversation_group_id": group_id or None,
|
|
148
|
+
"conversation_part_id": part.get("id"),
|
|
141
149
|
"feedback_type": ftype,
|
|
142
150
|
"feedback_text": fb.get("content") or "",
|
|
143
|
-
"user_message":
|
|
144
|
-
"assistant_excerpt":
|
|
151
|
+
"user_message": user_messages.get(group_id, "")[:user_excerpt_chars],
|
|
152
|
+
"assistant_excerpt": _part_text(part)[:assistant_excerpt_chars],
|
|
145
153
|
})
|
|
146
154
|
return out
|
|
147
155
|
|
|
@@ -151,6 +159,20 @@ def get(client: CodeerClient, history_id: int) -> dict:
|
|
|
151
159
|
|
|
152
160
|
|
|
153
161
|
def get_conversations(client: CodeerClient, history_id: int) -> list[dict]:
|
|
154
|
-
"""Return
|
|
162
|
+
"""Return legacy V1 conversation rows.
|
|
163
|
+
|
|
164
|
+
Prefer ``codeer_cli.chats.list_messages`` whenever exact Chat V2 parts,
|
|
165
|
+
tool inputs/results, or event order matter.
|
|
166
|
+
"""
|
|
155
167
|
return client.get(f"/external/histories/{history_id}/conversations")
|
|
156
168
|
|
|
169
|
+
|
|
170
|
+
def _part_text(part: dict) -> str:
|
|
171
|
+
content = part.get("content")
|
|
172
|
+
if isinstance(content, dict):
|
|
173
|
+
value = content.get("content")
|
|
174
|
+
else:
|
|
175
|
+
value = content
|
|
176
|
+
if value is None:
|
|
177
|
+
return ""
|
|
178
|
+
return value if isinstance(value, str) else str(value)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
from codeer_cli import agents
|
|
13
|
+
from codeer_cli.commands import agent as agent_cmd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MODEL_ID = "openai/gpt-5.6-luna"
|
|
17
|
+
MODEL_SETTINGS = {MODEL_ID: {"thinking": "xhigh"}}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FakeClient:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.calls: list[tuple[str, str, dict]] = []
|
|
23
|
+
|
|
24
|
+
def post(self, path: str, **kwargs):
|
|
25
|
+
self.calls.append(("POST", path, kwargs))
|
|
26
|
+
return {"id": "agent-1"}
|
|
27
|
+
|
|
28
|
+
def patch(self, path: str, **kwargs):
|
|
29
|
+
self.calls.append(("PATCH", path, kwargs))
|
|
30
|
+
return {"id": "agent-1"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentModelSettingsPayloadTests(unittest.TestCase):
|
|
34
|
+
def test_create_forwards_model_settings(self) -> None:
|
|
35
|
+
client = FakeClient()
|
|
36
|
+
|
|
37
|
+
agents.create(
|
|
38
|
+
client, # type: ignore[arg-type]
|
|
39
|
+
workspace_id="workspace-1",
|
|
40
|
+
name="Agent",
|
|
41
|
+
system_prompt="Help safely.",
|
|
42
|
+
llm_model=MODEL_ID,
|
|
43
|
+
llm_model_settings=MODEL_SETTINGS,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.assertEqual(client.calls[0][0:2], ("POST", "/external/agents"))
|
|
47
|
+
self.assertEqual(
|
|
48
|
+
client.calls[0][2]["json"]["llm_model_settings"],
|
|
49
|
+
MODEL_SETTINGS,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test_update_forwards_model_settings(self) -> None:
|
|
53
|
+
client = FakeClient()
|
|
54
|
+
|
|
55
|
+
agents.update(
|
|
56
|
+
client, # type: ignore[arg-type]
|
|
57
|
+
"agent-1",
|
|
58
|
+
name="Agent",
|
|
59
|
+
system_prompt="Help safely.",
|
|
60
|
+
unified_tools=[],
|
|
61
|
+
use_search=False,
|
|
62
|
+
llm_model=MODEL_ID,
|
|
63
|
+
llm_model_settings=MODEL_SETTINGS,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.assertEqual(
|
|
67
|
+
client.calls[0][0:2],
|
|
68
|
+
("PATCH", "/external/agents/agent-1"),
|
|
69
|
+
)
|
|
70
|
+
self.assertEqual(
|
|
71
|
+
client.calls[0][2]["json"]["llm_model_settings"],
|
|
72
|
+
MODEL_SETTINGS,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def test_update_forwards_empty_map_to_clear_settings(self) -> None:
|
|
76
|
+
client = FakeClient()
|
|
77
|
+
|
|
78
|
+
agents.update(
|
|
79
|
+
client, # type: ignore[arg-type]
|
|
80
|
+
"agent-1",
|
|
81
|
+
name="Agent",
|
|
82
|
+
system_prompt="Help safely.",
|
|
83
|
+
unified_tools=[],
|
|
84
|
+
use_search=False,
|
|
85
|
+
llm_model_settings={},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.assertEqual(client.calls[0][2]["json"]["llm_model_settings"], {})
|
|
89
|
+
|
|
90
|
+
def test_update_omits_model_settings_when_unspecified(self) -> None:
|
|
91
|
+
client = FakeClient()
|
|
92
|
+
|
|
93
|
+
agents.update(
|
|
94
|
+
client, # type: ignore[arg-type]
|
|
95
|
+
"agent-1",
|
|
96
|
+
name="Agent",
|
|
97
|
+
system_prompt="Help safely.",
|
|
98
|
+
unified_tools=[],
|
|
99
|
+
use_search=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.assertNotIn("llm_model_settings", client.calls[0][2]["json"])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AgentModelSettingsCommandTests(unittest.TestCase):
|
|
106
|
+
def _run_apply(
|
|
107
|
+
self,
|
|
108
|
+
payload: dict,
|
|
109
|
+
*,
|
|
110
|
+
agent_id: str | None = None,
|
|
111
|
+
dry_run: bool = False,
|
|
112
|
+
) -> tuple[int, dict]:
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
114
|
+
payload_path = Path(tmpdir) / "agent.json"
|
|
115
|
+
payload_path.write_text(json.dumps(payload))
|
|
116
|
+
args = SimpleNamespace(
|
|
117
|
+
payload=str(payload_path),
|
|
118
|
+
agent_id=agent_id,
|
|
119
|
+
dry_run=dry_run,
|
|
120
|
+
note="settings regression",
|
|
121
|
+
out=None,
|
|
122
|
+
)
|
|
123
|
+
stdout = io.StringIO()
|
|
124
|
+
with contextlib.redirect_stdout(stdout):
|
|
125
|
+
status = agent_cmd.run_apply(args, object())
|
|
126
|
+
return status, json.loads(stdout.getvalue()) if stdout.getvalue() else {}
|
|
127
|
+
|
|
128
|
+
def test_apply_create_forwards_model_settings(self) -> None:
|
|
129
|
+
payload = {
|
|
130
|
+
"workspace_id": "workspace-1",
|
|
131
|
+
"name": "Agent",
|
|
132
|
+
"system_prompt": "Help safely.",
|
|
133
|
+
"llm_model": MODEL_ID,
|
|
134
|
+
"llm_model_settings": MODEL_SETTINGS,
|
|
135
|
+
}
|
|
136
|
+
history = [{"id": "history-1", "version_number": 1, "status": "draft"}]
|
|
137
|
+
|
|
138
|
+
with (
|
|
139
|
+
patch.object(
|
|
140
|
+
agent_cmd.agents_mod,
|
|
141
|
+
"create",
|
|
142
|
+
return_value={"id": "agent-1"},
|
|
143
|
+
) as create,
|
|
144
|
+
patch.object(agent_cmd.agents_mod, "list_versions", return_value=history),
|
|
145
|
+
):
|
|
146
|
+
status, _ = self._run_apply(payload)
|
|
147
|
+
|
|
148
|
+
self.assertEqual(status, 0)
|
|
149
|
+
self.assertEqual(
|
|
150
|
+
create.call_args.kwargs["llm_model_settings"],
|
|
151
|
+
MODEL_SETTINGS,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def test_apply_update_preserves_present_and_empty_settings(self) -> None:
|
|
155
|
+
history = [{"id": "history-2", "version_number": 2, "status": "draft"}]
|
|
156
|
+
|
|
157
|
+
for model_settings in (MODEL_SETTINGS, {}):
|
|
158
|
+
with self.subTest(model_settings=model_settings):
|
|
159
|
+
payload = {
|
|
160
|
+
"name": "Agent",
|
|
161
|
+
"system_prompt": "Help safely.",
|
|
162
|
+
"llm_model_settings": model_settings,
|
|
163
|
+
}
|
|
164
|
+
with (
|
|
165
|
+
patch.object(agent_cmd.agents_mod, "update") as update,
|
|
166
|
+
patch.object(
|
|
167
|
+
agent_cmd.agents_mod,
|
|
168
|
+
"list_versions",
|
|
169
|
+
return_value=history,
|
|
170
|
+
),
|
|
171
|
+
):
|
|
172
|
+
status, _ = self._run_apply(payload, agent_id="agent-1")
|
|
173
|
+
|
|
174
|
+
self.assertEqual(status, 0)
|
|
175
|
+
self.assertEqual(
|
|
176
|
+
update.call_args.kwargs["llm_model_settings"],
|
|
177
|
+
model_settings,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def test_apply_update_does_not_supply_omitted_settings(self) -> None:
|
|
181
|
+
payload = {
|
|
182
|
+
"name": "Agent",
|
|
183
|
+
"system_prompt": "Help safely.",
|
|
184
|
+
}
|
|
185
|
+
history = [{"id": "history-2", "version_number": 2, "status": "draft"}]
|
|
186
|
+
|
|
187
|
+
with (
|
|
188
|
+
patch.object(agent_cmd.agents_mod, "update") as update,
|
|
189
|
+
patch.object(agent_cmd.agents_mod, "list_versions", return_value=history),
|
|
190
|
+
):
|
|
191
|
+
status, _ = self._run_apply(payload, agent_id="agent-1")
|
|
192
|
+
|
|
193
|
+
self.assertEqual(status, 0)
|
|
194
|
+
self.assertNotIn("llm_model_settings", update.call_args.kwargs)
|
|
195
|
+
|
|
196
|
+
def test_apply_dry_run_distinguishes_settings_states(self) -> None:
|
|
197
|
+
cases = (
|
|
198
|
+
(MODEL_SETTINGS, True, MODEL_SETTINGS),
|
|
199
|
+
({}, True, {}),
|
|
200
|
+
(None, False, None),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
for model_settings, expected_provided, expected_settings in cases:
|
|
204
|
+
with self.subTest(model_settings=model_settings):
|
|
205
|
+
payload: dict[str, object] = {
|
|
206
|
+
"name": "Agent",
|
|
207
|
+
"system_prompt": "Help safely.",
|
|
208
|
+
}
|
|
209
|
+
if expected_provided:
|
|
210
|
+
payload["llm_model_settings"] = model_settings
|
|
211
|
+
|
|
212
|
+
status, result = self._run_apply(
|
|
213
|
+
payload,
|
|
214
|
+
agent_id="agent-1",
|
|
215
|
+
dry_run=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
self.assertEqual(status, 0)
|
|
219
|
+
self.assertEqual(
|
|
220
|
+
result["llm_model_settings_provided"],
|
|
221
|
+
expected_provided,
|
|
222
|
+
)
|
|
223
|
+
self.assertEqual(
|
|
224
|
+
result["llm_model_settings"],
|
|
225
|
+
expected_settings,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
unittest.main()
|
|
@@ -19,6 +19,23 @@ class RecordingClient:
|
|
|
19
19
|
return {"chat_id": 123, "messages": []}
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class PaginatedClient(RecordingClient):
|
|
23
|
+
def __init__(self, messages: list[dict]) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.messages = messages
|
|
26
|
+
|
|
27
|
+
def get(self, path: str, **kwargs):
|
|
28
|
+
self.calls.append(("GET", path, kwargs))
|
|
29
|
+
params = kwargs["params"]
|
|
30
|
+
offset = params["offset"]
|
|
31
|
+
limit = params["limit"]
|
|
32
|
+
return {
|
|
33
|
+
"chat_id": 123,
|
|
34
|
+
"reply_context_anchor": {"part_id": 99, "sequence": 99},
|
|
35
|
+
"messages": self.messages[offset:offset + limit],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
22
39
|
class ChatV2RequestTests(unittest.TestCase):
|
|
23
40
|
def test_create_uses_v2_and_always_sends_name(self) -> None:
|
|
24
41
|
client = RecordingClient()
|
|
@@ -59,6 +76,42 @@ class ChatV2RequestTests(unittest.TestCase):
|
|
|
59
76
|
},
|
|
60
77
|
))
|
|
61
78
|
|
|
79
|
+
def test_list_messages_fetches_every_page_without_mutating_parts(self) -> None:
|
|
80
|
+
messages = [
|
|
81
|
+
{
|
|
82
|
+
"id": idx,
|
|
83
|
+
"part_kind": "tool-return",
|
|
84
|
+
"content": {"content": {"owner": f"owner-{idx}"}},
|
|
85
|
+
"metadata": {"agent_history_id": f"version-{idx}"},
|
|
86
|
+
}
|
|
87
|
+
for idx in range(5)
|
|
88
|
+
]
|
|
89
|
+
client = PaginatedClient(messages)
|
|
90
|
+
|
|
91
|
+
result = chats.list_messages(client, 123, limit=2) # type: ignore[arg-type]
|
|
92
|
+
|
|
93
|
+
self.assertEqual(result["messages"], messages)
|
|
94
|
+
self.assertEqual(result["reply_context_anchor"], {"part_id": 99, "sequence": 99})
|
|
95
|
+
self.assertEqual(
|
|
96
|
+
[call[2]["params"]["offset"] for call in client.calls],
|
|
97
|
+
[0, 2, 4],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def test_list_messages_checks_one_empty_page_for_exact_page_multiple(self) -> None:
|
|
101
|
+
client = PaginatedClient([{"id": idx} for idx in range(4)])
|
|
102
|
+
|
|
103
|
+
result = chats.list_messages(client, 123, limit=2) # type: ignore[arg-type]
|
|
104
|
+
|
|
105
|
+
self.assertEqual([message["id"] for message in result["messages"]], [0, 1, 2, 3])
|
|
106
|
+
self.assertEqual(
|
|
107
|
+
[call[2]["params"]["offset"] for call in client.calls],
|
|
108
|
+
[0, 2, 4],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def test_list_messages_rejects_non_positive_page_size(self) -> None:
|
|
112
|
+
with self.assertRaisesRegex(ValueError, "limit must be greater than zero"):
|
|
113
|
+
chats.list_messages(RecordingClient(), 123, limit=0) # type: ignore[arg-type]
|
|
114
|
+
|
|
62
115
|
|
|
63
116
|
class ChatV2StreamTests(unittest.TestCase):
|
|
64
117
|
def test_collects_structured_events_and_final_text(self) -> None:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import unittest
|
|
5
|
+
from contextlib import redirect_stdout
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import TemporaryDirectory
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
from codeer_cli import histories
|
|
13
|
+
from codeer_cli.commands import history as history_cmd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FakeClient:
|
|
17
|
+
def resolve_scope(self) -> tuple[str, str]:
|
|
18
|
+
return "workspace-1", "organization-1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HistoryReadTests(unittest.TestCase):
|
|
22
|
+
def test_run_conversations_uses_v2_and_writes_unmodified_parts(self) -> None:
|
|
23
|
+
client = FakeClient()
|
|
24
|
+
response = {
|
|
25
|
+
"chat_id": 18649,
|
|
26
|
+
"messages": [
|
|
27
|
+
{
|
|
28
|
+
"id": 10,
|
|
29
|
+
"conversation_group_id": "group-1",
|
|
30
|
+
"sequence": 1,
|
|
31
|
+
"part_kind": "user-prompt",
|
|
32
|
+
"content": {"content": "Question"},
|
|
33
|
+
"metadata": {"agent_history_id": "version-1"},
|
|
34
|
+
"source": "stack",
|
|
35
|
+
"attached_files": [],
|
|
36
|
+
"feedbacks": [],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": 11,
|
|
40
|
+
"conversation_group_id": "group-1",
|
|
41
|
+
"sequence": 2,
|
|
42
|
+
"part_kind": "tool-return",
|
|
43
|
+
"content": {"content": {"owner": "Ada", "members": ["Grace"]}},
|
|
44
|
+
"metadata": {"reasoning_step_type": "consultant_http_request"},
|
|
45
|
+
"source": "stack",
|
|
46
|
+
"attached_files": [],
|
|
47
|
+
"feedbacks": [],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
args = SimpleNamespace(history_id=18649, full=False, out=None)
|
|
52
|
+
|
|
53
|
+
with TemporaryDirectory() as tmpdir:
|
|
54
|
+
out_path = Path(tmpdir) / "history.json"
|
|
55
|
+
args.out = str(out_path)
|
|
56
|
+
stdout = StringIO()
|
|
57
|
+
with (
|
|
58
|
+
patch.object(history_cmd.chats_mod, "list_messages", return_value=response) as list_messages,
|
|
59
|
+
redirect_stdout(stdout),
|
|
60
|
+
):
|
|
61
|
+
result = history_cmd.run_conversations(args, client)
|
|
62
|
+
|
|
63
|
+
artifact = json.loads(out_path.read_text())
|
|
64
|
+
|
|
65
|
+
self.assertEqual(result, 0)
|
|
66
|
+
list_messages.assert_called_once_with(client, 18649)
|
|
67
|
+
self.assertEqual(artifact, response)
|
|
68
|
+
self.assertEqual(artifact["messages"][1]["content"]["content"]["owner"], "Ada")
|
|
69
|
+
summary = json.loads(stdout.getvalue())
|
|
70
|
+
self.assertEqual(summary["turn_count"], 1)
|
|
71
|
+
self.assertEqual(summary["part_count"], 2)
|
|
72
|
+
self.assertTrue(summary["stdout_is_summary"])
|
|
73
|
+
self.assertEqual(summary["parts"][1]["part_kind"], "tool-return")
|
|
74
|
+
|
|
75
|
+
def test_negative_feedback_uses_v2_part_feedback_and_grouped_user_prompt(self) -> None:
|
|
76
|
+
client = FakeClient()
|
|
77
|
+
history_rows = [{
|
|
78
|
+
"id": 18649,
|
|
79
|
+
"name": "Support chat",
|
|
80
|
+
"external_user_id": "user-1",
|
|
81
|
+
"created_at": "2026-08-10T00:00:00Z",
|
|
82
|
+
}]
|
|
83
|
+
parts = {
|
|
84
|
+
"chat_id": 18649,
|
|
85
|
+
"messages": [
|
|
86
|
+
{
|
|
87
|
+
"id": 1,
|
|
88
|
+
"conversation_group_id": "group-1",
|
|
89
|
+
"part_kind": "user-prompt",
|
|
90
|
+
"content": {"content": "Why did this fail?"},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": 2,
|
|
94
|
+
"conversation_group_id": "group-1",
|
|
95
|
+
"part_kind": "tool-call",
|
|
96
|
+
"content": {"tool_name": "http_request", "args": {"path": "/status"}},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": 3,
|
|
100
|
+
"conversation_group_id": "group-1",
|
|
101
|
+
"part_kind": "text",
|
|
102
|
+
"content": {"content": "The service is healthy."},
|
|
103
|
+
"feedbacks": [{"type": "sys_improve", "content": "Incorrect"}],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
with (
|
|
109
|
+
patch.object(histories, "list", return_value=history_rows),
|
|
110
|
+
patch("codeer_cli.chats.list_messages", return_value=parts) as list_messages,
|
|
111
|
+
):
|
|
112
|
+
rows = histories.list_negative_feedback_turns(client, agent_id="agent-1")
|
|
113
|
+
|
|
114
|
+
list_messages.assert_called_once_with(client, 18649)
|
|
115
|
+
self.assertEqual(rows, [{
|
|
116
|
+
"history_id": 18649,
|
|
117
|
+
"history_title": "Support chat",
|
|
118
|
+
"external_user_id": "user-1",
|
|
119
|
+
"created_at": "2026-08-10T00:00:00Z",
|
|
120
|
+
"turn_idx": 0,
|
|
121
|
+
"part_idx": 2,
|
|
122
|
+
"conversation_group_id": "group-1",
|
|
123
|
+
"conversation_part_id": 3,
|
|
124
|
+
"feedback_type": "sys_improve",
|
|
125
|
+
"feedback_text": "Incorrect",
|
|
126
|
+
"user_message": "Why did this fail?",
|
|
127
|
+
"assistant_excerpt": "The service is healthy.",
|
|
128
|
+
}])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|