codeer-cli 0.1.11__py3-none-any.whl → 0.1.13__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/agents.py CHANGED
@@ -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)
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]:
@@ -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") if isinstance(t.get("custom_form_schema"), dict) else {}
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 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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: codeer-cli
3
- Version: 0.1.11
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.
@@ -1,25 +1,25 @@
1
1
  codeer_cli/__init__.py,sha256=z3rkUdVCt11c8QE3-YFkG5cFObM6QhPe2yKugA8k3aA,1915
2
2
  codeer_cli/_validate.py,sha256=lqMwkhsBy1g0o5CINBp1HdBCjMawgUaEsZygjJaAPL0,6943
3
- codeer_cli/agents.py,sha256=HWfz6QgU4wn07df5s3twxVJfAhb0vLJHIfxTjvDMUzU,6045
4
- codeer_cli/chats.py,sha256=HaQYqbwv5Kq0RgBTKaa9vNYMlsBQonh85-v8yw_rJW8,6609
3
+ codeer_cli/agents.py,sha256=Yt_Rw4c9omsTnXsTe5n_9mD4TpDcx_1WuUkhtKmFY0g,6371
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
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
- codeer_cli/commands/agent.py,sha256=6qtYcQYgdJE26V6Nw6parB_3RYAYXcOpPE9iJLuLGHo,15888
15
+ codeer_cli/commands/agent.py,sha256=BjctOF0tflt8yVGzdc6320sWXVUAYrF39tNIiTxtQx4,16319
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.13.dist-info/METADATA,sha256=zslhdIB39lnbLxfGOM9sv8zrZdzDNzi6xjZWCDLrPQU,7885
23
+ codeer_cli-0.1.13.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
24
+ codeer_cli-0.1.13.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
25
+ codeer_cli-0.1.13.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any