agentstack-cli 0.6.0rc1__py3-none-manylinux_2_34_aarch64.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.
@@ -0,0 +1,198 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import datetime
5
+ import functools
6
+ import importlib.resources
7
+ import os
8
+ import pathlib
9
+ import platform
10
+ import shutil
11
+ import sys
12
+ import textwrap
13
+ import typing
14
+
15
+ import httpx
16
+ import typer
17
+ from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_delay, wait_fixed
18
+
19
+ from agentstack_cli.async_typer import AsyncTyper
20
+ from agentstack_cli.commands.platform.base_driver import BaseDriver
21
+ from agentstack_cli.commands.platform.lima_driver import LimaDriver
22
+ from agentstack_cli.commands.platform.wsl_driver import WSLDriver
23
+ from agentstack_cli.configuration import Configuration
24
+ from agentstack_cli.console import console
25
+ from agentstack_cli.utils import verbosity
26
+
27
+ app = AsyncTyper()
28
+
29
+ configuration = Configuration()
30
+
31
+
32
+ @functools.cache
33
+ def get_driver(vm_name: str = "agentstack") -> BaseDriver:
34
+ has_lima = (importlib.resources.files("agentstack_cli") / "data" / "limactl").is_file() or shutil.which("limactl")
35
+ has_vz = os.path.exists("/System/Library/Frameworks/Virtualization.framework")
36
+ arch = "aarch64" if platform.machine().lower() == "arm64" else platform.machine().lower()
37
+ has_qemu = bool(shutil.which(f"qemu-system-{arch}"))
38
+
39
+ if platform.system() == "Windows" or shutil.which("wsl.exe"):
40
+ return WSLDriver(vm_name=vm_name)
41
+ elif has_lima and (has_vz or has_qemu):
42
+ return LimaDriver(vm_name=vm_name)
43
+ else:
44
+ console.error("Could not find a compatible VM runtime.")
45
+ if platform.system() == "Darwin":
46
+ console.hint("This version of macOS is unsupported, please update the system.")
47
+ elif platform.system() == "Linux":
48
+ if not has_lima:
49
+ console.hint(
50
+ "This Linux distribution is not suppored by Lima VM binary releases (required: glibc>=2.34). Manually install Lima VM >=1.2.1 through either:\n"
51
+ + " - Your distribution's package manager, if available (https://repology.org/project/lima/versions)\n"
52
+ + " - Homebrew, which uses its own separate glibc on Linux (https://brew.sh)\n"
53
+ + " - Building it yourself, and ensuring that limactl is in PATH (https://lima-vm.io/docs/installation/source/)"
54
+ )
55
+ if not has_qemu:
56
+ console.hint(
57
+ f"QEMU is needed on Linux, please install it and ensure that qemu-system-{arch} is in PATH. Refer to https://www.qemu.org/download/ for instructions."
58
+ )
59
+ sys.exit(1)
60
+
61
+
62
+ @app.command("start", help="Start Agent Stack platform. [Local only]")
63
+ async def start(
64
+ set_values_list: typing.Annotated[
65
+ list[str], typer.Option("--set", help="Set Helm chart values using <key>=<value> syntax", default_factory=list)
66
+ ],
67
+ import_images: typing.Annotated[
68
+ list[str],
69
+ typer.Option(
70
+ "--import", help="Import an image from a local Docker CLI into Agent Stack platform", default_factory=list
71
+ ),
72
+ ],
73
+ pull_on_host: typing.Annotated[
74
+ bool,
75
+ typer.Option(
76
+ "--pull-on-host",
77
+ help="Pull images on host Docker daemon and import them instead of pulling inside the VM. Acts as a pull cache layer.",
78
+ ),
79
+ ] = False,
80
+ values_file: typing.Annotated[
81
+ pathlib.Path | None, typer.Option("-f", help="Set Helm chart values using yaml values file")
82
+ ] = None,
83
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
84
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
85
+ skip_pull: typing.Annotated[bool, typer.Option(hidden=True)] = False,
86
+ skip_restart_deployments: typing.Annotated[bool, typer.Option(hidden=True)] = False,
87
+ no_wait_for_platform: typing.Annotated[bool, typer.Option(hidden=True)] = False,
88
+ ):
89
+ import agentstack_cli.commands.server
90
+
91
+ values_file_path = None
92
+ if values_file:
93
+ values_file_path = pathlib.Path(values_file)
94
+ if not values_file_path.is_file():
95
+ raise FileNotFoundError(f"Values file {values_file} not found.")
96
+
97
+ with verbosity(verbose):
98
+ driver = get_driver(vm_name=vm_name)
99
+ await driver.create_vm()
100
+ await driver.install_tools()
101
+ await driver.deploy(
102
+ set_values_list=set_values_list,
103
+ values_file=values_file_path,
104
+ import_images=import_images,
105
+ pull_on_host=pull_on_host,
106
+ skip_pull=skip_pull,
107
+ skip_restart_deployments=skip_restart_deployments,
108
+ )
109
+
110
+ if not no_wait_for_platform:
111
+ with console.status("Waiting for Agent Stack platform to be ready...", spinner="dots"):
112
+ timeout = datetime.timedelta(minutes=20)
113
+ async with httpx.AsyncClient() as client:
114
+ try:
115
+ async for attempt in AsyncRetrying(
116
+ stop=stop_after_delay(timeout),
117
+ wait=wait_fixed(datetime.timedelta(seconds=1)),
118
+ retry=retry_if_exception_type((httpx.HTTPError, ConnectionError)),
119
+ reraise=True,
120
+ ):
121
+ with attempt:
122
+ resp = await client.get("http://localhost:8333/healthcheck")
123
+ resp.raise_for_status()
124
+ except Exception as ex:
125
+ raise ConnectionError(
126
+ f"Server did not start in {timeout}. Please check your internet connection."
127
+ ) from ex
128
+
129
+ console.success("Agent Stack platform started successfully!")
130
+
131
+ if any("phoenix.enabled=true" in value.lower() for value in set_values_list):
132
+ console.print(
133
+ textwrap.dedent("""\
134
+
135
+ License Notice:
136
+ When you enable Phoenix, be aware that Arize Phoenix is licensed under the Elastic License v2 (ELv2),
137
+ which has specific terms regarding commercial use and distribution. By enabling Phoenix, you acknowledge
138
+ that you are responsible for ensuring compliance with the ELv2 license terms for your specific use case.
139
+ Please review the Phoenix license (https://github.com/Arize-ai/phoenix/blob/main/LICENSE) before enabling
140
+ this feature in production environments.
141
+ """),
142
+ style="dim",
143
+ )
144
+
145
+ await agentstack_cli.commands.server.server_login("http://localhost:8333")
146
+
147
+
148
+ @app.command("stop", help="Stop Agent Stack platform. [Local only]")
149
+ async def stop(
150
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
151
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
152
+ ):
153
+ with verbosity(verbose):
154
+ driver = get_driver(vm_name=vm_name)
155
+ if not await driver.status():
156
+ console.info("Agent Stack platform not found. Nothing to stop.")
157
+ return
158
+ await driver.stop()
159
+ console.success("Agent Stack platform stopped successfully.")
160
+
161
+
162
+ @app.command("delete", help="Delete Agent Stack platform. [Local only]")
163
+ async def delete(
164
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
165
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
166
+ ):
167
+ with verbosity(verbose):
168
+ driver = get_driver(vm_name=vm_name)
169
+ await driver.delete()
170
+ console.success("Agent Stack platform deleted successfully.")
171
+
172
+
173
+ @app.command("import", help="Import a local docker image into the Agent Stack platform. [Local only]")
174
+ async def import_image_cmd(
175
+ tag: typing.Annotated[str, typer.Argument(help="Docker image tag to import")],
176
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
177
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
178
+ ):
179
+ with verbosity(verbose):
180
+ driver = get_driver(vm_name=vm_name)
181
+ if (await driver.status()) != "running":
182
+ console.error("Agent Stack platform is not running.")
183
+ sys.exit(1)
184
+ await driver.import_images(tag)
185
+
186
+
187
+ @app.command("exec", help="For debugging -- execute a command inside the Agent Stack platform VM. [Local only]")
188
+ async def exec_cmd(
189
+ command: typing.Annotated[list[str] | None, typer.Argument()] = None,
190
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
191
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
192
+ ):
193
+ with verbosity(verbose, show_success_status=False):
194
+ driver = get_driver(vm_name=vm_name)
195
+ if (await driver.status()) != "running":
196
+ console.error("Agent Stack platform is not running.")
197
+ sys.exit(1)
198
+ await driver.exec(command or ["/bin/bash"])
@@ -0,0 +1,217 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import abc
5
+ import importlib.resources
6
+ import pathlib
7
+ import shlex
8
+ import typing
9
+ from subprocess import CompletedProcess
10
+ from textwrap import dedent
11
+
12
+ import anyio
13
+ import yaml
14
+ from tenacity import AsyncRetrying, stop_after_attempt
15
+
16
+ from agentstack_cli.configuration import Configuration
17
+ from agentstack_cli.utils import merge, run_command
18
+
19
+
20
+ class BaseDriver(abc.ABC):
21
+ vm_name: str
22
+
23
+ def __init__(self, vm_name: str = "agentstack"):
24
+ self.vm_name = vm_name
25
+ self.loaded_images: set[str] = set()
26
+
27
+ @abc.abstractmethod
28
+ async def run_in_vm(
29
+ self,
30
+ command: list[str],
31
+ message: str,
32
+ env: dict[str, str] | None = None,
33
+ input: bytes | None = None,
34
+ ) -> CompletedProcess[bytes]: ...
35
+
36
+ @abc.abstractmethod
37
+ async def status(self) -> typing.Literal["running"] | str | None: ...
38
+
39
+ @abc.abstractmethod
40
+ async def create_vm(self) -> None: ...
41
+
42
+ @abc.abstractmethod
43
+ async def stop(self) -> None: ...
44
+
45
+ @abc.abstractmethod
46
+ async def delete(self) -> None: ...
47
+
48
+ @abc.abstractmethod
49
+ async def import_images(self, *tags: str) -> None: ...
50
+
51
+ @abc.abstractmethod
52
+ async def import_image_to_internal_registry(self, tag: str) -> None: ...
53
+
54
+ @abc.abstractmethod
55
+ async def exec(self, command: list[str]) -> None: ...
56
+
57
+ async def install_tools(self) -> None:
58
+ # Configure k3s registry for local registry access
59
+ registry_config = dedent(
60
+ """\
61
+ mirrors:
62
+ "agentstack-registry-svc.default:5001":
63
+ endpoint:
64
+ - "http://localhost:30501"
65
+ configs:
66
+ "agentstack-registry-svc.default:5001":
67
+ tls:
68
+ insecure_skip_verify: true
69
+ """
70
+ )
71
+
72
+ await self.run_in_vm(
73
+ [
74
+ "sh",
75
+ "-c",
76
+ (
77
+ f"sudo mkdir -p /etc/rancher/k3s /registry-data && "
78
+ f"echo '{registry_config}' | "
79
+ "sudo tee /etc/rancher/k3s/registries.yaml > /dev/null"
80
+ ),
81
+ ],
82
+ "Configuring k3s registry",
83
+ )
84
+
85
+ await self.run_in_vm(
86
+ [
87
+ "sh",
88
+ "-c",
89
+ "which k3s || curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644 --https-listen-port=16443",
90
+ ],
91
+ "Installing k3s",
92
+ )
93
+ await self.run_in_vm(
94
+ [
95
+ "sh",
96
+ "-c",
97
+ "which helm || curl -sfL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash",
98
+ ],
99
+ "Installing Helm",
100
+ )
101
+
102
+ async def deploy(
103
+ self,
104
+ set_values_list: list[str],
105
+ values_file: pathlib.Path | None = None,
106
+ import_images: list[str] | None = None,
107
+ pull_on_host: bool = False,
108
+ skip_pull: bool = False,
109
+ skip_restart_deployments: bool = False,
110
+ ) -> None:
111
+ _ = await self.run_in_vm(
112
+ ["sh", "-c", "mkdir -p /tmp/agentstack && cat >/tmp/agentstack/chart.tgz"],
113
+ "Preparing Helm chart",
114
+ input=(importlib.resources.files("agentstack_cli") / "data" / "helm-chart.tgz").read_bytes(),
115
+ )
116
+ values = {
117
+ **{svc: {"service": {"type": "LoadBalancer"}} for svc in ["collector", "docling", "ui", "phoenix"]},
118
+ "service": {"type": "LoadBalancer"},
119
+ "externalRegistries": {"public_github": str(Configuration().agent_registry)},
120
+ "encryptionKey": "Ovx8qImylfooq4-HNwOzKKDcXLZCB3c_m0JlB9eJBxc=",
121
+ "trustProxyHeaders": True,
122
+ "keycloak": {
123
+ "uiClientSecret": "agentstack-ui-secret",
124
+ "serverClientSecret": "agentstack-server-secret",
125
+ "service": {"type": "LoadBalancer"},
126
+ "auth": {"adminPassword": "admin"},
127
+ },
128
+ "features": {"uiLocalSetup": True},
129
+ "providerBuilds": {"enabled": True},
130
+ "localDockerRegistry": {"enabled": True},
131
+ "auth": {"enabled": False},
132
+ }
133
+ if values_file:
134
+ values = merge(values, yaml.safe_load(values_file.read_text()))
135
+ await self.run_in_vm(
136
+ ["sh", "-c", "cat >/tmp/agentstack/values.yaml"],
137
+ "Preparing Helm values",
138
+ input=yaml.dump(values).encode("utf-8"),
139
+ )
140
+
141
+ images_str = (
142
+ await self.run_in_vm(
143
+ [
144
+ "/bin/bash",
145
+ "-c",
146
+ "helm template agentstack /tmp/agentstack/chart.tgz --values=/tmp/agentstack/values.yaml "
147
+ + " ".join(shlex.quote(f"--set={value}") for value in set_values_list)
148
+ + " | sed -n '/^\\s*image:/{ /{{/!{ s/.*image:\\s*//p } }'",
149
+ ],
150
+ "Listing necessary images",
151
+ )
152
+ ).stdout.decode()
153
+
154
+ def canonify(tag: str) -> str:
155
+ return tag if "." in tag.split("/")[0] else f"docker.io/{tag}"
156
+
157
+ required_images = {canonify(typing.cast(str, yaml.safe_load(line))) for line in images_str.splitlines()}
158
+ images_to_import = {canonify(tag) for tag in import_images or []}
159
+ images_to_pull = required_images - images_to_import
160
+
161
+ if not skip_pull:
162
+ if pull_on_host:
163
+ for image in images_to_pull:
164
+ await run_command(["docker", "pull", image], f"Pulling image {image} on host")
165
+ images_to_import = required_images
166
+ images_to_pull = set[str]()
167
+
168
+ if images_to_import:
169
+ await self.import_images(*images_to_import)
170
+
171
+ for image in images_to_pull:
172
+ async for attempt in AsyncRetrying(stop=stop_after_attempt(5)):
173
+ with attempt:
174
+ attempt_num = attempt.retry_state.attempt_number
175
+ await self.run_in_vm(
176
+ ["k3s", "ctr", "image", "pull", image],
177
+ f"Pulling image {image}" + (f" (attempt {attempt_num})" if attempt_num > 1 else ""),
178
+ )
179
+ elif images_to_import:
180
+ await self.import_images(*images_to_import)
181
+
182
+ self.loaded_images = required_images
183
+
184
+ kubeconfig_path = anyio.Path(Configuration().lima_home) / self.vm_name / "copied-from-guest" / "kubeconfig.yaml"
185
+ await kubeconfig_path.parent.mkdir(parents=True, exist_ok=True)
186
+ await kubeconfig_path.write_text(
187
+ (
188
+ await self.run_in_vm(
189
+ ["/bin/cat", "/etc/rancher/k3s/k3s.yaml"],
190
+ "Copying kubeconfig from Agent Stack platform",
191
+ )
192
+ ).stdout.decode()
193
+ )
194
+
195
+ await self.run_in_vm(
196
+ [
197
+ "helm",
198
+ "upgrade",
199
+ "--install",
200
+ "agentstack",
201
+ "/tmp/agentstack/chart.tgz",
202
+ "--namespace=default",
203
+ "--create-namespace",
204
+ "--values=/tmp/agentstack/values.yaml",
205
+ "--timeout=20m",
206
+ "--wait",
207
+ "--kubeconfig=/etc/rancher/k3s/k3s.yaml",
208
+ *(f"--set={value}" for value in set_values_list),
209
+ ],
210
+ "Deploying Agent Stack platform with Helm",
211
+ )
212
+
213
+ if import_images and not skip_restart_deployments:
214
+ await self.run_in_vm(
215
+ ["k3s", "kubectl", "rollout", "restart", "deployment"],
216
+ "Restarting deployments to load imported images",
217
+ )