From 43b873687becf1a44c87efaeffec43f44e353f9d Mon Sep 17 00:00:00 2001 From: Roxanne Farhad Date: Thu, 15 Jan 2026 17:17:16 -0500 Subject: [PATCH 1/3] adding the compression stuff --- ab000-hello-acp-latest.tar.gz | Bin 0 -> 5185 bytes requirements-dev.lock | 26 +++- requirements.lock | 39 ++++- src/agentex/lib/cli/commands/agents.py | 98 ++++++++++++ .../lib/cli/handlers/agent_handlers.py | 142 ++++++++++++++++-- uv.lock | 2 +- 6 files changed, 285 insertions(+), 22 deletions(-) create mode 100644 ab000-hello-acp-latest.tar.gz diff --git a/ab000-hello-acp-latest.tar.gz b/ab000-hello-acp-latest.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a492158d7209476e551c9ca4c366e4933867a95f GIT binary patch literal 5185 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 literal 0 HcmV?d00001 diff --git a/requirements-dev.lock b/requirements-dev.lock index 1078b30de..c1f1802be 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,12 +16,16 @@ aiohttp==3.13.2 # via agentex-sdk # via httpx-aiohttp # via litellm -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic +anthropic==0.75.0 + # via agentex-sdk anyio==4.10.0 # via agentex-sdk + # via anthropic + # via claude-agent-sdk # via httpx # via mcp # via openai @@ -51,6 +55,8 @@ certifi==2023.7.22 # via requests charset-normalizer==3.4.3 # via requests +claude-agent-sdk==0.1.19 + # via agentex-sdk click==8.2.1 # via litellm # via typer @@ -76,9 +82,12 @@ distlib==0.3.7 # via virtualenv distro==1.9.0 # via agentex-sdk + # via anthropic # via openai # via scale-gp # via scale-gp-beta +docstring-parser==0.17.0 + # via anthropic envier==0.6.1 # via ddtrace execnet==2.1.1 @@ -108,6 +117,7 @@ httpcore==1.0.9 # via httpx httpx==0.27.2 # via agentex-sdk + # via anthropic # via httpx-aiohttp # via litellm # via mcp @@ -143,6 +153,7 @@ jinja2==3.1.6 # via agentex-sdk # via litellm jiter==0.10.0 + # via anthropic # via openai json-log-formatter==1.1.1 # via agentex-sdk @@ -172,6 +183,7 @@ matplotlib-inline==0.1.7 # via ipython mcp==1.12.4 # via agentex-sdk + # via claude-agent-sdk # via openai-agents mdurl==0.1.2 # via markdown-it-py @@ -204,10 +216,10 @@ packaging==23.2 # via ipykernel # via nox # via pytest -pathspec==0.12.1 - # via mypy parso==0.8.4 # via jedi +pathspec==0.12.1 + # via mypy pexpect==4.9.0 # via ipython platformdirs==3.11.0 @@ -237,6 +249,7 @@ pyasn1-modules==0.4.2 # via google-auth pydantic==2.11.9 # via agentex-sdk + # via anthropic # via fastapi # via litellm # via mcp @@ -329,6 +342,7 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via agentex-sdk + # via anthropic # via anyio # via httpx # via openai @@ -343,6 +357,8 @@ starlette==0.46.2 # via mcp temporalio==1.18.2 # via agentex-sdk +termcolor==3.3.0 + # via yaspin tiktoken==0.11.0 # via litellm time-machine==2.9.0 @@ -374,6 +390,7 @@ types-urllib3==1.26.25.14 typing-extensions==4.12.2 # via agentex-sdk # via aiosignal + # via anthropic # via anyio # via fastapi # via huggingface-hub @@ -392,7 +409,6 @@ typing-extensions==4.12.2 # via temporalio # via typer # via typing-inspection - # via virtualenv typing-inspection==0.4.2 # via pydantic # via pydantic-settings @@ -418,5 +434,7 @@ wrapt==1.17.3 # via ddtrace yarl==1.20.0 # via aiohttp +yaspin==3.4.0 + # via agentex-sdk zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 79519671e..499edc1da 100644 --- a/requirements.lock +++ b/requirements.lock @@ -16,12 +16,16 @@ aiohttp==3.13.2 # via agentex-sdk # via httpx-aiohttp # via litellm -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic +anthropic==0.75.0 + # via agentex-sdk anyio==4.10.0 # via agentex-sdk + # via anthropic + # via claude-agent-sdk # via httpx # via mcp # via openai @@ -49,6 +53,8 @@ certifi==2023.7.22 # via requests charset-normalizer==3.4.3 # via requests +claude-agent-sdk==0.1.19 + # via agentex-sdk click==8.2.1 # via litellm # via typer @@ -69,9 +75,12 @@ decorator==5.2.1 # via ipython distro==1.8.0 # via agentex-sdk + # via anthropic # via openai # via scale-gp # via scale-gp-beta +docstring-parser==0.17.0 + # via anthropic envier==0.6.1 # via ddtrace executing==2.2.0 @@ -98,6 +107,7 @@ httpcore==1.0.9 # via httpx httpx==0.27.2 # via agentex-sdk + # via anthropic # via httpx-aiohttp # via litellm # via mcp @@ -132,6 +142,7 @@ jinja2==3.1.6 # via agentex-sdk # via litellm jiter==0.10.0 + # via anthropic # via openai json-log-formatter==1.1.1 # via agentex-sdk @@ -161,6 +172,7 @@ matplotlib-inline==0.1.7 # via ipython mcp==1.12.4 # via agentex-sdk + # via claude-agent-sdk # via openai-agents mdurl==0.1.2 # via markdown-it-py @@ -200,9 +212,6 @@ prompt-toolkit==3.0.51 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.12.5 - # via agentex-sdk -pydantic-core==2.41.5 protobuf==5.29.5 # via ddtrace # via temporalio @@ -217,6 +226,19 @@ pyasn1==0.6.1 # via rsa pyasn1-modules==0.4.2 # via google-auth +pydantic==2.12.5 + # via agentex-sdk + # via anthropic + # via fastapi + # via litellm + # via mcp + # via openai + # via openai-agents + # via pydantic-settings + # via python-on-whales + # via scale-gp + # via scale-gp-beta +pydantic-core==2.41.5 # via pydantic pydantic-settings==2.10.1 # via mcp @@ -290,7 +312,8 @@ six==1.17.0 # via python-dateutil sniffio==1.3.0 # via agentex-sdk -typing-extensions==4.15.0 + # via anthropic + # via anyio # via httpx # via openai # via scale-gp @@ -304,6 +327,8 @@ starlette==0.46.2 # via mcp temporalio==1.18.2 # via agentex-sdk +termcolor==3.3.0 + # via yaspin tiktoken==0.11.0 # via litellm tokenizers==0.21.4 @@ -331,8 +356,10 @@ types-requests==2.31.0.6 # via openai-agents types-urllib3==1.26.25.14 # via types-requests +typing-extensions==4.15.0 # via agentex-sdk # via aiosignal + # via anthropic # via anyio # via fastapi # via huggingface-hub @@ -372,5 +399,7 @@ wrapt==1.17.3 # via ddtrace yarl==1.20.0 # via aiohttp +yaspin==3.4.0 + # via agentex-sdk zipp==3.23.0 # via importlib-metadata diff --git a/src/agentex/lib/cli/commands/agents.py b/src/agentex/lib/cli/commands/agents.py index 3c1abdf8a..9f5bbbe21 100644 --- a/src/agentex/lib/cli/commands/agents.py +++ b/src/agentex/lib/cli/commands/agents.py @@ -26,6 +26,9 @@ from agentex.lib.cli.handlers.agent_handlers import ( run_agent, build_agent, + prepare_cloud_build_context, + parse_build_args, + CloudBuildContext, ) from agentex.lib.cli.handlers.deploy_handlers import ( HelmError, @@ -164,6 +167,101 @@ def build( raise typer.Exit(1) from e +@agents.command(name="package") +def package( + manifest: str = typer.Option(..., help="Path to the manifest you want to use"), + tag: str | None = typer.Option( + None, + "--tag", + "-t", + help="Image tag (defaults to deployment.image.tag from manifest, or 'latest')", + ), + output: str | None = typer.Option( + None, + "--output", + "-o", + help="Output filename for the tarball (defaults to -.tar.gz)", + ), + build_arg: builtins.list[str] | None = typer.Option( # noqa: B008 + None, + "--build-arg", + "-b", + help="Build argument in KEY=VALUE format (can be repeated)", + ), +): + """ + Package an agent's build context into a tarball for cloud builds. + + Reads manifest.yaml, prepares build context according to include_paths and + dockerignore, then saves a compressed tarball to the current directory. + + The tag defaults to the value in deployment.image.tag from the manifest. + + Example: + agentex agents package --manifest manifest.yaml + agentex agents package --manifest manifest.yaml --tag v1.0 + """ + typer.echo(f"Packaging build context from manifest: {manifest}") + + # Validate manifest exists + manifest_path = Path(manifest) + if not manifest_path.exists(): + typer.echo(f"Error: manifest not found at {manifest_path}", err=True) + raise typer.Exit(1) + + try: + # Prepare the build context (tag defaults from manifest if not provided) + build_context = prepare_cloud_build_context( + manifest_path=str(manifest_path), + tag=tag, + build_args=build_arg, + ) + + # Determine output filename using the resolved tag + if output: + output_filename = output + else: + output_filename = f"{build_context.agent_name}-{build_context.tag}.tar.gz" + + # Save tarball to current working directory + output_path = Path.cwd() / output_filename + output_path.write_bytes(build_context.archive_bytes) + + typer.echo(f"\nTarball saved to: {output_path}") + typer.echo(f"Size: {build_context.build_context_size_kb:.1f} KB") + + # Output the build parameters needed for cloud build + typer.echo("\n" + "=" * 60) + typer.echo("Build Parameters for Cloud Build API:") + typer.echo("=" * 60) + typer.echo(f" agent_name: {build_context.agent_name}") + typer.echo(f" image_name: {build_context.image_name}") + typer.echo(f" tag: {build_context.tag}") + typer.echo(f" context_file: {output_path}") + + if build_arg: + parsed_args = parse_build_args(build_arg) + typer.echo(f" build_args: {parsed_args}") + + typer.echo("") + typer.echo("Command:") + build_args_str = "" + if build_arg: + build_args_str = " ".join(f'--build-arg "{arg}"' for arg in build_arg) + build_args_str = f" {build_args_str}" + typer.echo( + f' sgp agentex build --context "{output_path}" ' + f'--image-name "{build_context.image_name}" ' + f'--tag "{build_context.tag}"{build_args_str}' + ) + typer.echo("=" * 60) + + except Exception as e: + typer.echo(f"Error packaging build context: {str(e)}", err=True) + logger.exception("Error packaging build context") + raise typer.Exit(1) from e + + @agents.command() def run( manifest: str = typer.Option(..., help="Path to the manifest you want to use"), diff --git a/src/agentex/lib/cli/handlers/agent_handlers.py b/src/agentex/lib/cli/handlers/agent_handlers.py index 816080181..41cf44f67 100644 --- a/src/agentex/lib/cli/handlers/agent_handlers.py +++ b/src/agentex/lib/cli/handlers/agent_handlers.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import NamedTuple from rich.console import Console from python_on_whales import DockerException, docker @@ -8,7 +9,7 @@ from agentex.lib.cli.debug import DebugConfig from agentex.lib.utils.logging import make_logger from agentex.lib.cli.handlers.run_handlers import RunError, run_agent as _run_agent -from agentex.lib.sdk.config.agent_manifest import AgentManifest +from agentex.lib.sdk.config.agent_manifest import AgentManifest, BuildContextManager logger = make_logger(__name__) console = Console() @@ -18,6 +19,17 @@ class DockerBuildError(Exception): """An error occurred during docker build""" +class CloudBuildContext(NamedTuple): + """Contains the prepared build context for cloud builds.""" + + archive_bytes: bytes + dockerfile_path: str + agent_name: str + tag: str + image_name: str + build_context_size_kb: float + + def build_agent( manifest_path: str, registry_url: str, @@ -42,9 +54,7 @@ def build_agent( The image URL """ agent_manifest = AgentManifest.from_yaml(file_path=manifest_path) - build_context_root = ( - Path(manifest_path).parent / agent_manifest.build.context.root - ).resolve() + build_context_root = (Path(manifest_path).parent / agent_manifest.build.context.root).resolve() repository_name = repository_name or agent_manifest.agent.name @@ -85,9 +95,7 @@ def build_agent( key, value = arg.split("=", 1) docker_build_args[key] = value else: - logger.warning( - f"Invalid build arg format: {arg}. Expected KEY=VALUE" - ) + logger.warning(f"Invalid build arg format: {arg}. Expected KEY=VALUE") if docker_build_args: docker_build_kwargs["build_args"] = docker_build_args @@ -100,9 +108,7 @@ def build_agent( if push: # Build and push in one step for multi-platform builds logger.info("Building and pushing image...") - docker_build_kwargs["push"] = ( - True # Push directly after build for multi-platform - ) + docker_build_kwargs["push"] = True # Push directly after build for multi-platform docker.buildx.build(**docker_build_kwargs) logger.info(f"Successfully built and pushed {image_name}") @@ -146,11 +152,11 @@ def signal_handler(signum, _frame): shutting_down = True logger.info(f"Received signal {signum}, shutting down...") raise KeyboardInterrupt() - + # Set up signal handling for the main thread signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - + try: asyncio.run(_run_agent(manifest_path, debug_config)) except KeyboardInterrupt: @@ -158,3 +164,115 @@ def signal_handler(signum, _frame): sys.exit(0) except RunError as e: raise RuntimeError(str(e)) from e + + +def parse_build_args(build_args: list[str] | None) -> dict[str, str]: + """Parse build arguments from KEY=VALUE format to a dictionary. + + Args: + build_args: List of build arguments in KEY=VALUE format + + Returns: + Dictionary mapping keys to values + """ + result: dict[str, str] = {} + if not build_args: + return result + + for arg in build_args: + if "=" in arg: + key, value = arg.split("=", 1) + result[key] = value + else: + logger.warning(f"Invalid build arg format: {arg}. Expected KEY=VALUE") + + return result + + +def prepare_cloud_build_context( + manifest_path: str, + tag: str | None = None, + build_args: list[str] | None = None, +) -> CloudBuildContext: + """Prepare the build context for cloud-based container builds. + + Reads the manifest, prepares the build context by copying files according to + the include_paths and dockerignore, then creates a compressed tar.gz archive + ready for upload to a cloud build service. + + Args: + manifest_path: Path to the agent manifest file + tag: Image tag override (if None, reads from manifest's deployment.image.tag) + build_args: List of build arguments in KEY=VALUE format + + Returns: + CloudBuildContext containing the archive bytes, dockerfile path, and metadata + """ + agent_manifest = AgentManifest.from_yaml(file_path=manifest_path) + build_context_root = (Path(manifest_path).parent / agent_manifest.build.context.root).resolve() + + agent_name = agent_manifest.agent.name + dockerfile_path = agent_manifest.build.context.dockerfile + + # Validate that the Dockerfile exists + full_dockerfile_path = build_context_root / dockerfile_path + if not full_dockerfile_path.exists(): + raise FileNotFoundError( + f"Dockerfile not found at: {full_dockerfile_path}\n" + f"Check that 'build.context.dockerfile' in your manifest points to an existing file." + ) + if not full_dockerfile_path.is_file(): + raise ValueError( + f"Dockerfile path is not a file: {full_dockerfile_path}\n" + f"'build.context.dockerfile' must point to a file, not a directory." + ) + + # Get tag and repository from manifest if not provided + if tag is None: + if agent_manifest.deployment and agent_manifest.deployment.image: + tag = agent_manifest.deployment.image.tag + else: + tag = "latest" + + # Get repository name from manifest (just the repo name, not the full registry URL) + if agent_manifest.deployment and agent_manifest.deployment.image: + repository = agent_manifest.deployment.image.repository + if repository: + # Extract just the repo name (last part after any slashes) + image_name = repository.split("/")[-1] + else: + image_name = "" + else: + image_name = "" + + logger.info(f"Agent: {agent_name}") + logger.info(f"Image name: {image_name}") + logger.info(f"Build context root: {build_context_root}") + logger.info(f"Dockerfile: {dockerfile_path}") + logger.info(f"Tag: {tag}") + + if agent_manifest.build.context.include_paths: + logger.info(f"Include paths: {agent_manifest.build.context.include_paths}") + + parsed_build_args = parse_build_args(build_args) + if parsed_build_args: + logger.info(f"Build args: {list(parsed_build_args.keys())}") + + logger.info("Preparing build context...") + + with agent_manifest.context_manager(build_context_root) as build_context: + # Compress the prepared context using the static zipped method + with BuildContextManager.zipped(root_path=build_context.path) as archive_buffer: + archive_bytes = archive_buffer.read() + + build_context_size_kb = len(archive_bytes) / 1024 + logger.info(f"Build context size: {build_context_size_kb:.1f} KB") + + return CloudBuildContext( + archive_bytes=archive_bytes, + dockerfile_path=build_context.dockerfile_path, + agent_name=agent_name, + tag=tag, + image_name=image_name, + build_context_size_kb=build_context_size_kb, + ) diff --git a/uv.lock b/uv.lock index 391297102..f17484759 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentex-sdk" -version = "0.8.0" +version = "0.8.2" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From 10af1242abe41dffa9d938ab34644d0744c9afe5 Mon Sep 17 00:00:00 2001 From: Roxanne Farhad Date: Fri, 16 Jan 2026 14:12:33 -0500 Subject: [PATCH 2/3] adding tests for the new command --- ab000-hello-acp-latest.tar.gz | Bin 5185 -> 0 bytes pyproject.toml | 3 +- requirements-dev.lock | 26 +- requirements.lock | 39 +- .../lib/sdk/config/deployment_config.py | 2 +- tests/lib/cli/test_agent_handlers.py | 424 ++++++++++++++++++ 6 files changed, 436 insertions(+), 58 deletions(-) delete mode 100644 ab000-hello-acp-latest.tar.gz create mode 100644 tests/lib/cli/test_agent_handlers.py diff --git a/ab000-hello-acp-latest.tar.gz b/ab000-hello-acp-latest.tar.gz deleted file mode 100644 index a492158d7209476e551c9ca4c366e4933867a95f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5185 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 diff --git a/pyproject.toml b/pyproject.toml index b9629dbcd..aa57f7516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,7 +179,8 @@ xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" filterwarnings = [ - "error" + "error", + "ignore::pydantic.warnings.PydanticDeprecatedSince20", ] [tool.pyright] diff --git a/requirements-dev.lock b/requirements-dev.lock index c1f1802be..1078b30de 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,16 +16,12 @@ aiohttp==3.13.2 # via agentex-sdk # via httpx-aiohttp # via litellm -aiosignal==1.4.0 +aiosignal==1.3.2 # via aiohttp annotated-types==0.7.0 # via pydantic -anthropic==0.75.0 - # via agentex-sdk anyio==4.10.0 # via agentex-sdk - # via anthropic - # via claude-agent-sdk # via httpx # via mcp # via openai @@ -55,8 +51,6 @@ certifi==2023.7.22 # via requests charset-normalizer==3.4.3 # via requests -claude-agent-sdk==0.1.19 - # via agentex-sdk click==8.2.1 # via litellm # via typer @@ -82,12 +76,9 @@ distlib==0.3.7 # via virtualenv distro==1.9.0 # via agentex-sdk - # via anthropic # via openai # via scale-gp # via scale-gp-beta -docstring-parser==0.17.0 - # via anthropic envier==0.6.1 # via ddtrace execnet==2.1.1 @@ -117,7 +108,6 @@ httpcore==1.0.9 # via httpx httpx==0.27.2 # via agentex-sdk - # via anthropic # via httpx-aiohttp # via litellm # via mcp @@ -153,7 +143,6 @@ jinja2==3.1.6 # via agentex-sdk # via litellm jiter==0.10.0 - # via anthropic # via openai json-log-formatter==1.1.1 # via agentex-sdk @@ -183,7 +172,6 @@ matplotlib-inline==0.1.7 # via ipython mcp==1.12.4 # via agentex-sdk - # via claude-agent-sdk # via openai-agents mdurl==0.1.2 # via markdown-it-py @@ -216,10 +204,10 @@ packaging==23.2 # via ipykernel # via nox # via pytest -parso==0.8.4 - # via jedi pathspec==0.12.1 # via mypy +parso==0.8.4 + # via jedi pexpect==4.9.0 # via ipython platformdirs==3.11.0 @@ -249,7 +237,6 @@ pyasn1-modules==0.4.2 # via google-auth pydantic==2.11.9 # via agentex-sdk - # via anthropic # via fastapi # via litellm # via mcp @@ -342,7 +329,6 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via agentex-sdk - # via anthropic # via anyio # via httpx # via openai @@ -357,8 +343,6 @@ starlette==0.46.2 # via mcp temporalio==1.18.2 # via agentex-sdk -termcolor==3.3.0 - # via yaspin tiktoken==0.11.0 # via litellm time-machine==2.9.0 @@ -390,7 +374,6 @@ types-urllib3==1.26.25.14 typing-extensions==4.12.2 # via agentex-sdk # via aiosignal - # via anthropic # via anyio # via fastapi # via huggingface-hub @@ -409,6 +392,7 @@ typing-extensions==4.12.2 # via temporalio # via typer # via typing-inspection + # via virtualenv typing-inspection==0.4.2 # via pydantic # via pydantic-settings @@ -434,7 +418,5 @@ wrapt==1.17.3 # via ddtrace yarl==1.20.0 # via aiohttp -yaspin==3.4.0 - # via agentex-sdk zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 499edc1da..79519671e 100644 --- a/requirements.lock +++ b/requirements.lock @@ -16,16 +16,12 @@ aiohttp==3.13.2 # via agentex-sdk # via httpx-aiohttp # via litellm -aiosignal==1.4.0 +aiosignal==1.3.2 # via aiohttp annotated-types==0.7.0 # via pydantic -anthropic==0.75.0 - # via agentex-sdk anyio==4.10.0 # via agentex-sdk - # via anthropic - # via claude-agent-sdk # via httpx # via mcp # via openai @@ -53,8 +49,6 @@ certifi==2023.7.22 # via requests charset-normalizer==3.4.3 # via requests -claude-agent-sdk==0.1.19 - # via agentex-sdk click==8.2.1 # via litellm # via typer @@ -75,12 +69,9 @@ decorator==5.2.1 # via ipython distro==1.8.0 # via agentex-sdk - # via anthropic # via openai # via scale-gp # via scale-gp-beta -docstring-parser==0.17.0 - # via anthropic envier==0.6.1 # via ddtrace executing==2.2.0 @@ -107,7 +98,6 @@ httpcore==1.0.9 # via httpx httpx==0.27.2 # via agentex-sdk - # via anthropic # via httpx-aiohttp # via litellm # via mcp @@ -142,7 +132,6 @@ jinja2==3.1.6 # via agentex-sdk # via litellm jiter==0.10.0 - # via anthropic # via openai json-log-formatter==1.1.1 # via agentex-sdk @@ -172,7 +161,6 @@ matplotlib-inline==0.1.7 # via ipython mcp==1.12.4 # via agentex-sdk - # via claude-agent-sdk # via openai-agents mdurl==0.1.2 # via markdown-it-py @@ -212,6 +200,9 @@ prompt-toolkit==3.0.51 propcache==0.3.1 # via aiohttp # via yarl +pydantic==2.12.5 + # via agentex-sdk +pydantic-core==2.41.5 protobuf==5.29.5 # via ddtrace # via temporalio @@ -226,19 +217,6 @@ pyasn1==0.6.1 # via rsa pyasn1-modules==0.4.2 # via google-auth -pydantic==2.12.5 - # via agentex-sdk - # via anthropic - # via fastapi - # via litellm - # via mcp - # via openai - # via openai-agents - # via pydantic-settings - # via python-on-whales - # via scale-gp - # via scale-gp-beta -pydantic-core==2.41.5 # via pydantic pydantic-settings==2.10.1 # via mcp @@ -312,8 +290,7 @@ six==1.17.0 # via python-dateutil sniffio==1.3.0 # via agentex-sdk - # via anthropic - # via anyio +typing-extensions==4.15.0 # via httpx # via openai # via scale-gp @@ -327,8 +304,6 @@ starlette==0.46.2 # via mcp temporalio==1.18.2 # via agentex-sdk -termcolor==3.3.0 - # via yaspin tiktoken==0.11.0 # via litellm tokenizers==0.21.4 @@ -356,10 +331,8 @@ types-requests==2.31.0.6 # via openai-agents types-urllib3==1.26.25.14 # via types-requests -typing-extensions==4.15.0 # via agentex-sdk # via aiosignal - # via anthropic # via anyio # via fastapi # via huggingface-hub @@ -399,7 +372,5 @@ wrapt==1.17.3 # via ddtrace yarl==1.20.0 # via aiohttp -yaspin==3.4.0 - # via agentex-sdk zipp==3.23.0 # via importlib-metadata diff --git a/src/agentex/lib/sdk/config/deployment_config.py b/src/agentex/lib/sdk/config/deployment_config.py index 1ba5b348f..8f1a18b49 100644 --- a/src/agentex/lib/sdk/config/deployment_config.py +++ b/src/agentex/lib/sdk/config/deployment_config.py @@ -2,7 +2,7 @@ from typing import Any, Dict -from pydantic import Field +from pydantic import ConfigDict, Field from agentex.lib.utils.model_utils import BaseModel diff --git a/tests/lib/cli/test_agent_handlers.py b/tests/lib/cli/test_agent_handlers.py new file mode 100644 index 000000000..4d9f6e260 --- /dev/null +++ b/tests/lib/cli/test_agent_handlers.py @@ -0,0 +1,424 @@ +"""Tests for agent_handlers module - prepare_cloud_build_context and package CLI command.""" + +from __future__ import annotations + +import os +import tarfile +import tempfile +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from agentex.lib.cli.handlers.agent_handlers import ( + CloudBuildContext, + parse_build_args, + prepare_cloud_build_context, +) +from agentex.lib.cli.commands.agents import agents + + +runner = CliRunner() + + +class TestParseBuildArgs: + """Tests for parse_build_args helper function.""" + + def test_parse_empty_build_args(self): + """Test parsing None or empty list returns empty dict.""" + assert parse_build_args(None) == {} + assert parse_build_args([]) == {} + + def test_parse_single_build_arg(self): + """Test parsing a single KEY=VALUE argument.""" + result = parse_build_args(["FOO=bar"]) + assert result == {"FOO": "bar"} + + def test_parse_multiple_build_args(self): + """Test parsing multiple KEY=VALUE arguments.""" + result = parse_build_args(["FOO=bar", "BAZ=qux", "NUM=123"]) + assert result == {"FOO": "bar", "BAZ": "qux", "NUM": "123"} + + def test_parse_build_arg_with_equals_in_value(self): + """Test that values containing '=' are handled correctly.""" + result = parse_build_args(["URL=https://example.com?foo=bar"]) + assert result == {"URL": "https://example.com?foo=bar"} + + def test_parse_invalid_build_arg_ignored(self): + """Test that invalid format args (no '=') are ignored.""" + result = parse_build_args(["VALID=value", "invalid_no_equals"]) + assert result == {"VALID": "value"} + + +class TestPrepareCloudBuildContext: + """Tests for prepare_cloud_build_context function.""" + + @pytest.fixture + def temp_agent_dir(self) -> Path: + """Create a temporary agent directory with minimal required files.""" + with tempfile.TemporaryDirectory() as tmpdir: + agent_dir = Path(tmpdir) + + # Create a minimal Dockerfile + dockerfile = agent_dir / "Dockerfile" + dockerfile.write_text("FROM python:3.12-slim\nCMD ['echo', 'hello']") + + # Create a simple Python file to include + src_dir = agent_dir / "src" + src_dir.mkdir() + (src_dir / "main.py").write_text("print('hello')") + + # Create manifest.yaml + manifest = agent_dir / "manifest.yaml" + manifest.write_text( + """ +build: + context: + root: . + include_paths: + - src + dockerfile: Dockerfile + +agent: + name: test-agent + acp_type: sync + description: Test agent + temporal: + enabled: false + +deployment: + image: + repository: test-repo/test-agent + tag: v1.0.0 +""" + ) + + yield agent_dir + + @pytest.fixture + def temp_agent_dir_no_deployment(self) -> Path: + """Create a temporary agent directory without deployment config.""" + with tempfile.TemporaryDirectory() as tmpdir: + agent_dir = Path(tmpdir) + + dockerfile = agent_dir / "Dockerfile" + dockerfile.write_text("FROM python:3.12-slim") + + src_dir = agent_dir / "src" + src_dir.mkdir() + (src_dir / "main.py").write_text("print('hello')") + + manifest = agent_dir / "manifest.yaml" + manifest.write_text( + """ +build: + context: + root: . + include_paths: + - src + dockerfile: Dockerfile + +agent: + name: test-agent-no-deploy + acp_type: sync + description: Test agent without deployment config + temporal: + enabled: false +""" + ) + + yield agent_dir + + def test_prepare_cloud_build_context_returns_cloud_build_context( + self, temp_agent_dir: Path + ): + """Test that prepare_cloud_build_context returns a CloudBuildContext.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + result = prepare_cloud_build_context(manifest_path=manifest_path) + + assert isinstance(result, CloudBuildContext) + assert result.agent_name == "test-agent" + assert result.tag == "v1.0.0" # From manifest deployment.image.tag + assert result.image_name == "test-agent" # Last part of repository + assert result.dockerfile_path == "Dockerfile" + assert len(result.archive_bytes) > 0 + assert result.build_context_size_kb > 0 + + def test_prepare_cloud_build_context_with_tag_override(self, temp_agent_dir: Path): + """Test that tag parameter overrides manifest tag.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + result = prepare_cloud_build_context(manifest_path=manifest_path, tag="custom-tag") + + assert result.tag == "custom-tag" + + def test_prepare_cloud_build_context_defaults_to_latest_when_no_deployment( + self, temp_agent_dir_no_deployment: Path + ): + """Test that tag defaults to 'latest' when no deployment config exists.""" + manifest_path = str(temp_agent_dir_no_deployment / "manifest.yaml") + + result = prepare_cloud_build_context(manifest_path=manifest_path) + + assert result.tag == "latest" + assert result.image_name == "" # No repository in deployment config + + def test_prepare_cloud_build_context_archive_is_valid_tarball( + self, temp_agent_dir: Path + ): + """Test that the archive bytes are a valid tar.gz file.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + result = prepare_cloud_build_context(manifest_path=manifest_path) + + # Write to temp file and verify it's a valid tar.gz + with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as f: + f.write(result.archive_bytes) + temp_tar_path = f.name + + try: + with tarfile.open(temp_tar_path, "r:gz") as tar: + names = tar.getnames() + # Should contain Dockerfile and src/main.py + assert "Dockerfile" in names + assert "src/main.py" in names + finally: + os.unlink(temp_tar_path) + + def test_prepare_cloud_build_context_missing_dockerfile_raises_error(self): + """Test that missing Dockerfile raises FileNotFoundError.""" + with tempfile.TemporaryDirectory() as tmpdir: + agent_dir = Path(tmpdir) + + # Create manifest pointing to non-existent Dockerfile + manifest = agent_dir / "manifest.yaml" + manifest.write_text( + """ +build: + context: + root: . + include_paths: [] + dockerfile: NonExistentDockerfile + +agent: + name: test-agent + acp_type: sync + description: Test agent + temporal: + enabled: false +""" + ) + + with pytest.raises(FileNotFoundError, match="Dockerfile not found"): + prepare_cloud_build_context(manifest_path=str(manifest)) + + def test_prepare_cloud_build_context_dockerfile_is_directory_raises_error(self): + """Test that Dockerfile path pointing to directory raises ValueError.""" + with tempfile.TemporaryDirectory() as tmpdir: + agent_dir = Path(tmpdir) + + # Create a directory instead of a file for Dockerfile + dockerfile_dir = agent_dir / "Dockerfile" + dockerfile_dir.mkdir() + + manifest = agent_dir / "manifest.yaml" + manifest.write_text( + """ +build: + context: + root: . + include_paths: [] + dockerfile: Dockerfile + +agent: + name: test-agent + acp_type: sync + description: Test agent + temporal: + enabled: false +""" + ) + + with pytest.raises(ValueError, match="not a file"): + prepare_cloud_build_context(manifest_path=str(manifest)) + + def test_prepare_cloud_build_context_with_build_args(self, temp_agent_dir: Path): + """Test that build_args are accepted (they're logged but not included in archive).""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + # Should not raise - build_args are accepted even though they're just logged + result = prepare_cloud_build_context( + manifest_path=manifest_path, + build_args=["ARG1=value1", "ARG2=value2"], + ) + + assert isinstance(result, CloudBuildContext) + + +class TestPackageCommand: + """Tests for the 'agentex agents package' CLI command.""" + + @pytest.fixture + def temp_agent_dir(self) -> Path: + """Create a temporary agent directory with minimal required files.""" + with tempfile.TemporaryDirectory() as tmpdir: + agent_dir = Path(tmpdir) + + dockerfile = agent_dir / "Dockerfile" + dockerfile.write_text("FROM python:3.12-slim\nCMD ['echo', 'hello']") + + src_dir = agent_dir / "src" + src_dir.mkdir() + (src_dir / "main.py").write_text("print('hello')") + + manifest = agent_dir / "manifest.yaml" + manifest.write_text( + """ +build: + context: + root: . + include_paths: + - src + dockerfile: Dockerfile + +agent: + name: test-agent + acp_type: sync + description: Test agent + temporal: + enabled: false + +deployment: + image: + repository: test-repo/test-agent + tag: v1.0.0 +""" + ) + + yield agent_dir + + def test_package_command_creates_tarball(self, temp_agent_dir: Path): + """Test that package command creates a tarball file.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + # Change to temp dir so output goes there + original_cwd = os.getcwd() + os.chdir(temp_agent_dir) + + try: + result = runner.invoke(agents, ["package", "--manifest", manifest_path]) + + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Tarball saved to:" in result.output + + # Check that tarball was created + expected_tarball = temp_agent_dir / "test-agent-v1.0.0.tar.gz" + assert expected_tarball.exists() + + # Verify it's a valid tar.gz + with tarfile.open(expected_tarball, "r:gz") as tar: + names = tar.getnames() + assert "Dockerfile" in names + finally: + os.chdir(original_cwd) + + def test_package_command_with_custom_tag(self, temp_agent_dir: Path): + """Test package command with custom tag override.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + original_cwd = os.getcwd() + os.chdir(temp_agent_dir) + + try: + result = runner.invoke( + agents, ["package", "--manifest", manifest_path, "--tag", "custom-tag"] + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + + # Check that tarball with custom tag was created + expected_tarball = temp_agent_dir / "test-agent-custom-tag.tar.gz" + assert expected_tarball.exists() + finally: + os.chdir(original_cwd) + + def test_package_command_with_custom_output(self, temp_agent_dir: Path): + """Test package command with custom output filename.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + original_cwd = os.getcwd() + os.chdir(temp_agent_dir) + + try: + result = runner.invoke( + agents, + ["package", "--manifest", manifest_path, "--output", "my-custom-output.tar.gz"], + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + + expected_tarball = temp_agent_dir / "my-custom-output.tar.gz" + assert expected_tarball.exists() + finally: + os.chdir(original_cwd) + + def test_package_command_missing_manifest(self, temp_agent_dir: Path): + """Test package command fails gracefully with missing manifest.""" + original_cwd = os.getcwd() + os.chdir(temp_agent_dir) + + try: + result = runner.invoke( + agents, ["package", "--manifest", "nonexistent-manifest.yaml"] + ) + + assert result.exit_code == 1 + assert "manifest not found" in result.output + finally: + os.chdir(original_cwd) + + def test_package_command_shows_build_parameters(self, temp_agent_dir: Path): + """Test that package command outputs build parameters for cloud build.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + original_cwd = os.getcwd() + os.chdir(temp_agent_dir) + + try: + result = runner.invoke(agents, ["package", "--manifest", manifest_path]) + + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Build Parameters for Cloud Build API:" in result.output + assert "agent_name:" in result.output + assert "test-agent" in result.output + assert "image_name:" in result.output + assert "tag:" in result.output + finally: + os.chdir(original_cwd) + + def test_package_command_with_build_args(self, temp_agent_dir: Path): + """Test package command with build arguments.""" + manifest_path = str(temp_agent_dir / "manifest.yaml") + + original_cwd = os.getcwd() + os.chdir(temp_agent_dir) + + try: + result = runner.invoke( + agents, + [ + "package", + "--manifest", + manifest_path, + "--build-arg", + "ARG1=value1", + "--build-arg", + "ARG2=value2", + ], + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "build_args:" in result.output + finally: + os.chdir(original_cwd) From a8e71cbe0632d6639faeeb80d24b0b0b5d480897 Mon Sep 17 00:00:00 2001 From: Roxanne Farhad Date: Fri, 16 Jan 2026 14:16:43 -0500 Subject: [PATCH 3/3] fixing uv lock --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index f17484759..391297102 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentex-sdk" -version = "0.8.2" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "aiohttp" },