synth-ai 0.4.1__py3-none-any.whl → 0.4.4__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (153) hide show
  1. synth_ai/__init__.py +13 -13
  2. synth_ai/cli/__init__.py +6 -15
  3. synth_ai/cli/commands/eval/__init__.py +6 -15
  4. synth_ai/cli/commands/eval/config.py +338 -0
  5. synth_ai/cli/commands/eval/core.py +236 -1091
  6. synth_ai/cli/commands/eval/runner.py +704 -0
  7. synth_ai/cli/commands/eval/validation.py +44 -117
  8. synth_ai/cli/commands/filter/core.py +7 -7
  9. synth_ai/cli/commands/filter/validation.py +2 -2
  10. synth_ai/cli/commands/smoke/core.py +7 -17
  11. synth_ai/cli/commands/status/__init__.py +1 -64
  12. synth_ai/cli/commands/status/client.py +50 -151
  13. synth_ai/cli/commands/status/config.py +3 -83
  14. synth_ai/cli/commands/status/errors.py +4 -13
  15. synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
  16. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  17. synth_ai/cli/commands/status/subcommands/files.py +18 -63
  18. synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
  19. synth_ai/cli/commands/status/subcommands/models.py +18 -62
  20. synth_ai/cli/commands/status/subcommands/runs.py +16 -63
  21. synth_ai/cli/commands/status/subcommands/session.py +67 -172
  22. synth_ai/cli/commands/status/subcommands/summary.py +24 -32
  23. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  24. synth_ai/cli/commands/status/utils.py +16 -107
  25. synth_ai/cli/commands/train/__init__.py +18 -20
  26. synth_ai/cli/commands/train/errors.py +3 -3
  27. synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
  28. synth_ai/cli/commands/train/validation.py +7 -7
  29. synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
  30. synth_ai/cli/commands/train/verifier_validation.py +235 -0
  31. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
  32. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
  33. synth_ai/cli/demo_apps/math/config.toml +0 -1
  34. synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
  35. synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
  36. synth_ai/cli/lib/apps/task_app.py +12 -13
  37. synth_ai/cli/lib/task_app_discovery.py +6 -6
  38. synth_ai/cli/lib/train_cfgs.py +10 -10
  39. synth_ai/cli/task_apps/__init__.py +11 -0
  40. synth_ai/cli/task_apps/commands.py +7 -15
  41. synth_ai/core/env.py +12 -1
  42. synth_ai/core/errors.py +1 -2
  43. synth_ai/core/integrations/cloudflare.py +209 -33
  44. synth_ai/core/tracing_v3/abstractions.py +46 -0
  45. synth_ai/data/__init__.py +3 -30
  46. synth_ai/data/enums.py +1 -20
  47. synth_ai/data/rewards.py +100 -3
  48. synth_ai/products/graph_evolve/__init__.py +1 -2
  49. synth_ai/products/graph_evolve/config.py +16 -16
  50. synth_ai/products/graph_evolve/converters/__init__.py +3 -3
  51. synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
  52. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
  53. synth_ai/products/graph_gepa/__init__.py +23 -0
  54. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  55. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  56. synth_ai/sdk/__init__.py +45 -35
  57. synth_ai/sdk/api/eval/__init__.py +33 -0
  58. synth_ai/sdk/api/eval/job.py +732 -0
  59. synth_ai/sdk/api/research_agent/__init__.py +276 -66
  60. synth_ai/sdk/api/train/builders.py +181 -0
  61. synth_ai/sdk/api/train/cli.py +41 -33
  62. synth_ai/sdk/api/train/configs/__init__.py +6 -4
  63. synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
  64. synth_ai/sdk/api/train/configs/rl.py +264 -16
  65. synth_ai/sdk/api/train/configs/sft.py +165 -1
  66. synth_ai/sdk/api/train/graph_validators.py +12 -12
  67. synth_ai/sdk/api/train/graphgen.py +169 -51
  68. synth_ai/sdk/api/train/graphgen_models.py +95 -45
  69. synth_ai/sdk/api/train/local_api.py +10 -0
  70. synth_ai/sdk/api/train/pollers.py +36 -0
  71. synth_ai/sdk/api/train/prompt_learning.py +390 -60
  72. synth_ai/sdk/api/train/rl.py +41 -5
  73. synth_ai/sdk/api/train/sft.py +2 -0
  74. synth_ai/sdk/api/train/task_app.py +20 -0
  75. synth_ai/sdk/api/train/validators.py +17 -17
  76. synth_ai/sdk/graphs/completions.py +239 -33
  77. synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
  78. synth_ai/sdk/learning/__init__.py +35 -5
  79. synth_ai/sdk/learning/context_learning_client.py +531 -0
  80. synth_ai/sdk/learning/context_learning_types.py +294 -0
  81. synth_ai/sdk/learning/prompt_learning_client.py +1 -1
  82. synth_ai/sdk/learning/prompt_learning_types.py +2 -1
  83. synth_ai/sdk/learning/rl/__init__.py +0 -4
  84. synth_ai/sdk/learning/rl/contracts.py +0 -4
  85. synth_ai/sdk/localapi/__init__.py +40 -0
  86. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  87. synth_ai/sdk/localapi/client.py +10 -0
  88. synth_ai/sdk/localapi/contracts.py +10 -0
  89. synth_ai/sdk/localapi/helpers.py +519 -0
  90. synth_ai/sdk/localapi/rollouts.py +93 -0
  91. synth_ai/sdk/localapi/server.py +29 -0
  92. synth_ai/sdk/localapi/template.py +49 -0
  93. synth_ai/sdk/streaming/handlers.py +6 -6
  94. synth_ai/sdk/streaming/streamer.py +10 -6
  95. synth_ai/sdk/task/__init__.py +18 -5
  96. synth_ai/sdk/task/apps/__init__.py +37 -1
  97. synth_ai/sdk/task/client.py +9 -1
  98. synth_ai/sdk/task/config.py +6 -11
  99. synth_ai/sdk/task/contracts.py +137 -95
  100. synth_ai/sdk/task/in_process.py +32 -22
  101. synth_ai/sdk/task/in_process_runner.py +9 -4
  102. synth_ai/sdk/task/rubrics/__init__.py +2 -3
  103. synth_ai/sdk/task/rubrics/loaders.py +4 -4
  104. synth_ai/sdk/task/rubrics/strict.py +3 -4
  105. synth_ai/sdk/task/server.py +76 -16
  106. synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
  107. synth_ai/sdk/task/validators.py +34 -49
  108. synth_ai/sdk/training/__init__.py +7 -16
  109. synth_ai/sdk/tunnels/__init__.py +118 -0
  110. synth_ai/sdk/tunnels/cleanup.py +83 -0
  111. synth_ai/sdk/tunnels/ports.py +120 -0
  112. synth_ai/sdk/tunnels/tunneled_api.py +363 -0
  113. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
  114. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
  115. synth_ai/cli/commands/baseline/__init__.py +0 -12
  116. synth_ai/cli/commands/baseline/core.py +0 -636
  117. synth_ai/cli/commands/baseline/list.py +0 -94
  118. synth_ai/cli/commands/eval/errors.py +0 -81
  119. synth_ai/cli/commands/status/formatters.py +0 -164
  120. synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
  121. synth_ai/cli/commands/status/subcommands/usage.py +0 -203
  122. synth_ai/cli/commands/train/judge_validation.py +0 -305
  123. synth_ai/cli/usage.py +0 -159
  124. synth_ai/data/specs.py +0 -36
  125. synth_ai/sdk/api/research_agent/cli.py +0 -428
  126. synth_ai/sdk/api/research_agent/config.py +0 -357
  127. synth_ai/sdk/api/research_agent/job.py +0 -717
  128. synth_ai/sdk/baseline/__init__.py +0 -25
  129. synth_ai/sdk/baseline/config.py +0 -209
  130. synth_ai/sdk/baseline/discovery.py +0 -216
  131. synth_ai/sdk/baseline/execution.py +0 -154
  132. synth_ai/sdk/judging/__init__.py +0 -15
  133. synth_ai/sdk/judging/base.py +0 -24
  134. synth_ai/sdk/judging/client.py +0 -191
  135. synth_ai/sdk/judging/types.py +0 -42
  136. synth_ai/sdk/research_agent/__init__.py +0 -34
  137. synth_ai/sdk/research_agent/container_builder.py +0 -328
  138. synth_ai/sdk/research_agent/container_spec.py +0 -198
  139. synth_ai/sdk/research_agent/defaults.py +0 -34
  140. synth_ai/sdk/research_agent/results_collector.py +0 -69
  141. synth_ai/sdk/specs/__init__.py +0 -46
  142. synth_ai/sdk/specs/dataclasses.py +0 -149
  143. synth_ai/sdk/specs/loader.py +0 -144
  144. synth_ai/sdk/specs/serializer.py +0 -199
  145. synth_ai/sdk/specs/validation.py +0 -250
  146. synth_ai/sdk/tracing/__init__.py +0 -39
  147. synth_ai/sdk/usage/__init__.py +0 -37
  148. synth_ai/sdk/usage/client.py +0 -171
  149. synth_ai/sdk/usage/models.py +0 -261
  150. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
  151. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
  152. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
  153. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/top_level.txt +0 -0
