diff --git a/agents/form/src/form/agent.py b/agents/form/src/form/agent.py index 47d2a3af0..e02cce740 100644 --- a/agents/form/src/form/agent.py +++ b/agents/form/src/form/agent.py @@ -70,12 +70,12 @@ class FormData(BaseModel): - location: str | None - date_from: str | None - date_to: str | None - notes: list[FileInfo] | None - flexible: bool | None - interests: list[str] | None + location: str + date_from: str | None = None + date_to: str | None = None + notes: list[FileInfo] | None = None + flexible: bool | None = None + interests: list[str] | None = None @server.agent( @@ -108,9 +108,10 @@ async def agent( ): """Example demonstrating a single-turn agent using a form to collect user input.""" - form_data = form.parse_initial_form(model=FormData) - - yield f"Hello {form_data.location}" + if form_data := form.parse_initial_form(model=FormData): + yield f"Hello {form_data.location}" + else: + yield "No form data received." def serve(): diff --git a/agentstack.code-workspace b/agentstack.code-workspace index b1bac9095..d8f21a916 100644 --- a/agentstack.code-workspace +++ b/agentstack.code-workspace @@ -32,6 +32,10 @@ "name": "agent-chat", "path": "agents/chat" }, + { + "name": "agent-form", + "path": "agents/form" + }, { "name": "agent-rag", "path": "agents/rag" diff --git a/apps/agentstack-cli/src/agentstack_cli/commands/agent.py b/apps/agentstack-cli/src/agentstack_cli/commands/agent.py index 4c51bfbef..6f6780919 100644 --- a/apps/agentstack-cli/src/agentstack_cli/commands/agent.py +++ b/apps/agentstack-cli/src/agentstack_cli/commands/agent.py @@ -880,8 +880,6 @@ async def run_agent( ] = None, ) -> None: """Run an agent.""" - if search_path is not None and input is None and sys.stdin.isatty(): - input = sys.stdin.read() async with configuration.use_platform_client(): providers = await Provider.list() await ensure_llm_provider() diff --git a/apps/agentstack-cli/src/agentstack_cli/commands/build.py b/apps/agentstack-cli/src/agentstack_cli/commands/build.py index ad8445113..1564bea18 100644 --- a/apps/agentstack-cli/src/agentstack_cli/commands/build.py +++ b/apps/agentstack-cli/src/agentstack_cli/commands/build.py @@ -113,7 +113,7 @@ async def client_side_build( context_hash = hashlib.sha256((context + (dockerfile or "")).encode()).hexdigest()[:6] context_shorter = re.sub(r"https?://", "", context).replace(r".git", "") context_shorter = re.sub(r"[^a-zA-Z0-9_-]+", "-", context_shorter)[:32].lstrip("-") or "provider" - tag = (tag or f"agentstack.local/{context_shorter}-{context_hash}:latest").lower() + tag = (tag or f"agentstack-registry-svc.default:5001/{context_shorter}-{context_hash}:latest").lower() await run_command( command=[ *( @@ -135,11 +135,22 @@ async def client_side_build( if import_image: from agentstack_cli.commands.platform import get_driver + if "agentstack-registry-svc.default" not in tag: + source_tag = tag + tag = re.sub("^[^/]*/", "agentstack-registry-svc.default:5001/", tag) + await run_command(["docker", "tag", source_tag, tag], "Tagging image") + driver = get_driver(vm_name=vm_name) + if (await driver.status()) != "running": console.error("Agent Stack platform is not running.") sys.exit(1) - await driver.import_image(tag) + + await driver.import_image_to_internal_registry(tag) + console.success( + "Agent was imported to the agent stack internal registry.\n" + + f"You can add it using [blue]agentstack add {tag}[/blue]" + ) return tag, agent_card diff --git a/apps/agentstack-cli/src/agentstack_cli/commands/platform/base_driver.py b/apps/agentstack-cli/src/agentstack_cli/commands/platform/base_driver.py index 6390ea398..23447e315 100644 --- a/apps/agentstack-cli/src/agentstack_cli/commands/platform/base_driver.py +++ b/apps/agentstack-cli/src/agentstack_cli/commands/platform/base_driver.py @@ -22,6 +22,7 @@ class BaseDriver(abc.ABC): def __init__(self, vm_name: str = "agentstack"): self.vm_name = vm_name + self.loaded_images: set[str] = set() @abc.abstractmethod async def run_in_vm( @@ -47,6 +48,9 @@ async def delete(self) -> None: ... @abc.abstractmethod async def import_image(self, tag: str) -> None: ... + @abc.abstractmethod + async def import_image_to_internal_registry(self, tag: str) -> None: ... + @abc.abstractmethod async def exec(self, command: list[str]) -> None: ... @@ -138,20 +142,17 @@ async def deploy( ).stdout.decode() for image in import_images or []: await self.import_image(image) + self.loaded_images.add(image) for image in {typing.cast(str, yaml.safe_load(line)) for line in images_str.splitlines()} - set( import_images or [] ): async for attempt in AsyncRetrying(stop=stop_after_attempt(5)): with attempt: attempt_num = attempt.retry_state.attempt_number + image_id = image if "." in image.split("/")[0] else f"docker.io/{image}" + self.loaded_images.add(image_id) await self.run_in_vm( - [ - "k3s", - "ctr", - "image", - "pull", - image if "." in image.split("/")[0] else f"docker.io/{image}", - ], + ["k3s", "ctr", "image", "pull", image_id], f"Pulling image {image}" + (f" (attempt {attempt_num})" if attempt_num > 1 else ""), ) diff --git a/apps/agentstack-cli/src/agentstack_cli/commands/platform/lima_driver.py b/apps/agentstack-cli/src/agentstack_cli/commands/platform/lima_driver.py index 0fd1d05e7..a3bf94409 100644 --- a/apps/agentstack-cli/src/agentstack_cli/commands/platform/lima_driver.py +++ b/apps/agentstack-cli/src/agentstack_cli/commands/platform/lima_driver.py @@ -197,6 +197,72 @@ async def import_image(self, tag: str): finally: await image_path.unlink(missing_ok=True) + @typing.override + async def import_image_to_internal_registry(self, tag: str) -> None: + # 1. Check if registry is running + try: + await self.run_in_vm( + ["k3s", "kubectl", "get", "svc", "agentstack-registry-svc"], + "Checking internal registry availability", + ) + except Exception as e: + console.warning(f"Internal registry service not found. Push might fail: {e}") + + # 2. Export image from Docker to shared temp dir + image_dir = anyio.Path("/tmp/agentstack") + await image_dir.mkdir(exist_ok=True, parents=True) + image_file = f"{uuid.uuid4()}.tar" + image_path = image_dir / image_file + + try: + await run_command( + ["docker", "image", "save", "-o", str(image_path), tag], + f"Exporting image {tag} from Docker", + ) + + # 3 & 4. Run Crane Job + crane_image = "ghcr.io/i-am-bee/alpine/crane:0.20.6" + for image in self.loaded_images: + if "alpine/crane" in image: + crane_image = image + break + + job_name = f"push-{uuid.uuid4().hex[:6]}" + job_def = { + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": {"name": job_name, "namespace": "default"}, + "spec": { + "backoffLimit": 0, + "ttlSecondsAfterFinished": 60, + "template": { + "spec": { + "restartPolicy": "Never", + "containers": [ + { + "name": "crane", + "image": crane_image, + "command": ["crane", "push", f"/workspace/{image_file}", tag, "--insecure"], + "volumeMounts": [{"name": "workspace", "mountPath": "/workspace"}], + } + ], + "volumes": [{"name": "workspace", "hostPath": {"path": "/tmp/agentstack"}}], + } + }, + }, + } + + await self.run_in_vm( + ["k3s", "kubectl", "apply", "-f", "-"], "Starting push job", input=yaml.dump(job_def).encode() + ) + await self.run_in_vm( + ["k3s", "kubectl", "wait", "--for=condition=complete", f"job/{job_name}", "--timeout=300s"], + "Waiting for push to complete", + ) + await self.run_in_vm(["k3s", "kubectl", "delete", "job", job_name], "Cleaning up push job") + finally: + await image_path.unlink(missing_ok=True) + @typing.override async def exec(self, command: list[str]): await anyio.run_process( diff --git a/apps/agentstack-cli/src/agentstack_cli/commands/platform/wsl_driver.py b/apps/agentstack-cli/src/agentstack_cli/commands/platform/wsl_driver.py index 14eae4610..136997c29 100644 --- a/apps/agentstack-cli/src/agentstack_cli/commands/platform/wsl_driver.py +++ b/apps/agentstack-cli/src/agentstack_cli/commands/platform/wsl_driver.py @@ -220,6 +220,10 @@ async def delete(self): async def import_image(self, tag: str) -> None: raise NotImplementedError("Importing images is not supported on this platform.") + @typing.override + async def import_image_to_internal_registry(self, tag: str) -> None: + raise NotImplementedError("Importing images to internal registry is not supported on this platform.") + @typing.override async def exec(self, command: list[str]): await anyio.run_process(