plato-sdk-v2 2.1.14__py3-none-any.whl → 2.1.16__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.
plato/agents/runner.py CHANGED
@@ -1,252 +1,90 @@
1
- """Agent runner - discovers and runs Plato agents."""
1
+ """Agent runner - run agents in Docker containers."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import json
6
7
  import logging
8
+ import os
9
+ import tempfile
7
10
  from pathlib import Path
8
- from typing import Annotated
9
11
 
10
- import typer
11
-
12
- from plato.agents.callback import ChronosCallback
13
-
14
- app = typer.Typer(
15
- name="plato-agent-runner",
16
- help="Run Plato agents",
17
- no_args_is_help=True,
18
- )
12
+ from plato.agents.logging import log_event, span, upload_artifacts
19
13
 
20
14
  logger = logging.getLogger(__name__)
21
15
 
22
16
 
23
- def discover_agents() -> None:
24
- """Discover and load installed agent packages via entry points.
25
-
26
- Agent packages declare entry points in pyproject.toml:
27
- [project.entry-points."plato.agents"]
28
- openhands = "openhands_agent:OpenHandsAgent"
29
-
30
- This function loads all such entry points, triggering registration.
31
- """
32
- import importlib.metadata
33
-
34
- try:
35
- eps = importlib.metadata.entry_points(group="plato.agents")
36
- except TypeError:
37
- # Python < 3.10 compatibility
38
- eps = importlib.metadata.entry_points().get("plato.agents", [])
39
-
40
- for ep in eps:
41
- try:
42
- ep.load()
43
- logger.debug(f"Loaded agent: {ep.name}")
44
- except Exception as e:
45
- logger.warning(f"Failed to load agent '{ep.name}': {e}")
46
-
47
-
48
- @app.command()
49
- def run(
50
- agent: Annotated[str, typer.Option("--agent", "-a", help="Agent name to run")],
51
- instruction: Annotated[str, typer.Option("--instruction", "-i", help="Task instruction")],
52
- config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
53
- verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
17
+ async def run_agent(
18
+ image: str,
19
+ config: dict,
20
+ secrets: dict[str, str],
21
+ instruction: str,
22
+ workspace: str,
23
+ logs_dir: str | None = None,
24
+ pull: bool = True,
54
25
  ) -> None:
55
- """Run an agent with the given instruction."""
56
- # Setup logging
57
- log_level = logging.DEBUG if verbose else logging.INFO
58
- logging.basicConfig(
59
- level=log_level,
60
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
61
- )
62
-
63
- if not config.exists():
64
- typer.echo(f"Error: Config file not found: {config}", err=True)
65
- raise typer.Exit(1)
66
-
67
- # Discover agents
68
- discover_agents()
69
-
70
- from plato.agents.base import get_agent, get_registered_agents
71
-
72
- agent_cls = get_agent(agent)
73
- if agent_cls is None:
74
- available = list(get_registered_agents().keys())
75
- typer.echo(f"Error: Agent '{agent}' not found. Available: {available}", err=True)
76
- raise typer.Exit(1)
77
-
78
- # Load config using the agent's typed config class
79
- config_class = agent_cls.get_config_class()
80
- agent_config = config_class.from_file(config)
81
-
82
- try:
83
- agent_instance = agent_cls()
84
- agent_instance.config = agent_config
85
- asyncio.run(agent_instance.run(instruction))
86
- except Exception as e:
87
- logger.exception(f"Agent execution failed: {e}")
88
- raise typer.Exit(1)
89
-
90
-
91
- @app.command("list")
92
- def list_agents(
93
- verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
94
- ) -> None:
95
- """List available agents."""
96
- if verbose:
97
- logging.basicConfig(level=logging.DEBUG)
98
-
99
- discover_agents()
100
-
101
- from plato.agents.base import get_registered_agents
102
-
103
- agents = get_registered_agents()
104
- if not agents:
105
- typer.echo("No agents found.")
106
- return
107
-
108
- typer.echo("Available agents:")
109
- for name, cls in agents.items():
110
- desc = getattr(cls, "description", "") or ""
111
- version = cls.get_version()
112
- typer.echo(f" {name} (v{version}): {desc}")
113
-
114
-
115
- def main() -> None:
116
- """CLI entry point."""
117
- app()
118
-
119
-
120
- # =============================================================================
121
- # AgentRunner - Docker-based execution (existing functionality)
122
- # =============================================================================
123
-
124
-
125
- class AgentRunResult:
126
- """Result of running an agent in Docker.
127
-
128
- This class is an async iterator that yields output lines from the agent.
129
- It also provides access to the logs directory where agent logs are stored.
26
+ """Run an agent in a Docker container.
27
+
28
+ Args:
29
+ image: Docker image URI
30
+ config: Agent configuration dict
31
+ secrets: Secret values (API keys, etc.)
32
+ instruction: Task instruction for the agent
33
+ workspace: Host directory to mount as /workspace
34
+ logs_dir: Host directory for logs (temp dir if None)
35
+ pull: Whether to pull the image first
130
36
  """
