agentstack-cli 0.6.1__tar.gz → 0.6.2rc2__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.6.1 → agentstack_cli-0.6.2rc2}/PKG-INFO +1 -1
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/pyproject.toml +7 -7
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/async_typer.py +2 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/auth_manager.py +4 -1
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/agent.py +66 -59
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/build.py +0 -1
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/connector.py +1 -1
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/model.py +50 -39
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/base_driver.py +81 -7
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/lima_driver.py +17 -92
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/wsl_driver.py +14 -15
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/self.py +6 -6
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/server.py +7 -15
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/configuration.py +1 -1
- agentstack_cli-0.6.2rc2/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/server_utils.py +1 -3
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/utils.py +72 -5
- agentstack_cli-0.6.1/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/README.md +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/__init__.py +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/api.py +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/__init__.py +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/user.py +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/data/.gitignore +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentstack-cli"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.2-rc2"
|
|
4
4
|
description = "Agent Stack CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "IBM Corp." }]
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
|
|
28
28
|
[dependency-groups]
|
|
29
29
|
dev = [
|
|
30
|
-
"
|
|
30
|
+
"pyrefly>=0.52.0",
|
|
31
31
|
"pytest>=9.0.2",
|
|
32
32
|
"ruff>=0.14.14",
|
|
33
33
|
"wheel>=0.46.3",
|
|
@@ -74,8 +74,8 @@ lint.ignore = [
|
|
|
74
74
|
]
|
|
75
75
|
force-exclude = true
|
|
76
76
|
|
|
77
|
-
[tool.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
[tool.pyrefly]
|
|
78
|
+
project-includes = [
|
|
79
|
+
"**/*.py*",
|
|
80
|
+
"**/*.ipynb",
|
|
81
|
+
]
|
|
@@ -22,6 +22,8 @@ from agentstack_cli.utils import extract_messages, format_error
|
|
|
22
22
|
|
|
23
23
|
DEBUG = Configuration().debug
|
|
24
24
|
|
|
25
|
+
sys.unraisablehook = lambda _: None # Suppress benign cleanup errors
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
class _LeftAlignedHeading(Heading):
|
|
27
29
|
def __rich_console__(self, *args, **kwargs) -> RenderResult:
|
|
@@ -287,7 +287,10 @@ class AuthManager:
|
|
|
287
287
|
def active_auth_server(self, auth_server: str | None) -> None:
|
|
288
288
|
if auth_server is not None and (
|
|
289
289
|
self._auth.active_server not in self._auth.servers
|
|
290
|
-
or
|
|
290
|
+
or (
|
|
291
|
+
self._auth.active_server is not None
|
|
292
|
+
and auth_server not in self._auth.servers[self._auth.active_server].authorization_servers
|
|
293
|
+
)
|
|
291
294
|
):
|
|
292
295
|
raise ValueError(f"Auth server {auth_server} not found in active server")
|
|
293
296
|
self._auth.active_auth_server = auth_server
|
|
@@ -93,12 +93,14 @@ from agentstack_cli.commands.build import _server_side_build
|
|
|
93
93
|
from agentstack_cli.commands.model import ensure_llm_provider
|
|
94
94
|
from agentstack_cli.configuration import Configuration
|
|
95
95
|
|
|
96
|
+
# This is necessary for proper handling of arrow keys in interactive input
|
|
96
97
|
if sys.platform != "win32":
|
|
98
|
+
import importlib
|
|
99
|
+
|
|
97
100
|
try:
|
|
98
|
-
|
|
99
|
-
import gnureadline as readline
|
|
101
|
+
readline = importlib.import_module("gnureadline")
|
|
100
102
|
except ImportError:
|
|
101
|
-
|
|
103
|
+
readline = importlib.import_module("readline")
|
|
102
104
|
|
|
103
105
|
from collections.abc import Callable
|
|
104
106
|
from pathlib import Path
|
|
@@ -115,6 +117,8 @@ from agentstack_cli.async_typer import AsyncTyper, console, create_table, err_co
|
|
|
115
117
|
from agentstack_cli.server_utils import announce_server_action, confirm_server_action
|
|
116
118
|
from agentstack_cli.utils import (
|
|
117
119
|
generate_schema_example,
|
|
120
|
+
get_github_repo_tags,
|
|
121
|
+
github_url_verbose_pattern,
|
|
118
122
|
is_github_url,
|
|
119
123
|
parse_env_var,
|
|
120
124
|
print_log,
|
|
@@ -233,44 +237,50 @@ async def add_agent(
|
|
|
233
237
|
- **Combined Formats**: `https://github.com/myorg/myrepo.git@v1.0.0#path=/path/to/agent`
|
|
234
238
|
- **Enterprise GitHub**: `https://github.mycompany.com/myorg/myrepo`
|
|
235
239
|
- **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"`
|
|
236
|
-
|
|
237
|
-
[aliases: install]
|
|
238
240
|
"""
|
|
241
|
+
repo_input = location
|
|
239
242
|
if location is None:
|
|
240
243
|
repo_input = (
|
|
241
|
-
await inquirer.text(
|
|
244
|
+
await inquirer.text(
|
|
242
245
|
message="Enter GitHub repository (owner/repo or full URL):",
|
|
243
246
|
).execute_async()
|
|
244
247
|
or ""
|
|
245
248
|
)
|
|
246
249
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
if not repo_input:
|
|
251
|
+
console.error("No location provided. Exiting.")
|
|
252
|
+
sys.exit(1)
|
|
250
253
|
|
|
251
|
-
|
|
254
|
+
if match := re.match(github_url_verbose_pattern, repo_input, re.VERBOSE):
|
|
255
|
+
owner, repo, version, path = (
|
|
256
|
+
match.group("org"),
|
|
257
|
+
match.group("repo").removesuffix(".git"),
|
|
258
|
+
match.group("version"),
|
|
259
|
+
match.group("path"),
|
|
260
|
+
)
|
|
252
261
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
headers={"Accept": "application/vnd.github.v3+json"},
|
|
257
|
-
)
|
|
258
|
-
tags = [tag["name"] for tag in response.json()] if response.status_code == 200 else []
|
|
262
|
+
if version is None and path is None:
|
|
263
|
+
host = match.group("host")
|
|
264
|
+
tags = await get_github_repo_tags(host, owner, repo)
|
|
259
265
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
).execute_async()
|
|
265
|
-
else:
|
|
266
|
-
selected_tag = (
|
|
267
|
-
await inquirer.text( # pyright: ignore[reportPrivateImportUsage]
|
|
268
|
-
message="Enter tag to use:",
|
|
266
|
+
if tags:
|
|
267
|
+
selected_tag = await inquirer.fuzzy(
|
|
268
|
+
message="Select a tag to use:",
|
|
269
|
+
choices=tags,
|
|
269
270
|
).execute_async()
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
else:
|
|
272
|
+
selected_tag = (
|
|
273
|
+
await inquirer.text(
|
|
274
|
+
message="Enter tag to use:",
|
|
275
|
+
).execute_async()
|
|
276
|
+
or "main"
|
|
277
|
+
)
|
|
272
278
|
|
|
273
|
-
|
|
279
|
+
location = f"https://github.com/{owner}/{repo}@{selected_tag}"
|
|
280
|
+
else:
|
|
281
|
+
location = repo_input
|
|
282
|
+
else:
|
|
283
|
+
location = repo_input
|
|
274
284
|
|
|
275
285
|
url = announce_server_action(f"Installing agent '{location}' for")
|
|
276
286
|
await confirm_server_action("Proceed with installing this agent on", url=url, yes=yes)
|
|
@@ -327,7 +337,7 @@ async def update_agent(
|
|
|
327
337
|
provider_choices = [
|
|
328
338
|
Choice(value=p, name=f"{p.agent_card.name} ({ProviderUtils.short_location(p)})") for p in providers
|
|
329
339
|
]
|
|
330
|
-
provider = await inquirer.fuzzy(
|
|
340
|
+
provider = await inquirer.fuzzy(
|
|
331
341
|
message="Select an agent to update:",
|
|
332
342
|
choices=provider_choices,
|
|
333
343
|
).execute_async()
|
|
@@ -337,20 +347,20 @@ async def update_agent(
|
|
|
337
347
|
else:
|
|
338
348
|
provider = select_provider(search_path, providers=providers)
|
|
339
349
|
|
|
340
|
-
if location is None and is_github_url(provider.
|
|
341
|
-
match = re.
|
|
350
|
+
if location is None and is_github_url(provider.origin):
|
|
351
|
+
match = re.match(github_url_verbose_pattern, provider.origin, re.VERBOSE)
|
|
352
|
+
|
|
342
353
|
if match:
|
|
343
|
-
owner, repo =
|
|
354
|
+
host, owner, repo = (
|
|
355
|
+
match.group("host"),
|
|
356
|
+
match.group("owner"),
|
|
357
|
+
match.group("repo").removesuffix(".git"),
|
|
358
|
+
)
|
|
344
359
|
|
|
345
|
-
|
|
346
|
-
response = await client.get(
|
|
347
|
-
f"https://api.github.com/repos/{owner}/{repo}/tags",
|
|
348
|
-
headers={"Accept": "application/vnd.github.v3+json"},
|
|
349
|
-
)
|
|
350
|
-
tags = [tag["name"] for tag in response.json()] if response.status_code == 200 else []
|
|
360
|
+
tags = await get_github_repo_tags(host, owner, repo)
|
|
351
361
|
|
|
352
362
|
if tags:
|
|
353
|
-
selected_tag = await inquirer.fuzzy(
|
|
363
|
+
selected_tag = await inquirer.fuzzy(
|
|
354
364
|
message="Select a new tag to use:",
|
|
355
365
|
choices=tags,
|
|
356
366
|
).execute_async()
|
|
@@ -359,9 +369,9 @@ async def update_agent(
|
|
|
359
369
|
|
|
360
370
|
if location is None:
|
|
361
371
|
location = (
|
|
362
|
-
await inquirer.text(
|
|
372
|
+
await inquirer.text(
|
|
363
373
|
message="Enter new agent location (public docker image or github url):",
|
|
364
|
-
default=provider.
|
|
374
|
+
default=provider.origin.lstrip("git+"),
|
|
365
375
|
).execute_async()
|
|
366
376
|
or ""
|
|
367
377
|
)
|
|
@@ -370,7 +380,7 @@ async def update_agent(
|
|
|
370
380
|
console.error("No location provided. Exiting.")
|
|
371
381
|
sys.exit(1)
|
|
372
382
|
|
|
373
|
-
url = announce_server_action(f"Upgrading agent from '{provider.
|
|
383
|
+
url = announce_server_action(f"Upgrading agent from '{provider.origin}' to {location}")
|
|
374
384
|
await confirm_server_action("Proceed with upgrading agent on", url=url, yes=yes)
|
|
375
385
|
|
|
376
386
|
if is_github_url(location):
|
|
@@ -428,7 +438,7 @@ async def select_providers_multi(search_path: str, providers: list[Provider]) ->
|
|
|
428
438
|
# Multiple matches - show selection menu
|
|
429
439
|
choices = [Choice(value=p.id, name=f"{p.agent_card.name} - {p.id}") for p in provider_candidates.values()]
|
|
430
440
|
|
|
431
|
-
selected_ids = await inquirer.checkbox(
|
|
441
|
+
selected_ids = await inquirer.checkbox(
|
|
432
442
|
message="Select agents to remove (use ↑/↓ to navigate, Space to select):", choices=choices
|
|
433
443
|
).execute_async()
|
|
434
444
|
|
|
@@ -512,7 +522,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
512
522
|
|
|
513
523
|
for field in form_render.fields:
|
|
514
524
|
if isinstance(field, TextField):
|
|
515
|
-
answer = await inquirer.text(
|
|
525
|
+
answer = await inquirer.text(
|
|
516
526
|
message=field.label + ":",
|
|
517
527
|
default=field.default_value or "",
|
|
518
528
|
validate=EmptyInputValidator() if field.required else None,
|
|
@@ -520,7 +530,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
520
530
|
form_values[field.id] = TextFieldValue(value=answer)
|
|
521
531
|
elif isinstance(field, SingleSelectField):
|
|
522
532
|
choices = [Choice(value=opt.id, name=opt.label) for opt in field.options]
|
|
523
|
-
answer = await inquirer.fuzzy(
|
|
533
|
+
answer = await inquirer.fuzzy(
|
|
524
534
|
message=field.label + ":",
|
|
525
535
|
choices=choices,
|
|
526
536
|
default=field.default_value,
|
|
@@ -529,7 +539,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
529
539
|
form_values[field.id] = SingleSelectFieldValue(value=answer)
|
|
530
540
|
elif isinstance(field, MultiSelectField):
|
|
531
541
|
choices = [Choice(value=opt.id, name=opt.label) for opt in field.options]
|
|
532
|
-
answer = await inquirer.checkbox(
|
|
542
|
+
answer = await inquirer.checkbox(
|
|
533
543
|
message=field.label + ":",
|
|
534
544
|
choices=choices,
|
|
535
545
|
default=field.default_value,
|
|
@@ -538,14 +548,14 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
538
548
|
form_values[field.id] = MultiSelectFieldValue(value=answer)
|
|
539
549
|
|
|
540
550
|
elif isinstance(field, DateField):
|
|
541
|
-
year = await inquirer.text(
|
|
551
|
+
year = await inquirer.text(
|
|
542
552
|
message=f"{field.label} (year):",
|
|
543
553
|
validate=EmptyInputValidator() if field.required else None,
|
|
544
554
|
filter=lambda y: y.strip(),
|
|
545
555
|
).execute_async()
|
|
546
556
|
if not year:
|
|
547
557
|
continue
|
|
548
|
-
month = await inquirer.fuzzy(
|
|
558
|
+
month = await inquirer.fuzzy(
|
|
549
559
|
message=f"{field.label} (month):",
|
|
550
560
|
validate=EmptyInputValidator() if field.required else None,
|
|
551
561
|
choices=[
|
|
@@ -558,7 +568,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
558
568
|
).execute_async()
|
|
559
569
|
if not month:
|
|
560
570
|
continue
|
|
561
|
-
day = await inquirer.fuzzy(
|
|
571
|
+
day = await inquirer.fuzzy(
|
|
562
572
|
message=f"{field.label} (day):",
|
|
563
573
|
validate=EmptyInputValidator() if field.required else None,
|
|
564
574
|
choices=[
|
|
@@ -571,7 +581,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
|
|
|
571
581
|
full_date = f"{year}-{month}-{day}"
|
|
572
582
|
form_values[field.id] = DateFieldValue(value=full_date)
|
|
573
583
|
elif isinstance(field, CheckboxField):
|
|
574
|
-
answer = await inquirer.confirm(
|
|
584
|
+
answer = await inquirer.confirm(
|
|
575
585
|
message=field.label + ":",
|
|
576
586
|
default=field.default_value,
|
|
577
587
|
long_instruction=field.content or "",
|
|
@@ -591,7 +601,7 @@ async def _ask_settings_questions(settings_render: SettingsRender) -> AgentRunSe
|
|
|
591
601
|
if isinstance(field, CheckboxGroupField):
|
|
592
602
|
checkbox_values: dict[str, SettingsCheckboxFieldValue] = {}
|
|
593
603
|
for checkbox in field.fields:
|
|
594
|
-
answer = await inquirer.confirm(
|
|
604
|
+
answer = await inquirer.confirm(
|
|
595
605
|
message=checkbox.label + ":",
|
|
596
606
|
default=checkbox.default_value,
|
|
597
607
|
).execute_async()
|
|
@@ -599,7 +609,7 @@ async def _ask_settings_questions(settings_render: SettingsRender) -> AgentRunSe
|
|
|
599
609
|
settings_values[field.id] = CheckboxGroupFieldValue(values=checkbox_values)
|
|
600
610
|
elif isinstance(field, SettingsSingleSelectField):
|
|
601
611
|
choices = [Choice(value=opt.value, name=opt.label) for opt in field.options]
|
|
602
|
-
answer = await inquirer.fuzzy(
|
|
612
|
+
answer = await inquirer.fuzzy(
|
|
603
613
|
message=field.label + ":",
|
|
604
614
|
choices=choices,
|
|
605
615
|
default=field.default_value,
|
|
@@ -675,11 +685,7 @@ async def _run_agent(
|
|
|
675
685
|
else {}
|
|
676
686
|
)
|
|
677
687
|
| (
|
|
678
|
-
{
|
|
679
|
-
FormServiceExtensionSpec.URI: {
|
|
680
|
-
"form_fulfillments": {"initial_form": typing.cast(FormResponse, input).model_dump(mode="json")}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
688
|
+
{FormServiceExtensionSpec.URI: {"form_fulfillments": {"initial_form": input.model_dump(mode="json")}}}
|
|
683
689
|
if isinstance(input, FormResponse)
|
|
684
690
|
else {}
|
|
685
691
|
)
|
|
@@ -898,7 +904,8 @@ class ShowConfig(InteractiveCommand):
|
|
|
898
904
|
schema_table.add_row(
|
|
899
905
|
prop,
|
|
900
906
|
json.dumps(required_schema),
|
|
901
|
-
|
|
907
|
+
# pyrefly: ignore [bad-argument-type] -- probably a bug in Pyrefly
|
|
908
|
+
json.dumps(generate_schema_example(required_schema)),
|
|
902
909
|
)
|
|
903
910
|
|
|
904
911
|
renderables = [
|
|
@@ -1071,7 +1078,7 @@ async def run_agent(
|
|
|
1071
1078
|
if not providers:
|
|
1072
1079
|
err_console.error("No agents found. Add an agent first using 'agentstack agent add'.")
|
|
1073
1080
|
sys.exit(1)
|
|
1074
|
-
search_path = await inquirer.fuzzy(
|
|
1081
|
+
search_path = await inquirer.fuzzy(
|
|
1075
1082
|
message="Select an agent to run:",
|
|
1076
1083
|
choices=[provider.agent_card.name for provider in providers],
|
|
1077
1084
|
).execute_async()
|
|
@@ -62,7 +62,6 @@ async def client_side_build(
|
|
|
62
62
|
):
|
|
63
63
|
"""Build agent locally using Docker. [Local only]"""
|
|
64
64
|
with verbosity(verbose):
|
|
65
|
-
await run_command(["which", "docker"], "Checking docker")
|
|
66
65
|
image_id = "agentstack-agent-build-tmp:latest"
|
|
67
66
|
port = await find_free_port()
|
|
68
67
|
dockerfile_args = ("-f", dockerfile) if dockerfile else ()
|
|
@@ -75,7 +75,7 @@ async def select_connectors_multi(
|
|
|
75
75
|
# Multiple matches - show selection menu
|
|
76
76
|
choices = [Choice(value=c, name=f"{c.url} - {c.id} ({c.state})") for c in connector_candidates]
|
|
77
77
|
|
|
78
|
-
selected_connectors = await inquirer.checkbox(
|
|
78
|
+
selected_connectors = await inquirer.checkbox(
|
|
79
79
|
message=f"Select connectors to {operation_name} (use ↑/↓ to navigate, Space to select):", choices=choices
|
|
80
80
|
).execute_async()
|
|
81
81
|
|
|
@@ -19,7 +19,6 @@ from agentstack_sdk.platform import (
|
|
|
19
19
|
)
|
|
20
20
|
from InquirerPy import inquirer
|
|
21
21
|
from InquirerPy.base.control import Choice
|
|
22
|
-
from InquirerPy.validator import EmptyInputValidator
|
|
23
22
|
from rich.table import Column
|
|
24
23
|
|
|
25
24
|
from agentstack_cli.api import openai_client
|
|
@@ -184,26 +183,26 @@ async def _add_provider(capability: ModelCapability, use_true_localhost: bool =
|
|
|
184
183
|
base_url: str
|
|
185
184
|
watsonx_project_id, watsonx_space_id = None, None
|
|
186
185
|
choices = LLM_PROVIDERS if capability == ModelCapability.LLM else EMBEDDING_PROVIDERS
|
|
187
|
-
provider_type, provider_name, base_url = await inquirer.fuzzy(
|
|
186
|
+
provider_type, provider_name, base_url = await inquirer.fuzzy(
|
|
188
187
|
message=f"Select {capability} provider (type to search):", choices=choices
|
|
189
|
-
).execute_async()
|
|
188
|
+
).execute_async() or sys.exit(1)
|
|
190
189
|
|
|
191
190
|
watsonx_project_or_space: str = ""
|
|
192
191
|
watsonx_project_or_space_id: str = ""
|
|
193
192
|
|
|
194
193
|
if provider_type == ModelProviderType.OTHER:
|
|
195
|
-
base_url: str = await inquirer.text(
|
|
194
|
+
base_url: str = await inquirer.text(
|
|
196
195
|
message="Enter the base URL of your API (OpenAI-compatible):",
|
|
197
|
-
validate=lambda url:
|
|
196
|
+
validate=lambda url: url.startswith(("http://", "https://")),
|
|
198
197
|
transformer=lambda url: url.rstrip("/"),
|
|
199
|
-
).execute_async()
|
|
198
|
+
).execute_async() or sys.exit(1)
|
|
200
199
|
if re.match(r"^https://[a-z0-9.-]+\.rits\.fmaas\.res\.ibm\.com/.*$", base_url):
|
|
201
200
|
provider_type = ModelProviderType.RITS
|
|
202
201
|
if not base_url.endswith("/v1"):
|
|
203
202
|
base_url = base_url.removesuffix("/") + "/v1"
|
|
204
203
|
|
|
205
204
|
if provider_type == ModelProviderType.WATSONX:
|
|
206
|
-
region: str = await inquirer.select(
|
|
205
|
+
region: str = await inquirer.select(
|
|
207
206
|
message="Select IBM Cloud region:",
|
|
208
207
|
choices=[
|
|
209
208
|
Choice(name="us-south", value="us-south"),
|
|
@@ -213,34 +212,36 @@ async def _add_provider(capability: ModelCapability, use_true_localhost: bool =
|
|
|
213
212
|
Choice(name="jp-tok", value="jp-tok"),
|
|
214
213
|
Choice(name="au-syd", value="au-syd"),
|
|
215
214
|
],
|
|
216
|
-
).execute_async()
|
|
215
|
+
).execute_async() or sys.exit(1)
|
|
217
216
|
base_url: str = f"""https://{region}.ml.cloud.ibm.com"""
|
|
218
|
-
watsonx_project_or_space: str = await inquirer.select(
|
|
217
|
+
watsonx_project_or_space: str = await inquirer.select(
|
|
219
218
|
"Use a Project or a Space?", choices=["project", "space"]
|
|
220
|
-
).execute_async()
|
|
219
|
+
).execute_async() or sys.exit(1)
|
|
221
220
|
if (
|
|
222
221
|
not (watsonx_project_or_space_id := os.environ.get(f"WATSONX_{watsonx_project_or_space.upper()}_ID", ""))
|
|
223
|
-
or not await inquirer.confirm(
|
|
222
|
+
or not await inquirer.confirm(
|
|
224
223
|
message=f"Use the {watsonx_project_or_space} id from environment variable 'WATSONX_{watsonx_project_or_space.upper()}_ID'?",
|
|
225
224
|
default=True,
|
|
226
225
|
).execute_async()
|
|
227
226
|
):
|
|
228
|
-
watsonx_project_or_space_id = await inquirer.text(
|
|
227
|
+
watsonx_project_or_space_id = await inquirer.text(
|
|
229
228
|
message=f"Enter the {watsonx_project_or_space} id:"
|
|
230
|
-
).execute_async()
|
|
229
|
+
).execute_async() or sys.exit(1)
|
|
231
230
|
|
|
232
231
|
watsonx_project_id = watsonx_project_or_space_id if watsonx_project_or_space == "project" else None
|
|
233
232
|
watsonx_space_id = watsonx_project_or_space_id if watsonx_project_or_space == "space" else None
|
|
234
233
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
234
|
+
api_key: str = (
|
|
235
|
+
"dummy"
|
|
236
|
+
if provider_type in {ModelProviderType.OLLAMA, ModelProviderType.JAN}
|
|
237
|
+
else env_api_key
|
|
238
|
+
if (env_api_key := os.environ.get(f"{provider_type.upper()}_API_KEY"))
|
|
239
|
+
and await inquirer.confirm(
|
|
240
|
+
message=f"Use the API key from environment variable '{provider_type.upper()}_API_KEY'?",
|
|
241
|
+
default=True,
|
|
242
|
+
).execute_async()
|
|
243
|
+
else await inquirer.secret(message="Enter API key:").execute_async() or ""
|
|
244
|
+
)
|
|
244
245
|
|
|
245
246
|
try:
|
|
246
247
|
if provider_type == ModelProviderType.OLLAMA:
|
|
@@ -264,13 +265,13 @@ async def _add_provider(capability: ModelCapability, use_true_localhost: bool =
|
|
|
264
265
|
message = f"Do you want to pull the recommended LLM model '{recommended_llm_model}'?"
|
|
265
266
|
if not available_models:
|
|
266
267
|
message = f"There are no locally available models in Ollama. {message}"
|
|
267
|
-
if await inquirer.confirm(message, default=True).execute_async():
|
|
268
|
+
if await inquirer.confirm(message, default=True).execute_async():
|
|
268
269
|
await run_command(
|
|
269
270
|
[_ollama_exe(), "pull", recommended_llm_model], "Pulling the selected model", check=True
|
|
270
271
|
)
|
|
271
272
|
|
|
272
273
|
if recommended_embedding_model not in available_models and (
|
|
273
|
-
await inquirer.confirm(
|
|
274
|
+
await inquirer.confirm(
|
|
274
275
|
message=f"Do you want to pull the recommended embedding model '{recommended_embedding_model}'?",
|
|
275
276
|
default=True,
|
|
276
277
|
).execute_async()
|
|
@@ -293,8 +294,8 @@ async def _add_provider(capability: ModelCapability, use_true_localhost: bool =
|
|
|
293
294
|
)
|
|
294
295
|
|
|
295
296
|
except httpx.HTTPError as e:
|
|
296
|
-
if hasattr(e, "response") and hasattr(e.response, "json"):
|
|
297
|
-
err = str(e.response.json().get("detail", str(e)))
|
|
297
|
+
if hasattr(e, "response") and hasattr(e.response, "json"):
|
|
298
|
+
err = str(e.response.json().get("detail", str(e)))
|
|
298
299
|
else:
|
|
299
300
|
err = str(e)
|
|
300
301
|
match provider_type:
|
|
@@ -336,12 +337,12 @@ async def _select_default_model(capability: ModelCapability) -> str | None:
|
|
|
336
337
|
selected_model = (
|
|
337
338
|
recommended_model
|
|
338
339
|
if recommended_model
|
|
339
|
-
and await inquirer.confirm(
|
|
340
|
+
and await inquirer.confirm(
|
|
340
341
|
message=f"Do you want to use the recommended model as default: '{recommended_model}'?",
|
|
341
342
|
default=True,
|
|
342
343
|
).execute_async()
|
|
343
344
|
else (
|
|
344
|
-
await inquirer.fuzzy(
|
|
345
|
+
await inquirer.fuzzy(
|
|
345
346
|
message="Select a model to be used as default (type to search):",
|
|
346
347
|
choices=sorted(available_models),
|
|
347
348
|
).execute_async()
|
|
@@ -429,7 +430,7 @@ async def setup(
|
|
|
429
430
|
console.warning("The following providers are already configured:\n")
|
|
430
431
|
_list_providers(existing_providers)
|
|
431
432
|
console.print()
|
|
432
|
-
if await inquirer.confirm(
|
|
433
|
+
if await inquirer.confirm(
|
|
433
434
|
message="Do you want to reset the configuration?", default=True
|
|
434
435
|
).execute_async():
|
|
435
436
|
with console.status("Resetting configuration...", spinner="dots"):
|
|
@@ -450,13 +451,13 @@ async def setup(
|
|
|
450
451
|
!= ModelProviderType.RITS # RITS does not support embeddings, but we treat it as OTHER
|
|
451
452
|
and (
|
|
452
453
|
llm_provider.type != ModelProviderType.OTHER # OTHER may not support embeddings, so we ask
|
|
453
|
-
or inquirer.confirm(
|
|
454
|
+
or inquirer.confirm(
|
|
454
455
|
"Do you want to also set up an embedding model from the same provider?", default=True
|
|
455
456
|
)
|
|
456
457
|
)
|
|
457
458
|
):
|
|
458
459
|
default_embedding_model = await _select_default_model(ModelCapability.EMBEDDING)
|
|
459
|
-
elif await inquirer.confirm(
|
|
460
|
+
elif await inquirer.confirm(
|
|
460
461
|
message="Do you want to configure an embedding provider? (recommended)", default=True
|
|
461
462
|
).execute_async():
|
|
462
463
|
console.print("[bold]Setting up embedding provider...[/bold]")
|
|
@@ -490,7 +491,7 @@ async def select_default_model(
|
|
|
490
491
|
url = announce_server_action("Updating default model for")
|
|
491
492
|
await confirm_server_action("Proceed with updating default model on", url=url, yes=yes)
|
|
492
493
|
if not capability:
|
|
493
|
-
capability = await inquirer.select(
|
|
494
|
+
capability = await inquirer.select(
|
|
494
495
|
message="Which default model would you like to change?",
|
|
495
496
|
choices=[
|
|
496
497
|
Choice(name="llm", value=ModelCapability.LLM),
|
|
@@ -518,9 +519,17 @@ app.add_typer(model_provider_app, name="provider")
|
|
|
518
519
|
|
|
519
520
|
|
|
520
521
|
def _list_providers(providers: list[ModelProvider]):
|
|
521
|
-
with create_table(Column("Type"), Column("Name"), Column("Base URL", ratio=1)) as provider_table:
|
|
522
|
+
with create_table(Column("Type"), Column("Name"), Column("State"), Column("Base URL", ratio=1)) as provider_table:
|
|
522
523
|
for provider in providers:
|
|
523
|
-
provider_table.add_row(
|
|
524
|
+
provider_table.add_row(
|
|
525
|
+
provider.type,
|
|
526
|
+
provider.name,
|
|
527
|
+
{
|
|
528
|
+
"online": "[green]● connected[/green]",
|
|
529
|
+
"offline": "[bright_black]○ disconnected[/bright_black]",
|
|
530
|
+
}.get(provider.state, provider.state or "<unknown>"),
|
|
531
|
+
str(provider.base_url),
|
|
532
|
+
)
|
|
524
533
|
console.print(provider_table)
|
|
525
534
|
|
|
526
535
|
|
|
@@ -543,7 +552,7 @@ async def add_provider(
|
|
|
543
552
|
"""Add a new model provider. [Admin only]"""
|
|
544
553
|
announce_server_action("Adding provider for")
|
|
545
554
|
if not capability:
|
|
546
|
-
capability = await inquirer.select(
|
|
555
|
+
capability = await inquirer.select(
|
|
547
556
|
message="Which default provider would you like to add?",
|
|
548
557
|
choices=[
|
|
549
558
|
Choice(name="llm", value=ModelCapability.LLM),
|
|
@@ -598,13 +607,15 @@ async def remove_provider(
|
|
|
598
607
|
async with configuration.use_platform_client():
|
|
599
608
|
providers = await ModelProvider.list()
|
|
600
609
|
|
|
601
|
-
|
|
602
|
-
|
|
610
|
+
provider: ModelProvider = (
|
|
611
|
+
_select_provider(providers, search_path)
|
|
612
|
+
if search_path
|
|
613
|
+
else await inquirer.select(
|
|
603
614
|
message="Choose a provider to remove:",
|
|
604
615
|
choices=[Choice(name=f"{p.type} ({p.base_url})", value=p) for p in providers],
|
|
605
616
|
).execute_async()
|
|
606
|
-
|
|
607
|
-
|
|
617
|
+
or sys.exit(1)
|
|
618
|
+
)
|
|
608
619
|
|
|
609
620
|
await provider.delete()
|
|
610
621
|
|
{agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/base_driver.py
RENAMED
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import pathlib
|
|
8
8
|
import shlex
|
|
9
9
|
import typing
|
|
10
|
+
import uuid
|
|
10
11
|
from enum import StrEnum
|
|
11
12
|
from subprocess import CompletedProcess
|
|
12
13
|
from textwrap import dedent
|
|
@@ -55,13 +56,87 @@ class BaseDriver(abc.ABC):
|
|
|
55
56
|
async def delete(self) -> None: ...
|
|
56
57
|
|
|
57
58
|
@abc.abstractmethod
|
|
58
|
-
async def
|
|
59
|
+
async def exec(self, command: list[str]) -> None: ...
|
|
59
60
|
|
|
60
61
|
@abc.abstractmethod
|
|
61
|
-
|
|
62
|
+
def _get_export_import_paths(self) -> tuple[str, str]: ...
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
async def import_images(self, *tags: str) -> None:
|
|
65
|
+
if not tags:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
host_path, guest_path = self._get_export_import_paths()
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
await run_command(
|
|
72
|
+
["docker", "image", "save", "-o", host_path, *tags],
|
|
73
|
+
f"Exporting image{'' if len(tags) == 1 else 's'} {', '.join(tags)} from Docker",
|
|
74
|
+
)
|
|
75
|
+
await self.run_in_vm(
|
|
76
|
+
["/bin/sh", "-c", f"k3s ctr images import {guest_path}"],
|
|
77
|
+
f"Importing image{'' if len(tags) == 1 else 's'} {', '.join(tags)} into Agent Stack platform",
|
|
78
|
+
)
|
|
79
|
+
finally:
|
|
80
|
+
await anyio.Path(host_path).unlink(missing_ok=True)
|
|
81
|
+
|
|
82
|
+
async def import_image_to_internal_registry(self, tag: str) -> None:
|
|
83
|
+
host_path, guest_path = self._get_export_import_paths()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
await run_command(
|
|
87
|
+
["docker", "image", "save", "-o", str(host_path), tag],
|
|
88
|
+
f"Exporting image {tag} from Docker",
|
|
89
|
+
)
|
|
90
|
+
job_name = f"push-{uuid.uuid4().hex[:6]}"
|
|
91
|
+
await self.run_in_vm(
|
|
92
|
+
["k3s", "kubectl", "apply", "-f", "-"],
|
|
93
|
+
"Starting push job",
|
|
94
|
+
input=yaml.dump(
|
|
95
|
+
{
|
|
96
|
+
"apiVersion": "batch/v1",
|
|
97
|
+
"kind": "Job",
|
|
98
|
+
"metadata": {"name": job_name, "namespace": "default"},
|
|
99
|
+
"spec": {
|
|
100
|
+
"backoffLimit": 0,
|
|
101
|
+
"ttlSecondsAfterFinished": 60,
|
|
102
|
+
"template": {
|
|
103
|
+
"spec": {
|
|
104
|
+
"restartPolicy": "Never",
|
|
105
|
+
"containers": [
|
|
106
|
+
{
|
|
107
|
+
"name": "crane",
|
|
108
|
+
"image": next(
|
|
109
|
+
(image for image in self.loaded_images if "alpine/crane" in image),
|
|
110
|
+
"ghcr.io/i-am-bee/alpine/crane:0.20.6",
|
|
111
|
+
),
|
|
112
|
+
"command": [
|
|
113
|
+
"crane",
|
|
114
|
+
"push",
|
|
115
|
+
f"/workspace/{pathlib.Path(host_path).name}",
|
|
116
|
+
tag,
|
|
117
|
+
"--insecure",
|
|
118
|
+
],
|
|
119
|
+
"volumeMounts": [{"name": "workspace", "mountPath": "/workspace"}],
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"volumes": [
|
|
123
|
+
{
|
|
124
|
+
"name": "workspace",
|
|
125
|
+
"hostPath": {"path": str(pathlib.PurePosixPath(guest_path).parent)},
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
).encode(),
|
|
133
|
+
)
|
|
134
|
+
await self.run_in_vm(
|
|
135
|
+
["k3s", "kubectl", "wait", "--for=condition=complete", f"job/{job_name}", "--timeout=300s"],
|
|
136
|
+
"Waiting for push to complete",
|
|
137
|
+
)
|
|
138
|
+
finally:
|
|
139
|
+
await anyio.Path(host_path).unlink(missing_ok=True)
|
|
65
140
|
|
|
66
141
|
def _canonify(self, tag: str) -> str:
|
|
67
142
|
return tag if "." in tag.split("/")[0] else f"docker.io/{tag}"
|
|
@@ -92,7 +167,6 @@ class BaseDriver(abc.ABC):
|
|
|
92
167
|
}
|
|
93
168
|
|
|
94
169
|
async def install_tools(self) -> None:
|
|
95
|
-
# Configure k3s registry for local registry access
|
|
96
170
|
registry_config = dedent(
|
|
97
171
|
"""\
|
|
98
172
|
mirrors:
|
|
@@ -116,7 +190,7 @@ class BaseDriver(abc.ABC):
|
|
|
116
190
|
"sudo tee /etc/rancher/k3s/registries.yaml > /dev/null"
|
|
117
191
|
),
|
|
118
192
|
],
|
|
119
|
-
"Configuring
|
|
193
|
+
"Configuring Kubernetes registry",
|
|
120
194
|
)
|
|
121
195
|
|
|
122
196
|
await self.run_in_vm(
|
|
@@ -125,7 +199,7 @@ class BaseDriver(abc.ABC):
|
|
|
125
199
|
"-c",
|
|
126
200
|
"which k3s || curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644 --https-listen-port=16443",
|
|
127
201
|
],
|
|
128
|
-
"Installing
|
|
202
|
+
"Installing Kubernetes",
|
|
129
203
|
)
|
|
130
204
|
await self.run_in_vm(
|
|
131
205
|
[
|
{agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/lima_driver.py
RENAMED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
import importlib.resources
|
|
5
5
|
import os
|
|
6
|
+
import pathlib
|
|
6
7
|
import shutil
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
import typing
|
|
10
11
|
import uuid
|
|
11
12
|
from subprocess import CompletedProcess
|
|
13
|
+
from typing import TypedDict
|
|
12
14
|
|
|
13
15
|
import anyio
|
|
14
16
|
import psutil
|
|
@@ -62,9 +64,12 @@ class LimaDriver(BaseDriver):
|
|
|
62
64
|
for line in result.stdout.decode().split("\n"):
|
|
63
65
|
if not line:
|
|
64
66
|
continue
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
|
|
68
|
+
class Status(TypedDict):
|
|
69
|
+
name: str
|
|
70
|
+
status: str
|
|
71
|
+
|
|
72
|
+
status = pydantic.TypeAdapter(Status).validate_json(line)
|
|
68
73
|
if status["name"] == self.vm_name:
|
|
69
74
|
return status["status"].lower()
|
|
70
75
|
return None
|
|
@@ -102,7 +107,7 @@ class LimaDriver(BaseDriver):
|
|
|
102
107
|
if total_memory_gib < 8:
|
|
103
108
|
console.warning("Less than 8 GB of RAM detected. Performance may be degraded.")
|
|
104
109
|
|
|
105
|
-
vm_memory_gib = round(min(8, max(3, total_memory_gib / 2)))
|
|
110
|
+
vm_memory_gib = round(min(8.0, max(3.0, total_memory_gib / 2)))
|
|
106
111
|
|
|
107
112
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete_on_close=False) as template_file:
|
|
108
113
|
template_file.write(
|
|
@@ -180,100 +185,20 @@ class LimaDriver(BaseDriver):
|
|
|
180
185
|
)
|
|
181
186
|
|
|
182
187
|
@typing.override
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
image_file = str(uuid.uuid4())
|
|
189
|
-
image_path = image_dir / image_file
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
await run_command(
|
|
193
|
-
["docker", "image", "save", "-o", str(image_path), *tags],
|
|
194
|
-
f"Exporting image{'' if len(tags) == 1 else 's'} {', '.join(tags)} from Docker",
|
|
195
|
-
)
|
|
196
|
-
await self.run_in_vm(
|
|
197
|
-
["/bin/sh", "-c", f"k3s ctr images import /tmp/agentstack/{image_file}"],
|
|
198
|
-
f"Importing image{'' if len(tags) == 1 else 's'} {', '.join(tags)} into Agent Stack platform",
|
|
199
|
-
)
|
|
200
|
-
finally:
|
|
201
|
-
await image_path.unlink(missing_ok=True)
|
|
202
|
-
|
|
203
|
-
@typing.override
|
|
204
|
-
async def import_image_to_internal_registry(self, tag: str) -> None:
|
|
205
|
-
# 1. Check if registry is running
|
|
206
|
-
try:
|
|
207
|
-
await self.run_in_vm(
|
|
208
|
-
["k3s", "kubectl", "get", "svc", "agentstack-registry-svc"],
|
|
209
|
-
"Checking internal registry availability",
|
|
210
|
-
)
|
|
211
|
-
except Exception as e:
|
|
212
|
-
console.warning(f"Internal registry service not found. Push might fail: {e}")
|
|
213
|
-
|
|
214
|
-
# 2. Export image from Docker to shared temp dir
|
|
215
|
-
image_dir = anyio.Path("/tmp/agentstack")
|
|
216
|
-
await image_dir.mkdir(exist_ok=True, parents=True)
|
|
217
|
-
image_file = f"{uuid.uuid4()}.tar"
|
|
218
|
-
image_path = image_dir / image_file
|
|
219
|
-
|
|
220
|
-
try:
|
|
221
|
-
await run_command(
|
|
222
|
-
["docker", "image", "save", "-o", str(image_path), tag],
|
|
223
|
-
f"Exporting image {tag} from Docker",
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# 3 & 4. Run Crane Job
|
|
227
|
-
crane_image = "ghcr.io/i-am-bee/alpine/crane:0.20.6"
|
|
228
|
-
for image in self.loaded_images:
|
|
229
|
-
if "alpine/crane" in image:
|
|
230
|
-
crane_image = image
|
|
231
|
-
break
|
|
232
|
-
|
|
233
|
-
job_name = f"push-{uuid.uuid4().hex[:6]}"
|
|
234
|
-
job_def = {
|
|
235
|
-
"apiVersion": "batch/v1",
|
|
236
|
-
"kind": "Job",
|
|
237
|
-
"metadata": {"name": job_name, "namespace": "default"},
|
|
238
|
-
"spec": {
|
|
239
|
-
"backoffLimit": 0,
|
|
240
|
-
"ttlSecondsAfterFinished": 60,
|
|
241
|
-
"template": {
|
|
242
|
-
"spec": {
|
|
243
|
-
"restartPolicy": "Never",
|
|
244
|
-
"containers": [
|
|
245
|
-
{
|
|
246
|
-
"name": "crane",
|
|
247
|
-
"image": crane_image,
|
|
248
|
-
"command": ["crane", "push", f"/workspace/{image_file}", tag, "--insecure"],
|
|
249
|
-
"volumeMounts": [{"name": "workspace", "mountPath": "/workspace"}],
|
|
250
|
-
}
|
|
251
|
-
],
|
|
252
|
-
"volumes": [{"name": "workspace", "hostPath": {"path": "/tmp/agentstack"}}],
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
await self.run_in_vm(
|
|
259
|
-
["k3s", "kubectl", "apply", "-f", "-"], "Starting push job", input=yaml.dump(job_def).encode()
|
|
260
|
-
)
|
|
261
|
-
await self.run_in_vm(
|
|
262
|
-
["k3s", "kubectl", "wait", "--for=condition=complete", f"job/{job_name}", "--timeout=300s"],
|
|
263
|
-
"Waiting for push to complete",
|
|
264
|
-
)
|
|
265
|
-
await self.run_in_vm(["k3s", "kubectl", "delete", "job", job_name], "Cleaning up push job")
|
|
266
|
-
finally:
|
|
267
|
-
await image_path.unlink(missing_ok=True)
|
|
188
|
+
def _get_export_import_paths(self) -> tuple[str, str]:
|
|
189
|
+
image_dir = pathlib.Path("/tmp/agentstack")
|
|
190
|
+
image_dir.mkdir(exist_ok=True, parents=True)
|
|
191
|
+
image_path = str(image_dir / f"{uuid.uuid4()}.tar")
|
|
192
|
+
return (image_path, image_path)
|
|
268
193
|
|
|
269
194
|
@typing.override
|
|
270
195
|
async def exec(self, command: list[str]):
|
|
271
196
|
await anyio.run_process(
|
|
272
197
|
[self.limactl_exe, "shell", f"--tty={sys.stdin.isatty()}", self.vm_name, "--", *command],
|
|
273
|
-
input=None if sys.stdin.isatty() else sys.stdin.read().encode(),
|
|
274
198
|
check=False,
|
|
275
|
-
|
|
276
|
-
|
|
199
|
+
stdin=sys.stdin,
|
|
200
|
+
stdout=sys.stdout,
|
|
201
|
+
stderr=sys.stderr,
|
|
277
202
|
env={**os.environ, "LIMA_HOME": str(Configuration().lima_home)},
|
|
278
203
|
cwd="/",
|
|
279
204
|
)
|
{agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/wsl_driver.py
RENAMED
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import pathlib
|
|
7
7
|
import platform
|
|
8
8
|
import sys
|
|
9
|
+
import tempfile
|
|
9
10
|
import textwrap
|
|
10
11
|
import typing
|
|
11
12
|
|
|
@@ -132,9 +133,6 @@ class WSLDriver(BaseDriver):
|
|
|
132
133
|
values_file: pathlib.Path | None = None,
|
|
133
134
|
image_pull_mode: ImagePullMode = ImagePullMode.guest,
|
|
134
135
|
) -> None:
|
|
135
|
-
if image_pull_mode in {ImagePullMode.host, ImagePullMode.hybrid}:
|
|
136
|
-
raise NotImplementedError("Importing host images is not supported on Windows.")
|
|
137
|
-
|
|
138
136
|
host_ip = (
|
|
139
137
|
(
|
|
140
138
|
await self.run_in_vm(
|
|
@@ -170,7 +168,7 @@ class WSLDriver(BaseDriver):
|
|
|
170
168
|
|
|
171
169
|
[Service]
|
|
172
170
|
Type=simple
|
|
173
|
-
ExecStart=/bin/bash -c 'IFS=":" read svc port <<< "%i"; exec /usr/local/bin/kubectl port-forward --address=127.0.0.1 svc/$svc $port:$port'
|
|
171
|
+
ExecStart=/bin/bash -c 'IFS=":" read svc port <<< "%i"; exec /usr/local/bin/k3s kubectl port-forward --kubeconfig=/etc/rancher/k3s/k3s.yaml --address=127.0.0.1 svc/$svc $port:$port'
|
|
174
172
|
Restart=on-failure
|
|
175
173
|
User=root
|
|
176
174
|
|
|
@@ -207,20 +205,21 @@ class WSLDriver(BaseDriver):
|
|
|
207
205
|
async def delete(self):
|
|
208
206
|
await run_command(["wsl.exe", "--unregister", self.vm_name], "Deleting Agent Stack platform", check=False)
|
|
209
207
|
|
|
210
|
-
@typing.override
|
|
211
|
-
async def import_images(self, *tags: str) -> None:
|
|
212
|
-
raise NotImplementedError("Importing images is not supported on this platform.")
|
|
213
|
-
|
|
214
|
-
@typing.override
|
|
215
|
-
async def import_image_to_internal_registry(self, tag: str) -> None:
|
|
216
|
-
raise NotImplementedError("Importing images to internal registry is not supported on this platform.")
|
|
217
|
-
|
|
218
208
|
@typing.override
|
|
219
209
|
async def exec(self, command: list[str]):
|
|
220
210
|
await anyio.run_process(
|
|
221
211
|
["wsl.exe", "--user", "root", "--distribution", self.vm_name, "--", *command],
|
|
222
|
-
input=None if sys.stdin.isatty() else sys.stdin.read().encode(),
|
|
223
212
|
check=False,
|
|
224
|
-
|
|
225
|
-
|
|
213
|
+
stdin=sys.stdin,
|
|
214
|
+
stdout=sys.stdout,
|
|
215
|
+
stderr=sys.stderr,
|
|
216
|
+
cwd="/",
|
|
226
217
|
)
|
|
218
|
+
|
|
219
|
+
@typing.override
|
|
220
|
+
def _get_export_import_paths(self) -> tuple[str, str]:
|
|
221
|
+
fd, tmp_path = tempfile.mkstemp(suffix=".tar")
|
|
222
|
+
os.close(fd)
|
|
223
|
+
windows_path = str(pathlib.Path(tmp_path).resolve().absolute())
|
|
224
|
+
wsl_path = f"/mnt/{windows_path[0].lower()}/{windows_path[2:].replace('\\', '/').removeprefix('/')}"
|
|
225
|
+
return (windows_path, wsl_path)
|
|
@@ -31,9 +31,9 @@ def _path() -> str:
|
|
|
31
31
|
# These are PATHs where `uv` installs itself when installed through own install script
|
|
32
32
|
# Package managers may install elsewhere, but that location should already be in PATH
|
|
33
33
|
return os.pathsep.join(
|
|
34
|
-
[
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
([xdg_bin_home] if (xdg_bin_home := os.getenv("XDG_BIN_HOME")) else [])
|
|
35
|
+
+ ([os.path.realpath(f"{xdg_data_home}/../bin")] if (xdg_data_home := os.getenv("XDG_DATA_HOME")) else [])
|
|
36
|
+
+ [
|
|
37
37
|
os.path.expanduser("~/.local/bin"),
|
|
38
38
|
os.getenv("PATH", ""),
|
|
39
39
|
]
|
|
@@ -124,7 +124,7 @@ async def install(
|
|
|
124
124
|
console.print()
|
|
125
125
|
if (
|
|
126
126
|
ready_to_start
|
|
127
|
-
and await inquirer.confirm(
|
|
127
|
+
and await inquirer.confirm(
|
|
128
128
|
message="Do you want to start the Agent Stack platform now? Will run: agentstack platform start",
|
|
129
129
|
default=True,
|
|
130
130
|
).execute_async()
|
|
@@ -139,7 +139,7 @@ async def install(
|
|
|
139
139
|
already_configured = False
|
|
140
140
|
if (
|
|
141
141
|
already_started
|
|
142
|
-
and await inquirer.confirm(
|
|
142
|
+
and await inquirer.confirm(
|
|
143
143
|
message="Do you want to configure your LLM provider now? Will run: agentstack model setup", default=True
|
|
144
144
|
).execute_async()
|
|
145
145
|
):
|
|
@@ -151,7 +151,7 @@ async def install(
|
|
|
151
151
|
|
|
152
152
|
if (
|
|
153
153
|
already_configured
|
|
154
|
-
and await inquirer.confirm(
|
|
154
|
+
and await inquirer.confirm(
|
|
155
155
|
message="Do you want to open the web UI now? Will run: agentstack ui", default=True
|
|
156
156
|
).execute_async()
|
|
157
157
|
):
|
|
@@ -78,7 +78,7 @@ def get_unique_app_name() -> str:
|
|
|
78
78
|
async def server_login(server: typing.Annotated[str | None, typer.Argument()] = None):
|
|
79
79
|
"""Login to a server or switch between logged in servers."""
|
|
80
80
|
server = server or (
|
|
81
|
-
await inquirer.select(
|
|
81
|
+
await inquirer.select(
|
|
82
82
|
message="Select a server, or log in to a new one:",
|
|
83
83
|
choices=[
|
|
84
84
|
*(
|
|
@@ -95,7 +95,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
95
95
|
if config.auth_manager.servers
|
|
96
96
|
else None
|
|
97
97
|
)
|
|
98
|
-
server = server or await inquirer.text(message="Enter server URL:").execute_async()
|
|
98
|
+
server = server or await inquirer.text(message="Enter server URL:").execute_async()
|
|
99
99
|
|
|
100
100
|
if not server:
|
|
101
101
|
raise RuntimeError("No server selected. Action cancelled.")
|
|
@@ -120,7 +120,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
120
120
|
elif len(auth_servers) == 1:
|
|
121
121
|
auth_server = auth_servers[0]
|
|
122
122
|
elif len(auth_servers) > 1:
|
|
123
|
-
auth_server = await inquirer.select(
|
|
123
|
+
auth_server = await inquirer.select(
|
|
124
124
|
message="Select an authorization server:",
|
|
125
125
|
choices=[
|
|
126
126
|
Choice(
|
|
@@ -199,13 +199,10 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
199
199
|
if len(auth_servers) == 1:
|
|
200
200
|
auth_server = auth_servers[0]
|
|
201
201
|
else:
|
|
202
|
-
auth_server = await inquirer.select(
|
|
202
|
+
auth_server = await inquirer.select(
|
|
203
203
|
message="Select an authorization server:",
|
|
204
204
|
choices=auth_servers,
|
|
205
|
-
).execute_async()
|
|
206
|
-
|
|
207
|
-
if not auth_server:
|
|
208
|
-
raise RuntimeError("No authorization server selected.")
|
|
205
|
+
).execute_async() or sys.exit(1)
|
|
209
206
|
|
|
210
207
|
async with httpx.AsyncClient() as client:
|
|
211
208
|
try:
|
|
@@ -250,7 +247,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
250
247
|
|
|
251
248
|
if not client_id:
|
|
252
249
|
client_id = (
|
|
253
|
-
await inquirer.text(
|
|
250
|
+
await inquirer.text(
|
|
254
251
|
message="Enter Client ID:",
|
|
255
252
|
instruction=f"(Redirect URI: {REDIRECT_URI})",
|
|
256
253
|
).execute_async()
|
|
@@ -258,12 +255,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
258
255
|
)
|
|
259
256
|
if not client_id:
|
|
260
257
|
raise RuntimeError("Client ID is mandatory. Action cancelled.")
|
|
261
|
-
client_secret = (
|
|
262
|
-
await inquirer.text( # type: ignore
|
|
263
|
-
message="Enter Client Secret (optional):"
|
|
264
|
-
).execute_async()
|
|
265
|
-
or None
|
|
266
|
-
)
|
|
258
|
+
client_secret = await inquirer.secret(message="Enter Client Secret (optional):").execute_async() or None
|
|
267
259
|
|
|
268
260
|
code_verifier = generate_token(64)
|
|
269
261
|
|
|
@@ -87,7 +87,7 @@ class Configuration(pydantic_settings.BaseSettings):
|
|
|
87
87
|
async with use_platform_client(
|
|
88
88
|
auth=(self.username, self.password.get_secret_value()) if self.password else None,
|
|
89
89
|
auth_token=auth_token,
|
|
90
|
-
base_url=self.auth_manager.active_server + "/",
|
|
90
|
+
base_url=(self.auth_manager.active_server or "") + "/",
|
|
91
91
|
) as client:
|
|
92
92
|
yield client
|
|
93
93
|
|
|
Binary file
|
|
@@ -32,9 +32,7 @@ async def confirm_server_action(message: str, url: str | None = None, *, yes: bo
|
|
|
32
32
|
if yes:
|
|
33
33
|
return
|
|
34
34
|
url = url or require_active_server()
|
|
35
|
-
confirmed = await inquirer.confirm(
|
|
36
|
-
message=f"{message} {url}?", default=False
|
|
37
|
-
).execute_async()
|
|
35
|
+
confirmed = await inquirer.confirm(message=f"{message} {url}?", default=False).execute_async()
|
|
38
36
|
if not confirmed:
|
|
39
37
|
console.info("Action cancelled.")
|
|
40
38
|
sys.exit(1)
|
|
@@ -288,10 +288,8 @@ def print_log(line, ansi_mode=False, out_console: Console | None = None):
|
|
|
288
288
|
(out_console or console).print(line)
|
|
289
289
|
|
|
290
290
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
pattern = r"""
|
|
291
|
+
# ! This pattern is taken from agentstack_server.utils.github.GithubUrl, make sure to keep it in sync
|
|
292
|
+
github_url_verbose_pattern = r"""
|
|
295
293
|
^
|
|
296
294
|
(?:git\+)? # Optional git+ prefix
|
|
297
295
|
https?://(?P<host>github(?:\.[^/]+)+)/ # GitHub host (github.com or github.enterprise.com)
|
|
@@ -307,7 +305,76 @@ def is_github_url(url: str) -> bool:
|
|
|
307
305
|
(?:\#path=(?P<path>.+))? # Optional path after #path=
|
|
308
306
|
$
|
|
309
307
|
"""
|
|
310
|
-
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def is_github_url(url: str) -> bool:
|
|
311
|
+
return bool(re.match(github_url_verbose_pattern, url, re.VERBOSE))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_local_github_token() -> str | None:
|
|
315
|
+
"""Get GitHub token from standard local sources for authenticated API requests.
|
|
316
|
+
|
|
317
|
+
Checks multiple sources in order of preference:
|
|
318
|
+
1. GITHUB_TOKEN environment variable
|
|
319
|
+
2. GH_TOKEN environment variable (used by GitHub CLI)
|
|
320
|
+
3. GitHub CLI (gh auth token) if user is logged in
|
|
321
|
+
|
|
322
|
+
Authenticated requests have much higher rate limits (5000/hour vs 60/hour).
|
|
323
|
+
"""
|
|
324
|
+
# Check environment variables first
|
|
325
|
+
if token := os.getenv("GITHUB_TOKEN"):
|
|
326
|
+
return token
|
|
327
|
+
if token := os.getenv("GH_TOKEN"):
|
|
328
|
+
return token
|
|
329
|
+
|
|
330
|
+
# Try GitHub CLI if available
|
|
331
|
+
try:
|
|
332
|
+
result = subprocess.run(
|
|
333
|
+
["gh", "auth", "token"],
|
|
334
|
+
capture_output=True,
|
|
335
|
+
text=True,
|
|
336
|
+
timeout=5,
|
|
337
|
+
)
|
|
338
|
+
if result.returncode == 0 and (token := result.stdout.strip()):
|
|
339
|
+
return token
|
|
340
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
async def get_github_repo_tags(host: str, owner: str, repo: str) -> list[str]:
|
|
347
|
+
headers = {"Accept": "application/vnd.github.v3+json"}
|
|
348
|
+
|
|
349
|
+
# Add authentication if GITHUB_TOKEN is available (avoids rate limiting)
|
|
350
|
+
if token := get_local_github_token():
|
|
351
|
+
headers["Authorization"] = f"token {token}"
|
|
352
|
+
|
|
353
|
+
# Determine API host
|
|
354
|
+
if host == "github.com":
|
|
355
|
+
api_host = "api.github.com"
|
|
356
|
+
if not get_local_github_token():
|
|
357
|
+
console.info(
|
|
358
|
+
"No GitHub token found. Using unauthenticated API (60 requests/hour limit).\n"
|
|
359
|
+
"For higher limits, run [green]gh auth login[/green] or set GITHUB_TOKEN or GH_TOKEN env variable."
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
# GitHub Enterprise uses host/api/v3
|
|
363
|
+
api_host = f"{host}/api/v3"
|
|
364
|
+
|
|
365
|
+
async with httpx.AsyncClient() as client:
|
|
366
|
+
# Correct format: /repos/{owner}/{repo}/tags
|
|
367
|
+
url = f"https://{api_host}/repos/{owner}/{repo}/tags"
|
|
368
|
+
response = await client.get(url, headers=headers)
|
|
369
|
+
|
|
370
|
+
tags = [tag["name"] for tag in response.json()] if response.status_code == 200 else []
|
|
371
|
+
|
|
372
|
+
if not tags and response.status_code != 200:
|
|
373
|
+
console.warning(f"Failed to fetch tags from '{url}' (status code {response.status_code}). ")
|
|
374
|
+
elif not tags and response.status_code <= 200:
|
|
375
|
+
console.warning(f"No tags fetched from '{url}' (status code {response.status_code}). ")
|
|
376
|
+
|
|
377
|
+
return tags
|
|
311
378
|
|
|
312
379
|
|
|
313
380
|
def get_httpx_response_error_details(response: httpx.Response | None) -> tuple[str, str] | None:
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentstack_cli-0.6.1 → agentstack_cli-0.6.2rc2}/src/agentstack_cli/commands/platform/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|