daita-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.
- daita_cli/__init__.py +5 -0
- daita_cli/api_client.py +103 -0
- daita_cli/command_helpers.py +58 -0
- daita_cli/commands/__init__.py +0 -0
- daita_cli/commands/agents.py +52 -0
- daita_cli/commands/conversations.py +75 -0
- daita_cli/commands/create.py +182 -0
- daita_cli/commands/deployments.py +73 -0
- daita_cli/commands/executions.py +76 -0
- daita_cli/commands/init.py +206 -0
- daita_cli/commands/logs.py +110 -0
- daita_cli/commands/memory.py +41 -0
- daita_cli/commands/operations.py +46 -0
- daita_cli/commands/push.py +291 -0
- daita_cli/commands/run.py +190 -0
- daita_cli/commands/schedules.py +48 -0
- daita_cli/commands/secrets.py +74 -0
- daita_cli/commands/status.py +94 -0
- daita_cli/commands/test.py +165 -0
- daita_cli/commands/traces.py +75 -0
- daita_cli/commands/webhooks.py +21 -0
- daita_cli/main.py +108 -0
- daita_cli/mcp_server.py +582 -0
- daita_cli/output.py +100 -0
- daita_cli/project_utils.py +42 -0
- daita_cli-0.1.0.dist-info/METADATA +218 -0
- daita_cli-0.1.0.dist-info/RECORD +31 -0
- daita_cli-0.1.0.dist-info/WHEEL +5 -0
- daita_cli-0.1.0.dist-info/entry_points.txt +2 -0
- daita_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- daita_cli-0.1.0.dist-info/top_level.txt +1 -0
daita_cli/__init__.py
ADDED
daita_cli/api_client.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DaitaAPIClient — async httpx client shared by CLI commands and MCP server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import httpx
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from daita_cli import __version__
|
|
10
|
+
|
|
11
|
+
_DEFAULT_BASE_URL = "https://api.daita-tech.io"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class APIError(Exception):
|
|
15
|
+
def __init__(self, status_code: int, detail: str, raw: Any = None):
|
|
16
|
+
super().__init__(detail)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.detail = detail
|
|
19
|
+
self.raw = raw
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthError(APIError): ...
|
|
23
|
+
class NotFoundError(APIError): ...
|
|
24
|
+
class ValidationError(APIError): ...
|
|
25
|
+
class RateLimitError(APIError): ...
|
|
26
|
+
class ServerError(APIError): ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _raise_for(status: int, detail: str, raw: Any) -> None:
|
|
30
|
+
if status in (401, 403):
|
|
31
|
+
raise AuthError(status, detail, raw)
|
|
32
|
+
if status == 404:
|
|
33
|
+
raise NotFoundError(status, detail, raw)
|
|
34
|
+
if status in (400, 422):
|
|
35
|
+
raise ValidationError(status, detail, raw)
|
|
36
|
+
if status == 429:
|
|
37
|
+
raise RateLimitError(status, detail, raw)
|
|
38
|
+
if status >= 500:
|
|
39
|
+
raise ServerError(status, detail, raw)
|
|
40
|
+
raise APIError(status, detail, raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DaitaAPIClient:
|
|
44
|
+
def __init__(self, api_key: str = None, base_url: str = None):
|
|
45
|
+
self.api_key = api_key or os.getenv("DAITA_API_KEY")
|
|
46
|
+
self.base_url = (base_url or os.getenv("DAITA_API_ENDPOINT") or _DEFAULT_BASE_URL).rstrip("/")
|
|
47
|
+
self._client: httpx.AsyncClient | None = None
|
|
48
|
+
|
|
49
|
+
def _headers(self) -> dict:
|
|
50
|
+
if not self.api_key:
|
|
51
|
+
raise AuthError(401, "DAITA_API_KEY is not set. Export it or pass --api-key.")
|
|
52
|
+
return {
|
|
53
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
54
|
+
"User-Agent": f"Daita-CLI/{__version__}",
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async def __aenter__(self) -> "DaitaAPIClient":
|
|
59
|
+
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def __aexit__(self, *_) -> None:
|
|
63
|
+
if self._client:
|
|
64
|
+
await self._client.aclose()
|
|
65
|
+
self._client = None
|
|
66
|
+
|
|
67
|
+
def _check_client(self) -> httpx.AsyncClient:
|
|
68
|
+
if self._client is None:
|
|
69
|
+
raise RuntimeError("DaitaAPIClient must be used as an async context manager")
|
|
70
|
+
return self._client
|
|
71
|
+
|
|
72
|
+
async def _handle(self, resp: httpx.Response) -> Any:
|
|
73
|
+
if resp.is_success:
|
|
74
|
+
try:
|
|
75
|
+
return resp.json()
|
|
76
|
+
except Exception:
|
|
77
|
+
return resp.text
|
|
78
|
+
try:
|
|
79
|
+
body = resp.json()
|
|
80
|
+
detail = body.get("detail") or body.get("message") or str(body)
|
|
81
|
+
except Exception:
|
|
82
|
+
detail = resp.text or f"HTTP {resp.status_code}"
|
|
83
|
+
_raise_for(resp.status_code, detail, resp)
|
|
84
|
+
|
|
85
|
+
async def get(self, path: str, params: dict = None) -> Any:
|
|
86
|
+
resp = await self._check_client().get(path, headers=self._headers(), params=params)
|
|
87
|
+
return await self._handle(resp)
|
|
88
|
+
|
|
89
|
+
async def post(self, path: str, json: dict = None) -> Any:
|
|
90
|
+
resp = await self._check_client().post(path, headers=self._headers(), json=json)
|
|
91
|
+
return await self._handle(resp)
|
|
92
|
+
|
|
93
|
+
async def put(self, path: str, json: dict = None) -> Any:
|
|
94
|
+
resp = await self._check_client().put(path, headers=self._headers(), json=json)
|
|
95
|
+
return await self._handle(resp)
|
|
96
|
+
|
|
97
|
+
async def patch(self, path: str, json: dict = None) -> Any:
|
|
98
|
+
resp = await self._check_client().patch(path, headers=self._headers(), json=json)
|
|
99
|
+
return await self._handle(resp)
|
|
100
|
+
|
|
101
|
+
async def delete(self, path: str, params: dict = None) -> Any:
|
|
102
|
+
resp = await self._check_client().delete(path, headers=self._headers(), params=params)
|
|
103
|
+
return await self._handle(resp)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@api_command decorator — wraps async Click commands with client injection,
|
|
3
|
+
OutputFormatter injection, and standardised error/exit-code handling.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
@cli.command()
|
|
7
|
+
@click.argument("name")
|
|
8
|
+
@api_command
|
|
9
|
+
async def my_cmd(client, formatter, name): ...
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import functools
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from daita_cli.api_client import AuthError, NotFoundError, APIError, DaitaAPIClient
|
|
19
|
+
from daita_cli.output import OutputFormatter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def api_command(f):
|
|
23
|
+
"""
|
|
24
|
+
Decorator that:
|
|
25
|
+
1. Runs the coroutine via asyncio.run()
|
|
26
|
+
2. Injects `client` (DaitaAPIClient) and `formatter` (OutputFormatter) as first two args
|
|
27
|
+
3. Maps exceptions to exit codes: 0=ok, 1=error, 2=auth, 130=interrupt
|
|
28
|
+
"""
|
|
29
|
+
@functools.wraps(f)
|
|
30
|
+
@click.pass_context
|
|
31
|
+
def wrapper(ctx, *args, **kwargs):
|
|
32
|
+
obj = ctx.obj or {}
|
|
33
|
+
formatter: OutputFormatter = obj.get("formatter", OutputFormatter())
|
|
34
|
+
|
|
35
|
+
async def _run():
|
|
36
|
+
async with DaitaAPIClient() as client:
|
|
37
|
+
return await f(client, formatter, *args, **kwargs)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
asyncio.run(_run())
|
|
41
|
+
except AuthError as e:
|
|
42
|
+
formatter.error("AUTH_ERROR", str(e))
|
|
43
|
+
sys.exit(2)
|
|
44
|
+
except NotFoundError as e:
|
|
45
|
+
formatter.error("NOT_FOUND", str(e))
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
except APIError as e:
|
|
48
|
+
formatter.error("API_ERROR", str(e), {"status_code": e.status_code})
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
except KeyboardInterrupt:
|
|
51
|
+
sys.exit(130)
|
|
52
|
+
except click.ClickException:
|
|
53
|
+
raise
|
|
54
|
+
except Exception as e:
|
|
55
|
+
formatter.error("ERROR", str(e))
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from daita_cli.command_helpers import api_command
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def agents():
|
|
7
|
+
"""Manage agents."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@agents.command("list")
|
|
12
|
+
@click.option("--type", "agent_type", type=click.Choice(["agent", "workflow"]), help="Filter by type")
|
|
13
|
+
@click.option("--status", type=click.Choice(["active", "inactive"]), help="Filter by status")
|
|
14
|
+
@click.option("--page", default=1, show_default=True, help="Page number")
|
|
15
|
+
@click.option("--per-page", default=20, show_default=True, help="Items per page")
|
|
16
|
+
@api_command
|
|
17
|
+
async def list_agents(client, formatter, agent_type, status, page, per_page):
|
|
18
|
+
"""List agents."""
|
|
19
|
+
params = {"page": page, "per_page": per_page}
|
|
20
|
+
if agent_type:
|
|
21
|
+
params["agent_type"] = agent_type
|
|
22
|
+
if status:
|
|
23
|
+
params["status_filter"] = status
|
|
24
|
+
data = await client.get("/api/v1/agents/agents", params=params)
|
|
25
|
+
items = data if isinstance(data, list) else data.get("agents", data.get("items", []))
|
|
26
|
+
formatter.list_items(
|
|
27
|
+
items,
|
|
28
|
+
columns=["id", "name", "type", "status", "created_at"],
|
|
29
|
+
title="Agents",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@agents.command("show")
|
|
34
|
+
@click.argument("agent_id")
|
|
35
|
+
@api_command
|
|
36
|
+
async def show_agent(client, formatter, agent_id):
|
|
37
|
+
"""Show agent details."""
|
|
38
|
+
data = await client.get(f"/api/v1/agents/agents/{agent_id}")
|
|
39
|
+
formatter.item(data)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@agents.command("deployed")
|
|
43
|
+
@api_command
|
|
44
|
+
async def deployed_agents(client, formatter):
|
|
45
|
+
"""List deployed agents with configuration."""
|
|
46
|
+
data = await client.get("/api/v1/agents/agents/deployed")
|
|
47
|
+
items = data if isinstance(data, list) else data.get("agents", data.get("items", []))
|
|
48
|
+
formatter.list_items(
|
|
49
|
+
items,
|
|
50
|
+
columns=["id", "name", "type", "status", "deployed_at"],
|
|
51
|
+
title="Deployed Agents",
|
|
52
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversations — requires user_id for all requests.
|
|
3
|
+
The backend scopes conversations per user. From the CLI, user_id defaults to "cli"
|
|
4
|
+
(all CLI-created conversations share this scope). Override with --user-id.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from daita_cli.command_helpers import api_command
|
|
9
|
+
|
|
10
|
+
_USER_ID_OPT = click.option(
|
|
11
|
+
"--user-id", default="cli", show_default=True,
|
|
12
|
+
help="User ID for scoping conversations (default: 'cli')",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def conversations():
|
|
18
|
+
"""Manage conversations."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@conversations.command("list")
|
|
23
|
+
@_USER_ID_OPT
|
|
24
|
+
@click.option("--agent-name")
|
|
25
|
+
@click.option("--limit", default=20, show_default=True)
|
|
26
|
+
@api_command
|
|
27
|
+
async def list_conversations(client, formatter, user_id, agent_name, limit):
|
|
28
|
+
"""List conversations."""
|
|
29
|
+
params = {"user_id": user_id, "limit": limit}
|
|
30
|
+
if agent_name:
|
|
31
|
+
params["agent_name"] = agent_name
|
|
32
|
+
data = await client.get("/api/v1/conversations", params=params)
|
|
33
|
+
items = data if isinstance(data, list) else data.get("conversations", data.get("items", []))
|
|
34
|
+
formatter.list_items(
|
|
35
|
+
items,
|
|
36
|
+
columns=["id", "title", "agent_name", "created_at"],
|
|
37
|
+
title="Conversations",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@conversations.command("show")
|
|
42
|
+
@click.argument("conversation_id")
|
|
43
|
+
@_USER_ID_OPT
|
|
44
|
+
@api_command
|
|
45
|
+
async def show_conversation(client, formatter, conversation_id, user_id):
|
|
46
|
+
"""Show conversation details."""
|
|
47
|
+
data = await client.get(f"/api/v1/conversations/{conversation_id}", params={"user_id": user_id})
|
|
48
|
+
formatter.item(data)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@conversations.command("create")
|
|
52
|
+
@click.option("--agent-name", required=True)
|
|
53
|
+
@click.option("--title")
|
|
54
|
+
@_USER_ID_OPT
|
|
55
|
+
@api_command
|
|
56
|
+
async def create_conversation(client, formatter, agent_name, title, user_id):
|
|
57
|
+
"""Create a new conversation."""
|
|
58
|
+
payload = {"agent_name": agent_name, "user_id": user_id}
|
|
59
|
+
if title:
|
|
60
|
+
payload["title"] = title
|
|
61
|
+
data = await client.post("/api/v1/conversations", json=payload)
|
|
62
|
+
formatter.success(data, message=f"Conversation created: {data.get('id', '')}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@conversations.command("delete")
|
|
66
|
+
@click.argument("conversation_id")
|
|
67
|
+
@_USER_ID_OPT
|
|
68
|
+
@api_command
|
|
69
|
+
async def delete_conversation(client, formatter, conversation_id, user_id):
|
|
70
|
+
"""Delete a conversation."""
|
|
71
|
+
data = await client.delete(
|
|
72
|
+
f"/api/v1/conversations/{conversation_id}",
|
|
73
|
+
params={"user_id": user_id},
|
|
74
|
+
)
|
|
75
|
+
formatter.success(data, message=f"Conversation {conversation_id} deleted.")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
daita create agent|workflow <name> — add a component to the current project.
|
|
3
|
+
No daita-agents dependency required.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from daita_cli.output import OutputFormatter
|
|
14
|
+
from daita_cli.project_utils import ensure_project_root
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group("create")
|
|
18
|
+
def create_group():
|
|
19
|
+
"""Create agents, workflows, and other components."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@create_group.command("agent")
|
|
24
|
+
@click.argument("name")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def create_agent(ctx, name):
|
|
27
|
+
"""Create a new agent."""
|
|
28
|
+
obj = ctx.obj or {}
|
|
29
|
+
formatter = obj.get("formatter", OutputFormatter())
|
|
30
|
+
try:
|
|
31
|
+
_create_component("agent", name, formatter)
|
|
32
|
+
except click.ClickException:
|
|
33
|
+
raise
|
|
34
|
+
except Exception as e:
|
|
35
|
+
formatter.error("ERROR", str(e))
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@create_group.command("workflow")
|
|
40
|
+
@click.argument("name")
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def create_workflow(ctx, name):
|
|
43
|
+
"""Create a new workflow."""
|
|
44
|
+
obj = ctx.obj or {}
|
|
45
|
+
formatter = obj.get("formatter", OutputFormatter())
|
|
46
|
+
try:
|
|
47
|
+
_create_component("workflow", name, formatter)
|
|
48
|
+
except click.ClickException:
|
|
49
|
+
raise
|
|
50
|
+
except Exception as e:
|
|
51
|
+
formatter.error("ERROR", str(e))
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _to_class_name(name: str) -> str:
|
|
56
|
+
return "".join(w.capitalize() for w in name.replace("-", "_").split("_") if w)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _clean_name(name: str) -> str:
|
|
60
|
+
return name.replace("-", "_").lower()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _create_component(template: str, name: str, formatter: OutputFormatter):
|
|
64
|
+
project_root = ensure_project_root()
|
|
65
|
+
clean_name = _clean_name(name)
|
|
66
|
+
class_name = _to_class_name(clean_name)
|
|
67
|
+
|
|
68
|
+
if template == "agent":
|
|
69
|
+
dest_dir = project_root / "agents"
|
|
70
|
+
dest_file = dest_dir / f"{clean_name}.py"
|
|
71
|
+
if dest_file.exists():
|
|
72
|
+
raise click.ClickException(f"Agent '{clean_name}' already exists.")
|
|
73
|
+
dest_dir.mkdir(exist_ok=True)
|
|
74
|
+
dest_file.write_text(_agent_template(clean_name, class_name))
|
|
75
|
+
elif template == "workflow":
|
|
76
|
+
dest_dir = project_root / "workflows"
|
|
77
|
+
dest_file = dest_dir / f"{clean_name}.py"
|
|
78
|
+
if dest_file.exists():
|
|
79
|
+
raise click.ClickException(f"Workflow '{clean_name}' already exists.")
|
|
80
|
+
dest_dir.mkdir(exist_ok=True)
|
|
81
|
+
dest_file.write_text(_workflow_template(clean_name, class_name))
|
|
82
|
+
else:
|
|
83
|
+
raise click.ClickException(f"Unknown template: {template}")
|
|
84
|
+
|
|
85
|
+
# Prompt for display name (non-interactive: use default)
|
|
86
|
+
default_display = clean_name.replace("_", " ").title()
|
|
87
|
+
if sys.stdin.isatty():
|
|
88
|
+
display_name = click.prompt(
|
|
89
|
+
f" Display name for deployment",
|
|
90
|
+
default=default_display,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
display_name = default_display
|
|
94
|
+
|
|
95
|
+
_update_config(project_root, template + "s", clean_name, display_name)
|
|
96
|
+
|
|
97
|
+
formatter.success(
|
|
98
|
+
{"name": clean_name, "display_name": display_name, "file": str(dest_file)},
|
|
99
|
+
message=f" Created {template}: {clean_name} (display: '{display_name}')",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _update_config(project_root: Path, component_key: str, name: str, display_name: str):
|
|
104
|
+
cfg_file = project_root / "daita-project.yaml"
|
|
105
|
+
if cfg_file.exists():
|
|
106
|
+
with open(cfg_file) as f:
|
|
107
|
+
config = yaml.safe_load(f) or {}
|
|
108
|
+
else:
|
|
109
|
+
config = {}
|
|
110
|
+
|
|
111
|
+
config.setdefault(component_key, [])
|
|
112
|
+
if not any(c["name"] == name for c in config[component_key]):
|
|
113
|
+
config[component_key].append({
|
|
114
|
+
"name": name,
|
|
115
|
+
"display_name": display_name,
|
|
116
|
+
"type": "standard" if component_key == "agents" else "basic",
|
|
117
|
+
"created_at": datetime.now().isoformat(),
|
|
118
|
+
})
|
|
119
|
+
with open(cfg_file, "w") as f:
|
|
120
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _agent_template(name: str, class_name: str) -> str:
|
|
124
|
+
return f'''\
|
|
125
|
+
"""
|
|
126
|
+
{class_name} Agent
|
|
127
|
+
"""
|
|
128
|
+
from daita import Agent
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_agent():
|
|
132
|
+
"""Create the agent instance."""
|
|
133
|
+
return Agent(
|
|
134
|
+
name="{class_name}",
|
|
135
|
+
model="gpt-4o-mini",
|
|
136
|
+
prompt="You are a helpful AI assistant.",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
import asyncio
|
|
142
|
+
|
|
143
|
+
async def main():
|
|
144
|
+
agent = create_agent()
|
|
145
|
+
await agent.start()
|
|
146
|
+
try:
|
|
147
|
+
answer = await agent.run("Hello!")
|
|
148
|
+
print(answer)
|
|
149
|
+
finally:
|
|
150
|
+
await agent.stop()
|
|
151
|
+
|
|
152
|
+
asyncio.run(main())
|
|
153
|
+
'''
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _workflow_template(name: str, class_name: str) -> str:
|
|
157
|
+
return f'''\
|
|
158
|
+
"""
|
|
159
|
+
{class_name} Workflow
|
|
160
|
+
"""
|
|
161
|
+
from daita import Agent, Workflow
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_workflow():
|
|
165
|
+
"""Create the workflow instance."""
|
|
166
|
+
workflow = Workflow("{class_name}")
|
|
167
|
+
agent = Agent(name="Agent", model="gpt-4o-mini", prompt="You are helpful.")
|
|
168
|
+
workflow.add_agent("agent", agent)
|
|
169
|
+
return workflow
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
import asyncio
|
|
174
|
+
|
|
175
|
+
async def main():
|
|
176
|
+
wf = create_workflow()
|
|
177
|
+
await wf.start()
|
|
178
|
+
await wf.stop()
|
|
179
|
+
print("Done")
|
|
180
|
+
|
|
181
|
+
asyncio.run(main())
|
|
182
|
+
'''
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from daita_cli.command_helpers import api_command
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def deployments():
|
|
7
|
+
"""Manage deployments."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@deployments.command("list")
|
|
12
|
+
@click.option("--limit", default=10, show_default=True)
|
|
13
|
+
@api_command
|
|
14
|
+
async def list_deployments(client, formatter, limit):
|
|
15
|
+
"""List deployments."""
|
|
16
|
+
data = await client.get("/api/v1/deployments/api-key", params={"per_page": limit})
|
|
17
|
+
items = data if isinstance(data, list) else data.get("deployments", data.get("items", []))
|
|
18
|
+
formatter.list_items(
|
|
19
|
+
items,
|
|
20
|
+
columns=["deployment_id", "project_name", "environment", "status", "version", "deployed_at"],
|
|
21
|
+
title="Deployments",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@deployments.command("show")
|
|
26
|
+
@click.argument("deployment_id")
|
|
27
|
+
@api_command
|
|
28
|
+
async def show_deployment(client, formatter, deployment_id):
|
|
29
|
+
"""Show deployment details."""
|
|
30
|
+
data = await client.get(f"/api/v1/deployments/{deployment_id}")
|
|
31
|
+
formatter.item(data)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@deployments.command("delete")
|
|
35
|
+
@click.argument("deployment_id")
|
|
36
|
+
@click.option("--force", is_flag=True, help="Skip confirmation")
|
|
37
|
+
@api_command
|
|
38
|
+
async def delete_deployment(client, formatter, deployment_id, force):
|
|
39
|
+
"""Delete a deployment."""
|
|
40
|
+
if not force:
|
|
41
|
+
click.confirm(f"Delete deployment {deployment_id}?", abort=True)
|
|
42
|
+
data = await client.delete(f"/api/v1/deployments/{deployment_id}")
|
|
43
|
+
formatter.success(data, message=f"Deployment {deployment_id} deleted.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@deployments.command("history")
|
|
47
|
+
@click.argument("project_name")
|
|
48
|
+
@click.option("--limit", default=10, show_default=True)
|
|
49
|
+
@api_command
|
|
50
|
+
async def deployment_history(client, formatter, project_name, limit):
|
|
51
|
+
"""Show deployment history for a project."""
|
|
52
|
+
data = await client.get(
|
|
53
|
+
f"/api/v1/deployments/history/{project_name}",
|
|
54
|
+
params={"per_page": limit},
|
|
55
|
+
)
|
|
56
|
+
items = data if isinstance(data, list) else data.get("deployments", data.get("items", []))
|
|
57
|
+
formatter.list_items(
|
|
58
|
+
items,
|
|
59
|
+
columns=["deployment_id", "environment", "status", "version", "deployed_at"],
|
|
60
|
+
title=f"Deployment History: {project_name}",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@deployments.command("rollback")
|
|
65
|
+
@click.argument("deployment_id")
|
|
66
|
+
@click.option("--force", is_flag=True, help="Skip confirmation")
|
|
67
|
+
@api_command
|
|
68
|
+
async def rollback_deployment(client, formatter, deployment_id, force):
|
|
69
|
+
"""Rollback to a previous deployment."""
|
|
70
|
+
if not force:
|
|
71
|
+
click.confirm(f"Rollback to deployment {deployment_id}?", abort=True)
|
|
72
|
+
data = await client.post(f"/api/v1/deployments/rollback/{deployment_id}")
|
|
73
|
+
formatter.success(data, message=f"Rolled back to deployment {deployment_id}.")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from daita_cli.command_helpers import api_command
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.group(invoke_without_command=True)
|
|
6
|
+
@click.option("--limit", default=10, show_default=True)
|
|
7
|
+
@click.option("--status", type=click.Choice(["queued", "running", "completed", "failed", "cancelled"]))
|
|
8
|
+
@click.option("--type", "target_type", type=click.Choice(["agent", "workflow"]))
|
|
9
|
+
@click.pass_context
|
|
10
|
+
def executions(ctx, limit, status, target_type):
|
|
11
|
+
"""Manage executions."""
|
|
12
|
+
if ctx.invoked_subcommand is None:
|
|
13
|
+
# Backward compat: `daita executions` with no subcommand → list
|
|
14
|
+
ctx.invoke(list_executions, limit=limit, status=status, target_type=target_type)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@executions.command("list")
|
|
18
|
+
@click.option("--limit", default=10, show_default=True)
|
|
19
|
+
@click.option("--status", type=click.Choice(["queued", "running", "completed", "failed", "cancelled"]))
|
|
20
|
+
@click.option("--type", "target_type", type=click.Choice(["agent", "workflow"]))
|
|
21
|
+
@api_command
|
|
22
|
+
async def list_executions(client, formatter, limit, status, target_type):
|
|
23
|
+
"""List executions."""
|
|
24
|
+
params = {"limit": limit}
|
|
25
|
+
if status:
|
|
26
|
+
params["status"] = status
|
|
27
|
+
if target_type:
|
|
28
|
+
params["target_type"] = target_type
|
|
29
|
+
data = await client.get("/api/v1/autonomous/executions", params=params)
|
|
30
|
+
items = data if isinstance(data, list) else data.get("executions", data.get("items", []))
|
|
31
|
+
formatter.list_items(
|
|
32
|
+
items,
|
|
33
|
+
columns=["execution_id", "target_name", "target_type", "status", "created_at", "duration_ms"],
|
|
34
|
+
title="Executions",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@executions.command("show")
|
|
39
|
+
@click.argument("execution_id")
|
|
40
|
+
@api_command
|
|
41
|
+
async def show_execution(client, formatter, execution_id):
|
|
42
|
+
"""Show execution details."""
|
|
43
|
+
data = await client.get(f"/api/v1/autonomous/executions/{execution_id}")
|
|
44
|
+
formatter.item(data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@executions.command("logs")
|
|
48
|
+
@click.argument("execution_id")
|
|
49
|
+
@click.option("--follow", "-f", is_flag=True, help="Poll until complete")
|
|
50
|
+
@api_command
|
|
51
|
+
async def execution_logs(client, formatter, execution_id, follow):
|
|
52
|
+
"""Show logs for an execution."""
|
|
53
|
+
import asyncio
|
|
54
|
+
import sys
|
|
55
|
+
|
|
56
|
+
if follow:
|
|
57
|
+
while True:
|
|
58
|
+
data = await client.get(f"/api/v1/autonomous/executions/{execution_id}")
|
|
59
|
+
formatter.item(data, fields=["execution_id", "status", "created_at", "duration_ms", "error"])
|
|
60
|
+
if data.get("status") in ("completed", "success", "failed", "error", "cancelled"):
|
|
61
|
+
break
|
|
62
|
+
if not formatter.is_json:
|
|
63
|
+
print(" polling...")
|
|
64
|
+
await asyncio.sleep(2)
|
|
65
|
+
else:
|
|
66
|
+
data = await client.get(f"/api/v1/autonomous/executions/{execution_id}")
|
|
67
|
+
formatter.item(data)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@executions.command("cancel")
|
|
71
|
+
@click.argument("execution_id")
|
|
72
|
+
@api_command
|
|
73
|
+
async def cancel_execution(client, formatter, execution_id):
|
|
74
|
+
"""Cancel a running execution."""
|
|
75
|
+
data = await client.delete(f"/api/v1/autonomous/executions/{execution_id}")
|
|
76
|
+
formatter.success(data, message=f"Execution {execution_id} cancelled.")
|