37
+ logs_dir = logs_dir or tempfile.mkdtemp(prefix="agent_logs_")
38
+ agent_name = image.split("/")[-1].split(":")[0]
131
39
 
132
- def __init__(
133
- self,
134
- image: str,
135
- config: dict,
136
- secrets: dict[str, str],
137
- instruction: str,
138
- workspace: str,
139
- logs_dir: str | None,
140
- pull: bool,
141
- callback_url: str,
142
- session_id: str,
143
- ):
144
- self._image = image
145
- self._config = config
146
- self._secrets = secrets
147
- self._instruction = instruction
148
- self._workspace = workspace
149
- self._pull = pull
150
-
151
- self._callback = ChronosCallback(
152
- callback_url=callback_url,
153
- session_id=session_id,
154
- )
155
-
156
- if logs_dir is None:
157
- import tempfile
158
-
159
- self._logs_dir = tempfile.mkdtemp(prefix="agent_logs_")
160
- else:
161
- self._logs_dir = logs_dir
162
-
163
- @property
164
- def logs_dir(self) -> str:
165
- """Host path where agent logs are stored."""
166
- return self._logs_dir
167
-
168
- @property
169
- def callback(self) -> ChronosCallback:
170
- """The Chronos callback client."""
171
- return self._callback
40
+ async with span(agent_name, span_type="agent", source="agent") as agent_span:
41
+ agent_span.log(f"Starting agent: {agent_name} ({image})")
172
42
 
173
- def __aiter__(self):
174
- return self._stream()
175
-
176
- async def _check_iptables_support(self) -> bool:
177
- """Check if iptables is available."""
178
- try:
179
- proc = await asyncio.create_subprocess_exec(
180
- "iptables",
181
- "-L",
182
- "-n",
183
- stdout=asyncio.subprocess.DEVNULL,
184
- stderr=asyncio.subprocess.DEVNULL,
185
- )
186
- await proc.wait()
187
- return proc.returncode == 0
188
- except (FileNotFoundError, PermissionError):
189
- return False
190
-
191
- async def _stream(self):
192
- """Stream output from the agent."""
193
- import json
194
- import os
195
- import tempfile
196
-
197
- log_buffer: list[dict] = []
198
-
199
- async def flush_logs():
200
- if not log_buffer or not self._callback.enabled:
201
- return
202
- await self._callback.push_logs(log_buffer.copy())
203
- log_buffer.clear()
204
-
205
- agent_name = self._image.split("/")[-1].split(":")[0]
206
- await self._callback.push_log(f"Starting agent: {agent_name} ({self._image})")
207
-
208
- if self._pull:
43
+ # Pull image if requested
44
+ if pull:
45
+ agent_span.log(f"Pulling image: {image}")
209
46
  pull_proc = await asyncio.create_subprocess_exec(
210
47
  "docker",
211
48
  "pull",
212
- self._image,
49
+ image,
213
50
  stdout=asyncio.subprocess.PIPE,
214
51
  stderr=asyncio.subprocess.STDOUT,
215
52
  )
