codeer-cli 0.1.11__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 CHANGED
@@ -186,14 +186,50 @@ def list_messages(
186
186
  external_user_id: Optional[str] = None,
187
187
  limit: int = 500,
188
188
  ) -> dict:
189
- params: dict[str, Any] = {"limit": limit, "offset": 0}
190
- if external_user_id is not None:
191
- params["external_user_id"] = external_user_id
192
- return client.get(
193
- f"/chats/{chat_id}/messages",
194
- api_version="v2",
195
- params=params,
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]:
@@ -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 turns in a history. Complete turns/tool payloads require --out.",
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 write complete stripped turns there; stdout remains a summary.")
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 complete stripped turns to this file. Use for debugging or eval-case extraction.")
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 _turn_summary(turn: dict, idx: int, *, full: bool = False) -> dict:
159
- content = turn.get("content") or turn.get("message") or ""
160
- tool_calls = turn.get("tool_calls") or turn.get("tools") or []
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
- "turn_idx": idx,
163
- "id": turn.get("id"),
164
- "role": turn.get("role"),
165
- "created_at": turn.get("created_at"),
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
- "tool_call_count": len(tool_calls) if isinstance(tool_calls, list) else None,
169
- "feedback_count": len(turn.get("feedbacks") or []),
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 (turn.get("feedbacks") or [])
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 = hist_mod.get_conversations(client, args.history_id)
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, strip_noisy_fields(result))
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(result),
263
+ "turn_count": len(group_ids),
264
+ "part_count": len(parts),
242
265
  "wrote_full_detail": bool(args.out),
243
- "turns": [_turn_summary(t, i, full=args.full) for i, t in enumerate(result)],
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, strip_noisy_fields(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, strip_noisy_fields(out))
445
+ write_json(args.out, out)
422
446
  print_json({
423
447
  "agent_id": agent_id,
424
448
  "history_id": args.history_id,
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) network calls — one ``/histories/{id}/conversations``
98
- per history. Filter aggressively via ``exclude_users`` and ``limit``
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 .parse import strip_tool_markers # local import to avoid cycle
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
- convs = get_conversations(client, hid)
117
+ parts = (chats.list_messages(client, hid).get("messages") or [])
119
118
  except Exception:
120
119
  continue
121
- for i, c in enumerate(convs):
122
- if (c.get("role") or "") != "assistant":
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 = c.get("feedbacks") or []
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": i,
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": prior_user,
144
- "assistant_excerpt": strip_tool_markers(c.get("content") or "")[:assistant_excerpt_chars],
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 all conversation turns for a history — includes tool calls and reasoning."""
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.11
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,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.
@@ -1,12 +1,12 @@
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=HaQYqbwv5Kq0RgBTKaa9vNYMlsBQonh85-v8yw_rJW8,6609
4
+ codeer_cli/chats.py,sha256=Vg1DIu8fT6mPyPpfqLpExKvXhdZEWCyJGPvalQy3BXg,7989
5
5
  codeer_cli/cli.py,sha256=ghZMn7LUxDDvb3f82mDYW5hIFLk7dtlrCrGGP40Gygw,4637
6
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=tk28git_peX4x703CIDU8u72JtlGaytyrtlHfxlK-7A,5979
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
12
  codeer_cli/parse.py,sha256=-txMtIxPfeg_i4McEskVKxo8p4438gKS-sFTI38RoBs,21772
@@ -15,11 +15,11 @@ codeer_cli/commands/_util.py,sha256=X9-9cYgnBYz94hVG4Pe9SsI97DHNv3HE5_7GlPMn93I,
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=qb0YXggbJuSahDp-l2A6Abj3g-LpIXWzgHadTA-4ZIU,16500
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.11.dist-info/METADATA,sha256=tPOpQ2LTjMvWLk_KjwzXOKtJWr8Iq3XV0j0HEiScI88,7520
23
- codeer_cli-0.1.11.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
24
- codeer_cli-0.1.11.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
25
- codeer_cli-0.1.11.dist-info/RECORD,,
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,,