agentstack-cli 0.4.3rc2__tar.gz → 0.5.0__tar.gz
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-0.4.3rc2 → agentstack_cli-0.5.0}/PKG-INFO +1 -1
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/pyproject.toml +1 -1
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/__init__.py +22 -3
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/api.py +24 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/agent.py +186 -15
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/build.py +6 -5
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/model.py +2 -1
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/base_driver.py +0 -25
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/self.py +4 -1
- agentstack_cli-0.5.0/src/agentstack_cli/commands/user.py +92 -0
- agentstack_cli-0.5.0/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/utils.py +10 -5
- agentstack_cli-0.4.3rc2/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/README.md +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/async_typer.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/auth_manager.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/mcp.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/__init__.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/istio.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/lima_driver.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/wsl_driver.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/server.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/configuration.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/data/.gitignore +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
|
+
import re
|
|
5
6
|
import typing
|
|
6
7
|
from copy import deepcopy
|
|
7
8
|
|
|
@@ -14,6 +15,7 @@ import agentstack_cli.commands.model
|
|
|
14
15
|
import agentstack_cli.commands.platform
|
|
15
16
|
import agentstack_cli.commands.self
|
|
16
17
|
import agentstack_cli.commands.server
|
|
18
|
+
import agentstack_cli.commands.user
|
|
17
19
|
from agentstack_cli.async_typer import AliasGroup, AsyncTyper
|
|
18
20
|
from agentstack_cli.configuration import Configuration
|
|
19
21
|
|
|
@@ -47,6 +49,7 @@ Usage: agentstack [OPTIONS] COMMAND [ARGS]...
|
|
|
47
49
|
│ model Configure 15+ LLM providers │
|
|
48
50
|
│ platform Start, stop, or delete local platform │
|
|
49
51
|
│ server Connect to remote Agent Stack servers │
|
|
52
|
+
│ user Manage users and roles │
|
|
50
53
|
│ self version Show Agent Stack CLI and Platform version │
|
|
51
54
|
│ self upgrade Upgrade Agent Stack CLI and Platform │
|
|
52
55
|
│ self uninstall Uninstall Agent Stack CLI and Platform │
|
|
@@ -83,6 +86,12 @@ app.add_typer(
|
|
|
83
86
|
help="Manage Agent Stack installation.",
|
|
84
87
|
hidden=True,
|
|
85
88
|
)
|
|
89
|
+
app.add_typer(
|
|
90
|
+
agentstack_cli.commands.user.app,
|
|
91
|
+
name="user",
|
|
92
|
+
no_args_is_help=True,
|
|
93
|
+
help="Manage users.",
|
|
94
|
+
)
|
|
86
95
|
|
|
87
96
|
|
|
88
97
|
agent_alias = deepcopy(agentstack_cli.commands.agent.app)
|
|
@@ -110,9 +119,19 @@ async def ui():
|
|
|
110
119
|
import agentstack_cli.commands.model
|
|
111
120
|
|
|
112
121
|
await agentstack_cli.commands.model.ensure_llm_provider()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
|
|
123
|
+
config = Configuration()
|
|
124
|
+
active_server = config.auth_manager.active_server
|
|
125
|
+
|
|
126
|
+
if active_server:
|
|
127
|
+
if re.search(r"(localhost|127\.0\.0\.1):8333", active_server):
|
|
128
|
+
ui_url = re.sub(r":8333", ":8334", active_server)
|
|
129
|
+
else:
|
|
130
|
+
ui_url = active_server
|
|
131
|
+
else:
|
|
132
|
+
ui_url = "http://localhost:8334"
|
|
133
|
+
|
|
134
|
+
webbrowser.open(ui_url)
|
|
116
135
|
|
|
117
136
|
|
|
118
137
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import re
|
|
5
6
|
import urllib
|
|
6
7
|
import urllib.parse
|
|
@@ -12,6 +13,7 @@ from typing import Any
|
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
14
15
|
import openai
|
|
16
|
+
import pydantic
|
|
15
17
|
from a2a.client import A2AClientHTTPError, Client, ClientConfig, ClientFactory
|
|
16
18
|
from a2a.types import AgentCard
|
|
17
19
|
from httpx import HTTPStatusError
|
|
@@ -20,6 +22,8 @@ from httpx._types import RequestFiles
|
|
|
20
22
|
from agentstack_cli import configuration
|
|
21
23
|
from agentstack_cli.configuration import Configuration
|
|
22
24
|
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
23
27
|
config = Configuration()
|
|
24
28
|
|
|
25
29
|
API_BASE_URL = "api/v1/"
|
|
@@ -102,6 +106,26 @@ async def api_stream(
|
|
|
102
106
|
yield jsonlib.loads(re.sub("^data:", "", line).strip())
|
|
103
107
|
|
|
104
108
|
|
|
109
|
+
async def fetch_server_version() -> str | None:
|
|
110
|
+
"""Fetch server version from OpenAPI schema."""
|
|
111
|
+
|
|
112
|
+
class OpenAPIInfo(pydantic.BaseModel):
|
|
113
|
+
version: str
|
|
114
|
+
|
|
115
|
+
class OpenAPISchema(pydantic.BaseModel):
|
|
116
|
+
info: OpenAPIInfo
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
response = await api_request("GET", "openapi.json", use_auth=False)
|
|
120
|
+
if not response:
|
|
121
|
+
return None
|
|
122
|
+
schema = OpenAPISchema.model_validate(response)
|
|
123
|
+
return schema.info.version
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.warning("Failed to fetch server version: %s", e)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
105
129
|
@asynccontextmanager
|
|
106
130
|
async def a2a_client(agent_card: AgentCard, use_auth: bool = True) -> AsyncIterator[Client]:
|
|
107
131
|
try:
|
|
@@ -61,7 +61,7 @@ from agentstack_sdk.a2a.extensions.common.form import (
|
|
|
61
61
|
TextField,
|
|
62
62
|
TextFieldValue,
|
|
63
63
|
)
|
|
64
|
-
from agentstack_sdk.platform import BuildState, ModelProvider, Provider
|
|
64
|
+
from agentstack_sdk.platform import BuildState, File, ModelProvider, Provider, UserFeedback
|
|
65
65
|
from agentstack_sdk.platform.context import Context, ContextPermissions, ContextToken, Permissions
|
|
66
66
|
from agentstack_sdk.platform.model_provider import ModelCapability
|
|
67
67
|
from InquirerPy import inquirer
|
|
@@ -152,12 +152,50 @@ configuration = Configuration()
|
|
|
152
152
|
|
|
153
153
|
@app.command("add")
|
|
154
154
|
async def add_agent(
|
|
155
|
-
location: typing.Annotated[
|
|
155
|
+
location: typing.Annotated[
|
|
156
|
+
str | None, typer.Argument(help="Agent location (public docker image or github url)")
|
|
157
|
+
] = None,
|
|
156
158
|
dockerfile: typing.Annotated[str | None, typer.Option(help="Use custom dockerfile path")] = None,
|
|
157
159
|
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
158
160
|
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
159
161
|
) -> None:
|
|
160
162
|
"""Add a docker image or GitHub repository [aliases: install]"""
|
|
163
|
+
if location is None:
|
|
164
|
+
repo_input = (
|
|
165
|
+
await inquirer.text( # pyright: ignore[reportPrivateImportUsage]
|
|
166
|
+
message="Enter GitHub repository (owner/repo or full URL):",
|
|
167
|
+
).execute_async()
|
|
168
|
+
or ""
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
match = re.search(r"^(?:(?:https?://)?(?:www\.)?github\.com/)?([^/]+)/([^/?&]+)", repo_input)
|
|
172
|
+
if not match:
|
|
173
|
+
raise ValueError(f"Invalid GitHub URL format: {repo_input}. Expected 'owner/repo' or a full GitHub URL.")
|
|
174
|
+
|
|
175
|
+
owner, repo = match.group(1), match.group(2).removesuffix(".git")
|
|
176
|
+
|
|
177
|
+
async with httpx.AsyncClient() as client:
|
|
178
|
+
response = await client.get(
|
|
179
|
+
f"https://api.github.com/repos/{owner}/{repo}/tags",
|
|
180
|
+
headers={"Accept": "application/vnd.github.v3+json"},
|
|
181
|
+
)
|
|
182
|
+
tags = [tag["name"] for tag in response.json()] if response.status_code == 200 else []
|
|
183
|
+
|
|
184
|
+
if tags:
|
|
185
|
+
selected_tag = await inquirer.fuzzy( # pyright: ignore[reportPrivateImportUsage]
|
|
186
|
+
message="Select a tag to use:",
|
|
187
|
+
choices=tags,
|
|
188
|
+
).execute_async()
|
|
189
|
+
else:
|
|
190
|
+
selected_tag = (
|
|
191
|
+
await inquirer.text( # pyright: ignore[reportPrivateImportUsage]
|
|
192
|
+
message="Enter tag to use:",
|
|
193
|
+
).execute_async()
|
|
194
|
+
or "main"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
location = f"https://github.com/{owner}/{repo}@{selected_tag}"
|
|
198
|
+
|
|
161
199
|
url = announce_server_action(f"Installing agent '{location}' for")
|
|
162
200
|
await confirm_server_action("Proceed with installing this agent on", url=url, yes=yes)
|
|
163
201
|
with verbosity(verbose):
|
|
@@ -182,9 +220,11 @@ async def add_agent(
|
|
|
182
220
|
@app.command("update")
|
|
183
221
|
async def update_agent(
|
|
184
222
|
search_path: typing.Annotated[
|
|
185
|
-
str, typer.Argument(
|
|
186
|
-
],
|
|
187
|
-
location: typing.Annotated[
|
|
223
|
+
str | None, typer.Argument(help="Short ID, agent name or part of the provider location of agent to replace")
|
|
224
|
+
] = None,
|
|
225
|
+
location: typing.Annotated[
|
|
226
|
+
str | None, typer.Argument(help="Agent location (public docker image or github url)")
|
|
227
|
+
] = None,
|
|
188
228
|
dockerfile: typing.Annotated[str | None, typer.Option(help="Use custom dockerfile path")] = None,
|
|
189
229
|
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
190
230
|
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
@@ -192,7 +232,58 @@ async def update_agent(
|
|
|
192
232
|
"""Upgrade agent to a newer docker image or build from GitHub repository"""
|
|
193
233
|
with verbosity(verbose):
|
|
194
234
|
async with configuration.use_platform_client():
|
|
195
|
-
|
|
235
|
+
providers = await Provider.list()
|
|
236
|
+
|
|
237
|
+
if search_path is None:
|
|
238
|
+
if not providers:
|
|
239
|
+
console.error("No agents found. Add an agent first using 'agentstack agent add'.")
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
provider_choices = [
|
|
243
|
+
Choice(value=p, name=f"{p.agent_card.name} ({ProviderUtils.short_location(p)})") for p in providers
|
|
244
|
+
]
|
|
245
|
+
provider = await inquirer.fuzzy( # pyright: ignore[reportPrivateImportUsage]
|
|
246
|
+
message="Select an agent to update:",
|
|
247
|
+
choices=provider_choices,
|
|
248
|
+
).execute_async()
|
|
249
|
+
if not provider:
|
|
250
|
+
console.error("No agent selected. Exiting.")
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
else:
|
|
253
|
+
provider = select_provider(search_path, providers=providers)
|
|
254
|
+
|
|
255
|
+
if location is None and is_github_url(provider.source):
|
|
256
|
+
match = re.search(r"^(?:(?:https?://)?(?:www\.)?github\.com/)?([^/]+)/([^/@?&]+)", provider.source)
|
|
257
|
+
if match:
|
|
258
|
+
owner, repo = match.group(1), match.group(2).removesuffix(".git")
|
|
259
|
+
|
|
260
|
+
async with httpx.AsyncClient() as client:
|
|
261
|
+
response = await client.get(
|
|
262
|
+
f"https://api.github.com/repos/{owner}/{repo}/tags",
|
|
263
|
+
headers={"Accept": "application/vnd.github.v3+json"},
|
|
264
|
+
)
|
|
265
|
+
tags = [tag["name"] for tag in response.json()] if response.status_code == 200 else []
|
|
266
|
+
|
|
267
|
+
if tags:
|
|
268
|
+
selected_tag = await inquirer.fuzzy( # pyright: ignore[reportPrivateImportUsage]
|
|
269
|
+
message="Select a new tag to use:",
|
|
270
|
+
choices=tags,
|
|
271
|
+
).execute_async()
|
|
272
|
+
if selected_tag:
|
|
273
|
+
location = f"https://github.com/{owner}/{repo}@{selected_tag}"
|
|
274
|
+
|
|
275
|
+
if location is None:
|
|
276
|
+
location = (
|
|
277
|
+
await inquirer.text( # pyright: ignore[reportPrivateImportUsage]
|
|
278
|
+
message="Enter new agent location (public docker image or github url):",
|
|
279
|
+
default=provider.source,
|
|
280
|
+
).execute_async()
|
|
281
|
+
or ""
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if not location:
|
|
285
|
+
console.error("No location provided. Exiting.")
|
|
286
|
+
sys.exit(1)
|
|
196
287
|
|
|
197
288
|
url = announce_server_action(f"Upgrading agent from '{provider.source}' to {location}")
|
|
198
289
|
await confirm_server_action("Proceed with upgrading agent on", url=url, yes=yes)
|
|
@@ -560,10 +651,13 @@ async def _run_agent(
|
|
|
560
651
|
match part.root.file:
|
|
561
652
|
case FileWithBytes(bytes=bytes_str):
|
|
562
653
|
full_path.write_bytes(base64.b64decode(bytes_str))
|
|
563
|
-
case FileWithUri(uri=uri):
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
654
|
+
case FileWithUri(uri=uri):
|
|
655
|
+
if uri.startswith("agentstack://"):
|
|
656
|
+
async with File.load_content(uri.removeprefix("agentstack://")) as file:
|
|
657
|
+
full_path.write_bytes(file.content)
|
|
658
|
+
else:
|
|
659
|
+
async with httpx.AsyncClient() as httpx_client:
|
|
660
|
+
full_path.write_bytes((await httpx_client.get(uri)).content)
|
|
567
661
|
console.print(f"📁 Saved {full_path}")
|
|
568
662
|
case TextPart(text=text):
|
|
569
663
|
full_path.write_text(text)
|
|
@@ -770,24 +864,41 @@ def _create_input_handler(
|
|
|
770
864
|
@app.command("run")
|
|
771
865
|
async def run_agent(
|
|
772
866
|
search_path: typing.Annotated[
|
|
773
|
-
str
|
|
774
|
-
|
|
867
|
+
str | None,
|
|
868
|
+
typer.Argument(
|
|
869
|
+
help="Short ID, agent name or part of the provider location",
|
|
870
|
+
),
|
|
871
|
+
] = None,
|
|
775
872
|
input: typing.Annotated[
|
|
776
873
|
str | None,
|
|
777
874
|
typer.Argument(
|
|
778
|
-
default_factory=lambda: None if sys.stdin.isatty() else sys.stdin.read(),
|
|
779
875
|
help="Agent input as text or JSON",
|
|
780
876
|
),
|
|
781
|
-
],
|
|
877
|
+
] = None,
|
|
782
878
|
dump_files: typing.Annotated[
|
|
783
879
|
Path | None, typer.Option(help="Folder path to save any files returned by the agent")
|
|
784
880
|
] = None,
|
|
785
881
|
) -> None:
|
|
786
882
|
"""Run an agent."""
|
|
787
|
-
|
|
883
|
+
if search_path is not None and input is None and sys.stdin.isatty():
|
|
884
|
+
input = sys.stdin.read()
|
|
788
885
|
async with configuration.use_platform_client():
|
|
789
886
|
providers = await Provider.list()
|
|
790
887
|
await ensure_llm_provider()
|
|
888
|
+
|
|
889
|
+
if search_path is None:
|
|
890
|
+
if not providers:
|
|
891
|
+
err_console.error("No agents found. Add an agent first using 'agentstack agent add'.")
|
|
892
|
+
sys.exit(1)
|
|
893
|
+
search_path = await inquirer.fuzzy( # pyright: ignore[reportPrivateImportUsage]
|
|
894
|
+
message="Select an agent to run:",
|
|
895
|
+
choices=[provider.agent_card.name for provider in providers],
|
|
896
|
+
).execute_async()
|
|
897
|
+
if search_path is None:
|
|
898
|
+
err_console.error("No agent selected. Exiting.")
|
|
899
|
+
sys.exit(1)
|
|
900
|
+
|
|
901
|
+
announce_server_action(f"Running agent '{search_path}' on")
|
|
791
902
|
provider = select_provider(search_path, providers=providers)
|
|
792
903
|
|
|
793
904
|
context = await Context.create(
|
|
@@ -1053,3 +1164,63 @@ async def remove_env(
|
|
|
1053
1164
|
provider = select_provider(search_path, await Provider.list())
|
|
1054
1165
|
await provider.update_variables(variables=dict.fromkeys(env))
|
|
1055
1166
|
await _list_env(provider)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
feedback_app = AsyncTyper()
|
|
1170
|
+
app.add_typer(feedback_app, name="feedback", help="Manage user feedback for your agents", no_args_is_help=True)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
@feedback_app.command("list")
|
|
1174
|
+
async def list_feedback(
|
|
1175
|
+
search_path: typing.Annotated[
|
|
1176
|
+
str | None, typer.Argument(help="Short ID, agent name or part of the provider location")
|
|
1177
|
+
] = None,
|
|
1178
|
+
limit: typing.Annotated[int, typer.Option("--limit", help="Number of results per page [default: 50]")] = 50,
|
|
1179
|
+
after_cursor: typing.Annotated[str | None, typer.Option("--after", help="Cursor for pagination")] = None,
|
|
1180
|
+
):
|
|
1181
|
+
"""List your agent feedback"""
|
|
1182
|
+
|
|
1183
|
+
announce_server_action("Listing feedback on")
|
|
1184
|
+
|
|
1185
|
+
provider_id = None
|
|
1186
|
+
|
|
1187
|
+
async with configuration.use_platform_client():
|
|
1188
|
+
if search_path:
|
|
1189
|
+
providers = await Provider.list()
|
|
1190
|
+
provider = select_provider(search_path, providers)
|
|
1191
|
+
provider_id = str(provider.id)
|
|
1192
|
+
|
|
1193
|
+
response = await UserFeedback.list(
|
|
1194
|
+
provider_id=provider_id,
|
|
1195
|
+
limit=limit,
|
|
1196
|
+
after_cursor=after_cursor,
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
if not response.items:
|
|
1200
|
+
console.print("No feedback found.")
|
|
1201
|
+
return
|
|
1202
|
+
|
|
1203
|
+
with create_table(
|
|
1204
|
+
Column("Rating", style="yellow", ratio=1),
|
|
1205
|
+
Column("Agent", style="cyan", ratio=2),
|
|
1206
|
+
Column("Task ID", style="dim", ratio=1),
|
|
1207
|
+
Column("Comment", ratio=3),
|
|
1208
|
+
Column("Tags", ratio=2),
|
|
1209
|
+
Column("Date", style="dim", ratio=1),
|
|
1210
|
+
) as table:
|
|
1211
|
+
for item in response.items:
|
|
1212
|
+
rating_icon = "✓" if item.rating == 1 else "✗"
|
|
1213
|
+
agent_name = item.agent_name or str(item.provider_id)[:8]
|
|
1214
|
+
task_id_short = str(item.task_id)[:8]
|
|
1215
|
+
comment = item.comment or ""
|
|
1216
|
+
if len(comment) > 50:
|
|
1217
|
+
comment = comment[:50] + "..."
|
|
1218
|
+
tags = ", ".join(item.comment_tags or []) if item.comment_tags else "-"
|
|
1219
|
+
created_at = item.created_at.strftime("%Y-%m-%d")
|
|
1220
|
+
|
|
1221
|
+
table.add_row(rating_icon, agent_name, task_id_short, comment, tags, created_at)
|
|
1222
|
+
|
|
1223
|
+
console.print(table)
|
|
1224
|
+
console.print(f"Showing {len(response.items)} of {response.total_count} total feedback entries")
|
|
1225
|
+
if response.has_more and response.next_page_token:
|
|
1226
|
+
console.print(f"Use --after {response.next_page_token} to see more")
|
|
@@ -152,10 +152,10 @@ async def _server_side_build(
|
|
|
152
152
|
verbose: bool = False,
|
|
153
153
|
) -> ProviderBuild:
|
|
154
154
|
build = None
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
from agentstack_cli.configuration import Configuration
|
|
155
|
+
from agentstack_cli.commands.agent import select_provider
|
|
156
|
+
from agentstack_cli.configuration import Configuration
|
|
158
157
|
|
|
158
|
+
try:
|
|
159
159
|
if replace and add:
|
|
160
160
|
raise ValueError("Cannot specify both replace and add options.")
|
|
161
161
|
|
|
@@ -181,8 +181,9 @@ async def _server_side_build(
|
|
|
181
181
|
print_log(message, ansi_mode=True, out_console=err_console)
|
|
182
182
|
return await build.get()
|
|
183
183
|
except (KeyboardInterrupt, CancelledError):
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
async with Configuration().use_platform_client():
|
|
185
|
+
if build:
|
|
186
|
+
await build.delete()
|
|
186
187
|
console.error("Build aborted.")
|
|
187
188
|
raise
|
|
188
189
|
|
|
@@ -354,7 +354,8 @@ async def _select_default_model(capability: ModelCapability) -> str | None:
|
|
|
354
354
|
if capability == ModelCapability.LLM:
|
|
355
355
|
test_response = await client.chat.completions.create(
|
|
356
356
|
model=selected_model,
|
|
357
|
-
|
|
357
|
+
# reasoning models need some tokens to think about this
|
|
358
|
+
max_completion_tokens=500 if not selected_model.startswith("mistral") else None,
|
|
358
359
|
messages=[
|
|
359
360
|
{
|
|
360
361
|
"role": "system",
|
{agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/base_driver.py
RENAMED
|
@@ -10,7 +10,6 @@ from subprocess import CompletedProcess
|
|
|
10
10
|
from textwrap import dedent
|
|
11
11
|
|
|
12
12
|
import anyio
|
|
13
|
-
import pydantic
|
|
14
13
|
import yaml
|
|
15
14
|
from tenacity import AsyncRetrying, stop_after_attempt
|
|
16
15
|
|
|
@@ -193,27 +192,3 @@ class BaseDriver(abc.ABC):
|
|
|
193
192
|
["k3s", "kubectl", "rollout", "restart", "deployment"],
|
|
194
193
|
"Restarting deployments to load imported images",
|
|
195
194
|
)
|
|
196
|
-
|
|
197
|
-
async def version(self) -> str | None:
|
|
198
|
-
if (await self.status()) != "running":
|
|
199
|
-
return None
|
|
200
|
-
HelmStatus = typing.TypedDict("HelmStatus", {"status": str, "app_version": str})
|
|
201
|
-
helm_status = pydantic.TypeAdapter(list[HelmStatus]).validate_json(
|
|
202
|
-
(
|
|
203
|
-
await self.run_in_vm(
|
|
204
|
-
[
|
|
205
|
-
"/usr/local/bin/helm",
|
|
206
|
-
"--kubeconfig=/etc/rancher/k3s/k3s.yaml",
|
|
207
|
-
"ls",
|
|
208
|
-
"--namespace=default",
|
|
209
|
-
"--filter=^agentstack$",
|
|
210
|
-
"-o",
|
|
211
|
-
"json",
|
|
212
|
-
],
|
|
213
|
-
"Getting Agent Stack platform version",
|
|
214
|
-
)
|
|
215
|
-
).stdout
|
|
216
|
-
)
|
|
217
|
-
if helm_status[0]["status"] != "deployed":
|
|
218
|
-
return None
|
|
219
|
-
return helm_status[0]["app_version"]
|
|
@@ -15,6 +15,7 @@ import typer
|
|
|
15
15
|
from InquirerPy import inquirer
|
|
16
16
|
|
|
17
17
|
import agentstack_cli.commands.platform
|
|
18
|
+
from agentstack_cli.api import fetch_server_version
|
|
18
19
|
from agentstack_cli.async_typer import AsyncTyper
|
|
19
20
|
from agentstack_cli.commands.model import setup as model_setup
|
|
20
21
|
from agentstack_cli.configuration import Configuration
|
|
@@ -46,7 +47,8 @@ async def version(
|
|
|
46
47
|
"""Print version of the Agent Stack CLI."""
|
|
47
48
|
with verbosity(verbose=verbose):
|
|
48
49
|
cli_version = importlib.metadata.version("agentstack-cli")
|
|
49
|
-
platform_version = await
|
|
50
|
+
platform_version = await fetch_server_version()
|
|
51
|
+
active_server = configuration.auth_manager.active_server
|
|
50
52
|
|
|
51
53
|
latest_cli_version: str | None = None
|
|
52
54
|
with console.status("Checking for newer version...", spinner="dots"):
|
|
@@ -64,6 +66,7 @@ async def version(
|
|
|
64
66
|
console.print(
|
|
65
67
|
f"agentstack-platform version: [bold]{platform_version.replace('-', '') if platform_version is not None else 'not running'}[/bold]"
|
|
66
68
|
)
|
|
69
|
+
console.print(f" agentstack server: [bold]{active_server if active_server else 'none'}[/bold]")
|
|
67
70
|
console.print()
|
|
68
71
|
|
|
69
72
|
if latest_cli_version and packaging.version.parse(latest_cli_version) > packaging.version.parse(cli_version):
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import typing
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from agentstack_sdk.platform import User
|
|
9
|
+
from agentstack_sdk.platform.user import UserRole
|
|
10
|
+
from rich.table import Column
|
|
11
|
+
|
|
12
|
+
from agentstack_cli.async_typer import AsyncTyper, console, create_table
|
|
13
|
+
from agentstack_cli.configuration import Configuration
|
|
14
|
+
from agentstack_cli.utils import announce_server_action, confirm_server_action
|
|
15
|
+
|
|
16
|
+
app = AsyncTyper()
|
|
17
|
+
configuration = Configuration()
|
|
18
|
+
|
|
19
|
+
ROLE_DISPLAY = {
|
|
20
|
+
"admin": "[red]admin[/red]",
|
|
21
|
+
"developer": "[cyan]developer[/cyan]",
|
|
22
|
+
"user": "user",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("list")
|
|
27
|
+
async def list_users(
|
|
28
|
+
email: typing.Annotated[str | None, typer.Option(help="Filter by email (case-insensitive partial match)")] = None,
|
|
29
|
+
limit: typing.Annotated[int, typer.Option(help="Results per page (1-100)")] = 40,
|
|
30
|
+
after: typing.Annotated[str | None, typer.Option(help="Pagination cursor (page_token)")] = None,
|
|
31
|
+
):
|
|
32
|
+
"""List platform users (admin only)."""
|
|
33
|
+
announce_server_action("Listing users on")
|
|
34
|
+
|
|
35
|
+
async with configuration.use_platform_client():
|
|
36
|
+
result = await User.list(email=email, limit=limit, page_token=after)
|
|
37
|
+
|
|
38
|
+
items = result.items
|
|
39
|
+
has_more = result.has_more
|
|
40
|
+
next_page_token = result.next_page_token
|
|
41
|
+
|
|
42
|
+
with create_table(
|
|
43
|
+
Column("ID", style="yellow"),
|
|
44
|
+
Column("Email"),
|
|
45
|
+
Column("Role"),
|
|
46
|
+
Column("Created"),
|
|
47
|
+
Column("Role Updated"),
|
|
48
|
+
no_wrap=True,
|
|
49
|
+
) as table:
|
|
50
|
+
for user in items:
|
|
51
|
+
role_display = ROLE_DISPLAY.get(user.role, user.role)
|
|
52
|
+
|
|
53
|
+
created_at = _format_date(user.created_at)
|
|
54
|
+
role_updated_at = _format_date(user.role_updated_at) if user.role_updated_at else "-"
|
|
55
|
+
|
|
56
|
+
table.add_row(
|
|
57
|
+
user.id,
|
|
58
|
+
user.email,
|
|
59
|
+
role_display,
|
|
60
|
+
created_at,
|
|
61
|
+
role_updated_at,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
console.print()
|
|
65
|
+
console.print(table)
|
|
66
|
+
|
|
67
|
+
if has_more and next_page_token:
|
|
68
|
+
console.print(f"\n[dim]Use --after {next_page_token} to see more[/dim]")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("set-role")
|
|
72
|
+
async def set_role(
|
|
73
|
+
user_id: typing.Annotated[str, typer.Argument(help="User UUID")],
|
|
74
|
+
role: typing.Annotated[UserRole, typer.Argument(help="Target role (admin, developer, user)")],
|
|
75
|
+
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
76
|
+
):
|
|
77
|
+
"""Change user role (admin only)."""
|
|
78
|
+
url = announce_server_action(f"Changing user {user_id} to role '{role}' on")
|
|
79
|
+
await confirm_server_action("Proceed with role change on", url=url, yes=yes)
|
|
80
|
+
|
|
81
|
+
async with configuration.use_platform_client():
|
|
82
|
+
result = await User.set_role(user_id, UserRole(role))
|
|
83
|
+
|
|
84
|
+
role_display = ROLE_DISPLAY.get(result.new_role, result.new_role)
|
|
85
|
+
|
|
86
|
+
console.success(f"User role updated to [cyan]{role_display}[/cyan]")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _format_date(dt: datetime | None) -> str:
|
|
90
|
+
if not dt:
|
|
91
|
+
return "-"
|
|
92
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
Binary file
|
|
@@ -142,7 +142,7 @@ async def confirm_server_action(message: str, url: str | None = None, *, yes: bo
|
|
|
142
142
|
return
|
|
143
143
|
url = url or require_active_server()
|
|
144
144
|
confirmed = await inquirer.confirm( # type: ignore
|
|
145
|
-
message=f"{message}
|
|
145
|
+
message=f"{message} {url}?", default=False
|
|
146
146
|
).execute_async()
|
|
147
147
|
if not confirmed:
|
|
148
148
|
console.info("Action cancelled.")
|
|
@@ -309,10 +309,15 @@ def print_log(line, ansi_mode=False, out_console: Console | None = None):
|
|
|
309
309
|
def decode(text: str):
|
|
310
310
|
return Text.from_ansi(text) if ansi_mode else text
|
|
311
311
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
match line:
|
|
313
|
+
case {"stream": "stderr"}:
|
|
314
|
+
(out_console or err_console).print(decode(line["message"]))
|
|
315
|
+
case {"stream": "stdout"}:
|
|
316
|
+
(out_console or console).print(decode(line["message"]))
|
|
317
|
+
case {"event": "[DONE]"}:
|
|
318
|
+
return
|
|
319
|
+
case _:
|
|
320
|
+
(out_console or console).print(line)
|
|
316
321
|
|
|
317
322
|
|
|
318
323
|
def is_github_url(url: str) -> bool:
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/__init__.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/istio.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/lima_driver.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.3rc2 → agentstack_cli-0.5.0}/src/agentstack_cli/commands/platform/wsl_driver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|