216
- assert pull_proc.stdout is not None
217
- while True:
218
- line = await pull_proc.stdout.readline()
219
- if not line:
220
- break
221
- yield f"[pull] {line.decode().rstrip()}"
222
53
  await pull_proc.wait()
223
54
 
224
- agent_logs_subdir = os.path.join(self._logs_dir, "agent")
225
- os.makedirs(agent_logs_subdir, exist_ok=True)
226
-
55
+ # Setup
56
+ os.makedirs(os.path.join(logs_dir, "agent"), exist_ok=True)
227
57
  config_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
228
- json.dump(self._config, config_file)
58
+ json.dump(config, config_file)
229
59
  config_file.close()
230
60
 
231
- agent_failed = False
232
- error_message = ""
233
-
234
61
  try:
235
- use_host_network = not await self._check_iptables_support()
236
- if use_host_network:
237
- yield "[info] iptables not available, using host network mode"
238
-
62
+ # Build docker command
239
63
  docker_cmd = ["docker", "run", "--rm"]
240
64
 
241
- if use_host_network:
65
+ # Check if iptables is available for network isolation
66
+ try:
67
+ proc = await asyncio.create_subprocess_exec(
68
+ "iptables",
69
+ "-L",
70
+ "-n",
71
+ stdout=asyncio.subprocess.DEVNULL,
72
+ stderr=asyncio.subprocess.DEVNULL,
73
+ )
74
+ await proc.wait()
75
+ has_iptables = proc.returncode == 0
76
+ except (FileNotFoundError, PermissionError):
77
+ has_iptables = False
78
+
79
+ if not has_iptables:
242
80
  docker_cmd.extend(["--network=host", "--add-host=localhost:127.0.0.1"])
243
81
 
244
82
  docker_cmd.extend(
245
83
  [
246
84
  "-v",
247
- f"{self._workspace}:/workspace",
85
+ f"{workspace}:/workspace",
248
86
  "-v",
249
- f"{self._logs_dir}:/logs",
87
+ f"{logs_dir}:/logs",
250
88
  "-v",
251
89
  f"{config_file.name}:/config.json:ro",
252
90
  "-w",
@@ -254,121 +92,59 @@ class AgentRunResult:
254
92
  ]
255
93
  )
256
94
 
257
- for key, value in self._secrets.items():
95
+ for key, value in secrets.items():
258
96
  docker_cmd.extend(["-e", f"{key.upper()}={value}"])
259
97
 
260
- docker_cmd.append(self._image)
261
- docker_cmd.extend(["--instruction", self._instruction])
98
+ docker_cmd.append(image)
99
+ docker_cmd.extend(["--instruction", instruction])
262
100
 
101
+ # Run container and stream output
263
102
  process = await asyncio.create_subprocess_exec(
264
103
  *docker_cmd,
265
104
  stdout=asyncio.subprocess.PIPE,
266
105
  stderr=asyncio.subprocess.STDOUT,
267
106
  )
268
- assert process.stdout is not None
269
107
 
108
+ # Stream output line by line
109
+ assert process.stdout is not None
270
110
  while True:
271
111
  line = await process.stdout.readline()
272
112
  if not line:
273
113
  break
274
- decoded = line.decode().rstrip()
275
- yield decoded
276
-
277
- if self._callback.enabled:
278
- log_buffer.append({"level": "info", "message": decoded})
279
- if len(log_buffer) >= 10:
280
- await flush_logs()
114
+ logger.info(f"[agent] {line.decode().rstrip()}")
281
115
 
282
116
  await process.wait()
283
117
 
284
118
  if process.returncode != 0:
