maitai-cli 0.1.1__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.
- maitai_cli/__init__.py +3 -0
- maitai_cli/__main__.py +6 -0
- maitai_cli/api.py +90 -0
- maitai_cli/commands_generated.py +457 -0
- maitai_cli/config.py +74 -0
- maitai_cli/main.py +140 -0
- maitai_cli/utils.py +46 -0
- maitai_cli-0.1.1.dist-info/METADATA +250 -0
- maitai_cli-0.1.1.dist-info/RECORD +12 -0
- maitai_cli-0.1.1.dist-info/WHEEL +5 -0
- maitai_cli-0.1.1.dist-info/entry_points.txt +2 -0
- maitai_cli-0.1.1.dist-info/top_level.txt +1 -0
maitai_cli/__init__.py
ADDED
maitai_cli/__main__.py
ADDED
maitai_cli/api.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API client for the Maitai Platform Developer API (api/v1).
|
|
3
|
+
|
|
4
|
+
API documentation: docs/internal and docs/external in the maitai-mono repo.
|
|
5
|
+
Base URL: https://api.trymaitai.ai/api/v1
|
|
6
|
+
Authentication: X-Maitai-Api-Key header (create keys in Portal → Settings → API Keys).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from maitai_cli.config import get_api_key, get_base_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class APIError(Exception):
|
|
16
|
+
"""Raised when an API request fails."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, status_code: int, message: str, details: dict | None = None):
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.message = message
|
|
21
|
+
self.details = details or {}
|
|
22
|
+
super().__init__(f"{status_code}: {message}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIClient:
|
|
26
|
+
"""HTTP client for the Maitai Developer API."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
29
|
+
self.api_key = api_key or get_api_key()
|
|
30
|
+
self.base_url = (base_url or get_base_url()).rstrip("/")
|
|
31
|
+
self._client: httpx.Client | None = None
|
|
32
|
+
|
|
33
|
+
def _get_client(self) -> httpx.Client:
|
|
34
|
+
if self._client is None:
|
|
35
|
+
headers = {}
|
|
36
|
+
if self.api_key:
|
|
37
|
+
headers["X-Maitai-Api-Key"] = self.api_key
|
|
38
|
+
self._client = httpx.Client(
|
|
39
|
+
base_url=self.base_url,
|
|
40
|
+
headers=headers,
|
|
41
|
+
timeout=30.0,
|
|
42
|
+
)
|
|
43
|
+
return self._client
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
"""Close the HTTP client."""
|
|
47
|
+
if self._client:
|
|
48
|
+
self._client.close()
|
|
49
|
+
self._client = None
|
|
50
|
+
|
|
51
|
+
def _request(
|
|
52
|
+
self,
|
|
53
|
+
method: str,
|
|
54
|
+
path: str,
|
|
55
|
+
*,
|
|
56
|
+
params: dict | None = None,
|
|
57
|
+
json: dict | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Make an API request and return the parsed JSON response."""
|
|
60
|
+
if not self.api_key:
|
|
61
|
+
raise APIError(
|
|
62
|
+
401, "Not authenticated. Run 'maitai login' or set MAITAI_API_KEY."
|
|
63
|
+
)
|
|
64
|
+
client = self._get_client()
|
|
65
|
+
path = path if path.startswith("/") else f"/{path}"
|
|
66
|
+
r = client.request(method, path, params=params, json=json)
|
|
67
|
+
try:
|
|
68
|
+
body = r.json()
|
|
69
|
+
except Exception:
|
|
70
|
+
body = {}
|
|
71
|
+
if r.status_code >= 400:
|
|
72
|
+
err = body.get("error", {})
|
|
73
|
+
raise APIError(
|
|
74
|
+
r.status_code,
|
|
75
|
+
err.get("message", r.text or str(r.status_code)),
|
|
76
|
+
err.get("details"),
|
|
77
|
+
)
|
|
78
|
+
return body
|
|
79
|
+
|
|
80
|
+
def get(self, path: str, params: dict | None = None) -> dict[str, Any]:
|
|
81
|
+
return self._request("GET", path, params=params)
|
|
82
|
+
|
|
83
|
+
def post(self, path: str, json: dict | None = None) -> dict[str, Any]:
|
|
84
|
+
return self._request("POST", path, json=json)
|
|
85
|
+
|
|
86
|
+
def put(self, path: str, json: dict | None = None) -> dict[str, Any]:
|
|
87
|
+
return self._request("PUT", path, json=json)
|
|
88
|
+
|
|
89
|
+
def delete(self, path: str) -> dict[str, Any]:
|
|
90
|
+
return self._request("DELETE", path)
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Generated CLI commands from OpenAPI spec. Do not edit by hand."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from maitai_cli.utils import console, get_client, handle_api_errors, print_json
|
|
8
|
+
|
|
9
|
+
@handle_api_errors
|
|
10
|
+
def _agents(
|
|
11
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List agents"),
|
|
12
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
13
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
14
|
+
update_id: Optional[int] = typer.Option(None, "--update", "-u", help="Update by ID"),
|
|
15
|
+
delete_id: Optional[int] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
16
|
+
include_subagents: Optional[bool] = typer.Option(None, "--include-subagents", help="Include sub-agents in the response."),
|
|
17
|
+
include_actions: Optional[bool] = typer.Option(None, "--include-actions", help="Include agent actions in the response."),
|
|
18
|
+
detailed: Optional[bool] = typer.Option(None, "--detailed", help="Include full configuration details in results."),
|
|
19
|
+
page: Optional[int] = typer.Option(None, "--page", help="Page number (1-indexed) for page-based pagination."),
|
|
20
|
+
page_size: Optional[int] = typer.Option(None, "--page-size", help="Number of items per page."),
|
|
21
|
+
version: Optional[str] = typer.Option(None, "--version", help="Filter by version string."),
|
|
22
|
+
):
|
|
23
|
+
"""/agents - generated from OpenAPI."""
|
|
24
|
+
client = get_client()
|
|
25
|
+
params: dict = {}
|
|
26
|
+
if include_subagents is not None:
|
|
27
|
+
params["include_subagents"] = include_subagents
|
|
28
|
+
if include_actions is not None:
|
|
29
|
+
params["include_actions"] = include_actions
|
|
30
|
+
if detailed is not None:
|
|
31
|
+
params["detailed"] = detailed
|
|
32
|
+
if page is not None:
|
|
33
|
+
params["page"] = page
|
|
34
|
+
if page_size is not None:
|
|
35
|
+
params["page_size"] = page_size
|
|
36
|
+
if version is not None:
|
|
37
|
+
params["version"] = version
|
|
38
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
39
|
+
r = client.get("/agents", params=params)
|
|
40
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
41
|
+
return
|
|
42
|
+
if get_id is not None:
|
|
43
|
+
r = client.get(f"/agents/{get_id}", params=params)
|
|
44
|
+
print_json(r.get("data"))
|
|
45
|
+
return
|
|
46
|
+
if create_body:
|
|
47
|
+
import json
|
|
48
|
+
body = json.loads(create_body)
|
|
49
|
+
r = client.post("/agents", json=body)
|
|
50
|
+
print_json(r.get("data"))
|
|
51
|
+
return
|
|
52
|
+
if update_id is not None:
|
|
53
|
+
import json
|
|
54
|
+
body = json.loads(typer.prompt("JSON body for update") or "{}")
|
|
55
|
+
r = client.put(f"/agents/{update_id}", json=body)
|
|
56
|
+
print_json(r.get("data"))
|
|
57
|
+
return
|
|
58
|
+
if delete_id is not None:
|
|
59
|
+
client.delete(f"/agents/{delete_id}")
|
|
60
|
+
console.print("[green]Deleted.[/green]")
|
|
61
|
+
return
|
|
62
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
63
|
+
|
|
64
|
+
@handle_api_errors
|
|
65
|
+
def _applications(
|
|
66
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List applications"),
|
|
67
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
68
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
69
|
+
update_id: Optional[int] = typer.Option(None, "--update", "-u", help="Update by ID"),
|
|
70
|
+
delete_id: Optional[int] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
71
|
+
detailed: bool = typer.Option(False, "--detailed", help="Include full config"),
|
|
72
|
+
):
|
|
73
|
+
"""/applications - generated from OpenAPI."""
|
|
74
|
+
client = get_client()
|
|
75
|
+
params: dict = {}
|
|
76
|
+
if detailed is not None:
|
|
77
|
+
params["detailed"] = detailed
|
|
78
|
+
if detailed:
|
|
79
|
+
params["detailed"] = "true"
|
|
80
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
81
|
+
r = client.get("/applications", params=params)
|
|
82
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
83
|
+
return
|
|
84
|
+
if get_id is not None:
|
|
85
|
+
r = client.get(f"/applications/{get_id}", params=params)
|
|
86
|
+
print_json(r.get("data"))
|
|
87
|
+
return
|
|
88
|
+
if create_body:
|
|
89
|
+
import json
|
|
90
|
+
body = json.loads(create_body)
|
|
91
|
+
r = client.post("/applications", json=body)
|
|
92
|
+
print_json(r.get("data"))
|
|
93
|
+
return
|
|
94
|
+
if update_id is not None:
|
|
95
|
+
import json
|
|
96
|
+
body = json.loads(typer.prompt("JSON body for update") or "{}")
|
|
97
|
+
r = client.put(f"/applications/{update_id}", json=body)
|
|
98
|
+
print_json(r.get("data"))
|
|
99
|
+
return
|
|
100
|
+
if delete_id is not None:
|
|
101
|
+
client.delete(f"/applications/{delete_id}")
|
|
102
|
+
console.print("[green]Deleted.[/green]")
|
|
103
|
+
return
|
|
104
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
105
|
+
|
|
106
|
+
@handle_api_errors
|
|
107
|
+
def _conversation_trees(
|
|
108
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List conversation trees"),
|
|
109
|
+
get_id: Optional[str] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
110
|
+
delete_id: Optional[str] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
111
|
+
application_id: Optional[int] = typer.Option(None, "--application-id", help="Filter by application ID."),
|
|
112
|
+
):
|
|
113
|
+
"""/conversation-trees - generated from OpenAPI."""
|
|
114
|
+
client = get_client()
|
|
115
|
+
params: dict = {}
|
|
116
|
+
if application_id is not None:
|
|
117
|
+
params["application_id"] = application_id
|
|
118
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
119
|
+
r = client.get("/conversation-trees", params=params)
|
|
120
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
121
|
+
return
|
|
122
|
+
if get_id is not None:
|
|
123
|
+
r = client.get(f"/conversation-trees/{get_id}", params=params)
|
|
124
|
+
print_json(r.get("data"))
|
|
125
|
+
return
|
|
126
|
+
if delete_id is not None:
|
|
127
|
+
client.delete(f"/conversation-trees/{delete_id}")
|
|
128
|
+
console.print("[green]Deleted.[/green]")
|
|
129
|
+
return
|
|
130
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
131
|
+
|
|
132
|
+
@handle_api_errors
|
|
133
|
+
def _datasets(
|
|
134
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List datasets"),
|
|
135
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
136
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
137
|
+
update_id: Optional[int] = typer.Option(None, "--update", "-u", help="Update by ID"),
|
|
138
|
+
delete_id: Optional[int] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
139
|
+
):
|
|
140
|
+
"""/datasets - generated from OpenAPI."""
|
|
141
|
+
client = get_client()
|
|
142
|
+
params: dict = {}
|
|
143
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
144
|
+
r = client.get("/datasets", params=params)
|
|
145
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
146
|
+
return
|
|
147
|
+
if get_id is not None:
|
|
148
|
+
r = client.get(f"/datasets/{get_id}", params=params)
|
|
149
|
+
print_json(r.get("data"))
|
|
150
|
+
return
|
|
151
|
+
if create_body:
|
|
152
|
+
import json
|
|
153
|
+
body = json.loads(create_body)
|
|
154
|
+
r = client.post("/datasets", json=body)
|
|
155
|
+
print_json(r.get("data"))
|
|
156
|
+
return
|
|
157
|
+
if update_id is not None:
|
|
158
|
+
import json
|
|
159
|
+
body = json.loads(typer.prompt("JSON body for update") or "{}")
|
|
160
|
+
r = client.put(f"/datasets/{update_id}", json=body)
|
|
161
|
+
print_json(r.get("data"))
|
|
162
|
+
return
|
|
163
|
+
if delete_id is not None:
|
|
164
|
+
client.delete(f"/datasets/{delete_id}")
|
|
165
|
+
console.print("[green]Deleted.[/green]")
|
|
166
|
+
return
|
|
167
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
168
|
+
|
|
169
|
+
@handle_api_errors
|
|
170
|
+
def _evaluations(
|
|
171
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List evaluations"),
|
|
172
|
+
get_id: Optional[str] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
173
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
174
|
+
full: Optional[bool] = typer.Option(None, "--full", help="Include full evaluation details."),
|
|
175
|
+
):
|
|
176
|
+
"""/evaluations - generated from OpenAPI."""
|
|
177
|
+
client = get_client()
|
|
178
|
+
params: dict = {}
|
|
179
|
+
if full is not None:
|
|
180
|
+
params["full"] = full
|
|
181
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
182
|
+
r = client.get("/evaluations", params=params)
|
|
183
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
184
|
+
return
|
|
185
|
+
if get_id is not None:
|
|
186
|
+
r = client.get(f"/evaluations/{get_id}", params=params)
|
|
187
|
+
print_json(r.get("data"))
|
|
188
|
+
return
|
|
189
|
+
if create_body:
|
|
190
|
+
import json
|
|
191
|
+
body = json.loads(create_body)
|
|
192
|
+
r = client.post("/evaluations", json=body)
|
|
193
|
+
print_json(r.get("data"))
|
|
194
|
+
return
|
|
195
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
196
|
+
|
|
197
|
+
@handle_api_errors
|
|
198
|
+
def _finetune_runs(
|
|
199
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List finetune runs"),
|
|
200
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
201
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
202
|
+
status: Optional[str] = typer.Option(None, "--status", help="Filter by status (can be specified multiple times)"),
|
|
203
|
+
):
|
|
204
|
+
"""/finetune-runs - generated from OpenAPI."""
|
|
205
|
+
client = get_client()
|
|
206
|
+
params: dict = {}
|
|
207
|
+
if status is not None:
|
|
208
|
+
params["status"] = status
|
|
209
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
210
|
+
r = client.get("/finetune-runs", params=params)
|
|
211
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
212
|
+
return
|
|
213
|
+
if get_id is not None:
|
|
214
|
+
r = client.get(f"/finetune-runs/{get_id}", params=params)
|
|
215
|
+
print_json(r.get("data"))
|
|
216
|
+
return
|
|
217
|
+
if create_body:
|
|
218
|
+
import json
|
|
219
|
+
body = json.loads(create_body)
|
|
220
|
+
r = client.post("/finetune-runs", json=body)
|
|
221
|
+
print_json(r.get("data"))
|
|
222
|
+
return
|
|
223
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
224
|
+
|
|
225
|
+
@handle_api_errors
|
|
226
|
+
def _intent_groups(
|
|
227
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List intent groups"),
|
|
228
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
229
|
+
detailed: Optional[bool] = typer.Option(None, "--detailed", help="Include full configuration details in results."),
|
|
230
|
+
start_date: Optional[int] = typer.Option(None, "--start-date", help="Start of date range as Unix timestamp (millisecond"),
|
|
231
|
+
end_date: Optional[int] = typer.Option(None, "--end-date", help="End of date range as Unix timestamp (milliseconds)"),
|
|
232
|
+
include_messages: Optional[bool] = typer.Option(None, "--include-messages", help="Include full message history in results."),
|
|
233
|
+
):
|
|
234
|
+
"""/intent-groups - generated from OpenAPI."""
|
|
235
|
+
client = get_client()
|
|
236
|
+
params: dict = {}
|
|
237
|
+
if detailed is not None:
|
|
238
|
+
params["detailed"] = detailed
|
|
239
|
+
if start_date is not None:
|
|
240
|
+
params["start_date"] = start_date
|
|
241
|
+
if end_date is not None:
|
|
242
|
+
params["end_date"] = end_date
|
|
243
|
+
if include_messages is not None:
|
|
244
|
+
params["include_messages"] = include_messages
|
|
245
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
246
|
+
r = client.get("/intent-groups", params=params)
|
|
247
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
248
|
+
return
|
|
249
|
+
if get_id is not None:
|
|
250
|
+
r = client.get(f"/intent-groups/{get_id}", params=params)
|
|
251
|
+
print_json(r.get("data"))
|
|
252
|
+
return
|
|
253
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
254
|
+
|
|
255
|
+
@handle_api_errors
|
|
256
|
+
def _models(
|
|
257
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List models"),
|
|
258
|
+
model_type: Optional[str] = typer.Option(None, "--model-type", help="Filter by model type (can be specified multiple ti"),
|
|
259
|
+
available: bool = typer.Option(False, "--available", "-a", help="List available base models"),
|
|
260
|
+
):
|
|
261
|
+
"""/models - generated from OpenAPI."""
|
|
262
|
+
client = get_client()
|
|
263
|
+
params: dict = {}
|
|
264
|
+
if model_type is not None:
|
|
265
|
+
params["model_type"] = model_type
|
|
266
|
+
if available:
|
|
267
|
+
r = client.get("/models/available")
|
|
268
|
+
print_json(r.get("data", []))
|
|
269
|
+
return
|
|
270
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
271
|
+
r = client.get("/models", params=params)
|
|
272
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
273
|
+
return
|
|
274
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
275
|
+
|
|
276
|
+
@handle_api_errors
|
|
277
|
+
def _requests(
|
|
278
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List requests"),
|
|
279
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
280
|
+
application_id: Optional[int] = typer.Option(None, "--application-id", help="Filter by application ID."),
|
|
281
|
+
intent_id: Optional[int] = typer.Option(None, "--intent-id", help="Filter by intent ID."),
|
|
282
|
+
start_date: Optional[int] = typer.Option(None, "--start-date", help="Start of date range as Unix timestamp (millisecond"),
|
|
283
|
+
end_date: Optional[int] = typer.Option(None, "--end-date", help="End of date range as Unix timestamp (milliseconds)"),
|
|
284
|
+
include_messages: Optional[bool] = typer.Option(None, "--include-messages", help="Include full message history in results."),
|
|
285
|
+
):
|
|
286
|
+
"""/requests - generated from OpenAPI."""
|
|
287
|
+
client = get_client()
|
|
288
|
+
params: dict = {}
|
|
289
|
+
if application_id is not None:
|
|
290
|
+
params["application_id"] = application_id
|
|
291
|
+
if intent_id is not None:
|
|
292
|
+
params["intent_id"] = intent_id
|
|
293
|
+
if start_date is not None:
|
|
294
|
+
params["start_date"] = start_date
|
|
295
|
+
if end_date is not None:
|
|
296
|
+
params["end_date"] = end_date
|
|
297
|
+
if include_messages is not None:
|
|
298
|
+
params["include_messages"] = include_messages
|
|
299
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
300
|
+
r = client.get("/requests", params=params)
|
|
301
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
302
|
+
return
|
|
303
|
+
if get_id is not None:
|
|
304
|
+
r = client.get(f"/requests/{get_id}", params=params)
|
|
305
|
+
print_json(r.get("data"))
|
|
306
|
+
return
|
|
307
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
308
|
+
|
|
309
|
+
@handle_api_errors
|
|
310
|
+
def _sentinels(
|
|
311
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List sentinels"),
|
|
312
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
313
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
314
|
+
update_id: Optional[int] = typer.Option(None, "--update", "-u", help="Update by ID"),
|
|
315
|
+
delete_id: Optional[int] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
316
|
+
):
|
|
317
|
+
"""/sentinels - generated from OpenAPI."""
|
|
318
|
+
client = get_client()
|
|
319
|
+
params: dict = {}
|
|
320
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
321
|
+
r = client.get("/sentinels", params=params)
|
|
322
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
323
|
+
return
|
|
324
|
+
if get_id is not None:
|
|
325
|
+
r = client.get(f"/sentinels/{get_id}", params=params)
|
|
326
|
+
print_json(r.get("data"))
|
|
327
|
+
return
|
|
328
|
+
if create_body:
|
|
329
|
+
import json
|
|
330
|
+
body = json.loads(create_body)
|
|
331
|
+
r = client.post("/sentinels", json=body)
|
|
332
|
+
print_json(r.get("data"))
|
|
333
|
+
return
|
|
334
|
+
if update_id is not None:
|
|
335
|
+
import json
|
|
336
|
+
body = json.loads(typer.prompt("JSON body for update") or "{}")
|
|
337
|
+
r = client.put(f"/sentinels/{update_id}", json=body)
|
|
338
|
+
print_json(r.get("data"))
|
|
339
|
+
return
|
|
340
|
+
if delete_id is not None:
|
|
341
|
+
client.delete(f"/sentinels/{delete_id}")
|
|
342
|
+
console.print("[green]Deleted.[/green]")
|
|
343
|
+
return
|
|
344
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
345
|
+
|
|
346
|
+
@handle_api_errors
|
|
347
|
+
def _sessions(
|
|
348
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List sessions"),
|
|
349
|
+
get_id: Optional[str] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
350
|
+
application_id: Optional[int] = typer.Option(None, "--application-id", help="Filter by application ID."),
|
|
351
|
+
):
|
|
352
|
+
"""/sessions - generated from OpenAPI."""
|
|
353
|
+
client = get_client()
|
|
354
|
+
params: dict = {}
|
|
355
|
+
if application_id is not None:
|
|
356
|
+
params["application_id"] = application_id
|
|
357
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
358
|
+
r = client.get("/sessions", params=params)
|
|
359
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
360
|
+
return
|
|
361
|
+
if get_id is not None:
|
|
362
|
+
r = client.get(f"/sessions/{get_id}", params=params)
|
|
363
|
+
print_json(r.get("data"))
|
|
364
|
+
return
|
|
365
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
366
|
+
|
|
367
|
+
@handle_api_errors
|
|
368
|
+
def _test_runs(
|
|
369
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List test runs"),
|
|
370
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
371
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
372
|
+
delete_id: Optional[int] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
373
|
+
intent_group_id: Optional[int] = typer.Option(None, "--intent-group-id", help="intent_group_id"),
|
|
374
|
+
):
|
|
375
|
+
"""/test-runs - generated from OpenAPI."""
|
|
376
|
+
client = get_client()
|
|
377
|
+
params: dict = {}
|
|
378
|
+
if intent_group_id is not None:
|
|
379
|
+
params["intent_group_id"] = intent_group_id
|
|
380
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
381
|
+
r = client.get("/test-runs", params=params)
|
|
382
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
383
|
+
return
|
|
384
|
+
if get_id is not None:
|
|
385
|
+
r = client.get(f"/test-runs/{get_id}", params=params)
|
|
386
|
+
print_json(r.get("data"))
|
|
387
|
+
return
|
|
388
|
+
if create_body:
|
|
389
|
+
import json
|
|
390
|
+
body = json.loads(create_body)
|
|
391
|
+
r = client.post("/test-runs", json=body)
|
|
392
|
+
print_json(r.get("data"))
|
|
393
|
+
return
|
|
394
|
+
if delete_id is not None:
|
|
395
|
+
client.delete(f"/test-runs/{delete_id}")
|
|
396
|
+
console.print("[green]Deleted.[/green]")
|
|
397
|
+
return
|
|
398
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
399
|
+
|
|
400
|
+
@handle_api_errors
|
|
401
|
+
def _test_sets(
|
|
402
|
+
list_cmd: bool = typer.Option(False, "--list", "-l", help="List test sets"),
|
|
403
|
+
get_id: Optional[int] = typer.Option(None, "--get", "-g", help="Get by ID"),
|
|
404
|
+
create_body: Optional[str] = typer.Option(None, "--create", "-c", help="Create (JSON body)"),
|
|
405
|
+
update_id: Optional[int] = typer.Option(None, "--update", "-u", help="Update by ID"),
|
|
406
|
+
delete_id: Optional[int] = typer.Option(None, "--delete", "-d", help="Delete by ID"),
|
|
407
|
+
start_date: Optional[int] = typer.Option(None, "--start-date", help="Start of date range as Unix timestamp (millisecond"),
|
|
408
|
+
end_date: Optional[int] = typer.Option(None, "--end-date", help="End of date range as Unix timestamp (milliseconds)"),
|
|
409
|
+
):
|
|
410
|
+
"""/test-sets - generated from OpenAPI."""
|
|
411
|
+
client = get_client()
|
|
412
|
+
params: dict = {}
|
|
413
|
+
if start_date is not None:
|
|
414
|
+
params["start_date"] = start_date
|
|
415
|
+
if end_date is not None:
|
|
416
|
+
params["end_date"] = end_date
|
|
417
|
+
if list_cmd or (not get_id and not create_body and not update_id and not delete_id):
|
|
418
|
+
r = client.get("/test-sets", params=params)
|
|
419
|
+
print_json(r.get("data", []), r.get("pagination"))
|
|
420
|
+
return
|
|
421
|
+
if get_id is not None:
|
|
422
|
+
r = client.get(f"/test-sets/{get_id}", params=params)
|
|
423
|
+
print_json(r.get("data"))
|
|
424
|
+
return
|
|
425
|
+
if create_body:
|
|
426
|
+
import json
|
|
427
|
+
body = json.loads(create_body)
|
|
428
|
+
r = client.post("/test-sets", json=body)
|
|
429
|
+
print_json(r.get("data"))
|
|
430
|
+
return
|
|
431
|
+
if update_id is not None:
|
|
432
|
+
import json
|
|
433
|
+
body = json.loads(typer.prompt("JSON body for update") or "{}")
|
|
434
|
+
r = client.put(f"/test-sets/{update_id}", json=body)
|
|
435
|
+
print_json(r.get("data"))
|
|
436
|
+
return
|
|
437
|
+
if delete_id is not None:
|
|
438
|
+
client.delete(f"/test-sets/{delete_id}")
|
|
439
|
+
console.print("[green]Deleted.[/green]")
|
|
440
|
+
return
|
|
441
|
+
typer.echo("Use --list, --get ID, --create '{\"key\":\"val\"}', --update ID, or --delete ID")
|
|
442
|
+
|
|
443
|
+
def register_commands(typer_app: typer.Typer) -> None:
|
|
444
|
+
"""Register all generated commands with the Typer app."""
|
|
445
|
+
typer_app.command(name="agents")(_agents)
|
|
446
|
+
typer_app.command(name="applications")(_applications)
|
|
447
|
+
typer_app.command(name="conversation-trees")(_conversation_trees)
|
|
448
|
+
typer_app.command(name="datasets")(_datasets)
|
|
449
|
+
typer_app.command(name="evaluations")(_evaluations)
|
|
450
|
+
typer_app.command(name="finetune-runs")(_finetune_runs)
|
|
451
|
+
typer_app.command(name="intent-groups")(_intent_groups)
|
|
452
|
+
typer_app.command(name="models")(_models)
|
|
453
|
+
typer_app.command(name="requests")(_requests)
|
|
454
|
+
typer_app.command(name="sentinels")(_sentinels)
|
|
455
|
+
typer_app.command(name="sessions")(_sessions)
|
|
456
|
+
typer_app.command(name="test-runs")(_test_runs)
|
|
457
|
+
typer_app.command(name="test-sets")(_test_sets)
|
maitai_cli/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Configuration and authentication for the Maitai CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = Path.home() / ".maitai"
|
|
7
|
+
CONFIG_FILE = CONFIG_DIR / "config"
|
|
8
|
+
API_KEY_ENV = "MAITAI_API_KEY"
|
|
9
|
+
BASE_URL_ENV = "MAITAI_BASE_URL"
|
|
10
|
+
DEFAULT_BASE_URL = "https://api.trymaitai.ai/api/v1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_config_dir() -> Path:
|
|
14
|
+
"""Return the config directory, creating it if needed."""
|
|
15
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
return CONFIG_DIR
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_api_key() -> str | None:
|
|
20
|
+
"""Get API key from environment or config file."""
|
|
21
|
+
key = os.environ.get(API_KEY_ENV)
|
|
22
|
+
if key:
|
|
23
|
+
return key.strip()
|
|
24
|
+
if CONFIG_FILE.exists():
|
|
25
|
+
try:
|
|
26
|
+
content = CONFIG_FILE.read_text().strip()
|
|
27
|
+
for line in content.splitlines():
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if line.startswith("api_key="):
|
|
30
|
+
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
31
|
+
if line.startswith("MAITAI_API_KEY="):
|
|
32
|
+
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
33
|
+
except OSError:
|
|
34
|
+
pass
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_base_url() -> str:
|
|
39
|
+
"""Get base URL from environment or config file."""
|
|
40
|
+
url = os.environ.get(BASE_URL_ENV)
|
|
41
|
+
if url:
|
|
42
|
+
return url.strip().rstrip("/")
|
|
43
|
+
if CONFIG_FILE.exists():
|
|
44
|
+
try:
|
|
45
|
+
content = CONFIG_FILE.read_text().strip()
|
|
46
|
+
for line in content.splitlines():
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if line.startswith("base_url="):
|
|
49
|
+
return (
|
|
50
|
+
line.split("=", 1)[1].strip().strip('"').strip("'").rstrip("/")
|
|
51
|
+
)
|
|
52
|
+
if line.startswith("MAITAI_BASE_URL="):
|
|
53
|
+
return (
|
|
54
|
+
line.split("=", 1)[1].strip().strip('"').strip("'").rstrip("/")
|
|
55
|
+
)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
return DEFAULT_BASE_URL
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def save_config(api_key: str, base_url: str | None = None) -> None:
|
|
62
|
+
"""Save API key and optional base URL to config file."""
|
|
63
|
+
get_config_dir()
|
|
64
|
+
url = base_url or get_base_url()
|
|
65
|
+
content = f"api_key={api_key}\nbase_url={url}\n"
|
|
66
|
+
CONFIG_FILE.write_text(content)
|
|
67
|
+
# Restrict permissions
|
|
68
|
+
os.chmod(CONFIG_FILE, 0o600)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def clear_config() -> None:
|
|
72
|
+
"""Remove stored credentials."""
|
|
73
|
+
if CONFIG_FILE.exists():
|
|
74
|
+
CONFIG_FILE.unlink()
|
maitai_cli/main.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Maitai CLI - Command-line interface for the Maitai Platform Developer API.
|
|
3
|
+
|
|
4
|
+
API docs: See docs/internal and docs/external in the maitai-mono repository.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
# Import generated commands and register them
|
|
12
|
+
from maitai_cli.commands_generated import register_commands
|
|
13
|
+
from maitai_cli.config import clear_config, get_api_key, get_base_url, save_config
|
|
14
|
+
from maitai_cli.utils import console, get_client, handle_api_errors, print_json
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="maitai",
|
|
19
|
+
help="Maitai Platform Developer API CLI (api/v1). API docs: docs/internal and docs/external in maitai-mono. Create API keys at https://portal.trymaitai.com → Settings → API Keys.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Register generated resource commands
|
|
24
|
+
register_commands(app)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ─── Auth (hand-written) ────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def login(
|
|
32
|
+
api_key: Optional[str] = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--api-key",
|
|
35
|
+
"-k",
|
|
36
|
+
help="Maitai API key (or set MAITAI_API_KEY)",
|
|
37
|
+
),
|
|
38
|
+
base_url: Optional[str] = typer.Option(
|
|
39
|
+
None,
|
|
40
|
+
"--base-url",
|
|
41
|
+
"-u",
|
|
42
|
+
help="API base URL (default: https://api.trymaitai.ai/api/v1)",
|
|
43
|
+
),
|
|
44
|
+
):
|
|
45
|
+
"""Authenticate and store your API key for future commands."""
|
|
46
|
+
key = api_key or get_api_key()
|
|
47
|
+
if not key:
|
|
48
|
+
console.print(
|
|
49
|
+
"[yellow]Provide your API key via --api-key, or set MAITAI_API_KEY.[/yellow]"
|
|
50
|
+
)
|
|
51
|
+
console.print(
|
|
52
|
+
"Create keys at [link=https://portal.trymaitai.com]portal.trymaitai.com[/link] → Settings → API Keys"
|
|
53
|
+
)
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
save_config(key, base_url)
|
|
56
|
+
console.print("[green]✓ Authenticated. API key saved to ~/.maitai/config[/green]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command()
|
|
60
|
+
def logout():
|
|
61
|
+
"""Remove stored credentials."""
|
|
62
|
+
clear_config()
|
|
63
|
+
console.print("[green]✓ Logged out. Credentials removed.[/green]")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def docs():
|
|
68
|
+
"""Show API documentation references."""
|
|
69
|
+
console.print("[bold]Maitai Developer API (api/v1)[/bold]")
|
|
70
|
+
console.print()
|
|
71
|
+
console.print(" [link=https://docs.trymaitai.ai]External docs[/link]")
|
|
72
|
+
console.print(
|
|
73
|
+
" [link=https://docs.trymaitai.ai/api-reference]API Reference[/link]"
|
|
74
|
+
)
|
|
75
|
+
console.print(
|
|
76
|
+
" [link=https://portal.trymaitai.com]Portal (create API keys)[/link]"
|
|
77
|
+
)
|
|
78
|
+
console.print()
|
|
79
|
+
console.print("[dim]In maitai-mono repo:[/dim]")
|
|
80
|
+
console.print(" docs/external/openapi.yaml - OpenAPI spec")
|
|
81
|
+
console.print(" docs/external/api-reference/ - API reference pages")
|
|
82
|
+
console.print(" docs/internal/ - Internal design docs")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command()
|
|
86
|
+
def whoami():
|
|
87
|
+
"""Show current authentication status."""
|
|
88
|
+
key = get_api_key()
|
|
89
|
+
base_url = get_base_url()
|
|
90
|
+
if not key:
|
|
91
|
+
console.print("[red]Not authenticated.[/red]")
|
|
92
|
+
console.print("Run [bold]maitai login[/bold] or set MAITAI_API_KEY.")
|
|
93
|
+
raise typer.Exit(1)
|
|
94
|
+
console.print("[green]Authenticated[/green]")
|
|
95
|
+
console.print(f" Base URL: {base_url}")
|
|
96
|
+
console.print(f" API key: {key[:8]}...{key[-4:]}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ─── Raw API (hand-written) ──────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command("api")
|
|
103
|
+
@handle_api_errors
|
|
104
|
+
def raw_api(
|
|
105
|
+
method: str = typer.Argument("GET", help="HTTP method"),
|
|
106
|
+
path: str = typer.Argument(..., help="API path (e.g. /applications)"),
|
|
107
|
+
body: Optional[str] = typer.Option(
|
|
108
|
+
None, "--body", "-b", help="JSON body for POST/PUT"
|
|
109
|
+
),
|
|
110
|
+
):
|
|
111
|
+
"""Make a raw API request. Path is relative to base URL (e.g. /applications)."""
|
|
112
|
+
import json
|
|
113
|
+
|
|
114
|
+
client = get_client()
|
|
115
|
+
path = path if path.startswith("/") else f"/{path}"
|
|
116
|
+
json_body = json.loads(body) if body else None
|
|
117
|
+
if method.upper() == "GET":
|
|
118
|
+
r = client.get(path)
|
|
119
|
+
elif method.upper() == "POST":
|
|
120
|
+
r = client.post(path, json=json_body)
|
|
121
|
+
elif method.upper() == "PUT":
|
|
122
|
+
r = client.put(path, json=json_body)
|
|
123
|
+
elif method.upper() == "DELETE":
|
|
124
|
+
r = client.delete(path)
|
|
125
|
+
else:
|
|
126
|
+
console.print("[red]Unsupported method. Use GET, POST, PUT, or DELETE.[/red]")
|
|
127
|
+
raise typer.Exit(1)
|
|
128
|
+
print_json(r)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
"""Entry point."""
|
|
133
|
+
try:
|
|
134
|
+
app()
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
raise typer.Exit(130)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
maitai_cli/utils.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Shared utilities for Maitai CLI commands."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from maitai_cli.api import APIClient, APIError
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_client() -> APIClient:
|
|
14
|
+
"""Return an authenticated API client."""
|
|
15
|
+
return APIClient()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_json(data: Any, pagination: dict | None = None) -> None:
|
|
19
|
+
"""Pretty-print JSON with optional pagination info."""
|
|
20
|
+
import json
|
|
21
|
+
|
|
22
|
+
from rich.syntax import Syntax
|
|
23
|
+
|
|
24
|
+
text = json.dumps(data, indent=2, default=str)
|
|
25
|
+
if pagination:
|
|
26
|
+
p = pagination
|
|
27
|
+
console.print(
|
|
28
|
+
f"[dim]total={p.get('total', '?')} offset={p.get('offset', 0)} limit={p.get('limit', 25)}[/dim]"
|
|
29
|
+
)
|
|
30
|
+
console.print(Syntax(text, "json", theme="monokai", line_numbers=False))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_api_errors(f):
|
|
34
|
+
"""Decorator to catch APIError and print a clean message."""
|
|
35
|
+
|
|
36
|
+
@functools.wraps(f)
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
38
|
+
try:
|
|
39
|
+
return f(*args, **kwargs)
|
|
40
|
+
except APIError as e:
|
|
41
|
+
console.print(f"[red]API Error ({e.status_code}): {e.message}[/red]")
|
|
42
|
+
if e.status_code == 401:
|
|
43
|
+
console.print("Run [bold]maitai login[/bold] or set MAITAI_API_KEY.")
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
|
|
46
|
+
return wrapper
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maitai-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI for the Maitai Platform Developer API (api/v1)
|
|
5
|
+
Author-email: Maitai <support@trymaitai.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://docs.trymaitai.ai
|
|
8
|
+
Project-URL: API Reference, https://docs.trymaitai.ai/api-reference
|
|
9
|
+
Project-URL: Portal, https://portal.trymaitai.com
|
|
10
|
+
Keywords: maitai,cli,api,ai,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: typer>=0.9.0
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# Maitai CLI
|
|
30
|
+
|
|
31
|
+
Command-line interface for the [Maitai Platform Developer API](https://docs.trymaitai.ai/api-reference) (api/v1).
|
|
32
|
+
|
|
33
|
+
Resource commands (`applications`, `agents`, etc.) are **generated from the OpenAPI spec**. When the API changes, run `python scripts/generate_openapi.py` from the repo root to regenerate the CLI.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# From the maitai-mono repo (cli directory)
|
|
39
|
+
pip install .
|
|
40
|
+
|
|
41
|
+
# Or install in development mode
|
|
42
|
+
pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authenticate
|
|
46
|
+
|
|
47
|
+
Create an API key in the [Portal](https://portal.trymaitai.com) under **Settings → API Keys**, then:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Interactive: saves key to ~/.maitai/config
|
|
51
|
+
maitai login --api-key YOUR_MAITAI_API_KEY
|
|
52
|
+
|
|
53
|
+
# Or use environment variable (no login needed)
|
|
54
|
+
export MAITAI_API_KEY="YOUR_MAITAI_API_KEY"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For a custom base URL (e.g. local dev):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
maitai login --api-key YOUR_KEY --base-url http://localhost:5000/api/v1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Check auth status
|
|
67
|
+
maitai whoami
|
|
68
|
+
|
|
69
|
+
# List resources
|
|
70
|
+
maitai applications --list
|
|
71
|
+
maitai agents --list
|
|
72
|
+
maitai intent-groups --list
|
|
73
|
+
maitai sessions --list
|
|
74
|
+
maitai sentinels --list
|
|
75
|
+
maitai datasets --list
|
|
76
|
+
maitai test-sets --list
|
|
77
|
+
maitai test-runs --list
|
|
78
|
+
maitai models --list
|
|
79
|
+
maitai models --available
|
|
80
|
+
maitai evaluations --list
|
|
81
|
+
maitai finetune-runs --list
|
|
82
|
+
maitai conversation-trees --list
|
|
83
|
+
|
|
84
|
+
# Get by ID
|
|
85
|
+
maitai applications --get 42
|
|
86
|
+
maitai agents --get 1
|
|
87
|
+
maitai intent-groups --get 5
|
|
88
|
+
|
|
89
|
+
# Create application
|
|
90
|
+
maitai applications --create "My App:my-app"
|
|
91
|
+
|
|
92
|
+
# Delete application
|
|
93
|
+
maitai applications --delete 42
|
|
94
|
+
|
|
95
|
+
# Raw API calls (any endpoint)
|
|
96
|
+
maitai api GET /applications
|
|
97
|
+
maitai api GET /applications/42
|
|
98
|
+
maitai api POST /applications -b '{"application_name":"Test","application_ref_name":"test"}'
|
|
99
|
+
maitai api PUT /applications/42 -b '{"application_name":"Updated"}'
|
|
100
|
+
maitai api DELETE /applications/42
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
The CLI wraps the Maitai Developer API. Full documentation:
|
|
106
|
+
|
|
107
|
+
- **External docs**: [docs.trymaitai.ai](https://docs.trymaitai.ai)
|
|
108
|
+
- **OpenAPI spec**: `docs/external/openapi.yaml` in this repo
|
|
109
|
+
- **Internal docs**: `docs/internal/` in this repo
|
|
110
|
+
|
|
111
|
+
### Authentication
|
|
112
|
+
|
|
113
|
+
All requests use the `X-Maitai-Api-Key` header. Create keys at [portal.trymaitai.com](https://portal.trymaitai.com) → Settings → API Keys.
|
|
114
|
+
|
|
115
|
+
### Base URL
|
|
116
|
+
|
|
117
|
+
- Production: `https://api.trymaitai.ai/api/v1`
|
|
118
|
+
- Override with `MAITAI_BASE_URL` or `maitai login --base-url`
|
|
119
|
+
|
|
120
|
+
### Response Format
|
|
121
|
+
|
|
122
|
+
- Success: `{"data": {...}}`
|
|
123
|
+
- Paginated: `{"data": [...], "pagination": {"total": N, "offset": 0, "limit": 25}}`
|
|
124
|
+
- Error: `{"error": {"status": 401, "message": "..."}}`
|
|
125
|
+
|
|
126
|
+
<!-- GENERATED_ENDPOINTS_START -->
|
|
127
|
+
## Supported API endpoints
|
|
128
|
+
|
|
129
|
+
The CLI wraps all api/v1 endpoints. Use `maitai api METHOD path` for any endpoint:
|
|
130
|
+
|
|
131
|
+
| Method | Path | Operation |
|
|
132
|
+
|--------|------|-----------|
|
|
133
|
+
| GET | `/agents` | listAgents |
|
|
134
|
+
| POST | `/agents` | createAgent |
|
|
135
|
+
| DELETE | `/agents/actions/{action_id}` | deleteAgentAction |
|
|
136
|
+
| GET | `/agents/actions/{action_id}` | getAgentAction |
|
|
137
|
+
| PUT | `/agents/actions/{action_id}` | updateAgentAction |
|
|
138
|
+
| PUT | `/agents/actions/{action_id}/disable` | disableAction |
|
|
139
|
+
| PUT | `/agents/actions/{action_id}/enable` | enableAction |
|
|
140
|
+
| POST | `/agents/actions/{action_id}/publish` | publishActionVersion |
|
|
141
|
+
| GET | `/agents/actions/{action_id}/versions` | listActionVersions |
|
|
142
|
+
| GET | `/agents/tasks/{task_id}/tree` | getAgentTaskTree |
|
|
143
|
+
| DELETE | `/agents/{agent_id}` | deleteAgent |
|
|
144
|
+
| GET | `/agents/{agent_id}` | getAgent |
|
|
145
|
+
| PUT | `/agents/{agent_id}` | updateAgent |
|
|
146
|
+
| GET | `/agents/{agent_id}/actions` | listAgentActions |
|
|
147
|
+
| POST | `/agents/{agent_id}/actions` | createAgentAction |
|
|
148
|
+
| DELETE | `/agents/{agent_id}/form-fields` | deleteFormFields |
|
|
149
|
+
| GET | `/agents/{agent_id}/form-fields` | getFormFields |
|
|
150
|
+
| PUT | `/agents/{agent_id}/form-fields` | updateFormFields |
|
|
151
|
+
| POST | `/agents/{agent_id}/publish` | publishAgentVersion |
|
|
152
|
+
| GET | `/agents/{agent_id}/releases` | listAgentReleases |
|
|
153
|
+
| DELETE | `/agents/{agent_id}/releases/{name}` | deleteAgentRelease |
|
|
154
|
+
| PUT | `/agents/{agent_id}/releases/{name}` | upsertAgentRelease |
|
|
155
|
+
| GET | `/agents/{agent_id}/routing/config` | getRoutingConfig |
|
|
156
|
+
| PUT | `/agents/{agent_id}/routing/config` | updateRoutingConfig |
|
|
157
|
+
| POST | `/agents/{agent_id}/routing/rules` | createRoutingRule |
|
|
158
|
+
| DELETE | `/agents/{agent_id}/routing/rules/{rule_id}` | deleteRoutingRule |
|
|
159
|
+
| PUT | `/agents/{agent_id}/routing/rules/{rule_id}` | updateRoutingRule |
|
|
160
|
+
| GET | `/agents/{agent_id}/sessions` | listAgentSessions |
|
|
161
|
+
| GET | `/agents/{agent_id}/sessions/{session_id}` | getAgentSessionDetail |
|
|
162
|
+
| GET | `/agents/{agent_id}/sessions/{session_id}/timeline` | getAgentSessionTimeline |
|
|
163
|
+
| GET | `/agents/{agent_id}/subagents` | listSubAgents |
|
|
164
|
+
| POST | `/agents/{agent_id}/subagents` | addSubAgent |
|
|
165
|
+
| DELETE | `/agents/{agent_id}/subagents/{child_agent_id}` | removeSubAgent |
|
|
166
|
+
| PUT | `/agents/{agent_id}/subagents/{child_agent_id}/disable` | disableSubAgent |
|
|
167
|
+
| PUT | `/agents/{agent_id}/subagents/{child_agent_id}/enable` | enableSubAgent |
|
|
168
|
+
| GET | `/agents/{agent_id}/tasks/{task_id}/timeline` | getAgentTaskTimeline |
|
|
169
|
+
| GET | `/agents/{agent_id}/versions` | listAgentVersions |
|
|
170
|
+
| GET | `/agents/{agent_id}/versions/{version}` | getAgentVersion |
|
|
171
|
+
| GET | `/applications` | listApplications |
|
|
172
|
+
| POST | `/applications` | createApplication |
|
|
173
|
+
| DELETE | `/applications/{application_id}` | deleteApplication |
|
|
174
|
+
| GET | `/applications/{application_id}` | getApplication |
|
|
175
|
+
| PUT | `/applications/{application_id}` | updateApplication |
|
|
176
|
+
| GET | `/applications/{application_id}/config` | getApplicationConfig |
|
|
177
|
+
| PUT | `/applications/{application_id}/config` | updateApplicationConfig |
|
|
178
|
+
| GET | `/applications/{application_id}/intents` | listApplicationIntents |
|
|
179
|
+
| GET | `/applications/{application_id}/intents/{intent_id}` | getApplicationIntent |
|
|
180
|
+
| GET | `/applications/{application_id}/intents/{intent_id}/config` | getIntentConfig |
|
|
181
|
+
| PUT | `/applications/{application_id}/intents/{intent_id}/config` | updateIntentConfig |
|
|
182
|
+
| GET | `/applications/{application_id}/models` | listApplicationModels |
|
|
183
|
+
| GET | `/applications/{application_id}/sessions` | listApplicationSessions |
|
|
184
|
+
| GET | `/applications/{application_id}/workflow-runs` | listApplicationWorkflowRuns |
|
|
185
|
+
| GET | `/conversation-trees` | listConversationTrees |
|
|
186
|
+
| DELETE | `/conversation-trees/{tree_id}` | deleteConversationTree |
|
|
187
|
+
| GET | `/conversation-trees/{tree_id}` | getConversationTree |
|
|
188
|
+
| GET | `/conversation-trees/{tree_id}/status` | getConversationTreeStatus |
|
|
189
|
+
| GET | `/conversation-trees/{tree_id}/versions` | listConversationTreeVersions |
|
|
190
|
+
| GET | `/datasets` | listDatasets |
|
|
191
|
+
| POST | `/datasets` | createDataset |
|
|
192
|
+
| DELETE | `/datasets/{dataset_id}` | deleteDataset |
|
|
193
|
+
| GET | `/datasets/{dataset_id}` | getDataset |
|
|
194
|
+
| PUT | `/datasets/{dataset_id}` | updateDataset |
|
|
195
|
+
| DELETE | `/datasets/{dataset_id}/requests` | removeRequestsFromDataset |
|
|
196
|
+
| GET | `/datasets/{dataset_id}/requests` | listDatasetRequests |
|
|
197
|
+
| POST | `/datasets/{dataset_id}/requests` | addRequestsToDataset |
|
|
198
|
+
| GET | `/evaluations` | listEvaluations |
|
|
199
|
+
| POST | `/evaluations` | createEvaluation |
|
|
200
|
+
| GET | `/evaluations/{evaluation_run_id}` | getEvaluation |
|
|
201
|
+
| GET | `/evaluations/{evaluation_run_id}/results` | getEvaluationResults |
|
|
202
|
+
| GET | `/finetune-runs` | listFinetuneRuns |
|
|
203
|
+
| POST | `/finetune-runs` | createFinetuneRun |
|
|
204
|
+
| GET | `/finetune-runs/{run_id}` | getFinetuneRun |
|
|
205
|
+
| POST | `/finetune-runs/{run_id}/cancel` | cancelFinetuneRun |
|
|
206
|
+
| GET | `/finetune-runs/{run_id}/metrics` | getFinetuneRunMetrics |
|
|
207
|
+
| GET | `/intent-groups` | listIntentGroups |
|
|
208
|
+
| GET | `/intent-groups/{intent_group_id}` | getIntentGroup |
|
|
209
|
+
| GET | `/intent-groups/{intent_group_id}/compositions` | listIntentGroupCompositions |
|
|
210
|
+
| GET | `/intent-groups/{intent_group_id}/config` | getIntentGroupConfig |
|
|
211
|
+
| PUT | `/intent-groups/{intent_group_id}/config` | updateIntentGroupConfig |
|
|
212
|
+
| GET | `/intent-groups/{intent_group_id}/datasets` | listIntentGroupDatasets |
|
|
213
|
+
| GET | `/intent-groups/{intent_group_id}/intents` | listIntentsByGroup |
|
|
214
|
+
| GET | `/intent-groups/{intent_group_id}/models` | listIntentGroupModels |
|
|
215
|
+
| GET | `/intent-groups/{intent_group_id}/requests` | listIntentGroupRequests |
|
|
216
|
+
| GET | `/intent-groups/{intent_group_id}/sentinels` | listIntentGroupSentinels |
|
|
217
|
+
| GET | `/intent-groups/{intent_group_id}/test-sets` | listIntentGroupTestSets |
|
|
218
|
+
| GET | `/models` | listModels |
|
|
219
|
+
| GET | `/models/available` | listAvailableModels |
|
|
220
|
+
| POST | `/models/{model_id}/disable` | disableModel |
|
|
221
|
+
| POST | `/models/{model_id}/enable` | enableModel |
|
|
222
|
+
| GET | `/requests` | listRequests |
|
|
223
|
+
| GET | `/requests/{request_id}` | getRequest |
|
|
224
|
+
| PUT | `/requests/{request_id}/response` | updateRequestResponse |
|
|
225
|
+
| GET | `/sentinels` | listSentinels |
|
|
226
|
+
| POST | `/sentinels` | createSentinel |
|
|
227
|
+
| DELETE | `/sentinels/{sentinel_id}` | deleteSentinel |
|
|
228
|
+
| GET | `/sentinels/{sentinel_id}` | getSentinel |
|
|
229
|
+
| PUT | `/sentinels/{sentinel_id}` | updateSentinel |
|
|
230
|
+
| GET | `/sessions` | listSessions |
|
|
231
|
+
| GET | `/sessions/{session_id}` | getSession |
|
|
232
|
+
| POST | `/sessions/{session_id}/feedback` | setSessionFeedback |
|
|
233
|
+
| GET | `/sessions/{session_id}/timeline` | getSessionTimeline |
|
|
234
|
+
| GET | `/test-runs` | listTestRuns |
|
|
235
|
+
| POST | `/test-runs` | createTestRun |
|
|
236
|
+
| DELETE | `/test-runs/{test_run_id}` | deleteTestRun |
|
|
237
|
+
| GET | `/test-runs/{test_run_id}` | getTestRun |
|
|
238
|
+
| GET | `/test-runs/{test_run_id}/progress` | getTestRunProgress |
|
|
239
|
+
| GET | `/test-runs/{test_run_id}/requests` | listTestRunRequests |
|
|
240
|
+
| GET | `/test-sets` | listTestSets |
|
|
241
|
+
| POST | `/test-sets` | createTestSet |
|
|
242
|
+
| DELETE | `/test-sets/{test_set_id}` | deleteTestSet |
|
|
243
|
+
| GET | `/test-sets/{test_set_id}` | getTestSet |
|
|
244
|
+
| PUT | `/test-sets/{test_set_id}` | updateTestSet |
|
|
245
|
+
| DELETE | `/test-sets/{test_set_id}/requests` | removeRequestsFromTestSet |
|
|
246
|
+
| GET | `/test-sets/{test_set_id}/requests` | listTestSetRequests |
|
|
247
|
+
| POST | `/test-sets/{test_set_id}/requests` | addRequestsToTestSet |
|
|
248
|
+
|
|
249
|
+
<!-- GENERATED_ENDPOINTS_END -->
|
|
250
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
maitai_cli/__init__.py,sha256=DP17cdkmaB9BWDyNqe7FsDeMVkU2sjWLv5YKF3UgklE,104
|
|
2
|
+
maitai_cli/__main__.py,sha256=yTPCvg2BT1f-i59uxTzz64OXy7sU8GB2G5wz7fcau5c,118
|
|
3
|
+
maitai_cli/api.py,sha256=voFRukTGhauDocQyHQNr92q_UaqfZ0j7mv5Afr_x564,2953
|
|
4
|
+
maitai_cli/commands_generated.py,sha256=1Jgi13ijzbaqP_D2JEo-xLg0sXIEE02zfnK-9WncQgE,20838
|
|
5
|
+
maitai_cli/config.py,sha256=PcMIwfJkC5tMP9SKMhcbm5eGQWe6RzZx-mIjv2MKTxw,2383
|
|
6
|
+
maitai_cli/main.py,sha256=BuFB5F8XcDYqroW4H4YmnF17Ui4Mv3EolQrwRBJrEXs,4579
|
|
7
|
+
maitai_cli/utils.py,sha256=1gpMjzPwoJRSvMLFE2gz-2TEvYaa3anjirFNGtKuy_4,1289
|
|
8
|
+
maitai_cli-0.1.1.dist-info/METADATA,sha256=Sx9Ty1SBOezuEpSQ-7eXBqrdJsyAZ3AR_zoGhi4fspE,10951
|
|
9
|
+
maitai_cli-0.1.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
10
|
+
maitai_cli-0.1.1.dist-info/entry_points.txt,sha256=vxR-KCH2JQAnwJQDJ-i6uMD9O3nPFbQog7u66mY9OvU,47
|
|
11
|
+
maitai_cli-0.1.1.dist-info/top_level.txt,sha256=VDcocArAixYCOLTnI-KZl7AA_17CiuBvrieREKIig9w,11
|
|
12
|
+
maitai_cli-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maitai_cli
|