codeer-cli 0.1.10__tar.gz → 0.1.12__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.10 → codeer_cli-0.1.12}/API_REFERENCE.md +39 -27
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/PKG-INFO +14 -6
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/README.md +13 -5
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/pyproject.toml +1 -1
- codeer_cli-0.1.12/src/codeer_cli/chats.py +237 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/client.py +30 -5
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/history.py +69 -31
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/histories.py +40 -18
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/parse.py +7 -8
- codeer_cli-0.1.12/tests/test_chats_v2.py +188 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_client_transport.py +48 -0
- codeer_cli-0.1.12/tests/test_history_read.py +132 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_history_send.py +52 -12
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/uv.lock +1 -1
- codeer_cli-0.1.10/src/codeer_cli/chats.py +0 -87
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/.gitignore +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/__init__.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/_validate.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/agents.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/cli.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/_util.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/agent.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/eval_cmd.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/kb.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/model.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/eval_.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/src/codeer_cli/models.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_agent_handoff.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_eval_labels.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_eval_pairs.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_kb_nodes.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_kb_ranges.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_models.py +0 -0
- {codeer_cli-0.1.10 → codeer_cli-0.1.12}/tests/test_util.py +0 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
> This file is the request-shape reference for maintaining `codeer-cli`.
|
|
4
4
|
|
|
5
5
|
The 9 stages below mirror the user-docs lifecycle (`agent-creation` →
|
|
6
|
-
`optimization-loop` → `publish`).
|
|
7
|
-
|
|
6
|
+
`optimization-loop` → `publish`). Agent, KB, eval, history, and model paths are
|
|
7
|
+
under `/api/v1/`; Chat create/message/read paths use `/api/v2/chats`. All
|
|
8
|
+
endpoints authenticate via `x-api-key` from `CODEER_API_KEY`.
|
|
8
9
|
|
|
9
10
|
Envelope: successful responses look like
|
|
10
11
|
`{"error_code": 0, "message": "", "pagination": null, "data": <payload>}`.
|
|
@@ -26,6 +27,9 @@ need a default agent.
|
|
|
26
27
|
- `/histories` uses **`limit` + `offset`** (NOT `page` / `page_size`).
|
|
27
28
|
Default in `histories.list()` is `limit=500`. Backend hard-cap may be
|
|
28
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.
|
|
29
33
|
- `/agents/{id}/histories`, `/eval/agents/{id}/cases`, `/eval/evaluators` all
|
|
30
34
|
return everything in one shot today (no pagination).
|
|
31
35
|
- `order_by` defaults to `"desc"` (most recent first) on endpoints that
|
|
@@ -140,19 +144,21 @@ codeer kb files --kb-id <kb-id>
|
|
|
140
144
|
codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:0-12:42 --dry-run
|
|
141
145
|
```
|
|
142
146
|
|
|
143
|
-
## Stage 3 — Live Test on a specific version
|
|
147
|
+
## Stage 3 — Live Test on a specific version (legacy V1 compatibility)
|
|
144
148
|
|
|
145
149
|
| Method & path | Purpose |
|
|
146
150
|
| --- | --- |
|
|
147
|
-
| `POST /chats` | Create a
|
|
148
|
-
| `POST /chats/{chat_id}/messages` | Send a message
|
|
149
|
-
| `GET /chats/{chat_id}/messages` | Read
|
|
150
|
-
| `POST /chats/{chat_id}/regenerate` | Re-run the last turn |
|
|
151
|
-
| `POST /chats/{chat_id}/messages/{msg_id}/feedbacks` | Thumbs up/down on a reply |
|
|
152
|
-
|
|
153
|
-
`POST /chats/.../messages`
|
|
154
|
-
for the apply → test → publish workflow. Pass the draft
|
|
155
|
-
`PUT /agents/{id}` to test an unpublished version.
|
|
151
|
+
| `POST /api/v1/chats` | Create a legacy chat session bound to an agent |
|
|
152
|
+
| `POST /api/v1/chats/{chat_id}/messages` | Send a version-pinned message through legacy SSE |
|
|
153
|
+
| `GET /api/v1/chats/{chat_id}/messages` | Read legacy messages for a chat |
|
|
154
|
+
| `POST /api/v1/chats/{chat_id}/regenerate` | Re-run the last turn |
|
|
155
|
+
| `POST /api/v1/chats/{chat_id}/messages/{msg_id}/feedbacks` | Thumbs up/down on a reply |
|
|
156
|
+
|
|
157
|
+
Legacy V1 `POST /chats/.../messages` accepts `agent_history_id` — this is the
|
|
158
|
+
compatibility hook for the apply → test → publish workflow. Pass the draft
|
|
159
|
+
`AgentHistory.id` from `PUT /agents/{id}` to test an unpublished version.
|
|
160
|
+
The API-key external flow in Chat V2 accepts `agent_id` and runs its published
|
|
161
|
+
version; it does not currently replace this draft-pinning path.
|
|
156
162
|
|
|
157
163
|
## Stage 4 — Version management
|
|
158
164
|
|
|
@@ -267,18 +273,21 @@ the public CLI.
|
|
|
267
273
|
|
|
268
274
|
| Method & path | Purpose |
|
|
269
275
|
| --- | --- |
|
|
270
|
-
| `POST /chats` | Create a persisted history using an agent's current published version |
|
|
271
|
-
| `POST /chats/{id}/messages` | Append a turn
|
|
276
|
+
| `POST /api/v2/chats` | Create a persisted history using an agent's current published version |
|
|
277
|
+
| `POST /api/v2/chats/{id}/messages` | Append a turn through structured SSE using the current published version |
|
|
278
|
+
| `GET /api/v2/chats/{id}/messages` | Read persisted Chat V2 conversation parts |
|
|
272
279
|
| `GET /histories?agent_id=X&feedback_filter=improve_feedback&external_user_id=…` | List conversations with filters |
|
|
273
280
|
| `GET /histories/{id}` | Read one history's metadata |
|
|
274
|
-
| `GET /histories/{id}/conversations` |
|
|
281
|
+
| `GET /histories/{id}/conversations` | Legacy compact conversation rows; not complete tool I/O |
|
|
275
282
|
| `POST /histories/{hid}/conversations/{cid}/feedbacks` | Leave freeform improvement feedback |
|
|
276
283
|
| `POST /histories/{hid}/conversations/{cid}/score` | Numeric score |
|
|
277
284
|
|
|
278
285
|
The CLI exposes the first two operations as `codeer history create` and
|
|
279
|
-
`codeer history send`.
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
`codeer history send`. Messages explicitly set `stream: true`, consume Chat V2
|
|
287
|
+
SSE, and require `response.completed` before reporting success. Their
|
|
288
|
+
per-message SSE read timeout defaults to 240 seconds. A timeout,
|
|
289
|
+
`response.failed`, or disconnect before completion has an uncertain write
|
|
290
|
+
outcome, so read the history before retrying to avoid duplicate turns.
|
|
282
291
|
|
|
283
292
|
`feedback_filter` accepts the `FeedbackFilterType` enum values:
|
|
284
293
|
`no_feedback`, `with_feedback`, `helpful_feedback`, `improve_feedback`.
|
|
@@ -357,7 +366,7 @@ said (scope, factuality, tool-use rules).
|
|
|
357
366
|
|
|
358
367
|
### 4. Agent version pinning works everywhere — use it
|
|
359
368
|
|
|
360
|
-
|
|
369
|
+
Legacy V1 `POST /chats/{id}/messages` (`agent_history_id` required) and
|
|
361
370
|
`POST /eval/trigger` (`agent_history_id` optional, null = live state) accept
|
|
362
371
|
the draft history id. The apply-→-test-→-publish loop:
|
|
363
372
|
|
|
@@ -461,11 +470,12 @@ For bulk creation, `codeer eval cases-apply --attachments-dir <dir>` reads
|
|
|
461
470
|
each case's `attachment_files: ["x.jpg"]` array, uploads, and attaches in one
|
|
462
471
|
pass. Workspace scope is inferred from the API-key virtual user profile.
|
|
463
472
|
|
|
464
|
-
### 11. Tool args + outputs are
|
|
473
|
+
### 11. Tool args + outputs are not available in legacy V1 history reads
|
|
465
474
|
|
|
466
|
-
|
|
467
|
-
— there is no `tool` role row.
|
|
468
|
-
|
|
475
|
+
Legacy V1 `History` conversation rows have only three roles
|
|
476
|
+
(`OpenAIChatRole = system | user | assistant`) — there is no `tool` role row.
|
|
477
|
+
When you read those rows through `/histories/{id}/conversations`, here's what
|
|
478
|
+
you can and can't recover from each assistant turn:
|
|
469
479
|
|
|
470
480
|
| Recoverable | Where |
|
|
471
481
|
| --- | --- |
|
|
@@ -481,10 +491,12 @@ and can't recover from each assistant turn:
|
|
|
481
491
|
| Tool **outputs** (raw JSON returned by the tool) | same — stored only as derived `primary_sources` for retrieval tools |
|
|
482
492
|
| Reasoning steps mid-turn | `meta.reasoning_steps` is currently always `null` |
|
|
483
493
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
494
|
+
Chat V2 improves this contract: structured SSE emits tool calls and returns as
|
|
495
|
+
`response.part.created` / `response.part.completed`, and
|
|
496
|
+
`GET /api/v2/chats/{id}/messages` reads the persisted conversation parts.
|
|
497
|
+
Capture the SSE artifact with `--out` when exact event order matters; use the
|
|
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.
|
|
488
500
|
|
|
489
501
|
### 10. A KB has exactly ONE level of folders — no nesting
|
|
490
502
|
|
|
@@ -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.
|
|
@@ -154,15 +154,18 @@ 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
|
|
158
|
-
codeer history
|
|
157
|
+
codeer history conversations <history-id> --out .codeer/current/history-<history-id>.json
|
|
158
|
+
codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
|
|
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
|
|
160
161
|
```
|
|
161
162
|
|
|
162
163
|
`history create` and `history send` use the agent's current published version.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
They use Chat V2 structured SSE with `stream: true`; their per-message read
|
|
165
|
+
timeout defaults to 240 seconds. Success requires a `response.completed`
|
|
166
|
+
event. If the stream times out, reports `response.failed`, or disconnects
|
|
167
|
+
early, inspect the history before retrying: the server may already have
|
|
168
|
+
persisted the turn.
|
|
166
169
|
|
|
167
170
|
Eval case label commands always operate on the active API-key workspace. They
|
|
168
171
|
do not accept a workspace override; switch CLI profiles to target another
|
|
@@ -176,6 +179,11 @@ Flags:
|
|
|
176
179
|
for raw eval results, full conversation turns, full rubric matrices, and
|
|
177
180
|
other data that can grow with cases, versions, or turns.
|
|
178
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
|
+
|
|
179
187
|
Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
|
|
180
188
|
the coding agent to inspect targeted summaries, IDs, failing cases, or selected
|
|
181
189
|
snippets from the saved file.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Chat V2 creation, structured SSE responses, and persisted message reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterator, List, Optional
|
|
6
|
+
|
|
7
|
+
from .client import CodeerClient, TransportError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create(
|
|
11
|
+
client: CodeerClient,
|
|
12
|
+
*,
|
|
13
|
+
agent_id: str,
|
|
14
|
+
title: Optional[str] = None,
|
|
15
|
+
external_user_id: Optional[str] = None,
|
|
16
|
+
) -> dict:
|
|
17
|
+
body: dict[str, Any] = {
|
|
18
|
+
"agent_id": agent_id,
|
|
19
|
+
"name": title or "CLI conversation",
|
|
20
|
+
}
|
|
21
|
+
if external_user_id is not None:
|
|
22
|
+
body["external_user_id"] = external_user_id
|
|
23
|
+
return client.post("/chats", api_version="v2", json=body)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def send_published_agent_message(
|
|
27
|
+
client: CodeerClient,
|
|
28
|
+
*,
|
|
29
|
+
chat_id: int,
|
|
30
|
+
message: str,
|
|
31
|
+
agent_id: str,
|
|
32
|
+
external_user_id: Optional[str] = None,
|
|
33
|
+
attachment_ids: Optional[List[str]] = None,
|
|
34
|
+
stream: bool = True,
|
|
35
|
+
timeout: Optional[float] = None,
|
|
36
|
+
) -> Iterator[dict] | dict:
|
|
37
|
+
"""Send a user message through the API-key external chat flow.
|
|
38
|
+
|
|
39
|
+
API-key chat endpoints use the agent's published version. They accept
|
|
40
|
+
``agent_id`` rather than ``agent_history_id``.
|
|
41
|
+
"""
|
|
42
|
+
body: dict[str, Any] = {"message": message, "agent_id": agent_id, "stream": stream}
|
|
43
|
+
if external_user_id is not None:
|
|
44
|
+
body["external_user_id"] = external_user_id
|
|
45
|
+
if attachment_ids:
|
|
46
|
+
body["attached_file_uuids"] = attachment_ids
|
|
47
|
+
|
|
48
|
+
path = f"/chats/{chat_id}/messages"
|
|
49
|
+
if stream:
|
|
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)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def send_message(
|
|
61
|
+
client: CodeerClient,
|
|
62
|
+
*,
|
|
63
|
+
chat_id: int,
|
|
64
|
+
message: str,
|
|
65
|
+
agent_history_id: str,
|
|
66
|
+
attachment_ids: Optional[List[str]] = None,
|
|
67
|
+
stream: bool = True,
|
|
68
|
+
timeout: Optional[float] = None,
|
|
69
|
+
) -> Iterator[dict] | dict:
|
|
70
|
+
"""Use legacy Chat V1 to pin an unpublished agent version.
|
|
71
|
+
|
|
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.
|
|
75
|
+
"""
|
|
76
|
+
body: dict[str, Any] = {
|
|
77
|
+
"message": message,
|
|
78
|
+
"agent_history_id": agent_history_id,
|
|
79
|
+
"stream": stream,
|
|
80
|
+
}
|
|
81
|
+
if attachment_ids:
|
|
82
|
+
body["attachment_ids"] = attachment_ids
|
|
83
|
+
|
|
84
|
+
path = f"/chats/{chat_id}/messages"
|
|
85
|
+
if stream:
|
|
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
|
+
|
|
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.
|
|
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
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def list_chats(client: CodeerClient) -> list[dict]:
|
|
236
|
+
"""List chats through the legacy v1 endpoint; Chat V2 has no list route."""
|
|
237
|
+
return client.get("/chats")
|
|
@@ -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 []:
|