agentstack-cli 0.4.0__py3-none-macosx_12_0_arm64.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.
- agentstack_cli/__init__.py +77 -0
- agentstack_cli/api.py +125 -0
- agentstack_cli/async_typer.py +104 -0
- agentstack_cli/auth_manager.py +115 -0
- agentstack_cli/commands/__init__.py +3 -0
- agentstack_cli/commands/agent.py +1077 -0
- agentstack_cli/commands/build.py +184 -0
- agentstack_cli/commands/mcp.py +141 -0
- agentstack_cli/commands/model.py +624 -0
- agentstack_cli/commands/platform/__init__.py +181 -0
- agentstack_cli/commands/platform/base_driver.py +222 -0
- agentstack_cli/commands/platform/istio.py +186 -0
- agentstack_cli/commands/platform/lima_driver.py +210 -0
- agentstack_cli/commands/platform/wsl_driver.py +226 -0
- agentstack_cli/commands/self.py +206 -0
- agentstack_cli/commands/server.py +237 -0
- agentstack_cli/configuration.py +76 -0
- agentstack_cli/console.py +25 -0
- agentstack_cli/data/.gitignore +2 -0
- agentstack_cli/data/helm-chart.tgz +0 -0
- agentstack_cli/data/lima-guestagent.Linux-aarch64.gz +0 -0
- agentstack_cli/data/limactl +0 -0
- agentstack_cli/utils.py +281 -0
- agentstack_cli-0.4.0.dist-info/METADATA +104 -0
- agentstack_cli-0.4.0.dist-info/RECORD +27 -0
- agentstack_cli-0.4.0.dist-info/WHEEL +4 -0
- agentstack_cli-0.4.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import typing
|
|
10
|
+
import uuid
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import anyio
|
|
16
|
+
import anyio.abc
|
|
17
|
+
import typer
|
|
18
|
+
from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH
|
|
19
|
+
from agentstack_sdk.platform import AddProvider, BuildConfiguration, Provider, UpdateProvider
|
|
20
|
+
from agentstack_sdk.platform.provider_build import BuildState, ProviderBuild
|
|
21
|
+
from anyio import open_process
|
|
22
|
+
from httpx import AsyncClient, HTTPError
|
|
23
|
+
from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_delay, wait_fixed
|
|
24
|
+
|
|
25
|
+
from agentstack_cli.async_typer import AsyncTyper
|
|
26
|
+
from agentstack_cli.console import console
|
|
27
|
+
from agentstack_cli.utils import capture_output, extract_messages, print_log, run_command, status, verbosity
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def find_free_port():
|
|
31
|
+
"""Get a random free port assigned by the OS."""
|
|
32
|
+
listener = await anyio.create_tcp_listener()
|
|
33
|
+
port = listener.extra(anyio.abc.SocketAttribute.local_address)[1]
|
|
34
|
+
await listener.aclose()
|
|
35
|
+
return port
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
app = AsyncTyper()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("build")
|
|
42
|
+
async def build(
|
|
43
|
+
context: typing.Annotated[str, typer.Argument(help="Docker context for the agent")] = ".",
|
|
44
|
+
dockerfile: typing.Annotated[str | None, typer.Option(help="Use custom dockerfile path")] = None,
|
|
45
|
+
tag: typing.Annotated[str | None, typer.Option(help="Docker tag for the agent")] = None,
|
|
46
|
+
multi_platform: bool | None = False,
|
|
47
|
+
push: typing.Annotated[bool, typer.Option(help="Push the image to the target registry.")] = False,
|
|
48
|
+
import_image: typing.Annotated[
|
|
49
|
+
bool, typer.Option("--import/--no-import", is_flag=True, help="Import the image into Agent Stack platform")
|
|
50
|
+
] = True,
|
|
51
|
+
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
52
|
+
verbose: typing.Annotated[bool, typer.Option("-v")] = False,
|
|
53
|
+
):
|
|
54
|
+
with verbosity(verbose):
|
|
55
|
+
await run_command(["which", "docker"], "Checking docker")
|
|
56
|
+
image_id = "agentstack-agent-build-tmp:latest"
|
|
57
|
+
port = await find_free_port()
|
|
58
|
+
dockerfile_args = ("-f", dockerfile) if dockerfile else ()
|
|
59
|
+
|
|
60
|
+
await run_command(
|
|
61
|
+
["docker", "build", context, *dockerfile_args, "-t", image_id],
|
|
62
|
+
"Building agent image",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
agent_card = None
|
|
66
|
+
|
|
67
|
+
container_id = str(uuid.uuid4())
|
|
68
|
+
|
|
69
|
+
with status("Extracting agent metadata"):
|
|
70
|
+
async with (
|
|
71
|
+
await open_process(
|
|
72
|
+
f"docker run --name {container_id} --rm -p {port}:8000 -e HOST=0.0.0.0 -e PORT=8000 {image_id}",
|
|
73
|
+
) as process,
|
|
74
|
+
):
|
|
75
|
+
async with capture_output(process) as task_group:
|
|
76
|
+
try:
|
|
77
|
+
async for attempt in AsyncRetrying(
|
|
78
|
+
stop=stop_after_delay(timedelta(seconds=30)),
|
|
79
|
+
wait=wait_fixed(timedelta(seconds=0.5)),
|
|
80
|
+
retry=retry_if_exception_type(HTTPError),
|
|
81
|
+
reraise=True,
|
|
82
|
+
):
|
|
83
|
+
with attempt:
|
|
84
|
+
async with AsyncClient() as client:
|
|
85
|
+
resp = await client.get(
|
|
86
|
+
f"http://localhost:{port}{AGENT_CARD_WELL_KNOWN_PATH}", timeout=1
|
|
87
|
+
)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
agent_card = resp.json()
|
|
90
|
+
process.terminate()
|
|
91
|
+
with suppress(ProcessLookupError):
|
|
92
|
+
process.kill()
|
|
93
|
+
except BaseException as ex:
|
|
94
|
+
raise RuntimeError(f"Failed to build agent: {extract_messages(ex)}") from ex
|
|
95
|
+
finally:
|
|
96
|
+
task_group.cancel_scope.cancel()
|
|
97
|
+
with suppress(BaseException):
|
|
98
|
+
await run_command(["docker", "kill", container_id], "Killing container")
|
|
99
|
+
with suppress(ProcessLookupError):
|
|
100
|
+
process.kill()
|
|
101
|
+
|
|
102
|
+
context_hash = hashlib.sha256((context + (dockerfile or "")).encode()).hexdigest()[:6]
|
|
103
|
+
context_shorter = re.sub(r"https?://", "", context).replace(r".git", "")
|
|
104
|
+
context_shorter = re.sub(r"[^a-zA-Z0-9_-]+", "-", context_shorter)[:32].lstrip("-") or "provider"
|
|
105
|
+
tag = (tag or f"agentstack.local/{context_shorter}-{context_hash}:latest").lower()
|
|
106
|
+
await run_command(
|
|
107
|
+
command=[
|
|
108
|
+
*(
|
|
109
|
+
["docker", "buildx", "build", "--platform=linux/amd64,linux/arm64"]
|
|
110
|
+
if multi_platform
|
|
111
|
+
else ["docker", "build"]
|
|
112
|
+
),
|
|
113
|
+
"--push" if push else "--load",
|
|
114
|
+
context,
|
|
115
|
+
*dockerfile_args,
|
|
116
|
+
"-t",
|
|
117
|
+
tag,
|
|
118
|
+
f"--label=beeai.dev.agent.json={base64.b64encode(json.dumps(agent_card).encode()).decode()}",
|
|
119
|
+
],
|
|
120
|
+
message="Adding agent labels to container",
|
|
121
|
+
check=True,
|
|
122
|
+
)
|
|
123
|
+
console.success(f"Successfully built agent: {tag}")
|
|
124
|
+
if import_image:
|
|
125
|
+
from agentstack_cli.commands.platform import get_driver
|
|
126
|
+
|
|
127
|
+
driver = get_driver(vm_name=vm_name)
|
|
128
|
+
if (await driver.status()) != "running":
|
|
129
|
+
console.error("Agent Stack platform is not running.")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
await driver.import_image(tag)
|
|
132
|
+
|
|
133
|
+
return tag, agent_card
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command("server-side-build")
|
|
137
|
+
async def server_side_build_experimental(
|
|
138
|
+
github_url: typing.Annotated[
|
|
139
|
+
str, typer.Argument(..., help="Github repository URL (public or private if supported by the platform instance)")
|
|
140
|
+
],
|
|
141
|
+
dockerfile: typing.Annotated[
|
|
142
|
+
str | None, typer.Option(help="Use custom dockerfile path, relative to github url sub-path")
|
|
143
|
+
] = None,
|
|
144
|
+
replace: typing.Annotated[
|
|
145
|
+
str | None, typer.Option(help="Short ID, agent name or part of the provider location")
|
|
146
|
+
] = None,
|
|
147
|
+
add: typing.Annotated[bool, typer.Option(help="Add agent to the platform after build")] = False,
|
|
148
|
+
):
|
|
149
|
+
"""EXPERIMENTAL: Build agent from github repository in the platform."""
|
|
150
|
+
from agentstack_cli.commands.agent import select_provider
|
|
151
|
+
from agentstack_cli.configuration import Configuration
|
|
152
|
+
|
|
153
|
+
if replace and add:
|
|
154
|
+
raise ValueError("Cannot specify both replace and add options.")
|
|
155
|
+
|
|
156
|
+
build_configuration = None
|
|
157
|
+
if dockerfile:
|
|
158
|
+
build_configuration = BuildConfiguration(dockerfile_path=Path(dockerfile))
|
|
159
|
+
|
|
160
|
+
async with Configuration().use_platform_client():
|
|
161
|
+
on_complete = None
|
|
162
|
+
if replace:
|
|
163
|
+
provider = select_provider(replace, await Provider.list())
|
|
164
|
+
on_complete = UpdateProvider(provider_id=uuid.UUID(provider.id))
|
|
165
|
+
elif add:
|
|
166
|
+
on_complete = AddProvider()
|
|
167
|
+
|
|
168
|
+
build = await ProviderBuild.create(
|
|
169
|
+
location=github_url,
|
|
170
|
+
on_complete=on_complete,
|
|
171
|
+
build_configuration=build_configuration,
|
|
172
|
+
)
|
|
173
|
+
async for message in build.stream_logs():
|
|
174
|
+
print_log(message, ansi_mode=True)
|
|
175
|
+
build = await build.get()
|
|
176
|
+
if build.status == BuildState.COMPLETED:
|
|
177
|
+
if add:
|
|
178
|
+
message = "Agent added successfully. List agents using [green]agentstack list[/green]"
|
|
179
|
+
else:
|
|
180
|
+
message = f"Agent built successfully, add it to the platform using: [green]agentstack add {build.destination}[/green]"
|
|
181
|
+
console.success(message)
|
|
182
|
+
else:
|
|
183
|
+
error = build.error_message or "see logs above for details"
|
|
184
|
+
console.error(f"Agent build failed: {error}")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import typing
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.table import Column
|
|
9
|
+
|
|
10
|
+
from agentstack_cli.api import api_request
|
|
11
|
+
from agentstack_cli.async_typer import AsyncTyper, console, create_table
|
|
12
|
+
from agentstack_cli.utils import (
|
|
13
|
+
status,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
app = AsyncTyper()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Transport(StrEnum):
|
|
20
|
+
SSE = "sse"
|
|
21
|
+
STREAMABLE_HTTP = "streamable_http"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("add")
|
|
25
|
+
async def add_provider(
|
|
26
|
+
name: typing.Annotated[str, typer.Argument(help="Name for the MCP server")],
|
|
27
|
+
location: typing.Annotated[str, typer.Argument(help="Location of the MCP server")],
|
|
28
|
+
transport: typing.Annotated[
|
|
29
|
+
Transport, typer.Argument(help="Transport the MCP server uses")
|
|
30
|
+
] = Transport.STREAMABLE_HTTP,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Install discovered MCP server."""
|
|
33
|
+
|
|
34
|
+
with status("Registering server to platform"):
|
|
35
|
+
await api_request(
|
|
36
|
+
"POST", "mcp/providers", json={"name": name, "location": location, "transport": transport.value}
|
|
37
|
+
)
|
|
38
|
+
console.print("Registering server to platform [[green]DONE[/green]]")
|
|
39
|
+
await list_providers()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("list")
|
|
43
|
+
async def list_providers():
|
|
44
|
+
"""List MCP servers."""
|
|
45
|
+
|
|
46
|
+
providers = await api_request("GET", "mcp/providers")
|
|
47
|
+
assert providers
|
|
48
|
+
with create_table(
|
|
49
|
+
Column("Name"),
|
|
50
|
+
Column("Location"),
|
|
51
|
+
Column("Transport"),
|
|
52
|
+
Column("State"),
|
|
53
|
+
no_wrap=True,
|
|
54
|
+
) as table:
|
|
55
|
+
for provider in providers:
|
|
56
|
+
table.add_row(provider["name"], provider["location"], provider["transport"], provider["state"])
|
|
57
|
+
console.print()
|
|
58
|
+
console.print(table)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("remove | uninstall | rm | delete")
|
|
62
|
+
async def uninstall_provider(
|
|
63
|
+
name: typing.Annotated[str, typer.Argument(help="Name of the MCP provider to remove")],
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Remove MCP server."""
|
|
66
|
+
provider = await _get_provider_by_name(name)
|
|
67
|
+
if provider:
|
|
68
|
+
await api_request("delete", f"mcp/providers/{provider['id']}")
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Provider {name} not found")
|
|
71
|
+
await list_providers()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
tool_app = AsyncTyper()
|
|
75
|
+
app.add_typer(tool_app, name="tool", no_args_is_help=True, help="Inspect tools.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@tool_app.command("list")
|
|
79
|
+
async def list_tools() -> None:
|
|
80
|
+
"""List tools."""
|
|
81
|
+
|
|
82
|
+
tools = await api_request("GET", "mcp/tools")
|
|
83
|
+
assert tools
|
|
84
|
+
with create_table(
|
|
85
|
+
Column("Name"),
|
|
86
|
+
Column("Description", max_width=30),
|
|
87
|
+
no_wrap=True,
|
|
88
|
+
) as table:
|
|
89
|
+
for tool in tools:
|
|
90
|
+
table.add_row(tool["name"], tool["description"])
|
|
91
|
+
console.print()
|
|
92
|
+
console.print(table)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
toolkit_app = AsyncTyper()
|
|
96
|
+
app.add_typer(toolkit_app, name="toolkit", no_args_is_help=True, help="Create toolkits.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@toolkit_app.command("create")
|
|
100
|
+
async def toolkit(
|
|
101
|
+
tools: typing.Annotated[list[str], typer.Argument(help="Tools to put in the toolkit")],
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Create a toolkit."""
|
|
104
|
+
|
|
105
|
+
api_tools = await _get_tools_by_names(tools)
|
|
106
|
+
assert api_tools
|
|
107
|
+
toolkit = await api_request("POST", "mcp/toolkits", json={"tools": [tool["id"] for tool in api_tools]})
|
|
108
|
+
assert toolkit
|
|
109
|
+
with create_table(Column("Location"), Column("Transport"), Column("Expiration")) as table:
|
|
110
|
+
table.add_row(toolkit["location"], toolkit["transport"], toolkit["expires_at"])
|
|
111
|
+
console.print()
|
|
112
|
+
console.print(table)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def _get_provider_by_name(name: str):
|
|
116
|
+
providers = await api_request("GET", "mcp/providers")
|
|
117
|
+
assert providers
|
|
118
|
+
|
|
119
|
+
for provider in providers:
|
|
120
|
+
if provider["name"] == name:
|
|
121
|
+
return provider
|
|
122
|
+
|
|
123
|
+
raise ValueError(f"Provider {name} not found")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def _get_tools_by_names(names: list[str]) -> list[dict[str, typing.Any]]:
|
|
127
|
+
all_tools = await api_request("GET", "mcp/tools")
|
|
128
|
+
assert all_tools
|
|
129
|
+
|
|
130
|
+
tools = []
|
|
131
|
+
for name in names:
|
|
132
|
+
found = False
|
|
133
|
+
for tool in all_tools:
|
|
134
|
+
if tool["name"] == name:
|
|
135
|
+
tools.append(tool)
|
|
136
|
+
found = True
|
|
137
|
+
break
|
|
138
|
+
if not found:
|
|
139
|
+
raise ValueError(f"Tool {name} not found")
|
|
140
|
+
|
|
141
|
+
return tools
|