codeer-cli 0.1.10__py3-none-any.whl → 0.1.12__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/chats.py +172 -22
- codeer_cli/client.py +30 -5
- codeer_cli/commands/history.py +69 -31
- codeer_cli/histories.py +40 -18
- codeer_cli/parse.py +7 -8
- {codeer_cli-0.1.10.dist-info → codeer_cli-0.1.12.dist-info}/METADATA +14 -6
- {codeer_cli-0.1.10.dist-info → codeer_cli-0.1.12.dist-info}/RECORD +9 -9
- {codeer_cli-0.1.10.dist-info → codeer_cli-0.1.12.dist-info}/WHEEL +0 -0
- {codeer_cli-0.1.10.dist-info → codeer_cli-0.1.12.dist-info}/entry_points.txt +0 -0
codeer_cli/chats.py
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
"""Chat creation
|
|
2
|
-
|
|
3
|
-
The send_message() path is the main dogfood driver: open a chat against a
|
|
4
|
-
specific agent version (unpublished draft is fine) and consume the SSE stream
|
|
5
|
-
to get tool calls, reasoning, and final text back.
|
|
6
|
-
"""
|
|
1
|
+
"""Chat V2 creation, structured SSE responses, and persisted message reads."""
|
|
7
2
|
|
|
8
3
|
from __future__ import annotations
|
|
9
4
|
|
|
10
5
|
from typing import Any, Iterator, List, Optional
|
|
11
6
|
|
|
12
|
-
from .client import CodeerClient
|
|
7
|
+
from .client import CodeerClient, TransportError
|
|
13
8
|
|
|
14
9
|
|
|
15
10
|
def create(
|
|
@@ -19,12 +14,13 @@ def create(
|
|
|
19
14
|
title: Optional[str] = None,
|
|
20
15
|
external_user_id: Optional[str] = None,
|
|
21
16
|
) -> dict:
|
|
22
|
-
body: dict[str, Any] = {
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
body: dict[str, Any] = {
|
|
18
|
+
"agent_id": agent_id,
|
|
19
|
+
"name": title or "CLI conversation",
|
|
20
|
+
}
|
|
25
21
|
if external_user_id is not None:
|
|
26
22
|
body["external_user_id"] = external_user_id
|
|
27
|
-
return client.post("/chats", json=body)
|
|
23
|
+
return client.post("/chats", api_version="v2", json=body)
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
def send_published_agent_message(
|
|
@@ -35,7 +31,7 @@ def send_published_agent_message(
|
|
|
35
31
|
agent_id: str,
|
|
36
32
|
external_user_id: Optional[str] = None,
|
|
37
33
|
attachment_ids: Optional[List[str]] = None,
|
|
38
|
-
stream: bool =
|
|
34
|
+
stream: bool = True,
|
|
39
35
|
timeout: Optional[float] = None,
|
|
40
36
|
) -> Iterator[dict] | dict:
|
|
41
37
|
"""Send a user message through the API-key external chat flow.
|
|
@@ -51,8 +47,14 @@ def send_published_agent_message(
|
|
|
51
47
|
|
|
52
48
|
path = f"/chats/{chat_id}/messages"
|
|
53
49
|
if stream:
|
|
54
|
-
return client.stream_sse(
|
|
55
|
-
|
|
50
|
+
return client.stream_sse(
|
|
51
|
+
"POST",
|
|
52
|
+
path,
|
|
53
|
+
api_version="v2",
|
|
54
|
+
json=body,
|
|
55
|
+
timeout=timeout,
|
|
56
|
+
)
|
|
57
|
+
return client.post(path, api_version="v2", json=body, timeout=timeout)
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def send_message(
|
|
@@ -63,25 +65,173 @@ def send_message(
|
|
|
63
65
|
agent_history_id: str,
|
|
64
66
|
attachment_ids: Optional[List[str]] = None,
|
|
65
67
|
stream: bool = True,
|
|
68
|
+
timeout: Optional[float] = None,
|
|
66
69
|
) -> Iterator[dict] | dict:
|
|
67
|
-
"""
|
|
70
|
+
"""Use legacy Chat V1 to pin an unpublished agent version.
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
Chat V2's API-key external flow only accepts the published ``agent_id``.
|
|
73
|
+
Keep this low-level compatibility helper on V1 until V2 supports external
|
|
74
|
+
``agent_history_id`` pinning. It remains streaming by default.
|
|
71
75
|
"""
|
|
72
|
-
body: dict[str, Any] = {
|
|
76
|
+
body: dict[str, Any] = {
|
|
77
|
+
"message": message,
|
|
78
|
+
"agent_history_id": agent_history_id,
|
|
79
|
+
"stream": stream,
|
|
80
|
+
}
|
|
73
81
|
if attachment_ids:
|
|
74
82
|
body["attachment_ids"] = attachment_ids
|
|
75
83
|
|
|
76
84
|
path = f"/chats/{chat_id}/messages"
|
|
77
85
|
if stream:
|
|
78
|
-
return client.stream_sse(
|
|
79
|
-
|
|
86
|
+
return client.stream_sse(
|
|
87
|
+
"POST",
|
|
88
|
+
path,
|
|
89
|
+
json=body,
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
)
|
|
92
|
+
return client.post(path, json=body, timeout=timeout)
|
|
93
|
+
|
|
80
94
|
|
|
95
|
+
def collect_stream(events: Iterator[dict]) -> dict:
|
|
96
|
+
"""Collect a Chat V2 SSE stream and require an explicit completion event."""
|
|
97
|
+
raw_events: list[dict] = []
|
|
98
|
+
parts: list[dict] = []
|
|
99
|
+
interactions: list[dict] = []
|
|
100
|
+
text_deltas: list[str] = []
|
|
101
|
+
final_text: str | None = None
|
|
102
|
+
response_id: str | None = None
|
|
103
|
+
conversation_group_id: str | None = None
|
|
104
|
+
updated_title: str | None = None
|
|
105
|
+
completed = False
|
|
106
|
+
|
|
107
|
+
for event in events:
|
|
108
|
+
data = event.get("data")
|
|
109
|
+
if data == "[DONE]":
|
|
110
|
+
continue
|
|
111
|
+
raw_events.append(event)
|
|
112
|
+
if not isinstance(data, dict):
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
event_type = str(data.get("type") or event.get("event") or "message")
|
|
116
|
+
response_id = str(data.get("response_id") or response_id or "") or None
|
|
117
|
+
conversation_group_id = str(
|
|
118
|
+
data.get("conversation_group_id") or conversation_group_id or ""
|
|
119
|
+
) or None
|
|
120
|
+
|
|
121
|
+
if event_type == "response.part.delta":
|
|
122
|
+
delta = data.get("delta")
|
|
123
|
+
if data.get("part_kind") == "text" and isinstance(delta, str):
|
|
124
|
+
text_deltas.append(delta)
|
|
125
|
+
elif event_type in ("response.part.created", "response.part.completed"):
|
|
126
|
+
part = data.get("part")
|
|
127
|
+
if isinstance(part, dict):
|
|
128
|
+
parts.append({"event": event_type, "part": part})
|
|
129
|
+
content = part.get("content")
|
|
130
|
+
if (
|
|
131
|
+
event_type == "response.part.completed"
|
|
132
|
+
and part.get("part_kind") == "text"
|
|
133
|
+
and isinstance(content, dict)
|
|
134
|
+
and isinstance(content.get("content"), str)
|
|
135
|
+
):
|
|
136
|
+
final_text = content["content"]
|
|
137
|
+
elif event_type in ("response.interaction.created", "response.interaction.resolved"):
|
|
138
|
+
interactions.append(data)
|
|
139
|
+
elif event_type == "response.chat.title.updated":
|
|
140
|
+
name = data.get("name")
|
|
141
|
+
if isinstance(name, str):
|
|
142
|
+
updated_title = name
|
|
143
|
+
elif event_type == "response.failed":
|
|
144
|
+
message = data.get("message") or "Chat V2 stream failed"
|
|
145
|
+
raise TransportError(
|
|
146
|
+
str(message),
|
|
147
|
+
{
|
|
148
|
+
"code": data.get("code"),
|
|
149
|
+
"response_id": response_id,
|
|
150
|
+
"conversation_group_id": conversation_group_id,
|
|
151
|
+
"outcome_uncertain": True,
|
|
152
|
+
"events": raw_events,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
elif event_type == "response.completed":
|
|
156
|
+
completed = True
|
|
157
|
+
|
|
158
|
+
if not completed:
|
|
159
|
+
raise TransportError(
|
|
160
|
+
"Chat V2 stream ended before response.completed. Inspect the history before retrying.",
|
|
161
|
+
{
|
|
162
|
+
"response_id": response_id,
|
|
163
|
+
"conversation_group_id": conversation_group_id,
|
|
164
|
+
"outcome_uncertain": True,
|
|
165
|
+
"events": raw_events,
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"stream": True,
|
|
171
|
+
"completed": True,
|
|
172
|
+
"response_id": response_id,
|
|
173
|
+
"conversation_group_id": conversation_group_id,
|
|
174
|
+
"final_text": final_text if final_text is not None else "".join(text_deltas),
|
|
175
|
+
"updated_title": updated_title,
|
|
176
|
+
"parts": parts,
|
|
177
|
+
"interactions": interactions,
|
|
178
|
+
"events": raw_events,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def list_messages(
|
|
183
|
+
client: CodeerClient,
|
|
184
|
+
chat_id: int,
|
|
185
|
+
*,
|
|
186
|
+
external_user_id: Optional[str] = None,
|
|
187
|
+
limit: int = 500,
|
|
188
|
+
) -> dict:
|
|
189
|
+
"""Return every client-visible Chat V2 part for a persisted chat.
|
|
81
190
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
233
|
|
|
85
234
|
|
|
86
235
|
def list_chats(client: CodeerClient) -> list[dict]:
|
|
236
|
+
"""List chats through the legacy v1 endpoint; Chat V2 has no list route."""
|
|
87
237
|
return client.get("/chats")
|
codeer_cli/client.py
CHANGED
|
@@ -12,11 +12,12 @@ from __future__ import annotations
|
|
|
12
12
|
import json as json_lib
|
|
13
13
|
import os
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from typing import Any, Iterable, Iterator, Mapping, Optional
|
|
15
|
+
from typing import Any, Iterable, Iterator, Literal, Mapping, Optional
|
|
16
16
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
19
19
|
DEFAULT_CODEER_API_BASE = "https://api.codeer.ai"
|
|
20
|
+
ApiVersion = Literal["v1", "v2"]
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class CodeerError(RuntimeError):
|
|
@@ -154,13 +155,14 @@ class CodeerClient:
|
|
|
154
155
|
method: str,
|
|
155
156
|
path: str,
|
|
156
157
|
*,
|
|
158
|
+
api_version: ApiVersion = "v1",
|
|
157
159
|
params: Optional[Mapping[str, Any]] = None,
|
|
158
160
|
json: Any = None,
|
|
159
161
|
files: Any = None,
|
|
160
162
|
data: Any = None,
|
|
161
163
|
timeout: Optional[float] = None,
|
|
162
164
|
) -> Any:
|
|
163
|
-
url =
|
|
165
|
+
url = _api_url(path, api_version=api_version)
|
|
164
166
|
request_kwargs: dict[str, Any] = {}
|
|
165
167
|
if timeout is not None:
|
|
166
168
|
request_kwargs["timeout"] = timeout
|
|
@@ -206,17 +208,28 @@ class CodeerClient:
|
|
|
206
208
|
method: str,
|
|
207
209
|
path: str,
|
|
208
210
|
*,
|
|
211
|
+
api_version: ApiVersion = "v1",
|
|
209
212
|
params: Optional[Mapping[str, Any]] = None,
|
|
210
213
|
json: Any = None,
|
|
214
|
+
timeout: Optional[float] = None,
|
|
211
215
|
) -> Iterator[dict]:
|
|
212
216
|
"""Yield parsed SSE events from a streaming endpoint (e.g. POST /chats/{id}/messages).
|
|
213
217
|
|
|
214
218
|
Each event is a dict like ``{"event": "message", "data": <parsed-json-or-str>}``.
|
|
215
219
|
"""
|
|
216
|
-
url =
|
|
220
|
+
url = _api_url(path, api_version=api_version)
|
|
217
221
|
method_upper = method.upper()
|
|
222
|
+
request_kwargs: dict[str, Any] = {}
|
|
223
|
+
if timeout is not None:
|
|
224
|
+
request_kwargs["timeout"] = timeout
|
|
218
225
|
try:
|
|
219
|
-
with self._client.stream(
|
|
226
|
+
with self._client.stream(
|
|
227
|
+
method_upper,
|
|
228
|
+
url,
|
|
229
|
+
params=params,
|
|
230
|
+
json=json,
|
|
231
|
+
**request_kwargs,
|
|
232
|
+
) as r:
|
|
220
233
|
if r.status_code >= 400:
|
|
221
234
|
body = r.read().decode("utf-8", "replace")
|
|
222
235
|
self._raise_for_error(r.status_code, body)
|
|
@@ -244,7 +257,7 @@ class CodeerClient:
|
|
|
244
257
|
method_upper,
|
|
245
258
|
path,
|
|
246
259
|
exc,
|
|
247
|
-
timeout_seconds=self.timeout,
|
|
260
|
+
timeout_seconds=timeout if timeout is not None else self.timeout,
|
|
248
261
|
) from exc
|
|
249
262
|
except httpx.RequestError as exc:
|
|
250
263
|
raise self._transport_error(method_upper, path, exc) from exc
|
|
@@ -314,6 +327,18 @@ def _maybe_json(raw: str) -> Any:
|
|
|
314
327
|
return raw
|
|
315
328
|
|
|
316
329
|
|
|
330
|
+
def _api_url(path: str, *, api_version: ApiVersion = "v1") -> str:
|
|
331
|
+
"""Resolve a relative domain path without forcing every API onto one version."""
|
|
332
|
+
if path.startswith("http"):
|
|
333
|
+
return path
|
|
334
|
+
normalized = path if path.startswith("/") else f"/{path}"
|
|
335
|
+
if normalized.startswith("/api/"):
|
|
336
|
+
return normalized
|
|
337
|
+
if api_version not in ("v1", "v2"):
|
|
338
|
+
raise ValueError(f"Unsupported API version: {api_version}")
|
|
339
|
+
return f"/api/{api_version}{normalized}"
|
|
340
|
+
|
|
341
|
+
|
|
317
342
|
def _workspace_names(profile: Mapping[str, Any]) -> dict[str, str]:
|
|
318
343
|
names: dict[str, str] = {}
|
|
319
344
|
for ws in profile.get("workspaces") or []:
|
codeer_cli/commands/history.py
CHANGED
|
@@ -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
|
|
@@ -76,8 +77,8 @@ def register(subparsers):
|
|
|
76
77
|
p.add_argument("--user", default=None, help="external_user_id to associate with the history")
|
|
77
78
|
p.add_argument("--message", action="append", required=True,
|
|
78
79
|
help="User message to send. Repeat for multi-turn histories.")
|
|
79
|
-
p.add_argument("--timeout", type=float, default=
|
|
80
|
-
help="Per-message
|
|
80
|
+
p.add_argument("--timeout", type=float, default=240.0,
|
|
81
|
+
help="Per-message SSE read timeout in seconds (default: 240).")
|
|
81
82
|
p.add_argument("--out", default=None,
|
|
82
83
|
help="Write complete create response/conversation artifact to this file; stdout stays compact.")
|
|
83
84
|
p.set_defaults(func=run_create)
|
|
@@ -91,8 +92,8 @@ def register(subparsers):
|
|
|
91
92
|
help="external_user_id override (defaults to the history's user)")
|
|
92
93
|
p.add_argument("--message", action="append", required=True,
|
|
93
94
|
help="User message to send. Repeat to append multiple turns.")
|
|
94
|
-
p.add_argument("--timeout", type=float, default=
|
|
95
|
-
help="Per-message
|
|
95
|
+
p.add_argument("--timeout", type=float, default=240.0,
|
|
96
|
+
help="Per-message SSE read timeout in seconds (default: 240).")
|
|
96
97
|
p.add_argument("--out", default=None,
|
|
97
98
|
help="Write complete send response/conversation artifact to this file; stdout stays compact.")
|
|
98
99
|
p.set_defaults(func=run_send)
|
|
@@ -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
|
|
|
@@ -285,15 +309,19 @@ def _send_messages(
|
|
|
285
309
|
for idx, message in enumerate(messages, 1):
|
|
286
310
|
log(f"sending turn {idx}/{len(messages)}")
|
|
287
311
|
try:
|
|
288
|
-
|
|
312
|
+
stream = chats_mod.send_published_agent_message(
|
|
289
313
|
client,
|
|
290
314
|
chat_id=history_id,
|
|
291
315
|
message=message,
|
|
292
316
|
agent_id=agent_id,
|
|
293
317
|
external_user_id=external_user_id,
|
|
294
|
-
stream=
|
|
318
|
+
stream=True,
|
|
295
319
|
timeout=timeout,
|
|
296
320
|
)
|
|
321
|
+
if isinstance(stream, dict):
|
|
322
|
+
result = stream
|
|
323
|
+
else:
|
|
324
|
+
result = chats_mod.collect_stream(stream)
|
|
297
325
|
except TransportError as exc:
|
|
298
326
|
body = dict(exc.body) if isinstance(exc.body, dict) else {}
|
|
299
327
|
body.update({"history_id": history_id, "turn": idx, "turn_count": len(messages)})
|
|
@@ -337,23 +365,28 @@ def run_create(args, client) -> int:
|
|
|
337
365
|
timeout=args.timeout,
|
|
338
366
|
)
|
|
339
367
|
|
|
340
|
-
|
|
368
|
+
chat_messages = chats_mod.list_messages(
|
|
369
|
+
client,
|
|
370
|
+
history_id,
|
|
371
|
+
external_user_id=args.user,
|
|
372
|
+
)
|
|
373
|
+
conversation_parts = chat_messages.get("messages") or []
|
|
341
374
|
out = {
|
|
342
375
|
"agent_id": agent_id,
|
|
343
376
|
"history_id": history_id,
|
|
344
377
|
"external_user_id": args.user,
|
|
345
378
|
"url": _history_url(client, workspace_id, history_id),
|
|
346
379
|
"messages": message_results,
|
|
347
|
-
"
|
|
380
|
+
"conversation_parts": conversation_parts,
|
|
348
381
|
}
|
|
349
|
-
write_json(args.out,
|
|
382
|
+
write_json(args.out, out)
|
|
350
383
|
print_json({
|
|
351
384
|
"agent_id": agent_id,
|
|
352
385
|
"history_id": history_id,
|
|
353
386
|
"external_user_id": args.user,
|
|
354
387
|
"url": out["url"],
|
|
355
388
|
"message_count": len(message_results),
|
|
356
|
-
"
|
|
389
|
+
"part_count": len(conversation_parts),
|
|
357
390
|
"wrote_full_detail": bool(args.out),
|
|
358
391
|
})
|
|
359
392
|
return 0
|
|
@@ -395,23 +428,28 @@ def run_send(args, client) -> int:
|
|
|
395
428
|
messages=args.message,
|
|
396
429
|
timeout=args.timeout,
|
|
397
430
|
)
|
|
398
|
-
|
|
431
|
+
chat_messages = chats_mod.list_messages(
|
|
432
|
+
client,
|
|
433
|
+
args.history_id,
|
|
434
|
+
external_user_id=external_user_id,
|
|
435
|
+
)
|
|
436
|
+
conversation_parts = chat_messages.get("messages") or []
|
|
399
437
|
out = {
|
|
400
438
|
"agent_id": agent_id,
|
|
401
439
|
"history_id": args.history_id,
|
|
402
440
|
"external_user_id": external_user_id,
|
|
403
441
|
"url": _history_url(client, workspace_id, args.history_id),
|
|
404
442
|
"messages": message_results,
|
|
405
|
-
"
|
|
443
|
+
"conversation_parts": conversation_parts,
|
|
406
444
|
}
|
|
407
|
-
write_json(args.out,
|
|
445
|
+
write_json(args.out, out)
|
|
408
446
|
print_json({
|
|
409
447
|
"agent_id": agent_id,
|
|
410
448
|
"history_id": args.history_id,
|
|
411
449
|
"external_user_id": external_user_id,
|
|
412
450
|
"url": out["url"],
|
|
413
451
|
"message_count": len(message_results),
|
|
414
|
-
"
|
|
452
|
+
"part_count": len(conversation_parts),
|
|
415
453
|
"wrote_full_detail": bool(args.out),
|
|
416
454
|
})
|
|
417
455
|
return 0
|
codeer_cli/histories.py
CHANGED
|
@@ -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)
|
codeer_cli/parse.py
CHANGED
|
@@ -9,21 +9,20 @@ Parsers are deliberately tolerant: missing/extra fields don't raise, casing
|
|
|
9
9
|
mismatches (``FILE`` vs ``file``) are normalized to lowercase. They are NOT a
|
|
10
10
|
schema validator.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
LEGACY V1 HISTORY GAP, called out here so you don't waste time:
|
|
13
13
|
|
|
14
|
-
Tool **arguments**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
dropped after the assistant turn is saved. What you CAN recover from a
|
|
18
|
-
history is:
|
|
14
|
+
Tool **arguments** and **outputs** are not available on the legacy
|
|
15
|
+
``Conversation`` row returned by ``/histories/{id}/conversations``. What you
|
|
16
|
+
CAN recover from that legacy history shape is:
|
|
19
17
|
|
|
20
18
|
- tool name + call id (regex over ``content``: ``<tool id=...>name</tool>``)
|
|
21
19
|
- per-call token usage (``meta.token_usage.tool_calls[]``)
|
|
22
20
|
- retrieved primary sources (``primary_sources[]`` — the end-to-end trace)
|
|
23
21
|
- assistant's final text (``content`` with tool markers stripped)
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
Chat V2 structured SSE and ``GET /api/v2/chats/{id}/messages`` expose
|
|
24
|
+
persisted tool-call and tool-return parts. Use those surfaces when raw tool
|
|
25
|
+
I/O or exact event order matters.
|
|
27
26
|
"""
|
|
28
27
|
|
|
29
28
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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,15 +172,18 @@ 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
|
|
176
|
-
codeer history
|
|
175
|
+
codeer history conversations <history-id> --out .codeer/current/history-<history-id>.json
|
|
176
|
+
codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
|
|
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
|
|
178
179
|
```
|
|
179
180
|
|
|
180
181
|
`history create` and `history send` use the agent's current published version.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
They use Chat V2 structured SSE with `stream: true`; their per-message read
|
|
183
|
+
timeout defaults to 240 seconds. Success requires a `response.completed`
|
|
184
|
+
event. If the stream times out, reports `response.failed`, or disconnects
|
|
185
|
+
early, inspect the history before retrying: the server may already have
|
|
186
|
+
persisted the turn.
|
|
184
187
|
|
|
185
188
|
Eval case label commands always operate on the active API-key workspace. They
|
|
186
189
|
do not accept a workspace override; switch CLI profiles to target another
|
|
@@ -194,6 +197,11 @@ Flags:
|
|
|
194
197
|
for raw eval results, full conversation turns, full rubric matrices, and
|
|
195
198
|
other data that can grow with cases, versions, or turns.
|
|
196
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
|
+
|
|
197
205
|
Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
|
|
198
206
|
the coding agent to inspect targeted summaries, IDs, failing cases, or selected
|
|
199
207
|
snippets from the saved file.
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
codeer_cli/__init__.py,sha256=z3rkUdVCt11c8QE3-YFkG5cFObM6QhPe2yKugA8k3aA,1915
|
|
2
2
|
codeer_cli/_validate.py,sha256=lqMwkhsBy1g0o5CINBp1HdBCjMawgUaEsZygjJaAPL0,6943
|
|
3
3
|
codeer_cli/agents.py,sha256=HWfz6QgU4wn07df5s3twxVJfAhb0vLJHIfxTjvDMUzU,6045
|
|
4
|
-
codeer_cli/chats.py,sha256=
|
|
4
|
+
codeer_cli/chats.py,sha256=Vg1DIu8fT6mPyPpfqLpExKvXhdZEWCyJGPvalQy3BXg,7989
|
|
5
5
|
codeer_cli/cli.py,sha256=ghZMn7LUxDDvb3f82mDYW5hIFLk7dtlrCrGGP40Gygw,4637
|
|
6
|
-
codeer_cli/client.py,sha256=
|
|
6
|
+
codeer_cli/client.py,sha256=apUycjK-OvHEquAj58FqOy8FHmHjA6K9dScaYQ4M-VU,13175
|
|
7
7
|
codeer_cli/constants.py,sha256=D1pV3wCoqYybrKGKeoupYjjFWLfaFviKp1yL7oh6Qso,2323
|
|
8
8
|
codeer_cli/eval_.py,sha256=2Tz4qokctocZ0fqLYL8Sk9UV8yJ7p-9zYiUFljsRRCs,17478
|
|
9
|
-
codeer_cli/histories.py,sha256=
|
|
9
|
+
codeer_cli/histories.py,sha256=WvpqvuprPc1nJJCTElyM_eoUabyooACfmRCwYlcvPnI,6637
|
|
10
10
|
codeer_cli/kb.py,sha256=Ad4h65NByq5Rq5BTeMghLTKlWRhmOC2jxL0BaTGX3EM,10631
|
|
11
11
|
codeer_cli/models.py,sha256=Ama9fx8hyRMdysiUNA3Ty0sYIYwzFDkOcAZg-id3-vQ,362
|
|
12
|
-
codeer_cli/parse.py,sha256
|
|
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=6qtYcQYgdJE26V6Nw6parB_3RYAYXcOpPE9iJLuLGHo,15888
|
|
16
16
|
codeer_cli/commands/check.py,sha256=lTxolx1mIJ8jldPhJ5FXqie9nbCLVOO-sDPOHTSy1-w,3817
|
|
17
17
|
codeer_cli/commands/eval_cmd.py,sha256=Zlt_YIA3D5-Y8WL1HZ6PxZgL-TcwFTDoXvO2gv7yfOM,71989
|
|
18
|
-
codeer_cli/commands/history.py,sha256=
|
|
18
|
+
codeer_cli/commands/history.py,sha256=2p9VuPwyexr7Q2FFIBSuv9hNzn7VdIMa-AtvtP-8LvI,17253
|
|
19
19
|
codeer_cli/commands/kb.py,sha256=kVEinBVM6NN8_0djOqIQErh46dLmArwFngXMNzvGeAI,28345
|
|
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.12.dist-info/METADATA,sha256=CFrY9ndSNGm4K5UC0A9tZyCXAQPOgWMO5CcGMMNRda0,7885
|
|
23
|
+
codeer_cli-0.1.12.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
24
|
+
codeer_cli-0.1.12.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
|
|
25
|
+
codeer_cli-0.1.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|