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/__init__.py +15 -6
- plato/agents/logging.py +401 -0
- plato/agents/runner.py +88 -312
- plato/worlds/base.py +43 -174
- plato/worlds/config.py +1 -1
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/RECORD +9 -9
- plato/agents/callback.py +0 -332
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/entry_points.txt +0 -0
plato/agents/runner.py
CHANGED
|
@@ -1,252 +1,90 @@
|
|
|
1
|
-
"""Agent runner -
|
|
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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
os.makedirs(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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"{
|
|
85
|
+
f"{workspace}:/workspace",
|
|
248
86
|
"-v",
|
|
249
|
-
f"{
|
|
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
|
|
95
|
+
for key, value in secrets.items():
|
|
258
96
|
docker_cmd.extend(["-e", f"{key.upper()}={value}"])
|
|
259
97
|
|
|
260
|
-
docker_cmd.append(
|
|
261
|
-
docker_cmd.extend(["--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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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)
|