codeer-cli 0.1.7__tar.gz → 0.1.9__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.
Files changed (33) hide show
  1. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/API_REFERENCE.md +23 -12
  2. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/PKG-INFO +12 -1
  3. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/README.md +11 -0
  4. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/pyproject.toml +1 -1
  5. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/__init__.py +2 -2
  6. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/chats.py +2 -2
  7. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/cli.py +1 -1
  8. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/client.py +96 -24
  9. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/_util.py +3 -1
  10. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/eval_cmd.py +149 -46
  11. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/history.py +125 -13
  12. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/eval_.py +24 -7
  13. codeer_cli-0.1.9/tests/test_client_transport.py +84 -0
  14. codeer_cli-0.1.9/tests/test_eval_labels.py +238 -0
  15. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/tests/test_eval_pairs.py +94 -1
  16. codeer_cli-0.1.9/tests/test_history_send.py +159 -0
  17. codeer_cli-0.1.9/tests/test_util.py +22 -0
  18. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/uv.lock +1 -1
  19. codeer_cli-0.1.7/tests/test_eval_labels.py +0 -101
  20. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/.gitignore +0 -0
  21. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/_validate.py +0 -0
  22. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/agents.py +0 -0
  23. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/__init__.py +0 -0
  24. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/agent.py +0 -0
  25. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/check.py +0 -0
  26. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/kb.py +0 -0
  27. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/commands/profile.py +0 -0
  28. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/constants.py +0 -0
  29. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/histories.py +0 -0
  30. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/kb.py +0 -0
  31. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/src/codeer_cli/parse.py +0 -0
  32. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/tests/test_kb_nodes.py +0 -0
  33. {codeer_cli-0.1.7 → codeer_cli-0.1.9}/tests/test_kb_ranges.py +0 -0
@@ -149,10 +149,10 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
149
149
 
150
150
  | Method & path | Purpose |
151
151
  | --- | --- |
152
- | `GET /eval/workspaces/{workspace_id}/case-labels` | List reusable eval case labels |
153
- | `POST /eval/workspaces/{workspace_id}/case-labels` | Create reusable eval case label (`name`, `color?`) |
154
- | `PUT /eval/case-labels/{label_id}` | Update eval case label name/color |
155
- | `DELETE /eval/case-labels/{label_id}` | Delete eval case label and clear associations |
152
+ | `GET /external/eval/case-labels` | List reusable eval case labels in the API key workspace |
153
+ | `POST /external/eval/case-labels` | Create reusable eval case label (`name`, `color?`) in the API key workspace |
154
+ | `PUT /external/eval/case-labels/{label_id}` | Update an eval case label in the API key workspace |
155
+ | `DELETE /external/eval/case-labels/{label_id}` | Delete an eval case label and clear associations |
156
156
  | `POST /eval/cases` | Create case (`input`, `expected_output?`, `rubric?`, `label_ids?`); rubric = user-docs "Standard" |
157
157
  | `GET /eval/agents/{agent_id}/cases` | List cases for an agent |
158
158
  | `GET /eval/cases/{case_id}` | Read one |
@@ -171,8 +171,11 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
171
171
  | `POST /eval/rubric` | Set/override the rubric for one (case, evaluator); also creates assignment |
172
172
  | `POST /eval/rubrics/batch` | **Read** rubrics for a batch of (case, evaluator) pairs |
173
173
 
174
- Eval case labels are workspace-scoped reusable objects. The case create/update
175
- payload uses `label_ids` (stringified label IDs), not freeform label names:
174
+ Eval case labels are workspace-scoped reusable objects. The external label
175
+ endpoints infer the workspace from the API key and do not accept a workspace
176
+ override; switch CLI profiles to operate on another workspace. The case
177
+ create/update payload uses `label_ids` (stringified label IDs), not freeform
178
+ label names:
176
179
 