285
- agent_failed = True
286
- error_message = f"Agent failed with exit code {process.returncode}"
287
- except Exception as e:
288
- agent_failed = True
289
- error_message = str(e)
290
- raise
291
- finally:
292
- os.unlink(config_file.name)
293
- await flush_logs()
294
-
295
- if self._callback.enabled:
296
- logger.info(f"Callback enabled, uploading artifacts from {self._logs_dir}")
297
- if agent_failed:
298
- await self._callback.push_log(error_message, level="error")
299
- else:
300
- await self._callback.push_log("Agent completed successfully")
301
- result = await self._callback.upload_artifacts(
302
- logs_dir=self._logs_dir,
303
- agent_image=self._image,
304
- )
305
- logger.info(f"Artifact upload result: {result}")
306
- else:
307
- logger.info(
308
- f"Callback not enabled (url={self._callback.callback_url}, session={self._callback.session_id})"
309
- )
310
-
311
- if agent_failed:
312
- raise RuntimeError(error_message)
313
-
119
+ raise RuntimeError(f"Agent failed with exit code {process.returncode}")
314
120
 
315
- class AgentRunner:
316
- """Run agents in Docker containers.
317
-
318
- This provides isolated execution of agents. For direct execution,
319
- use the plato-agent-runner CLI or instantiate the agent directly.
320
-
321
- Example:
322
- async for line in AgentRunner.run(
323
- image="us-docker.pkg.dev/plato-prod/agents/openhands:latest",
324
- config={"model_name": "anthropic/claude-sonnet-4"},
325
- secrets={"anthropic_api_key": "sk-..."},
326
- instruction="Fix the bug",
327
- workspace="/path/to/repo",
328
- ):
329
- print(line)
330
- """
331
-
332
- @staticmethod
333
- def run(
334
- image: str,
335
- config: dict,
336
- secrets: dict[str, str],
337
- instruction: str,
338
- workspace: str,
339
- logs_dir: str | None = None,
340
- pull: bool = True,
341
- callback_url: str = "",
342
- session_id: str = "",
343
- ) -> AgentRunResult:
344
- """Run an agent in a Docker container.
345
-
346
- Args:
347
- image: Docker image URI
348
- config: Agent configuration dict
349
- secrets: Secret values (API keys, etc.)
350
- instruction: Task instruction for the agent
351
- workspace: Host directory to mount as /workspace
352
- logs_dir: Host directory for logs (temp dir if None)
353
- pull: Whether to pull the image first
354
- callback_url: Chronos callback URL
355
- session_id: Chronos session ID
356
-
357
- Returns:
358
- AgentRunResult that can be async-iterated for output.
359
- """
360
- return AgentRunResult(
361
- image=image,
362
- config=config,
363
- secrets=secrets,
364
- instruction=instruction,
365
- workspace=workspace,
366
- logs_dir=logs_dir,
367
- pull=pull,
368
- callback_url=callback_url,
369
- session_id=session_id,
370
- )
121
+ agent_span.log("Agent completed successfully")
371
122
 
123
+ finally:
124
+ os.unlink(config_file.name)
372
125
 
373
- if __name__ == "__main__":
374
- main()
126
+ # Load trajectory and add to span
127
+ trajectory_path = Path(logs_dir) / "agent" / "trajectory.json"
128
+ if trajectory_path.exists():
129
+ try:
130
+ with open(trajectory_path) as f:
131
+ trajectory = json.load(f)
132
+ if isinstance(trajectory, dict) and "schema_version" in trajectory:
133
+ # Add agent image to trajectory
134
+ agent_data = trajectory.get("agent", {})
135
+ extra = agent_data.get("extra") or {}
136
+ extra["image"] = image
137
+ agent_data["extra"] = extra
138
+ trajectory["agent"] = agent_data
139
+
140
+ # Log trajectory as separate event
141
+ await log_event(
142
+ span_type="trajectory",
143
+ log_type="atif",
144
+ extra=trajectory,
145
+ source="agent",
146
+ )
147
+ except Exception as e:
148
+ logger.warning(f"Failed to load trajectory: {e}")
149
+
150
+ await upload_artifacts(logs_dir)