plato-sdk-v2 2.0.64__py3-none-any.whl → 2.3.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.
- plato/__init__.py +0 -9
- plato/_sims_generator/__init__.py +19 -4
- plato/_sims_generator/instruction.py +203 -0
- plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
- plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
- plato/agents/__init__.py +99 -430
- plato/agents/base.py +145 -0
- plato/agents/build.py +61 -0
- plato/agents/config.py +160 -0
- plato/agents/logging.py +515 -0
- plato/agents/runner.py +191 -0
- plato/agents/trajectory.py +266 -0
- plato/chronos/models/__init__.py +1 -1
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +88 -84
- plato/v1/cli/pm.py +84 -44
- plato/v1/cli/sandbox.py +241 -61
- plato/v1/cli/ssh.py +16 -4
- plato/v1/cli/verify.py +685 -0
- plato/v1/cli/world.py +3 -0
- plato/v1/flow_executor.py +21 -17
- plato/v1/models/env.py +11 -11
- plato/v1/sdk.py +2 -2
- plato/v1/sync_env.py +11 -11
- plato/v1/sync_flow_executor.py +21 -17
- plato/v1/sync_sdk.py +4 -2
- plato/v2/__init__.py +2 -0
- plato/v2/async_/environment.py +31 -0
- plato/v2/async_/session.py +72 -4
- plato/v2/sync/environment.py +31 -0
- plato/v2/sync/session.py +72 -4
- plato/worlds/README.md +71 -56
- plato/worlds/__init__.py +56 -18
- plato/worlds/base.py +578 -93
- plato/worlds/config.py +276 -74
- plato/worlds/runner.py +475 -80
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/METADATA +3 -3
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/RECORD +41 -36
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/entry_points.txt +1 -0
- plato/agents/callback.py +0 -246
- plato/world/__init__.py +0 -44
- plato/world/base.py +0 -267
- plato/world/config.py +0 -139
- plato/world/types.py +0 -47
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/WHEEL +0 -0
plato/worlds/runner.py
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import argparse
|
|
6
5
|
import asyncio
|
|
6
|
+
import json
|
|
7
7
|
import logging
|
|
8
|
-
import
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
11
|
+
from typing import Annotated
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from plato.worlds.config import EnvConfig, RunConfig
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="plato-world-runner",
|
|
19
|
+
help="Run Plato worlds",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
14
22
|
|
|
15
23
|
logger = logging.getLogger(__name__)
|
|
16
24
|
|
|
@@ -45,12 +53,11 @@ async def run_world(world_name: str, config: RunConfig) -> None:
|
|
|
45
53
|
|
|
46
54
|
Args:
|
|
47
55
|
world_name: Name of the world to run
|
|
48
|
-
config: Run configuration
|
|
56
|
+
config: Run configuration (should be the world's typed config class)
|
|
49
57
|
|
|
50
58
|
Raises:
|
|
51
59
|
ValueError: If world not found
|
|
52
60
|
"""
|
|
53
|
-
# Discover installed world packages
|
|
54
61
|
discover_worlds()
|
|
55
62
|
|
|
56
63
|
from plato.worlds.base import get_registered_worlds, get_world
|
|
@@ -64,105 +71,493 @@ async def run_world(world_name: str, config: RunConfig) -> None:
|
|
|
64
71
|
await world.run(config)
|
|
65
72
|
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"""
|
|
74
|
-
parser = argparse.ArgumentParser(
|
|
75
|
-
prog="plato-world-runner",
|
|
76
|
-
description="Run Plato worlds",
|
|
77
|
-
)
|
|
78
|
-
parser.add_argument(
|
|
79
|
-
"--world",
|
|
80
|
-
"-w",
|
|
81
|
-
help="World name to run",
|
|
82
|
-
)
|
|
83
|
-
parser.add_argument(
|
|
84
|
-
"--config",
|
|
85
|
-
"-c",
|
|
86
|
-
help="Path to config JSON file",
|
|
87
|
-
)
|
|
88
|
-
parser.add_argument(
|
|
89
|
-
"--list",
|
|
90
|
-
"-l",
|
|
91
|
-
action="store_true",
|
|
92
|
-
help="List available worlds",
|
|
93
|
-
)
|
|
94
|
-
parser.add_argument(
|
|
95
|
-
"--verbose",
|
|
96
|
-
"-v",
|
|
97
|
-
action="store_true",
|
|
98
|
-
help="Enable verbose logging",
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
args = parser.parse_args()
|
|
102
|
-
|
|
74
|
+
@app.command()
|
|
75
|
+
def run(
|
|
76
|
+
world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
|
|
77
|
+
config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
|
|
78
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Run a world with the given configuration."""
|
|
103
81
|
# Setup logging
|
|
104
|
-
log_level = logging.DEBUG if
|
|
82
|
+
log_level = logging.DEBUG if verbose else logging.INFO
|
|
105
83
|
logging.basicConfig(
|
|
106
84
|
level=log_level,
|
|
107
85
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
108
86
|
)
|
|
109
87
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
discover_worlds()
|
|
88
|
+
if not config.exists():
|
|
89
|
+
typer.echo(f"Error: Config file not found: {config}", err=True)
|
|
90
|
+
raise typer.Exit(1)
|
|
114
91
|
|
|
115
|
-
|
|
92
|
+
# Discover worlds first to get config class
|
|
93
|
+
discover_worlds()
|
|
116
94
|
|
|
117
|
-
|
|
118
|
-
if not worlds:
|
|
119
|
-
print("No worlds found.")
|
|
120
|
-
else:
|
|
121
|
-
print("Available worlds:")
|
|
122
|
-
for name, cls in worlds.items():
|
|
123
|
-
desc = getattr(cls, "description", "") or ""
|
|
124
|
-
version = cls.get_version()
|
|
125
|
-
print(f" {name} (v{version}): {desc}")
|
|
126
|
-
return
|
|
95
|
+
from plato.worlds.base import get_registered_worlds, get_world
|
|
127
96
|
|
|
128
|
-
|
|
129
|
-
if
|
|
130
|
-
|
|
97
|
+
world_cls = get_world(world)
|
|
98
|
+
if world_cls is None:
|
|
99
|
+
available = list(get_registered_worlds().keys())
|
|
100
|
+
typer.echo(f"Error: World '{world}' not found. Available: {available}", err=True)
|
|
101
|
+
raise typer.Exit(1)
|
|
131
102
|
|
|
132
|
-
|
|
133
|
-
|
|
103
|
+
# Load config using the world's typed config class
|
|
104
|
+
config_class = world_cls.get_config_class()
|
|
105
|
+
run_config = config_class.from_file(config)
|
|
134
106
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
try:
|
|
108
|
+
world_instance = world_cls()
|
|
109
|
+
asyncio.run(world_instance.run(run_config))
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.exception(f"World execution failed: {e}")
|
|
112
|
+
raise typer.Exit(1)
|
|
139
113
|
|
|
140
|
-
# Import here to avoid circular imports
|
|
141
|
-
from plato.worlds.config import RunConfig
|
|
142
114
|
|
|
143
|
-
|
|
115
|
+
@app.command("list")
|
|
116
|
+
def list_worlds(
|
|
117
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""List available worlds."""
|
|
120
|
+
if verbose:
|
|
121
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
122
|
+
|
|
144
123
|
discover_worlds()
|
|
145
124
|
|
|
125
|
+
from plato.worlds.base import get_registered_worlds
|
|
126
|
+
|
|
127
|
+
worlds = get_registered_worlds()
|
|
128
|
+
if not worlds:
|
|
129
|
+
typer.echo("No worlds found.")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
typer.echo("Available worlds:")
|
|
133
|
+
for name, cls in worlds.items():
|
|
134
|
+
desc = getattr(cls, "description", "") or ""
|
|
135
|
+
version = cls.get_version()
|
|
136
|
+
typer.echo(f" {name} (v{version}): {desc}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _build_agent_image(
|
|
140
|
+
agent_name: str,
|
|
141
|
+
agents_dir: Path,
|
|
142
|
+
plato_client_root: Path | None = None,
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Build a local agent Docker image.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
agent_name: Name of the agent (e.g., "openhands")
|
|
148
|
+
agents_dir: Directory containing agent subdirectories
|
|
149
|
+
plato_client_root: Root of plato-client repo (for dev builds), or None for prod builds
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if build succeeded, False otherwise
|
|
153
|
+
"""
|
|
154
|
+
import subprocess
|
|
155
|
+
|
|
156
|
+
# Resolve paths to absolute
|
|
157
|
+
agents_dir = agents_dir.expanduser().resolve()
|
|
158
|
+
agent_path = agents_dir / agent_name
|
|
159
|
+
dockerfile_path = agent_path / "Dockerfile"
|
|
160
|
+
|
|
161
|
+
if not dockerfile_path.exists():
|
|
162
|
+
logger.warning(f"No Dockerfile found for agent '{agent_name}' at {dockerfile_path}")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
image_tag = f"{agent_name}:latest"
|
|
166
|
+
|
|
167
|
+
# Determine build context and target
|
|
168
|
+
if plato_client_root:
|
|
169
|
+
plato_client_root = plato_client_root.expanduser().resolve()
|
|
170
|
+
|
|
171
|
+
if plato_client_root and plato_client_root.exists():
|
|
172
|
+
# Dev build from plato-client root (includes local python-sdk)
|
|
173
|
+
build_context = str(plato_client_root)
|
|
174
|
+
dockerfile_abs = str(dockerfile_path)
|
|
175
|
+
target = "dev"
|
|
176
|
+
logger.info(f"Building {image_tag} (dev mode from {build_context})...")
|
|
177
|
+
else:
|
|
178
|
+
# Prod build from agent directory
|
|
179
|
+
build_context = str(agent_path)
|
|
180
|
+
dockerfile_abs = str(dockerfile_path)
|
|
181
|
+
target = "prod"
|
|
182
|
+
logger.info(f"Building {image_tag} (prod mode from {build_context})...")
|
|
183
|
+
|
|
184
|
+
cmd = [
|
|
185
|
+
"docker",
|
|
186
|
+
"build",
|
|
187
|
+
"--target",
|
|
188
|
+
target,
|
|
189
|
+
"-t",
|
|
190
|
+
image_tag,
|
|
191
|
+
"-f",
|
|
192
|
+
dockerfile_abs,
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
# Use native platform for local dev on ARM Macs (avoids slow emulation)
|
|
196
|
+
if platform.machine() == "arm64":
|
|
197
|
+
cmd.extend(["--build-arg", "PLATFORM=linux/arm64"])
|
|
198
|
+
|
|
199
|
+
cmd.append(build_context)
|
|
200
|
+
|
|
201
|
+
logger.debug(f"Build command: {' '.join(cmd)}")
|
|
202
|
+
|
|
203
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
204
|
+
|
|
205
|
+
if result.returncode != 0:
|
|
206
|
+
logger.error(f"Failed to build {image_tag}:\n{result.stderr}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
logger.info(f"Successfully built {image_tag}")
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _extract_agent_images_from_config(config_data: dict) -> list[str]:
|
|
214
|
+
"""Extract agent image names from config data.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
config_data: Raw config dictionary
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of image names (without tags) that are local (not from a registry)
|
|
221
|
+
"""
|
|
222
|
+
images = []
|
|
223
|
+
|
|
224
|
+
# Check agents section
|
|
225
|
+
agents = config_data.get("agents", {})
|
|
226
|
+
for agent_config in agents.values():
|
|
227
|
+
if isinstance(agent_config, dict):
|
|
228
|
+
image = agent_config.get("image", "")
|
|
229
|
+
# Only include local images (no registry prefix like ghcr.io/)
|
|
230
|
+
if image and "/" not in image.split(":")[0]:
|
|
231
|
+
# Extract name without tag
|
|
232
|
+
name = image.split(":")[0]
|
|
233
|
+
if name not in images:
|
|
234
|
+
images.append(name)
|
|
235
|
+
|
|
236
|
+
# Also check direct coder/verifier fields
|
|
237
|
+
for field in ["coder", "verifier"]:
|
|
238
|
+
agent_config = config_data.get(field, {})
|
|
239
|
+
if isinstance(agent_config, dict):
|
|
240
|
+
image = agent_config.get("image", "")
|
|
241
|
+
if image and "/" not in image.split(":")[0]:
|
|
242
|
+
name = image.split(":")[0]
|
|
243
|
+
if name not in images:
|
|
244
|
+
images.append(name)
|
|
245
|
+
|
|
246
|
+
return images
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
async def _create_chronos_session(
|
|
250
|
+
chronos_url: str,
|
|
251
|
+
api_key: str,
|
|
252
|
+
world_name: str,
|
|
253
|
+
world_config: dict,
|
|
254
|
+
plato_session_id: str | None = None,
|
|
255
|
+
) -> tuple[str, str]:
|
|
256
|
+
"""Create a session in Chronos.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
chronos_url: Chronos base URL (e.g., https://chronos.plato.so)
|
|
260
|
+
api_key: Plato API key for authentication
|
|
261
|
+
world_name: Name of the world being run
|
|
262
|
+
world_config: World configuration dict
|
|
263
|
+
plato_session_id: Optional Plato session ID if already created
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Tuple of (session_id, callback_url)
|
|
267
|
+
"""
|
|
268
|
+
import httpx
|
|
269
|
+
|
|
270
|
+
url = f"{chronos_url.rstrip('/')}/api/sessions"
|
|
271
|
+
|
|
272
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
273
|
+
response = await client.post(
|
|
274
|
+
url,
|
|
275
|
+
json={
|
|
276
|
+
"world_name": world_name,
|
|
277
|
+
"world_config": world_config,
|
|
278
|
+
"plato_session_id": plato_session_id,
|
|
279
|
+
},
|
|
280
|
+
headers={"x-api-key": api_key},
|
|
281
|
+
)
|
|
282
|
+
response.raise_for_status()
|
|
283
|
+
data = response.json()
|
|
284
|
+
|
|
285
|
+
return data["public_id"], data["callback_url"]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def _close_chronos_session(
|
|
289
|
+
chronos_url: str,
|
|
290
|
+
api_key: str,
|
|
291
|
+
session_id: str,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Close a Chronos session.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
chronos_url: Chronos base URL
|
|
297
|
+
api_key: Plato API key for authentication
|
|
298
|
+
session_id: Chronos session public ID to close
|
|
299
|
+
"""
|
|
300
|
+
import httpx
|
|
301
|
+
|
|
302
|
+
url = f"{chronos_url.rstrip('/')}/api/sessions/{session_id}/close"
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
306
|
+
response = await client.post(
|
|
307
|
+
url,
|
|
308
|
+
headers={"x-api-key": api_key},
|
|
309
|
+
)
|
|
310
|
+
response.raise_for_status()
|
|
311
|
+
logger.info(f"Closed Chronos session: {session_id}")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning(f"Failed to close Chronos session: {e}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def _run_dev(
|
|
317
|
+
world_name: str,
|
|
318
|
+
config_path: Path,
|
|
319
|
+
env_timeout: int = 7200,
|
|
320
|
+
agents_dir: Path | None = None,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Run a world locally with automatic environment creation.
|
|
323
|
+
|
|
324
|
+
This mimics what Chronos does but runs locally for debugging:
|
|
325
|
+
1. Load and parse the config
|
|
326
|
+
2. Build local agent images if --agents-dir is provided
|
|
327
|
+
3. Create Plato session with all environments
|
|
328
|
+
4. Create Chronos session for logging/callbacks
|
|
329
|
+
5. Run the world with the session attached
|
|
330
|
+
|
|
331
|
+
Requires environment variables:
|
|
332
|
+
CHRONOS_URL: Chronos base URL (e.g., https://chronos.plato.so)
|
|
333
|
+
PLATO_API_KEY: API key for Plato and Chronos authentication
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
world_name: Name of the world to run
|
|
337
|
+
config_path: Path to the config JSON file
|
|
338
|
+
env_timeout: Timeout for environment creation (seconds)
|
|
339
|
+
agents_dir: Optional directory containing agent source code
|
|
340
|
+
"""
|
|
341
|
+
from plato.v2 import AsyncPlato
|
|
146
342
|
from plato.worlds.base import get_world
|
|
147
343
|
|
|
148
|
-
|
|
344
|
+
# Get required env vars
|
|
345
|
+
chronos_url = os.environ.get("CHRONOS_URL")
|
|
346
|
+
api_key = os.environ.get("PLATO_API_KEY")
|
|
347
|
+
|
|
348
|
+
if not chronos_url:
|
|
349
|
+
raise ValueError("CHRONOS_URL environment variable is required")
|
|
350
|
+
if not api_key:
|
|
351
|
+
raise ValueError("PLATO_API_KEY environment variable is required")
|
|
352
|
+
|
|
353
|
+
discover_worlds()
|
|
354
|
+
|
|
355
|
+
world_cls = get_world(world_name)
|
|
149
356
|
if world_cls is None:
|
|
150
357
|
from plato.worlds.base import get_registered_worlds
|
|
151
358
|
|
|
152
359
|
available = list(get_registered_worlds().keys())
|
|
153
|
-
|
|
154
|
-
|
|
360
|
+
raise ValueError(f"World '{world_name}' not found. Available: {available}")
|
|
361
|
+
|
|
362
|
+
# Load config
|
|
363
|
+
config_class = world_cls.get_config_class()
|
|
364
|
+
with open(config_path) as f:
|
|
365
|
+
config_data = json.load(f)
|
|
366
|
+
|
|
367
|
+
# Parse the config to get typed access
|
|
368
|
+
run_config = config_class._from_dict(config_data.copy())
|
|
369
|
+
|
|
370
|
+
# Build local agent images if agents_dir is provided
|
|
371
|
+
if agents_dir:
|
|
372
|
+
# Resolve agents_dir to absolute path
|
|
373
|
+
agents_dir = agents_dir.expanduser().resolve()
|
|
374
|
+
agent_images = _extract_agent_images_from_config(config_data)
|
|
375
|
+
if agent_images:
|
|
376
|
+
logger.info(f"Building local agent images: {agent_images}")
|
|
377
|
+
# Determine if we're in a plato-client repo for dev builds
|
|
378
|
+
# (agents_dir is something like /path/to/plato-client/agents)
|
|
379
|
+
plato_client_root = agents_dir.parent if agents_dir.name == "agents" else None
|
|
380
|
+
for agent_name in agent_images:
|
|
381
|
+
success = await _build_agent_image(agent_name, agents_dir, plato_client_root)
|
|
382
|
+
if not success:
|
|
383
|
+
raise RuntimeError(f"Failed to build agent image: {agent_name}")
|
|
384
|
+
else:
|
|
385
|
+
logger.info("No local agent images found in config")
|
|
386
|
+
|
|
387
|
+
# Get environment configs from the parsed config
|
|
388
|
+
env_configs: list[EnvConfig] = run_config.get_envs()
|
|
155
389
|
|
|
156
|
-
#
|
|
157
|
-
|
|
390
|
+
# Create Plato client
|
|
391
|
+
plato = AsyncPlato()
|
|
392
|
+
session = None
|
|
393
|
+
plato_session_id: str | None = None
|
|
158
394
|
|
|
159
395
|
try:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
396
|
+
if env_configs:
|
|
397
|
+
logger.info(f"Creating {len(env_configs)} environments...")
|
|
398
|
+
session = await plato.sessions.create(envs=env_configs, timeout=env_timeout)
|
|
399
|
+
plato_session_id = session.session_id
|
|
400
|
+
logger.info(f"Created Plato session: {plato_session_id}")
|
|
401
|
+
logger.info(f"Environments: {[e.alias for e in session.envs]}")
|
|
402
|
+
|
|
403
|
+
# Serialize and add to config
|
|
404
|
+
serialized = session.dump()
|
|
405
|
+
run_config.plato_session = serialized
|
|
406
|
+
else:
|
|
407
|
+
logger.info("No environments defined for this world")
|
|
408
|
+
|
|
409
|
+
# Create Chronos session (after Plato session so we can link them)
|
|
410
|
+
logger.info(f"Creating Chronos session at {chronos_url}...")
|
|
411
|
+
chronos_session_id, callback_url = await _create_chronos_session(
|
|
412
|
+
chronos_url=chronos_url,
|
|
413
|
+
api_key=api_key,
|
|
414
|
+
world_name=world_name,
|
|
415
|
+
world_config=config_data,
|
|
416
|
+
plato_session_id=plato_session_id,
|
|
417
|
+
)
|
|
418
|
+
logger.info(f"Created Chronos session: {chronos_session_id}")
|
|
419
|
+
logger.info(f"View at: {chronos_url}/sessions/{chronos_session_id}")
|
|
420
|
+
|
|
421
|
+
# Initialize logging
|
|
422
|
+
from plato.agents import init_logging
|
|
423
|
+
|
|
424
|
+
init_logging(
|
|
425
|
+
callback_url=callback_url,
|
|
426
|
+
session_id=chronos_session_id,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Update run_config with session info for agents
|
|
430
|
+
run_config.session_id = chronos_session_id
|
|
431
|
+
run_config.callback_url = callback_url
|
|
432
|
+
|
|
433
|
+
# Run the world
|
|
434
|
+
logger.info(f"Starting world '{world_name}'...")
|
|
435
|
+
world_instance = world_cls()
|
|
436
|
+
await world_instance.run(run_config)
|
|
437
|
+
|
|
438
|
+
finally:
|
|
439
|
+
# Cleanup
|
|
440
|
+
if session:
|
|
441
|
+
logger.info("Closing Plato session...")
|
|
442
|
+
await session.close()
|
|
443
|
+
await plato.close()
|
|
444
|
+
|
|
445
|
+
# Close Chronos session
|
|
446
|
+
if chronos_session_id:
|
|
447
|
+
await _close_chronos_session(chronos_url, api_key, chronos_session_id)
|
|
448
|
+
|
|
449
|
+
# Reset logging
|
|
450
|
+
from plato.agents import reset_logging
|
|
451
|
+
|
|
452
|
+
reset_logging()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _setup_colored_logging(verbose: bool = False) -> None:
|
|
456
|
+
"""Setup colored logging with filtered noisy loggers."""
|
|
457
|
+
log_level = logging.DEBUG if verbose else logging.INFO
|
|
458
|
+
|
|
459
|
+
# Define colors for different log levels
|
|
460
|
+
colors = {
|
|
461
|
+
"DEBUG": "\033[36m", # Cyan
|
|
462
|
+
"INFO": "\033[32m", # Green
|
|
463
|
+
"WARNING": "\033[33m", # Yellow
|
|
464
|
+
"ERROR": "\033[31m", # Red
|
|
465
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
466
|
+
}
|
|
467
|
+
reset = "\033[0m"
|
|
468
|
+
|
|
469
|
+
class ColoredFormatter(logging.Formatter):
|
|
470
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
471
|
+
color = colors.get(record.levelname, "")
|
|
472
|
+
record.levelname = f"{color}{record.levelname}{reset}"
|
|
473
|
+
record.name = f"\033[34m{record.name}{reset}" # Blue for logger name
|
|
474
|
+
return super().format(record)
|
|
475
|
+
|
|
476
|
+
# Create handler with colored formatter
|
|
477
|
+
handler = logging.StreamHandler()
|
|
478
|
+
handler.setFormatter(
|
|
479
|
+
ColoredFormatter(
|
|
480
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
481
|
+
datefmt="%H:%M:%S",
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Configure root logger
|
|
486
|
+
root_logger = logging.getLogger()
|
|
487
|
+
root_logger.setLevel(log_level)
|
|
488
|
+
root_logger.handlers = [handler]
|
|
489
|
+
|
|
490
|
+
# Silence noisy HTTP loggers
|
|
491
|
+
for noisy_logger in ["httpcore", "httpx", "urllib3", "hpack"]:
|
|
492
|
+
logging.getLogger(noisy_logger).setLevel(logging.WARNING)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@app.command("dev")
|
|
496
|
+
def dev(
|
|
497
|
+
world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
|
|
498
|
+
config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
|
|
499
|
+
env_timeout: Annotated[
|
|
500
|
+
int, typer.Option("--env-timeout", help="Timeout for environment creation (seconds)")
|
|
501
|
+
] = 7200,
|
|
502
|
+
agents_dir: Annotated[
|
|
503
|
+
Path | None,
|
|
504
|
+
typer.Option("--agents-dir", "-a", help="Directory containing agent source code (builds local images)"),
|
|
505
|
+
] = None,
|
|
506
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
507
|
+
) -> None:
|
|
508
|
+
"""Run a world locally for development/debugging.
|
|
509
|
+
|
|
510
|
+
This creates Plato environments automatically (like Chronos does),
|
|
511
|
+
creates a Chronos session for logging, and runs the world.
|
|
512
|
+
|
|
513
|
+
Example config.json:
|
|
514
|
+
{
|
|
515
|
+
"instruction": "Create a git repo and upload files to S3",
|
|
516
|
+
"coder": {
|
|
517
|
+
"image": "openhands:latest",
|
|
518
|
+
"config": {"model_name": "gemini/gemini-3-flash-preview"}
|
|
519
|
+
},
|
|
520
|
+
"secrets": {
|
|
521
|
+
"gemini_api_key": "..."
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
Required environment variables:
|
|
526
|
+
CHRONOS_URL: Chronos base URL (e.g., https://chronos.plato.so)
|
|
527
|
+
PLATO_API_KEY: API key for Plato and Chronos authentication
|
|
528
|
+
|
|
529
|
+
Examples:
|
|
530
|
+
# Basic usage
|
|
531
|
+
CHRONOS_URL=https://chronos.plato.so plato-world-runner dev -w code -c config.json
|
|
532
|
+
|
|
533
|
+
# With local agent builds (from plato-client repo)
|
|
534
|
+
plato-world-runner dev -w code -c config.json --agents-dir ~/plato-client/agents
|
|
535
|
+
"""
|
|
536
|
+
# Setup colored logging with filtered noisy loggers
|
|
537
|
+
_setup_colored_logging(verbose)
|
|
538
|
+
|
|
539
|
+
if not config.exists():
|
|
540
|
+
typer.echo(f"Error: Config file not found: {config}", err=True)
|
|
541
|
+
raise typer.Exit(1)
|
|
542
|
+
|
|
543
|
+
if not os.environ.get("CHRONOS_URL"):
|
|
544
|
+
typer.echo("Error: CHRONOS_URL environment variable required", err=True)
|
|
545
|
+
raise typer.Exit(1)
|
|
546
|
+
|
|
547
|
+
if not os.environ.get("PLATO_API_KEY"):
|
|
548
|
+
typer.echo("Error: PLATO_API_KEY environment variable required", err=True)
|
|
549
|
+
raise typer.Exit(1)
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
asyncio.run(_run_dev(world, config, env_timeout, agents_dir))
|
|
163
553
|
except Exception as e:
|
|
164
554
|
logger.exception(f"World execution failed: {e}")
|
|
165
|
-
|
|
555
|
+
raise typer.Exit(1)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def main() -> None:
|
|
559
|
+
"""CLI entry point."""
|
|
560
|
+
app()
|
|
166
561
|
|
|
167
562
|
|
|
168
563
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plato-sdk-v2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.4
|
|
4
4
|
Summary: Python SDK for the Plato API
|
|
5
5
|
Author-email: Plato <support@plato.so>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,8 +30,6 @@ Requires-Dist: rich>=13.0.0
|
|
|
30
30
|
Requires-Dist: tenacity>=9.1.2
|
|
31
31
|
Requires-Dist: tomli>=2.0.0
|
|
32
32
|
Requires-Dist: typer>=0.9.0
|
|
33
|
-
Provides-Extra: agents
|
|
34
|
-
Requires-Dist: harbor>=0.1.35; (python_version >= '3.12') and extra == 'agents'
|
|
35
33
|
Provides-Extra: db-cleanup
|
|
36
34
|
Requires-Dist: aiomysql>=0.2; extra == 'db-cleanup'
|
|
37
35
|
Requires-Dist: aiosqlite>=0.20; extra == 'db-cleanup'
|
|
@@ -46,6 +44,8 @@ Description-Content-Type: text/markdown
|
|
|
46
44
|
|
|
47
45
|
# Plato Python SDK
|
|
48
46
|
|
|
47
|
+
|
|
48
|
+
|
|
49
49
|
Python SDK for the Plato platform. Uses [Harbor](https://harborframework.com) for agent execution.
|
|
50
50
|
|
|
51
51
|
## Installation
|