agentex-sdk 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

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.
Files changed (25) hide show
  1. agentex/_base_client.py +134 -11
  2. agentex/_models.py +16 -1
  3. agentex/_types.py +9 -0
  4. agentex/_version.py +1 -1
  5. agentex/lib/cli/commands/agents.py +141 -73
  6. agentex/lib/cli/commands/init.py +13 -2
  7. agentex/lib/cli/handlers/agent_handlers.py +130 -12
  8. agentex/lib/cli/templates/sync-openai-agents/.dockerignore.j2 +43 -0
  9. agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 +42 -0
  10. agentex/lib/cli/templates/sync-openai-agents/Dockerfile.j2 +43 -0
  11. agentex/lib/cli/templates/sync-openai-agents/README.md.j2 +313 -0
  12. agentex/lib/cli/templates/sync-openai-agents/dev.ipynb.j2 +167 -0
  13. agentex/lib/cli/templates/sync-openai-agents/environments.yaml.j2 +53 -0
  14. agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 +115 -0
  15. agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 +137 -0
  16. agentex/lib/cli/templates/sync-openai-agents/pyproject.toml.j2 +32 -0
  17. agentex/lib/cli/templates/sync-openai-agents/requirements.txt.j2 +5 -0
  18. agentex/lib/cli/templates/sync-openai-agents/test_agent.py.j2 +70 -0
  19. agentex/lib/sdk/config/environment_config.py +113 -73
  20. agentex/lib/sdk/config/validation.py +62 -61
  21. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/METADATA +1 -1
  22. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/RECORD +25 -14
  23. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/licenses/LICENSE +1 -1
  24. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/WHEEL +0 -0
  25. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/entry_points.txt +0 -0
@@ -26,6 +26,8 @@ from agentex.lib.sdk.config.agent_manifest import AgentManifest
26
26
  from agentex.lib.cli.handlers.agent_handlers import (
27
27
  run_agent,
28
28
  build_agent,
29
+ parse_build_args,
30
+ prepare_cloud_build_context,
29
31
  )
