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.
- agentex/_base_client.py +134 -11
- agentex/_models.py +16 -1
- agentex/_types.py +9 -0
- agentex/_version.py +1 -1
- agentex/lib/cli/commands/agents.py +141 -73
- agentex/lib/cli/commands/init.py +13 -2
- agentex/lib/cli/handlers/agent_handlers.py +130 -12
- agentex/lib/cli/templates/sync-openai-agents/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/sync-openai-agents/Dockerfile.j2 +43 -0
- agentex/lib/cli/templates/sync-openai-agents/README.md.j2 +313 -0
- agentex/lib/cli/templates/sync-openai-agents/dev.ipynb.j2 +167 -0
- agentex/lib/cli/templates/sync-openai-agents/environments.yaml.j2 +53 -0
- agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 +115 -0
- agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 +137 -0
- agentex/lib/cli/templates/sync-openai-agents/pyproject.toml.j2 +32 -0
- agentex/lib/cli/templates/sync-openai-agents/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/sync-openai-agents/test_agent.py.j2 +70 -0
- agentex/lib/sdk/config/environment_config.py +113 -73
- agentex/lib/sdk/config/validation.py +62 -61
- {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/METADATA +1 -1
- {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/RECORD +25 -14
- {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/licenses/LICENSE +1 -1
- {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/WHEEL +0 -0
- {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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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(
|
|
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(
|
|
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}")
|
agentex/lib/cli/commands/init.py
CHANGED
|
@@ -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":
|
|
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"]
|