codeer-cli 0.1.0__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/__init__.py +54 -0
- codeer_cli/_validate.py +131 -0
- codeer_cli/agents.py +155 -0
- codeer_cli/chats.py +87 -0
- codeer_cli/cli.py +92 -0
- codeer_cli/client.py +277 -0
- codeer_cli/commands/__init__.py +0 -0
- codeer_cli/commands/_util.py +12 -0
- codeer_cli/commands/agent.py +186 -0
- codeer_cli/commands/check.py +66 -0
- codeer_cli/commands/eval_cmd.py +919 -0
- codeer_cli/commands/history.py +200 -0
- codeer_cli/commands/kb.py +126 -0
- codeer_cli/commands/profile.py +205 -0
- codeer_cli/constants.py +66 -0
- codeer_cli/eval_.py +423 -0
- codeer_cli/histories.py +156 -0
- codeer_cli/kb.py +226 -0
- codeer_cli/parse.py +567 -0
- codeer_cli-0.1.0.dist-info/METADATA +108 -0
- codeer_cli-0.1.0.dist-info/RECORD +23 -0
- codeer_cli-0.1.0.dist-info/WHEEL +4 -0
- codeer_cli-0.1.0.dist-info/entry_points.txt +2 -0
codeer_cli/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Thin Python client for the Codeer internal API.
|
|
2
|
+
|
|
3
|
+
Import pattern for scripts::
|
|
4
|
+
|
|
5
|
+
from codeer_cli import CodeerClient, agents, kb, eval_, histories, chats
|
|
6
|
+
|
|
7
|
+
c = CodeerClient.from_env()
|
|
8
|
+
agent = agents.create(c, workspace_id=..., name="My Agent", system_prompt="...", unified_tools=[])
|
|
9
|
+
|
|
10
|
+
Per-project env convention: set CODEER_AGENT_ID as an environment variable so
|
|
11
|
+
scripts in a customer's directory don't have to re-pass it.
|
|
12
|
+
|
|
13
|
+
The workspace and organization come from the workspace API-key virtual user's
|
|
14
|
+
profile, not from CLI flags or environment overrides.
|
|
15
|
+
|
|
16
|
+
Auth uses CODEER_API_KEY from process env only. CODEER_API_BASE defaults to
|
|
17
|
+
production and can be overridden from process env. The CLI does not read
|
|
18
|
+
workspace-local dotenv files or credential files.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._validate import ToolValidationError
|
|
22
|
+
from .client import AuthError, CodeerClient, CodeerError
|
|
23
|
+
from .parse import (
|
|
24
|
+
AgentSummary,
|
|
25
|
+
ConversationTurn,
|
|
26
|
+
EvalResultSummary,
|
|
27
|
+
EvalToolCall,
|
|
28
|
+
HistorySummary,
|
|
29
|
+
KBNode,
|
|
30
|
+
ToolCall,
|
|
31
|
+
parse_agent,
|
|
32
|
+
parse_conversation_turn,
|
|
33
|
+
parse_conversations,
|
|
34
|
+
parse_eval_result,
|
|
35
|
+
parse_eval_tool_calls,
|
|
36
|
+
parse_kb_node,
|
|
37
|
+
parse_kb_nodes,
|
|
38
|
+
parse_rubrics_from_reason,
|
|
39
|
+
parse_tool_calls,
|
|
40
|
+
strip_tool_markers,
|
|
41
|
+
summarize_eval_tool_calls,
|
|
42
|
+
summarize_history,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"CodeerClient", "CodeerError", "AuthError", "ToolValidationError",
|
|
47
|
+
# parsers
|
|
48
|
+
"AgentSummary", "ConversationTurn", "EvalResultSummary", "HistorySummary",
|
|
49
|
+
"KBNode", "ToolCall", "EvalToolCall",
|
|
50
|
+
"parse_agent", "parse_conversation_turn", "parse_conversations",
|
|
51
|
+
"parse_eval_result", "parse_eval_tool_calls", "parse_kb_node", "parse_kb_nodes",
|
|
52
|
+
"parse_rubrics_from_reason",
|
|
53
|
+
"parse_tool_calls", "strip_tool_markers", "summarize_eval_tool_calls", "summarize_history",
|
|
54
|
+
]
|
codeer_cli/_validate.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Client-side validation for unified-tool payloads.
|
|
2
|
+
|
|
3
|
+
These checks exist because the backend's form-schema validator is lenient
|
|
4
|
+
(``extra="allow"``) and silently accepts unknown ``type`` strings, which then
|
|
5
|
+
render as blank fields in the web builder. Catching the common mistakes here
|
|
6
|
+
gives actionable errors before the PUT/POST round-trip.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
|
|
13
|
+
from .constants import (
|
|
14
|
+
FORM_FIELD_TYPES,
|
|
15
|
+
MAX_CALL_AGENT_TOOLS,
|
|
16
|
+
MAX_MEMORY_TOOLS,
|
|
17
|
+
MAX_TOOLS_PER_AGENT,
|
|
18
|
+
REQUIRED_FORM_FIELD_KEYS,
|
|
19
|
+
UNIFIED_TOOL_TYPES,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolValidationError(ValueError):
|
|
24
|
+
"""Raised when a unified_tools payload is definitely wrong."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_unified_tools(tools: Iterable[dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
28
|
+
"""Raise ToolValidationError on invalid payloads; otherwise return the list.
|
|
29
|
+
|
|
30
|
+
Validates type strings against the backend enum and walks into
|
|
31
|
+
``custom_form_schema.fields`` for request_form tools so we catch the
|
|
32
|
+
``type: "text" | "email" | "select"`` mistake the UI renders as blank.
|
|
33
|
+
"""
|
|
34
|
+
tools_list = list(tools or [])
|
|
35
|
+
if len(tools_list) > MAX_TOOLS_PER_AGENT:
|
|
36
|
+
raise ToolValidationError(
|
|
37
|
+
f"{len(tools_list)} tools configured, max allowed is {MAX_TOOLS_PER_AGENT} "
|
|
38
|
+
"(see user-docs/agent-creation/tools/index.md)."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
call_agent_count = sum(1 for t in tools_list if t.get("type") == "call_agent")
|
|
42
|
+
if call_agent_count > MAX_CALL_AGENT_TOOLS:
|
|
43
|
+
raise ToolValidationError(
|
|
44
|
+
f"{call_agent_count} call_agent tools configured, max is {MAX_CALL_AGENT_TOOLS}."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
memory_count = sum(1 for t in tools_list if t.get("type") == "memory")
|
|
48
|
+
if memory_count > MAX_MEMORY_TOOLS:
|
|
49
|
+
raise ToolValidationError(
|
|
50
|
+
f"{memory_count} memory tools configured, max is {MAX_MEMORY_TOOLS}."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for i, tool in enumerate(tools_list):
|
|
54
|
+
_validate_single_tool(tool, i)
|
|
55
|
+
|
|
56
|
+
return tools_list
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _validate_single_tool(tool: dict[str, Any], index: int) -> None:
|
|
60
|
+
prefix = f"unified_tools[{index}]"
|
|
61
|
+
tool_type = tool.get("type")
|
|
62
|
+
if tool_type not in UNIFIED_TOOL_TYPES:
|
|
63
|
+
raise ToolValidationError(
|
|
64
|
+
f"{prefix}.type = {tool_type!r} is not a valid UnifiedToolType. "
|
|
65
|
+
f"Valid values: {sorted(UNIFIED_TOOL_TYPES)}."
|
|
66
|
+
)
|
|
67
|
+
if not tool.get("id"):
|
|
68
|
+
raise ToolValidationError(f"{prefix}.id is required and must be non-empty.")
|
|
69
|
+
|
|
70
|
+
if tool_type == "request_form":
|
|
71
|
+
_validate_form_schema(tool.get("custom_form_schema"), prefix)
|
|
72
|
+
elif tool_type == "knowledge_base":
|
|
73
|
+
node_ids = tool.get("knowledge_node_ids")
|
|
74
|
+
if node_ids is None or (isinstance(node_ids, list) and not node_ids):
|
|
75
|
+
raise ToolValidationError(
|
|
76
|
+
f"{prefix}: knowledge_base tool needs non-empty knowledge_node_ids."
|
|
77
|
+
)
|
|
78
|
+
elif tool_type == "call_agent":
|
|
79
|
+
if not tool.get("agent_id"):
|
|
80
|
+
raise ToolValidationError(f"{prefix}: call_agent tool requires agent_id.")
|
|
81
|
+
elif tool_type == "http_request":
|
|
82
|
+
cfg = tool.get("http_request")
|
|
83
|
+
if not cfg or not cfg.get("method") or not cfg.get("url_template"):
|
|
84
|
+
raise ToolValidationError(
|
|
85
|
+
f"{prefix}: http_request tool requires http_request.method and http_request.url_template."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_form_schema(schema: Any, prefix: str) -> None:
|
|
90
|
+
if schema is None:
|
|
91
|
+
raise ToolValidationError(
|
|
92
|
+
f"{prefix}: request_form tool requires custom_form_schema "
|
|
93
|
+
"(with title, fields[], ...)."
|
|
94
|
+
)
|
|
95
|
+
if not isinstance(schema, dict):
|
|
96
|
+
raise ToolValidationError(f"{prefix}.custom_form_schema must be an object.")
|
|
97
|
+
fields = schema.get("fields")
|
|
98
|
+
if not isinstance(fields, list) or not fields:
|
|
99
|
+
raise ToolValidationError(
|
|
100
|
+
f"{prefix}.custom_form_schema.fields must be a non-empty list."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for j, field in enumerate(fields):
|
|
104
|
+
fp = f"{prefix}.custom_form_schema.fields[{j}]"
|
|
105
|
+
if not isinstance(field, dict):
|
|
106
|
+
raise ToolValidationError(f"{fp} must be an object.")
|
|
107
|
+
for key in REQUIRED_FORM_FIELD_KEYS:
|
|
108
|
+
if key not in field or field[key] in (None, ""):
|
|
109
|
+
raise ToolValidationError(
|
|
110
|
+
f"{fp}.{key} is required and must be non-empty."
|
|
111
|
+
)
|
|
112
|
+
ftype = field.get("type")
|
|
113
|
+
if ftype not in FORM_FIELD_TYPES:
|
|
114
|
+
# The two traps we already hit — surface a hint, not just a rejection.
|
|
115
|
+
hint = ""
|
|
116
|
+
if ftype == "text":
|
|
117
|
+
hint = " (use 'shortText' for single-line or 'longText' for multi-line)"
|
|
118
|
+
elif ftype == "email":
|
|
119
|
+
hint = " (there is no 'email' type — use 'shortText' and add placeholder/helpText)"
|
|
120
|
+
elif ftype == "select":
|
|
121
|
+
hint = " (use 'dropdown' with options=[{value,label}])"
|
|
122
|
+
raise ToolValidationError(
|
|
123
|
+
f"{fp}.type = {ftype!r} is not a valid FormFieldType. "
|
|
124
|
+
f"Valid: {sorted(FORM_FIELD_TYPES)}.{hint}"
|
|
125
|
+
)
|
|
126
|
+
if ftype == "dropdown" or ftype == "radio":
|
|
127
|
+
options = field.get("options")
|
|
128
|
+
if not isinstance(options, list) or not options:
|
|
129
|
+
raise ToolValidationError(
|
|
130
|
+
f"{fp}: type={ftype!r} requires non-empty options=[{{value,label}}]."
|
|
131
|
+
)
|
codeer_cli/agents.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Agent CRUD, versioning, and publishing.
|
|
2
|
+
|
|
3
|
+
Each function returns the parsed `data` field from the Ninja response envelope.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Optional
|
|
9
|
+
|
|
10
|
+
from ._validate import validate_unified_tools
|
|
11
|
+
from .client import CodeerClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create(
|
|
15
|
+
client: CodeerClient,
|
|
16
|
+
*,
|
|
17
|
+
workspace_id: str,
|
|
18
|
+
name: str,
|
|
19
|
+
system_prompt: str,
|
|
20
|
+
unified_tools: Optional[List[dict]] = None,
|
|
21
|
+
use_search: bool = False,
|
|
22
|
+
llm_model: Optional[str] = None,
|
|
23
|
+
description: Optional[str] = None,
|
|
24
|
+
suggested_questions: Optional[List[str]] = None,
|
|
25
|
+
primary_object_ids: Optional[List[int]] = None,
|
|
26
|
+
attachment_ids: Optional[List[str]] = None,
|
|
27
|
+
) -> dict:
|
|
28
|
+
validated_tools = validate_unified_tools(unified_tools)
|
|
29
|
+
body: dict[str, Any] = {
|
|
30
|
+
"name": name,
|
|
31
|
+
"system_prompt": system_prompt,
|
|
32
|
+
"unified_tools": validated_tools,
|
|
33
|
+
"primary_object_ids": primary_object_ids or [],
|
|
34
|
+
"attachment_ids": attachment_ids or [],
|
|
35
|
+
"use_search": use_search,
|
|
36
|
+
"suggested_questions": suggested_questions or [],
|
|
37
|
+
}
|
|
38
|
+
if description is not None:
|
|
39
|
+
body["description"] = description
|
|
40
|
+
if llm_model is not None:
|
|
41
|
+
body["llm_model"] = llm_model
|
|
42
|
+
return client.post("/external/agents", json=body)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update(
|
|
46
|
+
client: CodeerClient,
|
|
47
|
+
agent_id: str,
|
|
48
|
+
*,
|
|
49
|
+
name: str,
|
|
50
|
+
system_prompt: str,
|
|
51
|
+
unified_tools: List[dict],
|
|
52
|
+
use_search: bool,
|
|
53
|
+
version_note: str = "",
|
|
54
|
+
description: Optional[str] = None,
|
|
55
|
+
llm_model: Optional[str] = None,
|
|
56
|
+
suggested_questions: Optional[List[str]] = None,
|
|
57
|
+
primary_object_ids: Optional[List[int]] = None,
|
|
58
|
+
attachment_ids: Optional[List[str]] = None,
|
|
59
|
+
) -> dict:
|
|
60
|
+
"""PUT creates a new AgentHistory snapshot (draft)."""
|
|
61
|
+
validated_tools = validate_unified_tools(unified_tools)
|
|
62
|
+
body: dict[str, Any] = {
|
|
63
|
+
"name": name,
|
|
64
|
+
"system_prompt": system_prompt,
|
|
65
|
+
"unified_tools": validated_tools,
|
|
66
|
+
"primary_object_ids": primary_object_ids or [],
|
|
67
|
+
"attachment_ids": attachment_ids or [],
|
|
68
|
+
"use_search": use_search,
|
|
69
|
+
"version_note": version_note,
|
|
70
|
+
"suggested_questions": suggested_questions or [],
|
|
71
|
+
}
|
|
72
|
+
if description is not None:
|
|
73
|
+
body["description"] = description
|
|
74
|
+
if llm_model is not None:
|
|
75
|
+
body["llm_model"] = llm_model
|
|
76
|
+
return client.patch(f"/external/agents/{agent_id}", json=body)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get(client: CodeerClient, agent_id: str) -> dict:
|
|
80
|
+
return client.get(f"/external/agents/{agent_id}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_default(client: CodeerClient) -> dict:
|
|
84
|
+
"""Read whichever agent ``CODEER_AGENT_ID`` points to (project env).
|
|
85
|
+
|
|
86
|
+
Workspace and organization scope come from the API-key virtual user's
|
|
87
|
+
profile. Raises if no ``agent_id`` is in scope.
|
|
88
|
+
"""
|
|
89
|
+
if not client.agent_id:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"No agent_id in scope. Set CODEER_AGENT_ID in .claude/settings.json, "
|
|
92
|
+
"export it in your shell, or pass agent_id explicitly to from_env()."
|
|
93
|
+
)
|
|
94
|
+
return get(client, client.agent_id)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_latest_draft_history_id(client: CodeerClient, agent_id: str) -> Optional[str]:
|
|
98
|
+
"""Return the id of the most recent unpublished AgentHistory, or None.
|
|
99
|
+
|
|
100
|
+
"Most recent" = highest ``version_number`` among ``status == 'draft'``.
|
|
101
|
+
Returns ``None`` if every version has been published (so there's no
|
|
102
|
+
open draft to pin a test against).
|
|
103
|
+
"""
|
|
104
|
+
versions = list_versions(client, agent_id)
|
|
105
|
+
drafts = [v for v in versions if v.get("status") == "draft"]
|
|
106
|
+
if not drafts:
|
|
107
|
+
return None
|
|
108
|
+
drafts.sort(key=lambda v: v.get("version_number") or 0, reverse=True)
|
|
109
|
+
return drafts[0].get("id")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_latest_history_id(client: CodeerClient, agent_id: str) -> Optional[str]:
|
|
113
|
+
"""Return the id of the most recent AgentHistory, draft or published."""
|
|
114
|
+
versions = list_versions(client, agent_id)
|
|
115
|
+
if not versions:
|
|
116
|
+
return None
|
|
117
|
+
versions.sort(key=lambda v: v.get("version_number") or 0, reverse=True)
|
|
118
|
+
return versions[0].get("id")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def list_in_workspace(client: CodeerClient, workspace_id: str) -> list[dict]:
|
|
122
|
+
"""List **published** agents in a workspace (drafts are hidden).
|
|
123
|
+
|
|
124
|
+
If you need drafts too — which is almost always the case while iterating
|
|
125
|
+
on an agent — use :func:`list_all` with both workspace_id and organization_id.
|
|
126
|
+
"""
|
|
127
|
+
return client.get("/external/agents")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def list_all(
|
|
131
|
+
client: CodeerClient,
|
|
132
|
+
*,
|
|
133
|
+
workspace_id: str,
|
|
134
|
+
organization_id: str,
|
|
135
|
+
) -> list[dict]:
|
|
136
|
+
"""List every agent in a workspace including drafts.
|
|
137
|
+
|
|
138
|
+
Both IDs are required — ``GET /agents/all`` returns 400 ``Organization ID
|
|
139
|
+
is required`` if you omit ``oid``. Look up the org for a workspace via
|
|
140
|
+
``/accounts/me`` → ``profile.workspace_organization_map``.
|
|
141
|
+
"""
|
|
142
|
+
return client.get("/external/agents/all")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def list_versions(client: CodeerClient, agent_id: str) -> list[dict]:
|
|
146
|
+
return client.get(f"/external/agents/{agent_id}/versions")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_version(client: CodeerClient, agent_id: str, history_id: str) -> dict:
|
|
150
|
+
return client.get(f"/external/agents/{agent_id}/versions/{history_id}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_impact(client: CodeerClient, agent_id: str) -> dict:
|
|
154
|
+
"""List downstream agents that call this one. Call before publishing breaking changes."""
|
|
155
|
+
return client.get(f"/external/agents/{agent_id}/impact")
|
codeer_cli/chats.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Chat creation and SSE-streamed agent responses — the Live Test surface.
|
|
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
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Iterator, List, Optional
|
|
11
|
+
|
|
12
|
+
from .client import CodeerClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create(
|
|
16
|
+
client: CodeerClient,
|
|
17
|
+
*,
|
|
18
|
+
agent_id: str,
|
|
19
|
+
title: Optional[str] = None,
|
|
20
|
+
external_user_id: Optional[str] = None,
|
|
21
|
+
) -> dict:
|
|
22
|
+
body: dict[str, Any] = {"agent_id": agent_id}
|
|
23
|
+
if title is not None:
|
|
24
|
+
body["name"] = title
|
|
25
|
+
if external_user_id is not None:
|
|
26
|
+
body["external_user_id"] = external_user_id
|
|
27
|
+
return client.post("/chats", json=body)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def send_published_agent_message(
|
|
31
|
+
client: CodeerClient,
|
|
32
|
+
*,
|
|
33
|
+
chat_id: int,
|
|
34
|
+
message: str,
|
|
35
|
+
agent_id: str,
|
|
36
|
+
external_user_id: Optional[str] = None,
|
|
37
|
+
attachment_ids: Optional[List[str]] = None,
|
|
38
|
+
stream: bool = False,
|
|
39
|
+
) -> Iterator[dict] | dict:
|
|
40
|
+
"""Send a user message through the API-key external chat flow.
|
|
41
|
+
|
|
42
|
+
API-key chat endpoints use the agent's published version. They accept
|
|
43
|
+
``agent_id`` rather than ``agent_history_id``.
|
|
44
|
+
"""
|
|
45
|
+
body: dict[str, Any] = {"message": message, "agent_id": agent_id, "stream": stream}
|
|
46
|
+
if external_user_id is not None:
|
|
47
|
+
body["external_user_id"] = external_user_id
|
|
48
|
+
if attachment_ids:
|
|
49
|
+
body["attached_file_uuids"] = attachment_ids
|
|
50
|
+
|
|
51
|
+
path = f"/chats/{chat_id}/messages"
|
|
52
|
+
if stream:
|
|
53
|
+
return client.stream_sse("POST", path, json=body)
|
|
54
|
+
return client.post(path, json=body)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def send_message(
|
|
58
|
+
client: CodeerClient,
|
|
59
|
+
*,
|
|
60
|
+
chat_id: int,
|
|
61
|
+
message: str,
|
|
62
|
+
agent_history_id: str,
|
|
63
|
+
attachment_ids: Optional[List[str]] = None,
|
|
64
|
+
stream: bool = True,
|
|
65
|
+
) -> Iterator[dict] | dict:
|
|
66
|
+
"""Send a user message; yield SSE events (if stream=True) or return the final payload.
|
|
67
|
+
|
|
68
|
+
``agent_history_id`` is required — this is how you pin Live Test to a specific
|
|
69
|
+
(possibly unpublished) agent version without affecting production users.
|
|
70
|
+
"""
|
|
71
|
+
body: dict[str, Any] = {"message": message, "agent_history_id": agent_history_id}
|
|
72
|
+
if attachment_ids:
|
|
73
|
+
body["attachment_ids"] = attachment_ids
|
|
74
|
+
|
|
75
|
+
path = f"/chats/{chat_id}/messages"
|
|
76
|
+
if stream:
|
|
77
|
+
return client.stream_sse("POST", path, json=body)
|
|
78
|
+
return client.post(path, json=body)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def list_messages(client: CodeerClient, chat_id: int) -> list[dict]:
|
|
82
|
+
return client.get(f"/chats/{chat_id}/messages")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def list_chats(client: CodeerClient) -> list[dict]:
|
|
86
|
+
return client.get("/chats")
|
|
87
|
+
|
codeer_cli/cli.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Codeer CLI — unified interface for agent lifecycle operations.
|
|
2
|
+
|
|
3
|
+
codeer check
|
|
4
|
+
codeer agent list|get|apply|diff|versions
|
|
5
|
+
codeer kb list|files|upload
|
|
6
|
+
codeer eval list|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
|
|
7
|
+
codeer history list|get|conversations|negative-feedback
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from .client import AuthError, CodeerClient, CodeerError
|
|
17
|
+
from .commands import check
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="codeer")
|
|
22
|
+
sub = parser.add_subparsers(dest="group")
|
|
23
|
+
|
|
24
|
+
check.register(sub)
|
|
25
|
+
|
|
26
|
+
# Phase 2-4: agent, kb, eval commands will register here
|
|
27
|
+
try:
|
|
28
|
+
from .commands import agent as agent_cmd
|
|
29
|
+
agent_cmd.register(sub)
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from .commands import kb as kb_cmd
|
|
35
|
+
kb_cmd.register(sub)
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from .commands import eval_cmd
|
|
41
|
+
eval_cmd.register(sub)
|
|
42
|
+
except ImportError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from .commands import history as history_cmd
|
|
47
|
+
history_cmd.register(sub)
|
|
48
|
+
except ImportError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from .commands import profile as profile_cmd
|
|
53
|
+
profile_cmd.register(sub)
|
|
54
|
+
except ImportError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
args = parser.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
if not hasattr(args, "func"):
|
|
60
|
+
parser.print_help()
|
|
61
|
+
return 2
|
|
62
|
+
|
|
63
|
+
if getattr(args, "no_client", False):
|
|
64
|
+
client = None
|
|
65
|
+
else:
|
|
66
|
+
try:
|
|
67
|
+
client = CodeerClient.from_env()
|
|
68
|
+
except AuthError as e:
|
|
69
|
+
if args.group == "check":
|
|
70
|
+
print(f"FAIL Auth: {e}", file=sys.stderr)
|
|
71
|
+
print(" Configure CODEER_API_KEY or a codeer profile", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
print(f"auth error: {e}", file=sys.stderr)
|
|
74
|
+
return 2
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
return args.func(args, client)
|
|
78
|
+
except AuthError as e:
|
|
79
|
+
print(f"auth: {e}", file=sys.stderr)
|
|
80
|
+
return 3
|
|
81
|
+
except CodeerError as e:
|
|
82
|
+
print(f"error: {e}", file=sys.stderr)
|
|
83
|
+
if e.body:
|
|
84
|
+
print(json.dumps(e.body, ensure_ascii=False, indent=2, default=str), file=sys.stderr)
|
|
85
|
+
return 1
|
|
86
|
+
finally:
|
|
87
|
+
if client is not None:
|
|
88
|
+
client.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
raise SystemExit(main())
|