30
32
  from agentex.lib.cli.handlers.deploy_handlers import (
31
33
  HelmError,
@@ -83,26 +85,24 @@ def delete(
83
85
  @agents.command()
84
86
  def cleanup_workflows(
85
87
  agent_name: str = typer.Argument(..., help="Name of the agent to cleanup workflows for"),
86
- force: bool = typer.Option(False, help="Force cleanup using direct Temporal termination (bypasses development check)"),
88
+ force: bool = typer.Option(
89
+ False, help="Force cleanup using direct Temporal termination (bypasses development check)"
90
+ ),
87
91
  ):
88
92
  """
89
93
  Clean up all running workflows for an agent.
90
-
94
+
91
95
  By default, uses graceful cancellation via agent RPC.
92
96
  With --force, directly terminates workflows via Temporal client.
93
97
  This is a convenience command that does the same thing as 'agentex tasks cleanup'.
94
98
  """
95
99
  try:
96
100
  console.print(f"[blue]Cleaning up workflows for agent '{agent_name}'...[/blue]")
97
-
98
- cleanup_agent_workflows(
99
- agent_name=agent_name,
100
- force=force,
101
- development_only=True
102
- )
103
-
101
+
102
+ cleanup_agent_workflows(agent_name=agent_name, force=force, development_only=True)
103
+
104
104
  console.print(f"[green]✓ Workflow cleanup completed for agent '{agent_name}'[/green]")
105
-
105
+
106
106
  except Exception as e:
107
107
  console.print(f"[red]Cleanup failed: {str(e)}[/red]")
108
108
  logger.exception("Agent workflow cleanup failed")
@@ -112,12 +112,8 @@ def cleanup_workflows(
112
112
  @agents.command()
113
113
  def build(
114
114
  manifest: str = typer.Option(..., help="Path to the manifest you want to use"),
115
- registry: str | None = typer.Option(
116
- None, help="Registry URL for pushing the built image"
117
- ),
118
- repository_name: str | None = typer.Option(
119
- None, help="Repository name to use for the built image"
120
- ),
115
+ registry: str | None = typer.Option(None, help="Registry URL for pushing the built image"),
116
+ repository_name: str | None = typer.Option(None, help="Repository name to use for the built image"),
121
117
  platforms: str | None = typer.Option(
122
118
  None, help="Platform to build the image for. Please enter a comma separated list of platforms."
123
119
  ),
@@ -126,9 +122,7 @@ def build(
126
122
  None,
127
123
  help="Docker build secret in the format 'id=secret-id,src=path-to-secret-file'",
128
124
  ),
129
- tag: str | None = typer.Option(
130
- None, help="Image tag to use (defaults to 'latest')"
131
- ),
125
+ tag: str | None = typer.Option(None, help="Image tag to use (defaults to 'latest')"),
132
126
  build_arg: builtins.list[str] | None = typer.Option( # noqa: B008
133
127
  None,
134
128
  help="Docker build argument in the format 'KEY=VALUE' (can be used multiple times)",
@@ -143,7 +137,7 @@ def build(
143
137
  if push and not registry:
144
138
  typer.echo("Error: --registry is required when --push is enabled", err=True)
145
139
  raise typer.Exit(1)
146
-
140
+
147
141
  # Only proceed with build if we have a registry (for now, to match existing behavior)
148
142
  if not registry:
149
143
  typer.echo("No registry provided, skipping image build")
@@ -172,13 +166,105 @@ def build(
172
166
  raise typer.Exit(1) from e
173
167
 
174
168
 
169
+ @agents.command(name="package")
170
+ def package(
171
+ manifest: str = typer.Option(..., help="Path to the manifest you want to use"),
172
+ tag: str | None = typer.Option(
173
+ None,
174
+ "--tag",
175
+ "-t",
176
+ help="Image tag (defaults to deployment.image.tag from manifest, or 'latest')",
177
+ ),
178
+ output: str | None = typer.Option(
179
+ None,
180
+ "--output",
181
+ "-o",
182
+ help="Output filename for the tarball (defaults to <agent-name>-<tag>.tar.gz)",
183
+ ),
184
+ build_arg: builtins.list[str] | None = typer.Option( # noqa: B008
185
+ None,
186
+ "--build-arg",
187
+ "-b",
188
+ help="Build argument in KEY=VALUE format (can be repeated)",
189
+ ),
190
+ ):
191
+ """
192
+ Package an agent's build context into a tarball for cloud builds.
193
+
194
+ Reads manifest.yaml, prepares build context according to include_paths and
195
+ dockerignore, then saves a compressed tarball to the current directory.
196
+
197
+ The tag defaults to the value in deployment.image.tag from the manifest.
198
+
199
+ Example:
200
+ agentex agents package --manifest manifest.yaml
201
+ agentex agents package --manifest manifest.yaml --tag v1.0
202
+ """
203
+ typer.echo(f"Packaging build context from manifest: {manifest}")
204
+
205
+ # Validate manifest exists
206
+ manifest_path = Path(manifest)
207
+ if not manifest_path.exists():
208
+ typer.echo(f"Error: manifest not found at {manifest_path}", err=True)
209
+ raise typer.Exit(1)
210
+
211
+ try:
212
+ # Prepare the build context (tag defaults from manifest if not provided)
213
+ build_context = prepare_cloud_build_context(
214
+ manifest_path=str(manifest_path),
215
+ tag=tag,
216
+ build_args=build_arg,
217
+ )
218
+
219
+ # Determine output filename using the resolved tag
220
+ if output:
221
+ output_filename = output
222
+ else:
223
+ output_filename = f"{build_context.agent_name}-{build_context.tag}.tar.gz"
224
+
225
+ # Save tarball to current working directory
226
+ output_path = Path.cwd() / output_filename
227
+ output_path.write_bytes(build_context.archive_bytes)
228
+
229
+ typer.echo(f"\nTarball saved to: {output_path}")
230
+ typer.echo(f"Size: {build_context.build_context_size_kb:.1f} KB")
231
+
232
+ # Output the build parameters needed for cloud build
233
+ typer.echo("\n" + "=" * 60)
234
+ typer.echo("Build Parameters for Cloud Build API:")
235
+ typer.echo("=" * 60)
236
+ typer.echo(f" agent_name: {build_context.agent_name}")
237
+ typer.echo(f" image_name: {build_context.image_name}")
238
+ typer.echo(f" tag: {build_context.tag}")
239
+ typer.echo(f" context_file: {output_path}")
240
+
241
+ if build_arg:
242
+ parsed_args = parse_build_args(build_arg)
243
+ typer.echo(f" build_args: {parsed_args}")
244
+
245
+ typer.echo("")
246
+ typer.echo("Command:")
247
+ build_args_str = ""
248
+ if build_arg:
249
+ build_args_str = " ".join(f'--build-arg "{arg}"' for arg in build_arg)
250
+ build_args_str = f" {build_args_str}"
251
+ typer.echo(
252
+ f' sgp agentex build --context "{output_path}" '
253
+ f'--image-name "{build_context.image_name}" '
254
+ f'--tag "{build_context.tag}"{build_args_str}'
255
+ )
256
+ typer.echo("=" * 60)
257
+
258
+ except Exception as e:
259
+ typer.echo(f"Error packaging build context: {str(e)}", err=True)
260
+ logger.exception("Error packaging build context")
261
+ raise typer.Exit(1) from e
262
+
263
+
175
264
  @agents.command()
176
265
  def run(
177
266
  manifest: str = typer.Option(..., help="Path to the manifest you want to use"),
178
- cleanup_on_start: bool = typer.Option(
179
- False,
180
- help="Clean up existing workflows for this agent before starting"
181
- ),
267
+ cleanup_on_start: bool = typer.Option(False, help="Clean up existing workflows for this agent before starting"),
182
268
  # Debug options
183
269
  debug: bool = typer.Option(False, help="Enable debug mode for both worker and ACP (disables auto-reload)"),
184
270
  debug_worker: bool = typer.Option(False, help="Enable debug mode for temporal worker only"),
@@ -190,26 +276,22 @@ def run(
190
276
  Run an agent locally from the given manifest.
191
277
  """
192
278
  typer.echo(f"Running agent from manifest: {manifest}")
193
-
279
+
194
280
  # Optionally cleanup existing workflows before starting
195
281
  if cleanup_on_start:
196
282
  try:
197
283
  # Parse manifest to get agent name
198
284
  manifest_obj = AgentManifest.from_yaml(file_path=manifest)
199
285
  agent_name = manifest_obj.agent.name
200
-
286
+
201
287
  console.print(f"[yellow]Cleaning up existing workflows for agent '{agent_name}'...[/yellow]")
202
- cleanup_agent_workflows(
203
- agent_name=agent_name,
204
- force=False,
205
- development_only=True
206
- )
288
+ cleanup_agent_workflows(agent_name=agent_name, force=False, development_only=True)
207
289
  console.print("[green]✓ Pre-run cleanup completed[/green]")
208
-
290
+
209
291
  except Exception as e:
210
292
  console.print(f"[yellow]⚠ Pre-run cleanup failed: {str(e)}[/yellow]")
211
293
  logger.warning(f"Pre-run cleanup failed: {e}")
212
-
294
+
213
295
  # Create debug configuration based on CLI flags
214
296
  debug_config = None
215
297
  if debug or debug_worker or debug_acp:
@@ -224,19 +306,19 @@ def run(
224
306
  mode = DebugMode.ACP
225
307
  else:
226
308
  mode = DebugMode.NONE
227
-
309
+
228
310
  debug_config = DebugConfig(
229
311
  enabled=True,
230
312
  mode=mode,
231
313
  port=debug_port,
232
314
  wait_for_attach=wait_for_debugger,
233
- auto_port=False # Use fixed port to match VS Code launch.json
315
+ auto_port=False, # Use fixed port to match VS Code launch.json
234
316
  )
235
-
317
+
236
318
  console.print(f"[blue]🐛 Debug mode enabled: {mode.value}[/blue]")
237
319
  if wait_for_debugger:
238
320
  console.print("[yellow]⏳ Processes will wait for debugger attachment[/yellow]")
239
-
321
+
240
322
  try:
241
323
  run_agent(manifest_path=manifest, debug_config=debug_config)
242
324
  except Exception as e:
@@ -247,30 +329,23 @@ def run(
247
329
 
248
330
  @agents.command()
249
331
  def deploy(
250
- cluster: str = typer.Option(
251
- ..., help="Target cluster name (must match kubectl context)"
252
- ),
332
+ cluster: str = typer.Option(..., help="Target cluster name (must match kubectl context)"),
253
333
  manifest: str = typer.Option("manifest.yaml", help="Path to the manifest file"),
254
334
  namespace: str | None = typer.Option(
255
335
  None,
256
336
  help="Override Kubernetes namespace (defaults to namespace from environments.yaml)",
257
337
  ),
258
338
  environment: str | None = typer.Option(
259
- None, help="Environment name (dev, prod, etc.) - must be defined in environments.yaml. If not provided, the namespace must be set explicitly."
260
- ),
261
- tag: str | None = typer.Option(None, help="Override the image tag for deployment"),
262
- repository: str | None = typer.Option(
263
- None, help="Override the repository for deployment"
264
- ),
265
- interactive: bool = typer.Option(
266
- True, "--interactive/--no-interactive", help="Enable interactive prompts"
339
+ None,
340
+ help="Environment name (dev, prod, etc.) - must be defined in environments.yaml. If not provided, the namespace must be set explicitly.",
267
341
  ),
342
+ tag: str | None = typer.Option(None, help="Override the image tag for deployment"),
343
+ repository: str | None = typer.Option(None, help="Override the repository for deployment"),
344
+ interactive: bool = typer.Option(True, "--interactive/--no-interactive", help="Enable interactive prompts"),
268
345
  ):
269
346
  """Deploy an agent to a Kubernetes cluster using Helm"""
270
347
 
271
- console.print(
272
- Panel.fit("🚀 [bold blue]Deploy Agent[/bold blue]", border_style="blue")
273
- )
348
+ console.print(Panel.fit("🚀 [bold blue]Deploy Agent[/bold blue]", border_style="blue"))
274
349
 
275
350
  try:
276
351
  # Validate manifest exists
@@ -281,17 +356,12 @@ def deploy(
281
356
 
282
357
  # Validate manifest and environments configuration
283
358
  try:
284
- if environment:
285
- _, environments_config = validate_manifest_and_environments(
286
- str(manifest_path),
287
- required_environment=environment
288
- )
289
- agent_env_config = environments_config.get_config_for_env(environment)
290
- console.print(f"[green]✓[/green] Environment config validated: {environment}")
291
- else:
292
- agent_env_config = None
293
- console.print(f"[yellow]⚠[/yellow] No environment provided, skipping environment-specific config")
294
-
359
+ _, environments_config = validate_manifest_and_environments(
360
+ str(manifest_path), required_environment=environment
361
+ )
362
+ agent_env_config = environments_config.get_config_for_env(environment)
363
+ console.print(f"[green]✓[/green] Environment config validated: {environment}")
364
+
295
365
  except EnvironmentsValidationError as e:
296
366
  error_msg = generate_helpful_error_message(e, "Environment validation failed")
297
367
  console.print(f"[red]Configuration Error:[/red]\n{error_msg}")
@@ -310,9 +380,13 @@ def deploy(
310
380
  console.print(f"[blue]ℹ[/blue] Using namespace from environments.yaml: {namespace_from_config}")
311
381
  namespace = namespace_from_config
312
382
  else:
313
- raise DeploymentError(f"No namespace found in environments.yaml for environment: {environment}, and not passed in as --namespace")
383
+ raise DeploymentError(
384
+ f"No namespace found in environments.yaml for environment: {environment}, and not passed in as --namespace"
385
+ )
314
386
  elif not namespace:
315
- raise DeploymentError("No namespace provided, and not passed in as --namespace and no environment provided to read from an environments.yaml file")
387
+ raise DeploymentError(
388
+ "No namespace provided, and not passed in as --namespace and no environment provided to read from an environments.yaml file"
389
+ )
316
390
 
317
391
  # Confirm deployment (only in interactive mode)
318
392
  console.print("\n[bold]Deployment Summary:[/bold]")
@@ -325,9 +399,7 @@ def deploy(
325
399
 
326
400
  if interactive:
327
401
  proceed = questionary.confirm("Proceed with deployment?").ask()
328
- proceed = handle_questionary_cancellation(
329
- proceed, "deployment confirmation"
330
- )
402
+ proceed = handle_questionary_cancellation(proceed, "deployment confirmation")
331
403
 
332
404
  if not proceed:
333
405
  console.print("Deployment cancelled")
@@ -337,9 +409,7 @@ def deploy(
337
409
 
338
410
  check_and_switch_cluster_context(cluster)
339
411
  if not validate_namespace(namespace, cluster):
340
- console.print(
341
- f"[red]Error:[/red] Namespace '{namespace}' does not exist in cluster '{cluster}'"
342
- )
412
+ console.print(f"[red]Error:[/red] Namespace '{namespace}' does not exist in cluster '{cluster}'")
343
413
  raise typer.Exit(1)
344
414
 
345
415
  deploy_overrides = InputDeployOverrides(repository=repository, image_tag=tag)
@@ -356,9 +426,7 @@ def deploy(
356
426
  # Use the already loaded manifest object
357
427
  release_name = f"{manifest_obj.agent.name}-{cluster}"
358
428
 
359
- console.print(
360
- "\n[bold green]🎉 Deployment completed successfully![/bold green]"
361
- )
429
+ console.print("\n[bold green]🎉 Deployment completed successfully![/bold green]")
362
430
  console.print("\nTo check deployment status:")
363
431
  console.print(f" kubectl get pods -n {namespace}")
364
432
  console.print(f" helm status {release_name} -n {namespace}")
@@ -26,6 +26,7 @@ class TemplateType(str, Enum):
26
26
  TEMPORAL_OPENAI_AGENTS = "temporal-openai-agents"
27
27
  DEFAULT = "default"
28
28
  SYNC = "sync"
29
+ SYNC_OPENAI_AGENTS = "sync-openai-agents"
29
30
 
30
31
 
31
32
  def render_template(
@@ -58,6 +59,7 @@ def create_project_structure(
58
59
  TemplateType.TEMPORAL_OPENAI_AGENTS: ["acp.py", "workflow.py", "run_worker.py", "activities.py"],
59
60
  TemplateType.DEFAULT: ["acp.py"],
60
61
  TemplateType.SYNC: ["acp.py"],
62
+ TemplateType.SYNC_OPENAI_AGENTS: ["acp.py"],
61
63
  }[template_type]
62
64
 
63
65
  # Create project/code files
@@ -155,7 +157,7 @@ def init():
155
157
  choices=[
156
158
  {"name": "Async - ACP Only", "value": TemplateType.DEFAULT},
157
159
  {"name": "Async - Temporal", "value": "temporal_submenu"},
158
- {"name": "Sync ACP", "value": TemplateType.SYNC},
160
+ {"name": "Sync ACP", "value": "sync_submenu"},
159
161
  ],
160
162
  ).ask()
161
163
  if not template_type:
@@ -163,7 +165,6 @@ def init():
163
165
 
164
166
  # If Temporal was selected, show sub-menu for Temporal variants
165
167
  if template_type == "temporal_submenu":
166
- console.print()
167
168
  template_type = questionary.select(
168
169
  "Which Temporal template would you like to use?",
169
170
  choices=[
@@ -173,6 +174,16 @@ def init():
173
174
  ).ask()
174
175
  if not template_type:
175
176
  return
177
+ elif template_type == "sync_submenu":
178
+ template_type = questionary.select(
179
+ "Which Sync template would you like to use?",
180
+ choices=[
181
+ {"name": "Basic Sync ACP", "value": TemplateType.SYNC},
182
+ {"name": "Sync ACP + OpenAI Agents SDK (Recommended)", "value": TemplateType.SYNC_OPENAI_AGENTS},
183
+ ],
184
+ ).ask()
185
+ if not template_type:
186
+ return
176
187
 
177
188
  project_path = questionary.path(
178
189
  "Where would you like to create your project?", default="."
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import NamedTuple
3
4
  from pathlib import Path
4
5
 
5
6
  from rich.console import Console
@@ -8,7 +9,7 @@ from python_on_whales import DockerException, docker
8
9
  from agentex.lib.cli.debug import DebugConfig
9
10
  from agentex.lib.utils.logging import make_logger
10
11
  from agentex.lib.cli.handlers.run_handlers import RunError, run_agent as _run_agent
11
- from agentex.lib.sdk.config.agent_manifest import AgentManifest
12
+ from agentex.lib.sdk.config.agent_manifest import AgentManifest, BuildContextManager
12
13
 
13
14
  logger = make_logger(__name__)
14
15
  console = Console()
@@ -18,6 +19,17 @@ class DockerBuildError(Exception):
18
19
  """An error occurred during docker build"""
19
20
 
20
21
 
22
+ class CloudBuildContext(NamedTuple):
23
+ """Contains the prepared build context for cloud builds."""
24
+
25
+ archive_bytes: bytes
26
+ dockerfile_path: str
27
+ agent_name: str
28
+ tag: str
29
+ image_name: str
30
+ build_context_size_kb: float
31
+
32
+
21
33
  def build_agent(
22
34
  manifest_path: str,
23
35
  registry_url: str,
@@ -42,9 +54,7 @@ def build_agent(
42
54
  The image URL
43
55
  """
44
56
  agent_manifest = AgentManifest.from_yaml(file_path=manifest_path)
45
- build_context_root = (
46
- Path(manifest_path).parent / agent_manifest.build.context.root
47
- ).resolve()
57
+ build_context_root = (Path(manifest_path).parent / agent_manifest.build.context.root).resolve()
48
58
 
49
59
  repository_name = repository_name or agent_manifest.agent.name
50
60
 
@@ -85,9 +95,7 @@ def build_agent(
85
95
  key, value = arg.split("=", 1)
86
96
  docker_build_args[key] = value
87
97
  else:
88
- logger.warning(
89
- f"Invalid build arg format: {arg}. Expected KEY=VALUE"
90
- )
98
+ logger.warning(f"Invalid build arg format: {arg}. Expected KEY=VALUE")
91
99
 
92
100
  if docker_build_args:
93
101
  docker_build_kwargs["build_args"] = docker_build_args
@@ -100,9 +108,7 @@ def build_agent(
100
108
  if push:
101
109
  # Build and push in one step for multi-platform builds
102
110
  logger.info("Building and pushing image...")
103
- docker_build_kwargs["push"] = (
104
- True # Push directly after build for multi-platform
105
- )
111
+ docker_build_kwargs["push"] = True # Push directly after build for multi-platform
106
112
  docker.buildx.build(**docker_build_kwargs)
107
113
 
108
114
  logger.info(f"Successfully built and pushed {image_name}")
@@ -146,11 +152,11 @@ def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = None):
146
152
  shutting_down = True
147
153
  logger.info(f"Received signal {signum}, shutting down...")
148
154
  raise KeyboardInterrupt()
149
-
155
+
150
156
  # Set up signal handling for the main thread
151
157
  signal.signal(signal.SIGINT, signal_handler)
152
158
  signal.signal(signal.SIGTERM, signal_handler)
153
-
159
+
154
160
  try:
155
161
  asyncio.run(_run_agent(manifest_path, debug_config))
156
162
  except KeyboardInterrupt:
@@ -158,3 +164,115 @@ def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = None):
158
164
  sys.exit(0)
159
165
  except RunError as e:
160
166
  raise RuntimeError(str(e)) from e
167
+
168
+
169
+ def parse_build_args(build_args: list[str] | None) -> dict[str, str]:
170
+ """Parse build arguments from KEY=VALUE format to a dictionary.
171
+
172
+ Args:
173
+ build_args: List of build arguments in KEY=VALUE format
174
+
175
+ Returns:
176
+ Dictionary mapping keys to values
177
+ """
178
+ result: dict[str, str] = {}
179
+ if not build_args:
180
+ return result
181
+
182
+ for arg in build_args:
183
+ if "=" in arg:
184
+ key, value = arg.split("=", 1)
185
+ result[key] = value
186
+ else:
187
+ logger.warning(f"Invalid build arg format: {arg}. Expected KEY=VALUE")
188
+
189
+ return result
190
+
191
+
192
+ def prepare_cloud_build_context(
193
+ manifest_path: str,
194
+ tag: str | None = None,
195
+ build_args: list[str] | None = None,
196
+ ) -> CloudBuildContext:
197
+ """Prepare the build context for cloud-based container builds.
198
+
199
+ Reads the manifest, prepares the build context by copying files according to
200
+ the include_paths and dockerignore, then creates a compressed tar.gz archive
201
+ ready for upload to a cloud build service.
202
+
203
+ Args:
204
+ manifest_path: Path to the agent manifest file
205
+ tag: Image tag override (if None, reads from manifest's deployment.image.tag)
206
+ build_args: List of build arguments in KEY=VALUE format
207
+
208
+ Returns:
209
+ CloudBuildContext containing the archive bytes, dockerfile path, and metadata
210
+ """
211
+ agent_manifest = AgentManifest.from_yaml(file_path=manifest_path)
212
+ build_context_root = (Path(manifest_path).parent / agent_manifest.build.context.root).resolve()
213
+
214
+ agent_name = agent_manifest.agent.name
215
+ dockerfile_path = agent_manifest.build.context.dockerfile
216
+
217
+ # Validate that the Dockerfile exists
218
+ full_dockerfile_path = build_context_root / dockerfile_path
219
+ if not full_dockerfile_path.exists():
220
+ raise FileNotFoundError(
221
+ f"Dockerfile not found at: {full_dockerfile_path}\n"
222
+ f"Check that 'build.context.dockerfile' in your manifest points to an existing file."
223
+ )
224
+ if not full_dockerfile_path.is_file():
225
+ raise ValueError(
226
+ f"Dockerfile path is not a file: {full_dockerfile_path}\n"
227
+ f"'build.context.dockerfile' must point to a file, not a directory."
228
+ )
229
+
230
+ # Get tag and repository from manifest if not provided
231
+ if tag is None:
232
+ if agent_manifest.deployment and agent_manifest.deployment.image:
233
+ tag = agent_manifest.deployment.image.tag
234
+ else:
235
+ tag = "latest"
236
+
237
+ # Get repository name from manifest (just the repo name, not the full registry URL)
238
+ if agent_manifest.deployment and agent_manifest.deployment.image:
239
+ repository = agent_manifest.deployment.image.repository
240
+ if repository:
241
+ # Extract just the repo name (last part after any slashes)
242
+ image_name = repository.split("/")[-1]
243
+ else:
244
+ image_name = "<repository>"
245
+ else:
246
+ image_name = "<repository>"
247
+
248
+ logger.info(f"Agent: {agent_name}")
249
+ logger.info(f"Image name: {image_name}")
250
+ logger.info(f"Build context root: {build_context_root}")
251
+ logger.info(f"Dockerfile: {dockerfile_path}")
252
+ logger.info(f"Tag: {tag}")
253
+
254
+ if agent_manifest.build.context.include_paths:
255
+ logger.info(f"Include paths: {agent_manifest.build.context.include_paths}")
256
+
257
+ parsed_build_args = parse_build_args(build_args)
258
+ if parsed_build_args:
259
+ logger.info(f"Build args: {list(parsed_build_args.keys())}")
260
+
261
+ logger.info("Preparing build context...")
262
+
263
+ with agent_manifest.context_manager(build_context_root) as build_context:
264
+ # Compress the prepared context using the static zipped method
265
+ with BuildContextManager.zipped(root_path=build_context.path) as archive_buffer:
266
+ archive_bytes = archive_buffer.read()
267
+
268
+ build_context_size_kb = len(archive_bytes) / 1024
269
+ logger.info(f"Build context size: {build_context_size_kb:.1f} KB")
270
+
271
+ return CloudBuildContext(
272
+ archive_bytes=archive_bytes,
273
+ dockerfile_path=build_context.dockerfile_path,
274
+ agent_name=agent_name,
275
+ tag=tag,
276
+ image_name=image_name,
277
+ build_context_size_kb=build_context_size_kb,
278
+ )
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Environments
24
+ .env**
25
+ .venv
26
+ env/
27
+ venv/
28
+ ENV/
29
+ env.bak/
30
+ venv.bak/
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+
38
+ # Git
39
+ .git
40
+ .gitignore
41
+
42
+ # Misc
43
+ .DS_Store
@@ -0,0 +1,42 @@
1
+ # syntax=docker/dockerfile:1.3
2
+ FROM python:3.12-slim
3
+ COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ htop \
8
+ vim \
9
+ curl \
10
+ tar \
11
+ python3-dev \
12
+ postgresql-client \
13
+ build-essential \
14
+ libpq-dev \
15
+ gcc \
16
+ cmake \
17
+ netcat-openbsd \
18
+ nodejs \
19
+ npm \
20
+ && apt-get clean \
21
+ && rm -rf /var/lib/apt/lists/**
22
+
23
+ RUN uv pip install --system --upgrade pip setuptools wheel
24
+
25
+ ENV UV_HTTP_TIMEOUT=1000
26
+
27
+ # Copy just the pyproject.toml file to optimize caching
28
+ COPY {{ project_path_from_build_root }}/pyproject.toml /app/{{ project_path_from_build_root }}/pyproject.toml
29
+
30
+ WORKDIR /app/{{ project_path_from_build_root }}
31
+
32
+ # Install the required Python packages using uv
33
+ RUN uv pip install --system .
34
+
35
+ # Copy the project code
36
+ COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project
37
+
38
+ # Set environment variables
39
+ ENV PYTHONPATH=/app
40
+
41
+ # Run the agent using uvicorn
42
+ CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]