@@ -1,428 +0,0 @@
1
- """CLI for Research Agent jobs.
2
-
3
- Provides the `synth-ai agent` command group for running research agent jobs.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import sys
9
- from pathlib import Path
10
- from typing import Any, Optional
11
-
12
- import click
13
-
14
-
15
- def _print_event(event: dict[str, Any]) -> None:
16
- """Print an event to stdout."""
17
- event_type = event.get("type", "")
18
- message = event.get("message", "")
19
- level = event.get("level", "info")
20
-
21
- # Color based on level/type
22
- if level == "error" or "failed" in event_type:
23
- click.secho(f"[{event_type}] {message}", fg="red")
24
- elif "completed" in event_type or "succeeded" in event_type:
25
- click.secho(f"[{event_type}] {message}", fg="green")
26
- elif "warning" in event_type or "budget" in event_type:
27
- click.secho(f"[{event_type}] {message}", fg="yellow")
28
- else:
29
- click.echo(f"[{event_type}] {message}")
30
-
31
-
32
- @click.group()
33
- def agent_cmd() -> None:
34
- """Research Agent commands for AI-assisted code analysis and optimization."""
35
- pass
36
-
37
-
38
- @agent_cmd.command("run")
39
- @click.option(
40
- "--config",
41
- "-c",
42
- "config_path",
43
- type=click.Path(exists=True, path_type=Path),
44
- help="Path to TOML config file",
45
- )
46
- @click.option(
47
- "--algorithm",
48
- "-a",
49
- type=click.Choice(["scaffold_tuning", "evaluation", "trace_analysis"]),
50
- help="Algorithm to run (overrides config)",
51
- )
52
- @click.option(
53
- "--repo",
54
- "-r",
55
- "repo_url",
56
- help="Repository URL (overrides config)",
57
- )
58
- @click.option(
59
- "--branch",
60
- "-b",
61
- "repo_branch",
62
- default="main",
63
- help="Repository branch",
64
- )
65
- @click.option(
66
- "--backend",
67
- type=click.Choice(["daytona", "modal", "docker"]),
68
- default="daytona",
69
- help="Container backend to use",
70
- )
71
- @click.option(
72
- "--model",
73
- "-m",
74
- default="gpt-4o",
75
- help="Model for the agent to use",
76
- )
77
- @click.option(
78
- "--poll/--no-poll",
79
- default=True,
80
- help="Poll for completion and stream events",
81
- )
82
- @click.option(
83
- "--timeout",
84
- "-t",
85
- type=float,
86
- default=3600.0,
87
- help="Timeout in seconds (for --poll)",
88
- )
89
- @click.option(
90
- "--api-key",
91
- envvar="SYNTH_API_KEY",
92
- help="Synth API key",
93
- )
94
- @click.option(
95
- "--backend-url",
96
- envvar="SYNTH_BACKEND_URL",
97
- default="https://api.usesynth.ai",
98
- help="Backend API URL",
99
- )
100
- def run_cmd(
101
- config_path: Optional[Path],
102
- algorithm: Optional[str],
103
- repo_url: Optional[str],
104
- repo_branch: str,
105
- backend: str,
106
- model: str,
107
- poll: bool,
108
- timeout: float,
109
- api_key: Optional[str],
110
- backend_url: str,
111
- ) -> None:
112
- """Run a research agent job.
113
-
114
- You can provide configuration via a TOML file or command-line options.
115
-
116
- Examples:
117
-
118
- # From config file
119
- synth-ai agent run --config my_config.toml --poll
120
-
121
- # Quick scaffold tuning job
122
- synth-ai agent run \\
123
- --algorithm scaffold_tuning \\
124
- --repo https://github.com/your-org/repo \\
125
- --backend daytona
126
-
127
- """
128
- from .config import OptimizationTool, ResearchConfig
129
- from .job import ResearchAgentJob, ResearchAgentJobConfig
130
-
131
- if not api_key:
132
- click.secho("Error: SYNTH_API_KEY is required", fg="red", err=True)
133
- sys.exit(1)
134
-
135
- # Build config
136
- if config_path:
137
- click.echo(f"Loading config from {config_path}...")
138
- config = ResearchAgentJobConfig.from_toml(config_path)
139
- # api_key is guaranteed to be str at this point (checked above)
140
- assert api_key is not None, "api_key should be set by this point"
141
- config.api_key = api_key
142
- config.backend_url = backend_url
143
-
144
- # Apply CLI overrides
145
- if algorithm:
146
- # Map algorithm string to optimization tool if needed
147
- # Note: The algorithm parameter is kept for backward compatibility
148
- # but the actual optimization is controlled by config.research.tools
149
- pass # Algorithm is embedded in the research config from TOML
150
- if repo_url:
151
- config.repo_url = repo_url
152
- if repo_branch != "main":
153
- config.repo_branch = repo_branch
154
- if backend != "daytona":
155
- config.backend = backend # type: ignore
156
- if model != "gpt-4o":
157
- config.model = model
158
- else:
159
- # Build from CLI options
160
- if not algorithm:
161
- click.secho("Error: --algorithm is required when not using --config", fg="red", err=True)
162
- sys.exit(1)
163
- if not repo_url:
164
- click.secho("Error: --repo is required when not using --config", fg="red", err=True)
165
- sys.exit(1)
166
-
167
- # Create a minimal ResearchConfig
168
- # The algorithm parameter maps to the optimization tool
169
- tools = [OptimizationTool.MIPRO] # Default to MIPRO for CLI usage
170
- research = ResearchConfig(
171
- task_description=f"Research job via CLI with {algorithm}",
172
- tools=tools,
173
- )
174
-
175
- config = ResearchAgentJobConfig(
176
- research=research,
177
- repo_url=repo_url, # type: ignore[arg-type]
178
- repo_branch=repo_branch,
179
- backend=backend, # type: ignore
180
- model=model,
181
- backend_url=backend_url,
182
- api_key=api_key, # type: ignore[arg-type]
183
- )
184
-
185
- # Create and submit job
186
- job = ResearchAgentJob(config=config)
187
-
188
- click.echo("Submitting research job...")
189
- click.echo(f" Repository: {config.repo_url}")
190
- click.echo(f" Branch: {config.repo_branch}")
191
- click.echo(f" Backend: {config.backend}")
192
- click.echo(f" Model: {config.model}")
193
- click.echo(f" Tools: {', '.join(t.value for t in config.research.tools)}")
194
-
195
- try:
196
- job_id = job.submit()
197
- click.secho(f"Job submitted: {job_id}", fg="green")
198
- except Exception as e:
199
- click.secho(f"Failed to submit job: {e}", fg="red", err=True)
200
- sys.exit(1)
201
-
202
- if not poll:
203
- click.echo(f"\nTo check status: synth-ai agent status {job_id}")
204
- return
205
-
206
- # Poll for completion
207
- click.echo("\nPolling for completion...")
208
- try:
209
- result = job.poll_until_complete(
210
- timeout=timeout,
211
- poll_interval=5.0,
212
- on_event=_print_event,
213
- )
214
- click.secho(f"\nJob completed: {result.get('status', 'unknown')}", fg="green")
215
-
216
- # Print summary
217
- if result.get("best_metric_value") is not None:
218
- click.echo(f"Best metric value: {result['best_metric_value']}")
219
- if result.get("current_iteration"):
220
- click.echo(f"Iterations: {result['current_iteration']}")
221
-
222
- except TimeoutError:
223
- click.secho(f"\nJob timed out after {timeout}s", fg="yellow", err=True)
224
- click.echo(f"Job ID: {job_id}")
225
- click.echo("Use 'synth-ai agent status' to check progress")
226
- sys.exit(1)
227
- except RuntimeError as e:
228
- click.secho(f"\nJob failed: {e}", fg="red", err=True)
229
- sys.exit(1)
230
-
231
-
232
- @agent_cmd.command("status")
233
- @click.argument("job_id")
234
- @click.option(
235
- "--api-key",
236
- envvar="SYNTH_API_KEY",
237
- help="Synth API key",
238
- )
239
- @click.option(
240
- "--backend-url",
241
- envvar="SYNTH_BACKEND_URL",
242
- default="https://api.usesynth.ai",
243
- help="Backend API URL",
244
- )
245
- def status_cmd(job_id: str, api_key: Optional[str], backend_url: str) -> None:
246
- """Get status of a research agent job."""
247
- from .job import ResearchAgentJob
248
-
249
- if not api_key:
250
- click.secho("Error: SYNTH_API_KEY is required", fg="red", err=True)
251
- sys.exit(1)
252
-
253
- job = ResearchAgentJob.from_id(job_id, backend_url=backend_url, api_key=api_key)
254
-
255
- try:
256
- status = job.get_status()
257
- click.echo(f"Job ID: {job_id}")
258
- click.echo(f"Status: {status.get('status', 'unknown')}")
259
- click.echo(f"Algorithm: {status.get('algorithm', 'unknown')}")
260
- click.echo(f"Backend: {status.get('backend', 'unknown')}")
261
-
262
- if status.get("current_iteration"):
263
- total = status.get("total_iterations", "?")
264
- click.echo(f"Progress: {status['current_iteration']}/{total}")
265
-
266
- if status.get("best_metric_value") is not None:
267
- click.echo(f"Best metric: {status['best_metric_value']}")
268
-
269
- if status.get("error"):
270
- click.secho(f"Error: {status['error']}", fg="red")
271
-
272
- except Exception as e:
273
- click.secho(f"Failed to get status: {e}", fg="red", err=True)
274
- sys.exit(1)
275
-
276
-
277
- @agent_cmd.command("events")
278
- @click.argument("job_id")
279
- @click.option(
280
- "--since",
281
- type=int,
282
- default=0,
283
- help="Show events after this sequence number",
284
- )
285
- @click.option(
286
- "--follow",
287
- "-f",
288
- is_flag=True,
289
- help="Follow events in real-time",
290
- )
291
- @click.option(
292
- "--api-key",
293
- envvar="SYNTH_API_KEY",
294
- help="Synth API key",
295
- )
296
- @click.option(
297
- "--backend-url",
298
- envvar="SYNTH_BACKEND_URL",
299
- default="https://api.usesynth.ai",
300
- help="Backend API URL",
301
- )
302
- def events_cmd(
303
- job_id: str,
304
- since: int,
305
- follow: bool,
306
- api_key: Optional[str],
307
- backend_url: str,
308
- ) -> None:
309
- """Get events from a research agent job."""
310
- import time
311
-
312
- from .job import ResearchAgentJob
313
-
314
- if not api_key:
315
- click.secho("Error: SYNTH_API_KEY is required", fg="red", err=True)
316
- sys.exit(1)
317
-
318
- job = ResearchAgentJob.from_id(job_id, backend_url=backend_url, api_key=api_key)
319
- last_seq = since
320
-
321
- try:
322
- while True:
323
- events = job.get_events(since_seq=last_seq)
324
- for event in events:
325
- _print_event(event)
326
- last_seq = max(last_seq, event.get("seq", 0))
327
-
328
- if not follow:
329
- break
330
-
331
- # Check if job is done
332
- status = job.get_status()
333
- if status.get("status") in ("succeeded", "failed", "canceled"):
334
- break
335
-
336
- time.sleep(2.0)
337
-
338
- except KeyboardInterrupt:
339
- click.echo("\nStopped following events")
340
- except Exception as e:
341
- click.secho(f"Failed to get events: {e}", fg="red", err=True)
342
- sys.exit(1)
343
-
344
-
345
- @agent_cmd.command("cancel")
346
- @click.argument("job_id")
347
- @click.option(
348
- "--api-key",
349
- envvar="SYNTH_API_KEY",
350
- help="Synth API key",
351
- )
352
- @click.option(
353
- "--backend-url",
354
- envvar="SYNTH_BACKEND_URL",
355
- default="https://api.usesynth.ai",
356
- help="Backend API URL",
357
- )
358
- def cancel_cmd(job_id: str, api_key: Optional[str], backend_url: str) -> None:
359
- """Cancel a running research agent job."""
360
- from .job import ResearchAgentJob
361
-
362
- if not api_key:
363
- click.secho("Error: SYNTH_API_KEY is required", fg="red", err=True)
364
- sys.exit(1)
365
-
366
- job = ResearchAgentJob.from_id(job_id, backend_url=backend_url, api_key=api_key)
367
-
368
- if job.cancel():
369
- click.secho(f"Cancellation requested for job {job_id}", fg="yellow")
370
- else:
371
- click.secho(f"Failed to cancel job {job_id}", fg="red", err=True)
372
- sys.exit(1)
373
-
374
-
375
- @agent_cmd.command("results")
376
- @click.argument("job_id")
377
- @click.option(
378
- "--output",
379
- "-o",
380
- type=click.Path(path_type=Path),
381
- help="Write results to file (JSON)",
382
- )
383
- @click.option(
384
- "--api-key",
385
- envvar="SYNTH_API_KEY",
386
- help="Synth API key",
387
- )
388
- @click.option(
389
- "--backend-url",
390
- envvar="SYNTH_BACKEND_URL",
391
- default="https://api.usesynth.ai",
392
- help="Backend API URL",
393
- )
394
- def results_cmd(
395
- job_id: str,
396
- output: Optional[Path],
397
- api_key: Optional[str],
398
- backend_url: str,
399
- ) -> None:
400
- """Get results from a completed research agent job."""
401
- import json
402
-
403
- from .job import ResearchAgentJob
404
-
405
- if not api_key:
406
- click.secho("Error: SYNTH_API_KEY is required", fg="red", err=True)
407
- sys.exit(1)
408
-
409
- job = ResearchAgentJob.from_id(job_id, backend_url=backend_url, api_key=api_key)
410
-
411
- try:
412
- results = job.get_results()
413
-
414
- if output:
415
- with open(output, "w") as f:
416
- json.dump(results, f, indent=2)
417
- click.echo(f"Results written to {output}")
418
- else:
419
- click.echo(json.dumps(results, indent=2))
420
-
421
- except Exception as e:
422
- click.secho(f"Failed to get results: {e}", fg="red", err=True)
423
- sys.exit(1)
424
-
425
-
426
- def register(cli: Any) -> None:
427
- """Register the agent command group with the main CLI."""
428
- cli.add_command(agent_cmd, name="agent")