177
180
  ```json
178
181
  {
@@ -222,17 +225,18 @@ normalized `tool_calls`, `tool_calls_summary`, and `tool_total_duration_ms`;
222
225
  in `eval_table_full.json`. Per-tool time is computed from `start_at/end_at`.
223
226
 
224
227
  **`evaluator_id` is singular — one call returns results for one evaluator
225
- only.** Cases are also bound to specific evaluator assignments. To see the
226
- full picture for a case, first read the case/evaluator assignments, then call
227
- results once per assigned evaluator. `codeer eval run` and
228
- `codeer eval rubrics` handle this automatically; if calling the API directly,
229
- prefer `eval_mod.get_case_evaluator_infos(case_ids=[...])` as the source of
230
- truth for which pairs should run.
228
+ only.** To see the full picture for a case, call results once per evaluator
229
+ you care about. `codeer eval run` and `codeer eval rubrics` handle this
230
+ automatically; when no evaluator is supplied, the public CLI uses external
231
+ rubric batches to find case/evaluator pairs with configured rubrics.
231
232
 
232
233
  Regression workflow (apply prompt change → re-run all assigned pairs → spot
233
234
  side effects): `codeer eval run --agent <agent_id>` runs the latest
234
235
  AgentHistory by default. For the common "many cases, one tester" flow, use
235
236
  `codeer eval run --agent <agent_id> --cases <ids> --evaluator <evaluator_id>`.
237
+ When triggering runs, call `POST /external/eval/runs` once per evaluator with
238
+ that evaluator's case IDs. Do not call legacy internal trigger endpoints from
239
+ the public CLI.
236
240
 
237
241
  ## Stage 7 — Publish
238
242
 
@@ -246,12 +250,19 @@ AgentHistory by default. For the common "many cases, one tester" flow, use
246
250
 
247
251
  | Method & path | Purpose |
248
252
  | --- | --- |
253
+ | `POST /chats` | Create a persisted history using an agent's current published version |
254
+ | `POST /chats/{id}/messages` | Append a turn to an existing persisted history using the current published version |
249
255
  | `GET /histories?agent_id=X&feedback_filter=improve_feedback&external_user_id=…` | List conversations with filters |
250
256
  | `GET /histories/{id}` | Read one history's metadata |
251
257
  | `GET /histories/{id}/conversations` | Full conversation turns incl. tool calls |
252
258
  | `POST /histories/{hid}/conversations/{cid}/feedbacks` | Leave freeform improvement feedback |
253
259
  | `POST /histories/{hid}/conversations/{cid}/score` | Numeric score |
254
260
 
261
+ The CLI exposes the first two operations as `codeer history create` and
262
+ `codeer history send`. Non-streaming message requests default to a 120-second
263
+ per-message timeout. A timeout has an uncertain write outcome, so read the
264
+ history before retrying to avoid duplicate turns.
265
+
255
266
  `feedback_filter` accepts the `FeedbackFilterType` enum values:
256
267
  `no_feedback`, `with_feedback`, `helpful_feedback`, `improve_feedback`.
257
268
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.7
3
+ Version: 0.1.9
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
@@ -143,9 +143,20 @@ Use this pattern during agent lifecycle work:
143
143
  ```bash
144
144
  codeer agent list
145
145
  codeer history list --agent <agent-id> --limit 50
146
+ codeer history create --agent <agent-id> --message "Review this plan" --timeout 120
147
+ codeer history send <history-id> --message "Use the recommended options" --timeout 120
146
148
  codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
147
149
  ```
148
150
 
151
+ `history create` and `history send` use the agent's current published version.
152
+ Their per-message timeout defaults to 120 seconds. If a write request times
153
+ out, inspect the history before retrying: the server may have completed the
154
+ turn after the client stopped waiting.
155
+
156
+ Eval case label commands always operate on the active API-key workspace. They
157
+ do not accept a workspace override; switch CLI profiles to target another
158
+ workspace.
159
+
149
160
  Flags:
150
161
 
151
162
  - `--full` prints bounded extra detail for human inspection. It is still
@@ -125,9 +125,20 @@ Use this pattern during agent lifecycle work:
125
125
  ```bash
126
126
  codeer agent list
127
127
  codeer history list --agent <agent-id> --limit 50
128
+ codeer history create --agent <agent-id> --message "Review this plan" --timeout 120
129
+ codeer history send <history-id> --message "Use the recommended options" --timeout 120
128
130
  codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
