agentstack-cli 0.5.0rc5__tar.gz → 0.5.1rc3__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.5.0rc5 → agentstack_cli-0.5.1rc3}/PKG-INFO +1 -1
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/pyproject.toml +1 -2
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/__init__.py +15 -5
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/api.py +3 -6
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/agent.py +162 -19
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/build.py +13 -2
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/model.py +1 -1
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/base_driver.py +8 -7
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/lima_driver.py +66 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/wsl_driver.py +18 -26
- agentstack_cli-0.5.1rc3/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/utils.py +9 -4
- agentstack_cli-0.5.0rc5/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/README.md +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/async_typer.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/auth_manager.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/mcp.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/__init__.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/istio.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/self.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/server.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/user.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/configuration.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/data/.gitignore +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentstack-cli"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1-rc3"
|
|
4
4
|
description = "Agent Stack CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "IBM Corp." }]
|
|
@@ -75,7 +75,6 @@ lint.ignore = [
|
|
|
75
75
|
force-exclude = true
|
|
76
76
|
|
|
77
77
|
[tool.pyright]
|
|
78
|
-
reportUnusedCallResult = false
|
|
79
78
|
ignore = ["tests/**", "examples/cli.py"]
|
|
80
79
|
venvPath = "."
|
|
81
80
|
venv = ".venv"
|
|
@@ -16,16 +16,14 @@ import agentstack_cli.commands.platform
|
|
|
16
16
|
import agentstack_cli.commands.self
|
|
17
17
|
import agentstack_cli.commands.server
|
|
18
18
|
import agentstack_cli.commands.user
|
|
19
|
-
from agentstack_cli.async_typer import
|
|
19
|
+
from agentstack_cli.async_typer import AsyncTyper
|
|
20
20
|
from agentstack_cli.configuration import Configuration
|
|
21
21
|
|
|
22
22
|
logging.basicConfig(level=logging.INFO if Configuration().debug else logging.FATAL)
|
|
23
23
|
logging.getLogger("httpx").setLevel(logging.WARNING) # not sure why this is necessary
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
def get_help(self, ctx):
|
|
28
|
-
return """\
|
|
26
|
+
HELP_TEXT = """\
|
|
29
27
|
Usage: agentstack [OPTIONS] COMMAND [ARGS]...
|
|
30
28
|
|
|
31
29
|
╭─ Getting Started ──────────────────────────────────────────────────────────╮
|
|
@@ -63,7 +61,19 @@ Usage: agentstack [OPTIONS] COMMAND [ARGS]...
|
|
|
63
61
|
"""
|
|
64
62
|
|
|
65
63
|
|
|
66
|
-
app = AsyncTyper(
|
|
64
|
+
app = AsyncTyper()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.callback(invoke_without_command=True)
|
|
68
|
+
def main(
|
|
69
|
+
ctx: typer.Context,
|
|
70
|
+
help: bool = typer.Option(False, "--help", help="Show this message and exit."),
|
|
71
|
+
):
|
|
72
|
+
if help or ctx.invoked_subcommand is None:
|
|
73
|
+
typer.echo(HELP_TEXT)
|
|
74
|
+
raise typer.Exit()
|
|
75
|
+
|
|
76
|
+
|
|
67
77
|
app.add_typer(agentstack_cli.commands.model.app, name="model", no_args_is_help=True, help="Manage model providers.")
|
|
68
78
|
app.add_typer(agentstack_cli.commands.agent.app, name="agent", no_args_is_help=True, help="Manage agents.")
|
|
69
79
|
app.add_typer(
|
|
@@ -16,6 +16,7 @@ import openai
|
|
|
16
16
|
import pydantic
|
|
17
17
|
from a2a.client import A2AClientHTTPError, Client, ClientConfig, ClientFactory
|
|
18
18
|
from a2a.types import AgentCard
|
|
19
|
+
from agentstack_sdk.platform.context import ContextToken
|
|
19
20
|
from httpx import HTTPStatusError
|
|
20
21
|
from httpx._types import RequestFiles
|
|
21
22
|
|
|
@@ -127,14 +128,10 @@ async def fetch_server_version() -> str | None:
|
|
|
127
128
|
|
|
128
129
|
|
|
129
130
|
@asynccontextmanager
|
|
130
|
-
async def a2a_client(agent_card: AgentCard,
|
|
131
|
+
async def a2a_client(agent_card: AgentCard, context_token: ContextToken) -> AsyncIterator[Client]:
|
|
131
132
|
try:
|
|
132
133
|
async with httpx.AsyncClient(
|
|
133
|
-
headers=(
|
|
134
|
-
{"Authorization": f"Bearer {token}"}
|
|
135
|
-
if use_auth and (token := await config.auth_manager.load_auth_token())
|
|
136
|
-
else {}
|
|
137
|
-
),
|
|
134
|
+
headers={"Authorization": f"Bearer {context_token.token.get_secret_value()}"},
|
|
138
135
|
follow_redirects=True,
|
|
139
136
|
timeout=timedelta(hours=1).total_seconds(),
|
|
140
137
|
) as httpx_client:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import abc
|
|
5
|
+
import asyncio
|
|
5
6
|
import base64
|
|
6
7
|
import calendar
|
|
7
8
|
import inspect
|
|
@@ -61,6 +62,21 @@ from agentstack_sdk.a2a.extensions.common.form import (
|
|
|
61
62
|
TextField,
|
|
62
63
|
TextFieldValue,
|
|
63
64
|
)
|
|
65
|
+
from agentstack_sdk.a2a.extensions.ui.settings import (
|
|
66
|
+
AgentRunSettings,
|
|
67
|
+
CheckboxGroupField,
|
|
68
|
+
CheckboxGroupFieldValue,
|
|
69
|
+
SettingsExtensionSpec,
|
|
70
|
+
SettingsFieldValue,
|
|
71
|
+
SettingsRender,
|
|
72
|
+
)
|
|
73
|
+
from agentstack_sdk.a2a.extensions.ui.settings import (
|
|
74
|
+
CheckboxFieldValue as SettingsCheckboxFieldValue,
|
|
75
|
+
)
|
|
76
|
+
from agentstack_sdk.a2a.extensions.ui.settings import SingleSelectField as SettingsSingleSelectField
|
|
77
|
+
from agentstack_sdk.a2a.extensions.ui.settings import (
|
|
78
|
+
SingleSelectFieldValue as SettingsSingleSelectFieldValue,
|
|
79
|
+
)
|
|
64
80
|
from agentstack_sdk.platform import BuildState, File, ModelProvider, Provider, UserFeedback
|
|
65
81
|
from agentstack_sdk.platform.context import Context, ContextPermissions, ContextToken, Permissions
|
|
66
82
|
from agentstack_sdk.platform.model_provider import ModelCapability
|
|
@@ -159,7 +175,22 @@ async def add_agent(
|
|
|
159
175
|
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
160
176
|
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
161
177
|
) -> None:
|
|
162
|
-
"""Add a docker image or GitHub repository
|
|
178
|
+
"""Add a docker image or GitHub repository.
|
|
179
|
+
|
|
180
|
+
This command supports a variety of GitHub URL formats for deploying agents:
|
|
181
|
+
|
|
182
|
+
- **Basic URL**: `https://github.com/myorg/myrepo`
|
|
183
|
+
- **Git Protocol URL**: `git+https://github.com/myorg/myrepo`
|
|
184
|
+
- **URL with .git suffix**: `https://github.com/myorg/myrepo.git`
|
|
185
|
+
- **URL with Version Tag**: `https://github.com/myorg/myrepo@v1.0.0`
|
|
186
|
+
- **URL with Branch Name**: `https://github.com/myorg/myrepo@my-branch`
|
|
187
|
+
- **URL with Subfolder Path**: `https://github.com/myorg/myrepo#path=/path/to/agent`
|
|
188
|
+
- **Combined Formats**: `https://github.com/myorg/myrepo.git@v1.0.0#path=/path/to/agent`
|
|
189
|
+
- **Enterprise GitHub**: `https://github.mycompany.com/myorg/myrepo`
|
|
190
|
+
- **With a custom Dockerfile location**: `agentstack add --dockerfile /my-agent/path/to/Dockerfile "https://github.com/my-org/my-awesome-agents@main#path=/my-agent"`
|
|
191
|
+
|
|
192
|
+
[aliases: install]
|
|
193
|
+
"""
|
|
163
194
|
if location is None:
|
|
164
195
|
repo_input = (
|
|
165
196
|
await inquirer.text( # pyright: ignore[reportPrivateImportUsage]
|
|
@@ -308,11 +339,21 @@ async def update_agent(
|
|
|
308
339
|
await list_agents()
|
|
309
340
|
|
|
310
341
|
|
|
311
|
-
def
|
|
342
|
+
def search_path_match_providers(search_path: str, providers: list[Provider]) -> dict[str, Provider]:
|
|
312
343
|
search_path = search_path.lower()
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
344
|
+
return {
|
|
345
|
+
p.id: p
|
|
346
|
+
for p in providers
|
|
347
|
+
if (
|
|
348
|
+
search_path in p.id.lower()
|
|
349
|
+
or search_path in p.agent_card.name.lower()
|
|
350
|
+
or search_path in ProviderUtils.short_location(p)
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def select_provider(search_path: str, providers: list[Provider]):
|
|
356
|
+
provider_candidates = search_path_match_providers(search_path, providers)
|
|
316
357
|
if len(provider_candidates) != 1:
|
|
317
358
|
provider_candidates = [f" - {c}" for c in provider_candidates]
|
|
318
359
|
remove_providers_detail = ":\n" + "\n".join(provider_candidates) if provider_candidates else ""
|
|
@@ -321,20 +362,75 @@ def select_provider(search_path: str, providers: list[Provider]):
|
|
|
321
362
|
return selected_provider
|
|
322
363
|
|
|
323
364
|
|
|
365
|
+
async def select_providers_multi(search_path: str, providers: list[Provider]) -> list[Provider]:
|
|
366
|
+
"""Select multiple providers matching the search path."""
|
|
367
|
+
provider_candidates = search_path_match_providers(search_path, providers)
|
|
368
|
+
if not provider_candidates:
|
|
369
|
+
raise ValueError(f"No matching agents found for '{search_path}'")
|
|
370
|
+
|
|
371
|
+
if len(provider_candidates) == 1:
|
|
372
|
+
return list(provider_candidates.values())
|
|
373
|
+
|
|
374
|
+
# Multiple matches - show selection menu
|
|
375
|
+
choices = [Choice(value=p.id, name=f"{p.agent_card.name} - {p.id}") for p in provider_candidates.values()]
|
|
376
|
+
|
|
377
|
+
selected_ids = await inquirer.checkbox( # pyright: ignore[reportPrivateImportUsage]
|
|
378
|
+
message="Select agents to remove (use ↑/↓ to navigate, Space to select):", choices=choices
|
|
379
|
+
).execute_async()
|
|
380
|
+
|
|
381
|
+
return [provider_candidates[pid] for pid in (selected_ids or [])]
|
|
382
|
+
|
|
383
|
+
|
|
324
384
|
@app.command("remove | uninstall | rm | delete")
|
|
325
385
|
async def uninstall_agent(
|
|
326
386
|
search_path: typing.Annotated[
|
|
327
|
-
str, typer.Argument(
|
|
328
|
-
],
|
|
387
|
+
str, typer.Argument(help="Short ID, agent name or part of the provider location")
|
|
388
|
+
] = "",
|
|
329
389
|
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
390
|
+
all: typing.Annotated[bool, typer.Option("--all", "-a", help="Remove all agents without selection.")] = False,
|
|
330
391
|
) -> None:
|
|
331
392
|
"""Remove agent"""
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
393
|
+
if search_path and all:
|
|
394
|
+
console.error(
|
|
395
|
+
"[bold]Cannot specify both --all and a search path."
|
|
396
|
+
" Use --all to remove all agents, or provide a search path for specific agents."
|
|
397
|
+
"[/bold]"
|
|
398
|
+
)
|
|
399
|
+
raise typer.Exit(1)
|
|
400
|
+
|
|
401
|
+
async with configuration.use_platform_client():
|
|
402
|
+
providers = await Provider.list()
|
|
403
|
+
if len(providers) == 0:
|
|
404
|
+
console.info("No agents found to remove.")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
if all:
|
|
408
|
+
selected_providers = providers
|
|
409
|
+
else:
|
|
410
|
+
selected_providers = await select_providers_multi(search_path, providers)
|
|
411
|
+
if not selected_providers:
|
|
412
|
+
console.info("No agents selected for removal, exiting.")
|
|
413
|
+
return
|
|
414
|
+
elif len(selected_providers) == 1:
|
|
415
|
+
agent_names = f"{selected_providers[0].agent_card.name} - {selected_providers[0].id.split('-', 1)[0]}"
|
|
416
|
+
else:
|
|
417
|
+
agent_names = "\n".join([f" - {p.agent_card.name} - {p.id.split('-', 1)[0]}" for p in selected_providers])
|
|
418
|
+
|
|
419
|
+
message = f"\n[bold]Selected agents to remove:[/bold]\n{agent_names}\n from "
|
|
420
|
+
|
|
421
|
+
url = announce_server_action(message)
|
|
422
|
+
await confirm_server_action("Proceed with removing these agents from", url=url, yes=yes)
|
|
423
|
+
|
|
424
|
+
with console.status("Uninstalling agent(s) (may take a few minutes)...", spinner="dots"):
|
|
425
|
+
delete_tasks = [Provider.delete(provider.id) for provider in selected_providers]
|
|
426
|
+
results = await asyncio.gather(*delete_tasks, return_exceptions=True)
|
|
427
|
+
|
|
428
|
+
# Check results for exceptions
|
|
429
|
+
for provider, result in zip(selected_providers, results, strict=True):
|
|
430
|
+
if isinstance(result, Exception):
|
|
431
|
+
err_console.print(f"Failed to delete {provider.agent_card.name}: {result}")
|
|
432
|
+
# else: deletion succeeded
|
|
433
|
+
|
|
338
434
|
await list_agents()
|
|
339
435
|
|
|
340
436
|
|
|
@@ -431,11 +527,43 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
431
527
|
return FormResponse(values=form_values)
|
|
432
528
|
|
|
433
529
|
|
|
530
|
+
async def _ask_settings_questions(settings_render: SettingsRender) -> AgentRunSettings:
|
|
531
|
+
"""Ask user to configure settings using inquirer."""
|
|
532
|
+
settings_values: dict[str, SettingsFieldValue] = {}
|
|
533
|
+
|
|
534
|
+
console.print("[bold]Agent Settings[/bold]\n")
|
|
535
|
+
|
|
536
|
+
for field in settings_render.fields:
|
|
537
|
+
if isinstance(field, CheckboxGroupField):
|
|
538
|
+
checkbox_values: dict[str, SettingsCheckboxFieldValue] = {}
|
|
539
|
+
for checkbox in field.fields:
|
|
540
|
+
answer = await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
541
|
+
message=checkbox.label + ":",
|
|
542
|
+
default=checkbox.default_value,
|
|
543
|
+
).execute_async()
|
|
544
|
+
checkbox_values[checkbox.id] = SettingsCheckboxFieldValue(value=answer)
|
|
545
|
+
settings_values[field.id] = CheckboxGroupFieldValue(values=checkbox_values)
|
|
546
|
+
elif isinstance(field, SettingsSingleSelectField):
|
|
547
|
+
choices = [Choice(value=opt.value, name=opt.label) for opt in field.options]
|
|
548
|
+
answer = await inquirer.fuzzy( # pyright: ignore[reportPrivateImportUsage]
|
|
549
|
+
message=field.label + ":",
|
|
550
|
+
choices=choices,
|
|
551
|
+
default=field.default_value,
|
|
552
|
+
).execute_async()
|
|
553
|
+
settings_values[field.id] = SettingsSingleSelectFieldValue(value=answer)
|
|
554
|
+
else:
|
|
555
|
+
raise ValueError(f"Unsupported settings field type: {type(field).__name__}")
|
|
556
|
+
|
|
557
|
+
console.print()
|
|
558
|
+
return AgentRunSettings(values=settings_values)
|
|
559
|
+
|
|
560
|
+
|
|
434
561
|
async def _run_agent(
|
|
435
562
|
client: Client,
|
|
436
563
|
input: str | DataPart | FormResponse,
|
|
437
564
|
agent_card: AgentCard,
|
|
438
565
|
context_token: ContextToken,
|
|
566
|
+
settings: AgentRunSettings | None = None,
|
|
439
567
|
dump_files_path: Path | None = None,
|
|
440
568
|
handle_input: Callable[[], str] | None = None,
|
|
441
569
|
task_id: str | None = None,
|
|
@@ -508,6 +636,7 @@ async def _run_agent(
|
|
|
508
636
|
if platform_extension_spec
|
|
509
637
|
else {}
|
|
510
638
|
)
|
|
639
|
+
| ({SettingsExtensionSpec.URI: settings.model_dump(mode="json")} if settings else {})
|
|
511
640
|
)
|
|
512
641
|
|
|
513
642
|
msg = Message(
|
|
@@ -556,7 +685,7 @@ async def _run_agent(
|
|
|
556
685
|
console.print() # Add newline after completion
|
|
557
686
|
return
|
|
558
687
|
case Task(id=task_id), TaskStatusUpdateEvent(
|
|
559
|
-
status=TaskStatus(state=TaskState.working, message=message)
|
|
688
|
+
status=TaskStatus(state=TaskState.working | TaskState.submitted, message=message)
|
|
560
689
|
):
|
|
561
690
|
# Handle streaming content during working state
|
|
562
691
|
if message:
|
|
@@ -880,8 +1009,6 @@ async def run_agent(
|
|
|
880
1009
|
] = None,
|
|
881
1010
|
) -> None:
|
|
882
1011
|
"""Run an agent."""
|
|
883
|
-
if search_path is not None and input is None and sys.stdin.isatty():
|
|
884
|
-
input = sys.stdin.read()
|
|
885
1012
|
async with configuration.use_platform_client():
|
|
886
1013
|
providers = await Provider.list()
|
|
887
1014
|
await ensure_llm_provider()
|
|
@@ -926,6 +1053,15 @@ async def run_agent(
|
|
|
926
1053
|
splash_screen = Group(Markdown(f"# {agent.name} \n{agent.description}"), NewLine())
|
|
927
1054
|
handle_input = _create_input_handler([], splash_screen=splash_screen)
|
|
928
1055
|
|
|
1056
|
+
settings_render = next(
|
|
1057
|
+
(
|
|
1058
|
+
SettingsRender.model_validate(ext.params)
|
|
1059
|
+
for ext in agent.capabilities.extensions or ()
|
|
1060
|
+
if ext.uri == SettingsExtensionSpec.URI and ext.params
|
|
1061
|
+
),
|
|
1062
|
+
None,
|
|
1063
|
+
)
|
|
1064
|
+
|
|
929
1065
|
if not input:
|
|
930
1066
|
if interaction_mode not in {InteractionMode.MULTI_TURN, InteractionMode.SINGLE_TURN}:
|
|
931
1067
|
err_console.error(
|
|
@@ -946,8 +1082,9 @@ async def run_agent(
|
|
|
946
1082
|
|
|
947
1083
|
if interaction_mode == InteractionMode.MULTI_TURN:
|
|
948
1084
|
console.print(f"{user_greeting}\n")
|
|
1085
|
+
settings_input = await _ask_settings_questions(settings_render) if settings_render else None
|
|
949
1086
|
turn_input = await _ask_form_questions(initial_form_render) if initial_form_render else handle_input()
|
|
950
|
-
async with a2a_client(provider.agent_card) as client:
|
|
1087
|
+
async with a2a_client(provider.agent_card, context_token=context_token) as client:
|
|
951
1088
|
while True:
|
|
952
1089
|
console.print()
|
|
953
1090
|
await _run_agent(
|
|
@@ -955,6 +1092,7 @@ async def run_agent(
|
|
|
955
1092
|
input=turn_input,
|
|
956
1093
|
agent_card=agent,
|
|
957
1094
|
context_token=context_token,
|
|
1095
|
+
settings=settings_input,
|
|
958
1096
|
dump_files_path=dump_files,
|
|
959
1097
|
handle_input=handle_input,
|
|
960
1098
|
)
|
|
@@ -963,24 +1101,29 @@ async def run_agent(
|
|
|
963
1101
|
elif interaction_mode == InteractionMode.SINGLE_TURN:
|
|
964
1102
|
user_greeting = ui_annotations.get("user_greeting", None) or "Enter your instructions."
|
|
965
1103
|
console.print(f"{user_greeting}\n")
|
|
1104
|
+
settings_input = await _ask_settings_questions(settings_render) if settings_render else None
|
|
966
1105
|
console.print()
|
|
967
|
-
async with a2a_client(provider.agent_card) as client:
|
|
1106
|
+
async with a2a_client(provider.agent_card, context_token=context_token) as client:
|
|
968
1107
|
await _run_agent(
|
|
969
1108
|
client,
|
|
970
1109
|
input=await _ask_form_questions(initial_form_render) if initial_form_render else handle_input(),
|
|
971
1110
|
agent_card=agent,
|
|
972
1111
|
context_token=context_token,
|
|
1112
|
+
settings=settings_input,
|
|
973
1113
|
dump_files_path=dump_files,
|
|
974
1114
|
handle_input=handle_input,
|
|
975
1115
|
)
|
|
976
1116
|
|
|
977
1117
|
else:
|
|
978
|
-
|
|
1118
|
+
settings_input = await _ask_settings_questions(settings_render) if settings_render else None
|
|
1119
|
+
|
|
1120
|
+
async with a2a_client(provider.agent_card, context_token=context_token) as client:
|
|
979
1121
|
await _run_agent(
|
|
980
1122
|
client,
|
|
981
1123
|
input,
|
|
982
1124
|
agent_card=agent,
|
|
983
1125
|
context_token=context_token,
|
|
1126
|
+
settings=settings_input,
|
|
984
1127
|
dump_files_path=dump_files,
|
|
985
1128
|
handle_input=handle_input,
|
|
986
1129
|
)
|
|
@@ -113,7 +113,7 @@ async def client_side_build(
|
|
|
113
113
|
context_hash = hashlib.sha256((context + (dockerfile or "")).encode()).hexdigest()[:6]
|
|
114
114
|
context_shorter = re.sub(r"https?://", "", context).replace(r".git", "")
|
|
115
115
|
context_shorter = re.sub(r"[^a-zA-Z0-9_-]+", "-", context_shorter)[:32].lstrip("-") or "provider"
|
|
116
|
-
tag = (tag or f"agentstack.
|
|
116
|
+
tag = (tag or f"agentstack-registry-svc.default:5001/{context_shorter}-{context_hash}:latest").lower()
|
|
117
117
|
await run_command(
|
|
118
118
|
command=[
|
|
119
119
|
*(
|
|
@@ -135,11 +135,22 @@ async def client_side_build(
|
|
|
135
135
|
if import_image:
|
|
136
136
|
from agentstack_cli.commands.platform import get_driver
|
|
137
137
|
|
|
138
|
+
if "agentstack-registry-svc.default" not in tag:
|
|
139
|
+
source_tag = tag
|
|
140
|
+
tag = re.sub("^[^/]*/", "agentstack-registry-svc.default:5001/", tag)
|
|
141
|
+
await run_command(["docker", "tag", source_tag, tag], "Tagging image")
|
|
142
|
+
|
|
138
143
|
driver = get_driver(vm_name=vm_name)
|
|
144
|
+
|
|
139
145
|
if (await driver.status()) != "running":
|
|
140
146
|
console.error("Agent Stack platform is not running.")
|
|
141
147
|
sys.exit(1)
|
|
142
|
-
|
|
148
|
+
|
|
149
|
+
await driver.import_image_to_internal_registry(tag)
|
|
150
|
+
console.success(
|
|
151
|
+
"Agent was imported to the agent stack internal registry.\n"
|
|
152
|
+
+ f"You can add it using [blue]agentstack add {tag}[/blue]"
|
|
153
|
+
)
|
|
143
154
|
|
|
144
155
|
return tag, agent_card
|
|
145
156
|
|
|
@@ -36,7 +36,7 @@ class ModelProviderError(Exception): ...
|
|
|
36
36
|
|
|
37
37
|
@functools.cache
|
|
38
38
|
def _ollama_exe() -> str:
|
|
39
|
-
for exe in ("ollama", "ollama.exe"):
|
|
39
|
+
for exe in ("ollama", "ollama.exe", os.environ.get("LOCALAPPDATA", "") + "\\Programs\\Ollama\\ollama.exe"):
|
|
40
40
|
if shutil.which(exe):
|
|
41
41
|
return exe
|
|
42
42
|
raise RuntimeError("Ollama executable not found")
|
|
@@ -22,6 +22,7 @@ class BaseDriver(abc.ABC):
|
|
|
22
22
|
|
|
23
23
|
def __init__(self, vm_name: str = "agentstack"):
|
|
24
24
|
self.vm_name = vm_name
|
|
25
|
+
self.loaded_images: set[str] = set()
|
|
25
26
|
|
|
26
27
|
@abc.abstractmethod
|
|
27
28
|
async def run_in_vm(
|
|
@@ -47,6 +48,9 @@ class BaseDriver(abc.ABC):
|
|
|
47
48
|
@abc.abstractmethod
|
|
48
49
|
async def import_image(self, tag: str) -> None: ...
|
|
49
50
|
|
|
51
|
+
@abc.abstractmethod
|
|
52
|
+
async def import_image_to_internal_registry(self, tag: str) -> None: ...
|
|
53
|
+
|
|
50
54
|
@abc.abstractmethod
|
|
51
55
|
async def exec(self, command: list[str]) -> None: ...
|
|
52
56
|
|
|
@@ -138,20 +142,17 @@ class BaseDriver(abc.ABC):
|
|
|
138
142
|
).stdout.decode()
|
|
139
143
|
for image in import_images or []:
|
|
140
144
|
await self.import_image(image)
|
|
145
|
+
self.loaded_images.add(image)
|
|
141
146
|
for image in {typing.cast(str, yaml.safe_load(line)) for line in images_str.splitlines()} - set(
|
|
142
147
|
import_images or []
|
|
143
148
|
):
|
|
144
149
|
async for attempt in AsyncRetrying(stop=stop_after_attempt(5)):
|
|
145
150
|
with attempt:
|
|
146
151
|
attempt_num = attempt.retry_state.attempt_number
|
|
152
|
+
image_id = image if "." in image.split("/")[0] else f"docker.io/{image}"
|
|
153
|
+
self.loaded_images.add(image_id)
|
|
147
154
|
await self.run_in_vm(
|
|
148
|
-
[
|
|
149
|
-
"k3s",
|
|
150
|
-
"ctr",
|
|
151
|
-
"image",
|
|
152
|
-
"pull",
|
|
153
|
-
image if "." in image.split("/")[0] else f"docker.io/{image}",
|
|
154
|
-
],
|
|
155
|
+
["k3s", "ctr", "image", "pull", image_id],
|
|
155
156
|
f"Pulling image {image}" + (f" (attempt {attempt_num})" if attempt_num > 1 else ""),
|
|
156
157
|
)
|
|
157
158
|
|
|
@@ -197,6 +197,72 @@ class LimaDriver(BaseDriver):
|
|
|
197
197
|
finally:
|
|
198
198
|
await image_path.unlink(missing_ok=True)
|
|
199
199
|
|
|
200
|
+
@typing.override
|
|
201
|
+
async def import_image_to_internal_registry(self, tag: str) -> None:
|
|
202
|
+
# 1. Check if registry is running
|
|
203
|
+
try:
|
|
204
|
+
await self.run_in_vm(
|
|
205
|
+
["k3s", "kubectl", "get", "svc", "agentstack-registry-svc"],
|
|
206
|
+
"Checking internal registry availability",
|
|
207
|
+
)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
console.warning(f"Internal registry service not found. Push might fail: {e}")
|
|
210
|
+
|
|
211
|
+
# 2. Export image from Docker to shared temp dir
|
|
212
|
+
image_dir = anyio.Path("/tmp/agentstack")
|
|
213
|
+
await image_dir.mkdir(exist_ok=True, parents=True)
|
|
214
|
+
image_file = f"{uuid.uuid4()}.tar"
|
|
215
|
+
image_path = image_dir / image_file
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
await run_command(
|
|
219
|
+
["docker", "image", "save", "-o", str(image_path), tag],
|
|
220
|
+
f"Exporting image {tag} from Docker",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# 3 & 4. Run Crane Job
|
|
224
|
+
crane_image = "ghcr.io/i-am-bee/alpine/crane:0.20.6"
|
|
225
|
+
for image in self.loaded_images:
|
|
226
|
+
if "alpine/crane" in image:
|
|
227
|
+
crane_image = image
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
job_name = f"push-{uuid.uuid4().hex[:6]}"
|
|
231
|
+
job_def = {
|
|
232
|
+
"apiVersion": "batch/v1",
|
|
233
|
+
"kind": "Job",
|
|
234
|
+
"metadata": {"name": job_name, "namespace": "default"},
|
|
235
|
+
"spec": {
|
|
236
|
+
"backoffLimit": 0,
|
|
237
|
+
"ttlSecondsAfterFinished": 60,
|
|
238
|
+
"template": {
|
|
239
|
+
"spec": {
|
|
240
|
+
"restartPolicy": "Never",
|
|
241
|
+
"containers": [
|
|
242
|
+
{
|
|
243
|
+
"name": "crane",
|
|
244
|
+
"image": crane_image,
|
|
245
|
+
"command": ["crane", "push", f"/workspace/{image_file}", tag, "--insecure"],
|
|
246
|
+
"volumeMounts": [{"name": "workspace", "mountPath": "/workspace"}],
|
|
247
|
+
}
|
|
248
|
+
],
|
|
249
|
+
"volumes": [{"name": "workspace", "hostPath": {"path": "/tmp/agentstack"}}],
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await self.run_in_vm(
|
|
256
|
+
["k3s", "kubectl", "apply", "-f", "-"], "Starting push job", input=yaml.dump(job_def).encode()
|
|
257
|
+
)
|
|
258
|
+
await self.run_in_vm(
|
|
259
|
+
["k3s", "kubectl", "wait", "--for=condition=complete", f"job/{job_name}", "--timeout=300s"],
|
|
260
|
+
"Waiting for push to complete",
|
|
261
|
+
)
|
|
262
|
+
await self.run_in_vm(["k3s", "kubectl", "delete", "job", job_name], "Cleaning up push job")
|
|
263
|
+
finally:
|
|
264
|
+
await image_path.unlink(missing_ok=True)
|
|
265
|
+
|
|
200
266
|
@typing.override
|
|
201
267
|
async def exec(self, command: list[str]):
|
|
202
268
|
await anyio.run_process(
|
|
@@ -12,8 +12,6 @@ import typing
|
|
|
12
12
|
import anyio
|
|
13
13
|
import pydantic
|
|
14
14
|
import yaml
|
|
15
|
-
from InquirerPy import inquirer
|
|
16
|
-
from InquirerPy.base.control import Choice
|
|
17
15
|
|
|
18
16
|
from agentstack_cli.commands.platform.base_driver import BaseDriver
|
|
19
17
|
from agentstack_cli.configuration import Configuration
|
|
@@ -87,29 +85,9 @@ class WSLDriver(BaseDriver):
|
|
|
87
85
|
if not config.has_section("wsl2"):
|
|
88
86
|
config.add_section("wsl2")
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
textwrap.dedent("""\
|
|
94
|
-
The Agent Stack platform needs to switch WSL to `mirrored` networking mode in order to support connecting to Windows applications -- like Ollama or self-registered agents. If you skip this step, these features won't be available.
|
|
95
|
-
|
|
96
|
-
However, the default `nat` mode is required by some software, like Docker Desktop or Rancher Desktop, to function properly. If you use such software, you may want to keep the default `nat` mode.
|
|
97
|
-
|
|
98
|
-
(It can be changed anytime later in C:/Users/<your name>/.wslconfig, followed by `wsl --shutdown` and `agentstack platform start` to apply changes.)
|
|
99
|
-
"""),
|
|
100
|
-
choices=[
|
|
101
|
-
Choice(
|
|
102
|
-
value=True,
|
|
103
|
-
name="Change WSL2 networking mode to `mirrored`",
|
|
104
|
-
),
|
|
105
|
-
Choice(
|
|
106
|
-
value=False,
|
|
107
|
-
name="Leave WSL2 networking mode as `nat`",
|
|
108
|
-
),
|
|
109
|
-
],
|
|
110
|
-
).execute_async()
|
|
111
|
-
):
|
|
112
|
-
config.set("wsl2", "networkingMode", "mirrored")
|
|
88
|
+
wsl2_networking_mode = config.get("wsl2", "networkingMode", fallback=None)
|
|
89
|
+
if wsl2_networking_mode and wsl2_networking_mode != "nat":
|
|
90
|
+
config.set("wsl2", "networkingMode", "nat")
|
|
113
91
|
f.seek(0)
|
|
114
92
|
f.truncate(0)
|
|
115
93
|
config.write(f)
|
|
@@ -154,6 +132,16 @@ class WSLDriver(BaseDriver):
|
|
|
154
132
|
values_file: pathlib.Path | None = None,
|
|
155
133
|
import_images: list[str] | None = None,
|
|
156
134
|
) -> None:
|
|
135
|
+
host_ip = (
|
|
136
|
+
(
|
|
137
|
+
await self.run_in_vm(
|
|
138
|
+
["bash", "-c", "ip route show | grep -i default | cut -d' ' -f3"],
|
|
139
|
+
"Detecting host IP address",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
.stdout.decode()
|
|
143
|
+
.strip()
|
|
144
|
+
)
|
|
157
145
|
await self.run_in_vm(
|
|
158
146
|
["k3s", "kubectl", "apply", "-f", "-"],
|
|
159
147
|
"Setting up internal networking",
|
|
@@ -163,7 +151,7 @@ class WSLDriver(BaseDriver):
|
|
|
163
151
|
"kind": "ConfigMap",
|
|
164
152
|
"metadata": {"name": "coredns-custom", "namespace": "kube-system"},
|
|
165
153
|
"data": {
|
|
166
|
-
"default.server": "host.docker.internal {\n hosts {\n
|
|
154
|
+
"default.server": f"host.docker.internal {{\n hosts {{\n {host_ip} host.docker.internal\n fallthrough\n }}\n}}"
|
|
167
155
|
},
|
|
168
156
|
}
|
|
169
157
|
).encode(),
|
|
@@ -220,6 +208,10 @@ class WSLDriver(BaseDriver):
|
|
|
220
208
|
async def import_image(self, tag: str) -> None:
|
|
221
209
|
raise NotImplementedError("Importing images is not supported on this platform.")
|
|
222
210
|
|
|
211
|
+
@typing.override
|
|
212
|
+
async def import_image_to_internal_registry(self, tag: str) -> None:
|
|
213
|
+
raise NotImplementedError("Importing images to internal registry is not supported on this platform.")
|
|
214
|
+
|
|
223
215
|
@typing.override
|
|
224
216
|
async def exec(self, command: list[str]):
|
|
225
217
|
await anyio.run_process(
|
|
Binary file
|
|
@@ -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.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/__init__.py
RENAMED
|
File without changes
|
{agentstack_cli-0.5.0rc5 → agentstack_cli-0.5.1rc3}/src/agentstack_cli/commands/platform/istio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|