agentstack-cli 0.4.2__tar.gz → 0.4.2rc1__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.2 → agentstack_cli-0.4.2rc1}/PKG-INFO +1 -1
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/pyproject.toml +1 -1
- agentstack_cli-0.4.2rc1/src/agentstack_cli/__init__.py +77 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/api.py +14 -28
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/async_typer.py +1 -2
- agentstack_cli-0.4.2rc1/src/agentstack_cli/auth_manager.py +126 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/agent.py +78 -122
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/build.py +48 -74
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/mcp.py +3 -12
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/model.py +3 -28
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/__init__.py +5 -5
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/base_driver.py +0 -2
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/wsl_driver.py +2 -7
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/self.py +4 -8
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/server.py +5 -40
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/configuration.py +1 -1
- agentstack_cli-0.4.2rc1/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/utils.py +5 -61
- agentstack_cli-0.4.2/src/agentstack_cli/__init__.py +0 -119
- agentstack_cli-0.4.2/src/agentstack_cli/auth_manager.py +0 -241
- agentstack_cli-0.4.2/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/README.md +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/istio.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/lima_driver.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/data/.gitignore +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import typing
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
import agentstack_cli.commands.agent
|
|
11
|
+
import agentstack_cli.commands.build
|
|
12
|
+
import agentstack_cli.commands.mcp
|
|
13
|
+
import agentstack_cli.commands.model
|
|
14
|
+
import agentstack_cli.commands.platform
|
|
15
|
+
import agentstack_cli.commands.self
|
|
16
|
+
import agentstack_cli.commands.server
|
|
17
|
+
from agentstack_cli.async_typer import AsyncTyper
|
|
18
|
+
from agentstack_cli.configuration import Configuration
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO if Configuration().debug else logging.FATAL)
|
|
21
|
+
logging.getLogger("httpx").setLevel(logging.WARNING) # not sure why this is necessary
|
|
22
|
+
|
|
23
|
+
app = AsyncTyper(no_args_is_help=True)
|
|
24
|
+
app.add_typer(agentstack_cli.commands.model.app, name="model", no_args_is_help=True, help="Manage model providers.")
|
|
25
|
+
app.add_typer(agentstack_cli.commands.agent.app, name="agent", no_args_is_help=True, help="Manage agents.")
|
|
26
|
+
app.add_typer(
|
|
27
|
+
agentstack_cli.commands.platform.app, name="platform", no_args_is_help=True, help="Manage Agent Stack platform."
|
|
28
|
+
)
|
|
29
|
+
app.add_typer(
|
|
30
|
+
agentstack_cli.commands.mcp.app, name="mcp", no_args_is_help=True, help="Manage MCP servers and toolkits."
|
|
31
|
+
)
|
|
32
|
+
app.add_typer(agentstack_cli.commands.build.app, name="", no_args_is_help=True, help="Build agent images.")
|
|
33
|
+
app.add_typer(
|
|
34
|
+
agentstack_cli.commands.server.app,
|
|
35
|
+
name="server",
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
help="Manage Agent Stack servers and authentication.",
|
|
38
|
+
)
|
|
39
|
+
app.add_typer(
|
|
40
|
+
agentstack_cli.commands.self.app,
|
|
41
|
+
name="self",
|
|
42
|
+
no_args_is_help=True,
|
|
43
|
+
help="Manage Agent Stack installation.",
|
|
44
|
+
hidden=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
agent_alias = deepcopy(agentstack_cli.commands.agent.app)
|
|
49
|
+
for cmd in agent_alias.registered_commands:
|
|
50
|
+
cmd.rich_help_panel = "Agent commands"
|
|
51
|
+
|
|
52
|
+
app.add_typer(agent_alias, name="", no_args_is_help=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("version")
|
|
56
|
+
async def version(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
|
|
57
|
+
"""Print version of the Agent Stack CLI."""
|
|
58
|
+
import agentstack_cli.commands.self
|
|
59
|
+
|
|
60
|
+
await agentstack_cli.commands.self.version(verbose=verbose)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command("ui")
|
|
64
|
+
async def ui():
|
|
65
|
+
"""Launch the graphical interface."""
|
|
66
|
+
import webbrowser
|
|
67
|
+
|
|
68
|
+
import agentstack_cli.commands.model
|
|
69
|
+
|
|
70
|
+
await agentstack_cli.commands.model.ensure_llm_provider()
|
|
71
|
+
webbrowser.open(
|
|
72
|
+
"http://localhost:8334"
|
|
73
|
+
) # TODO: This always opens the local UI, how to open the UI of a logged in server instead?
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
app()
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import re
|
|
5
5
|
import urllib
|
|
6
6
|
import urllib.parse
|
|
7
7
|
from collections.abc import AsyncIterator
|
|
8
8
|
from contextlib import asynccontextmanager
|
|
9
9
|
from datetime import timedelta
|
|
10
|
-
from textwrap import indent
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
12
|
import httpx
|
|
14
13
|
import openai
|
|
15
|
-
from a2a.client import
|
|
14
|
+
from a2a.client import Client, ClientConfig, ClientFactory
|
|
16
15
|
from a2a.types import AgentCard
|
|
17
16
|
from httpx import HTTPStatusError
|
|
18
17
|
from httpx._types import RequestFiles
|
|
@@ -44,7 +43,7 @@ async def api_request(
|
|
|
44
43
|
timeout=60,
|
|
45
44
|
headers=(
|
|
46
45
|
{"Authorization": f"Bearer {token}"}
|
|
47
|
-
if use_auth and (token :=
|
|
46
|
+
if use_auth and (token := config.auth_manager.load_auth_token())
|
|
48
47
|
else {}
|
|
49
48
|
),
|
|
50
49
|
)
|
|
@@ -83,7 +82,7 @@ async def api_stream(
|
|
|
83
82
|
timeout=timedelta(hours=1).total_seconds(),
|
|
84
83
|
headers=(
|
|
85
84
|
{"Authorization": f"Bearer {token}"}
|
|
86
|
-
if use_auth and (token :=
|
|
85
|
+
if use_auth and (token := config.auth_manager.load_auth_token())
|
|
87
86
|
else {}
|
|
88
87
|
),
|
|
89
88
|
) as response,
|
|
@@ -104,29 +103,16 @@ async def api_stream(
|
|
|
104
103
|
|
|
105
104
|
@asynccontextmanager
|
|
106
105
|
async def a2a_client(agent_card: AgentCard, use_auth: bool = True) -> AsyncIterator[Client]:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
yield ClientFactory(ClientConfig(httpx_client=httpx_client, use_client_preference=True)).create(
|
|
118
|
-
card=agent_card
|
|
119
|
-
)
|
|
120
|
-
except A2AClientHTTPError as ex:
|
|
121
|
-
card_data = json.dumps(
|
|
122
|
-
agent_card.model_dump(include={"url", "additional_interfaces", "preferred_transport"}), indent=2
|
|
123
|
-
)
|
|
124
|
-
raise RuntimeError(
|
|
125
|
-
f"The agent is not reachable, please check that the agent card is configured properly.\n"
|
|
126
|
-
f"Agent connection info:\n{indent(card_data, prefix=' ')}\n"
|
|
127
|
-
"Full Error:\n"
|
|
128
|
-
f"{indent(str(ex), prefix=' ')}"
|
|
129
|
-
) from ex
|
|
106
|
+
async with httpx.AsyncClient(
|
|
107
|
+
headers=(
|
|
108
|
+
{"Authorization": f"Bearer {token}"}
|
|
109
|
+
if use_auth and (token := config.auth_manager.load_auth_token())
|
|
110
|
+
else {}
|
|
111
|
+
),
|
|
112
|
+
follow_redirects=True,
|
|
113
|
+
timeout=timedelta(hours=1).total_seconds(),
|
|
114
|
+
) as httpx_client:
|
|
115
|
+
yield ClientFactory(ClientConfig(httpx_client=httpx_client, use_client_preference=True)).create(card=agent_card)
|
|
130
116
|
|
|
131
117
|
|
|
132
118
|
@asynccontextmanager
|
|
@@ -67,8 +67,7 @@ class AliasGroup(TyperGroup):
|
|
|
67
67
|
|
|
68
68
|
class AsyncTyper(typer.Typer):
|
|
69
69
|
def __init__(self, *args, **kwargs):
|
|
70
|
-
|
|
71
|
-
super().__init__(*args, **kwargs)
|
|
70
|
+
super().__init__(*args, **kwargs, cls=AliasGroup)
|
|
72
71
|
|
|
73
72
|
def command(self, *args, **kwargs):
|
|
74
73
|
parent_decorator = super().command(*args, **kwargs)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
import typing
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthToken(BaseModel):
|
|
13
|
+
access_token: str
|
|
14
|
+
token_type: str = "Bearer"
|
|
15
|
+
expires_in: int | None = None
|
|
16
|
+
refresh_token: str | None = None
|
|
17
|
+
scope: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthServer(BaseModel):
|
|
21
|
+
client_id: str = "df82a687-d647-4247-838b-7080d7d83f6c" # Backwards compatibility default
|
|
22
|
+
client_secret: str | None = None
|
|
23
|
+
token: AuthToken | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Server(BaseModel):
|
|
27
|
+
authorization_servers: dict[str, AuthServer] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Auth(BaseModel):
|
|
31
|
+
version: typing.Literal[1] = 1
|
|
32
|
+
servers: defaultdict[str, typing.Annotated[Server, Field(default_factory=Server)]] = Field(
|
|
33
|
+
default_factory=lambda: defaultdict(Server)
|
|
34
|
+
)
|
|
35
|
+
active_server: str | None = None
|
|
36
|
+
active_auth_server: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@typing.final
|
|
40
|
+
class AuthManager:
|
|
41
|
+
def __init__(self, config_path: pathlib.Path):
|
|
42
|
+
self._auth_path = config_path
|
|
43
|
+
self._auth = self._load()
|
|
44
|
+
|
|
45
|
+
def _load(self) -> Auth:
|
|
46
|
+
if not self._auth_path.exists():
|
|
47
|
+
return Auth()
|
|
48
|
+
return Auth.model_validate_json(self._auth_path.read_bytes())
|
|
49
|
+
|
|
50
|
+
def _save(self) -> None:
|
|
51
|
+
self._auth_path.write_text(self._auth.model_dump_json(indent=2))
|
|
52
|
+
|
|
53
|
+
def save_auth_token(
|
|
54
|
+
self,
|
|
55
|
+
server: str,
|
|
56
|
+
auth_server: str | None = None,
|
|
57
|
+
client_id: str | None = None,
|
|
58
|
+
client_secret: str | None = None,
|
|
59
|
+
token: dict[str, Any] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
if auth_server is not None and client_id is not None and token is not None:
|
|
62
|
+
self._auth.servers[server].authorization_servers[auth_server] = AuthServer(
|
|
63
|
+
client_id=client_id, client_secret=client_secret, token=AuthToken(**token)
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
self._auth.servers[server] # touch
|
|
67
|
+
self._save()
|
|
68
|
+
|
|
69
|
+
def load_auth_token(self) -> str | None:
|
|
70
|
+
active_res = self._auth.active_server
|
|
71
|
+
active_auth_server = self._auth.active_auth_server
|
|
72
|
+
if not active_res or not active_auth_server:
|
|
73
|
+
return None
|
|
74
|
+
server = self._auth.servers.get(active_res)
|
|
75
|
+
if not server:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
auth_server = server.authorization_servers.get(active_auth_server)
|
|
79
|
+
if not auth_server or not auth_server.token:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return auth_server.token.access_token
|
|
83
|
+
|
|
84
|
+
def clear_auth_token(self, all: bool = False) -> None:
|
|
85
|
+
if all:
|
|
86
|
+
self._auth.servers = defaultdict(Server)
|
|
87
|
+
else:
|
|
88
|
+
if self._auth.active_server and self._auth.active_auth_server:
|
|
89
|
+
del self._auth.servers[self._auth.active_server].authorization_servers[self._auth.active_auth_server]
|
|
90
|
+
if self._auth.active_server and not self._auth.servers[self._auth.active_server].authorization_servers:
|
|
91
|
+
del self._auth.servers[self._auth.active_server]
|
|
92
|
+
self._auth.active_server = None
|
|
93
|
+
self._auth.active_auth_server = None
|
|
94
|
+
self._save()
|
|
95
|
+
|
|
96
|
+
def get_server(self, server: str) -> Server | None:
|
|
97
|
+
return self._auth.servers.get(server)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def servers(self) -> list[str]:
|
|
101
|
+
return list(self._auth.servers.keys())
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def active_server(self) -> str | None:
|
|
105
|
+
return self._auth.active_server
|
|
106
|
+
|
|
107
|
+
@active_server.setter
|
|
108
|
+
def active_server(self, server: str | None) -> None:
|
|
109
|
+
if server is not None and server not in self._auth.servers:
|
|
110
|
+
raise ValueError(f"Server {server} not found")
|
|
111
|
+
self._auth.active_server = server
|
|
112
|
+
self._save()
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def active_auth_server(self) -> str | None:
|
|
116
|
+
return self._auth.active_auth_server
|
|
117
|
+
|
|
118
|
+
@active_auth_server.setter
|
|
119
|
+
def active_auth_server(self, auth_server: str | None) -> None:
|
|
120
|
+
if auth_server is not None and (
|
|
121
|
+
self._auth.active_server not in self._auth.servers
|
|
122
|
+
or auth_server not in self._auth.servers[self._auth.active_server].authorization_servers
|
|
123
|
+
):
|
|
124
|
+
raise ValueError(f"Auth server {auth_server} not found in active server")
|
|
125
|
+
self._auth.active_auth_server = auth_server
|
|
126
|
+
self._save()
|