129
131
  ```
130
132
 
133
+ `history create` and `history send` use the agent's current published version.
134
+ Their per-message timeout defaults to 120 seconds. If a write request times
135
+ out, inspect the history before retrying: the server may have completed the
136
+ turn after the client stopped waiting.
137
+
138
+ Eval case label commands always operate on the active API-key workspace. They
139
+ do not accept a workspace override; switch CLI profiles to target another
140
+ workspace.
141
+
131
142
  Flags:
132
143
 
133
144
  - `--full` prints bounded extra detail for human inspection. It is still
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codeer-cli"
7
- version = "0.1.7"
7
+ version = "0.1.9"
8
8
  description = "Command line tools for managing Codeer agents over the Codeer API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -19,7 +19,7 @@ workspace-local dotenv files or credential files.
19
19
  """
20
20
 
21
21
  from ._validate import ToolValidationError
22
- from .client import AuthError, CodeerClient, CodeerError
22
+ from .client import AuthError, CodeerClient, CodeerError, TransportError
23
23
  from .parse import (
24
24
  AgentSummary,
25
25
  ConversationTurn,
@@ -43,7 +43,7 @@ from .parse import (
43
43
  )
44
44
 
45
45
  __all__ = [
46
- "CodeerClient", "CodeerError", "AuthError", "ToolValidationError",
46
+ "CodeerClient", "CodeerError", "TransportError", "AuthError", "ToolValidationError",
47
47
  # parsers
48
48
  "AgentSummary", "ConversationTurn", "EvalResultSummary", "HistorySummary",
49
49
  "KBNode", "ToolCall", "EvalToolCall",
@@ -36,6 +36,7 @@ def send_published_agent_message(
36
36
  external_user_id: Optional[str] = None,
37
37
  attachment_ids: Optional[List[str]] = None,
38
38
  stream: bool = False,
39
+ timeout: Optional[float] = None,
39
40
  ) -> Iterator[dict] | dict:
40
41
  """Send a user message through the API-key external chat flow.
41
42
 
@@ -51,7 +52,7 @@ def send_published_agent_message(
51
52
  path = f"/chats/{chat_id}/messages"
52
53
  if stream:
53
54
  return client.stream_sse("POST", path, json=body)
54
- return client.post(path, json=body)
55
+ return client.post(path, json=body, timeout=timeout)
55
56
 
56
57
 
57
58
  def send_message(
@@ -84,4 +85,3 @@ def list_messages(client: CodeerClient, chat_id: int) -> list[dict]:
84
85
 
85
86
  def list_chats(client: CodeerClient) -> list[dict]:
86
87
  return client.get("/chats")
87
-
@@ -4,7 +4,7 @@
4
4
  codeer agent list|get|apply|diff|versions
5
5
  codeer kb list|files|upload|node-rename|node-delete|faq-list|faq-get|faq-create|faq-update|faq-delete
6
6
  codeer eval list|label-list|label-create|label-update|label-delete|case-update|case-delete|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
7
- codeer history list|get|conversations|negative-feedback
7
+ codeer history list|get|conversations|negative-feedback|create|send
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -37,6 +37,16 @@ class ScopeResolutionError(CodeerError):
37
37
  """Raised when workspace or organization scope cannot be inferred."""
38
38
 
39
39
 
40
+ class TransportError(CodeerError):
41
+ """Raised when an HTTP request fails before a response is available."""
42
+
43
+ def __init__(self, message: str, body: Any = None):
44
+ RuntimeError.__init__(self, message)
45
+ self.status = 0
46
+ self.message = message
47
+ self.body = body
48
+
49
+
40
50
  @dataclass
41
51
  class CodeerClient:
42
52
  """Thin wrapper around httpx.Client with Codeer API-key auth.
@@ -148,9 +158,32 @@ class CodeerClient:
148
158
  json: Any = None,
149
159
  files: Any = None,
150
160
  data: Any = None,
161
+ timeout: Optional[float] = None,
151
162
  ) -> Any:
152
163
  url = path if path.startswith("http") else f"/api/v1{path if path.startswith('/') else '/' + path}"
153
- r = self._client.request(method, url, params=params, json=json, files=files, data=data)
164
+ request_kwargs: dict[str, Any] = {}
165
+ if timeout is not None:
166
+ request_kwargs["timeout"] = timeout
167
+ method_upper = method.upper()
168
+ try:
169
+ r = self._client.request(
170
+ method_upper,
171
+ url,
172
+ params=params,
173
+ json=json,
174
+ files=files,
175
+ data=data,
176
+ **request_kwargs,
177
+ )
178
+ except httpx.TimeoutException as exc:
179
+ raise self._transport_error(
180
+ method_upper,
181
+ path,
182
+ exc,
183
+ timeout_seconds=timeout if timeout is not None else self.timeout,
184
+ ) from exc
185
+ except httpx.RequestError as exc:
186
+ raise self._transport_error(method_upper, path, exc) from exc
154
187
  return self._parse(r)
155
188
 
156
189
  def get(self, path: str, **kwargs: Any) -> Any:
@@ -181,29 +214,68 @@ class CodeerClient:
181
214
  Each event is a dict like ``{"event": "message", "data": <parsed-json-or-str>}``.
182
215
  """
183
216
  url = path if path.startswith("http") else f"/api/v1{path if path.startswith('/') else '/' + path}"
184
- with self._client.stream(method, url, params=params, json=json) as r:
185
- if r.status_code >= 400:
186
- body = r.read().decode("utf-8", "replace")
187
- self._raise_for_error(r.status_code, body)
188
- event = "message"
189
- buf: list[str] = []
190
- for line in r.iter_lines():
191
- if line == "":
192
- if buf:
193
- raw = "\n".join(buf)
194
- yield {"event": event, "data": _maybe_json(raw)}
195
- buf = []
196
- event = "message"
197
- continue
198
- if line.startswith(":"):
199
- continue
200
- if line.startswith("event:"):
201
- event = line[len("event:"):].strip()
202
- continue
203
- if line.startswith("data:"):
204
- buf.append(line[len("data:"):].lstrip())
205
- if buf:
206
- yield {"event": event, "data": _maybe_json("\n".join(buf))}
217
+ method_upper = method.upper()
218
+ try:
219
+ with self._client.stream(method_upper, url, params=params, json=json) as r:
220
+ if r.status_code >= 400:
221
+ body = r.read().decode("utf-8", "replace")
222
+ self._raise_for_error(r.status_code, body)
223
+ event = "message"
224
+ buf: list[str] = []
225
+ for line in r.iter_lines():
226
+ if line == "":
227
+ if buf:
228
+ raw = "\n".join(buf)
229
+ yield {"event": event, "data": _maybe_json(raw)}
230
+ buf = []
231
+ event = "message"
232
+ continue
233
+ if line.startswith(":"):
234
+ continue
235
+ if line.startswith("event:"):
236
+ event = line[len("event:"):].strip()
237
+ continue
238
+ if line.startswith("data:"):
239
+ buf.append(line[len("data:"):].lstrip())
240
+ if buf:
241
+ yield {"event": event, "data": _maybe_json("\n".join(buf))}
242
+ except httpx.TimeoutException as exc:
243
+ raise self._transport_error(
244
+ method_upper,
245
+ path,
246
+ exc,
247
+ timeout_seconds=self.timeout,
248
+ ) from exc
249
+ except httpx.RequestError as exc:
250
+ raise self._transport_error(method_upper, path, exc) from exc
251
+
252
+ def _transport_error(
253
+ self,
254
+ method: str,
255
+ path: str,
256
+ exc: httpx.RequestError,
257
+ *,
258
+ timeout_seconds: float | None = None,
259
+ ) -> TransportError:
260
+ outcome_uncertain = (
261
+ method not in {"GET", "HEAD", "OPTIONS"}
262
+ and not isinstance(exc, httpx.ConnectError)
263
+ )
264
+ if isinstance(exc, httpx.TimeoutException):
265
+ timeout_value = timeout_seconds if timeout_seconds is not None else self.timeout
266
+ message = f"Request timed out after {timeout_value:g}s: {method} {path}."
267
+ if outcome_uncertain:
268
+ message += " The server may have completed the request; inspect current state before retrying."
269
+ else:
270
+ message = f"Request failed: {method} {path}: {exc}"
271
+ return TransportError(
272
+ message,
273
+ {
274
+ "method": method,
275
+ "path": path,
276
+ "outcome_uncertain": outcome_uncertain,
277
+ },
278
+ )
207
279
 
208
280
  def _parse(self, r: httpx.Response) -> Any:
209
281
  text = r.text
@@ -62,5 +62,7 @@ def print_json(value: Any) -> None:
62
62
  def write_json(path: str | None, value: Any) -> None:
63
63
  if not path:
64
64
  return
65
- Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n")
65
+ out = Path(path)
66
+ out.parent.mkdir(parents=True, exist_ok=True)
67
+ out.write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n")
66
68
  log(f"wrote full detail to {path}")