plato-sdk-v2 2.2.2__py3-none-any.whl → 2.2.3__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
@@ -6,6 +6,7 @@ import asyncio
6
6
  import json
7
7
  import logging
8
8
  import os
9
+ import platform
9
10
  import tempfile
10
11
  from pathlib import Path
11
12
 
@@ -62,21 +63,29 @@ async def run_agent(
62
63
  # Build docker command
63
64
  docker_cmd = ["docker", "run", "--rm"]
64
65
 
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:
66
+ # Determine if we need host networking:
67
+ # - Required on Linux without iptables for connectivity
68
+ # - Skip on macOS where --network=host doesn't work properly
69
+ use_host_network = False
70
+ is_macos = platform.system() == "Darwin"
71
+
72
+ if not is_macos:
73
+ try:
74
+ proc = await asyncio.create_subprocess_exec(
75
+ "iptables",
76
+ "-L",
77
+ "-n",
78
+ stdout=asyncio.subprocess.DEVNULL,
79
+ stderr=asyncio.subprocess.DEVNULL,
80
+ )
81
+ await proc.wait()
82
+ has_iptables = proc.returncode == 0
83
+ except (FileNotFoundError, PermissionError):
84
+ has_iptables = False
85
+
86
+ use_host_network = not has_iptables
87
+
88
+ if use_host_network:
80
89
  docker_cmd.extend(["--network=host", "--add-host=localhost:127.0.0.1"])
81
90
 
82
91
  docker_cmd.extend(
@@ -96,6 +105,8 @@ async def run_agent(
96
105
  docker_cmd.extend(["-e", f"{key.upper()}={value}"])
97
106
 
98
107
  docker_cmd.append(image)
108
+
109
+ # Pass instruction via CLI arg (agents expect --instruction flag)
99
110
  docker_cmd.extend(["--instruction", instruction])
100
111
 
101
112
  # Run container and stream output
plato/worlds/base.py CHANGED
@@ -145,6 +145,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
145
145
  "required": schema.get("required", []),
146
146
  "agents": schema.get("agents", []),
147
147
  "secrets": schema.get("secrets", []),
148
+ "envs": schema.get("envs", []),
148
149
  }
149
150
 
150
151
  @abstractmethod
plato/worlds/runner.py CHANGED
@@ -6,6 +6,7 @@ import asyncio
6
6
  import json
7
7
  import logging
8
8
  import os
9
+ import platform
9
10
  from pathlib import Path
10
11
  from typing import Annotated
11
12
 
@@ -135,18 +136,143 @@ def list_worlds(
135
136
  typer.echo(f" {name} (v{version}): {desc}")
136
137
 
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
+
138
249
  async def _run_dev(
139
250
  world_name: str,
140
251
  config_path: Path,
141
252
  env_timeout: int = 600,
253
+ chronos_url: str | None = None,
254
+ api_key: str | None = None,
255
+ agents_dir: Path | None = None,
142
256
  ) -> None:
143
257
  """Run a world locally with automatic environment creation.
144
258
 
145
259
  This mimics what Chronos does but runs locally for debugging:
146
260
  1. Load and parse the config
147
- 2. Create Plato session with all environments
148
- 3. Run the world with the session attached
261
+ 2. Build local agent images if --agents-dir is provided
262
+ 3. Create Plato session with all environments
263
+ 4. Optionally initialize Chronos logging for callbacks
264
+ 5. Run the world with the session attached
265
+
266
+ Args:
267
+ world_name: Name of the world to run
268
+ config_path: Path to the config JSON file
269
+ env_timeout: Timeout for environment creation (seconds)
270
+ chronos_url: Optional Chronos base URL for sending log events
271
+ api_key: Optional Plato API key (used for Chronos session creation)
272
+ agents_dir: Optional directory containing agent source code
149
273
  """
274
+ from uuid import uuid4
275
+
150
276
  from plato.v2 import AsyncPlato
151
277
  from plato.worlds.base import get_world
152
278
 
@@ -167,6 +293,23 @@ async def _run_dev(
167
293
  # Parse the config to get typed access
168
294
  run_config = config_class._from_dict(config_data.copy())
169
295
 
296
+ # Build local agent images if agents_dir is provided
297
+ if agents_dir:
298
+ # Resolve agents_dir to absolute path
299
+ agents_dir = agents_dir.expanduser().resolve()
300
+ agent_images = _extract_agent_images_from_config(config_data)
301
+ if agent_images:
302
+ logger.info(f"Building local agent images: {agent_images}")
303
+ # Determine if we're in a plato-client repo for dev builds
304
+ # (agents_dir is something like /path/to/plato-client/agents)
305
+ plato_client_root = agents_dir.parent if agents_dir.name == "agents" else None
306
+ for agent_name in agent_images:
307
+ success = await _build_agent_image(agent_name, agents_dir, plato_client_root)
308
+ if not success:
309
+ raise RuntimeError(f"Failed to build agent image: {agent_name}")
310
+ else:
311
+ logger.info("No local agent images found in config")
312
+
170
313
  # Get environment configs from the parsed config
171
314
  env_configs: list[EnvConfig] = run_config.get_envs()
172
315
 
@@ -174,6 +317,23 @@ async def _run_dev(
174
317
  plato = AsyncPlato()
175
318
  session = None
176
319
 
320
+ # Initialize Chronos logging if URL provided
321
+ chronos_session_id: str | None = None
322
+ if chronos_url:
323
+ from plato.agents import init_logging
324
+
325
+ chronos_session_id = f"dev-{uuid4().hex[:8]}"
326
+ callback_url = f"{chronos_url.rstrip('/')}/api/v1/callback"
327
+ init_logging(
328
+ callback_url=callback_url,
329
+ session_id=chronos_session_id,
330
+ )
331
+ logger.info(f"Chronos logging enabled: {callback_url} (session: {chronos_session_id})")
332
+
333
+ # Update run_config with session info for agents
334
+ run_config.session_id = chronos_session_id
335
+ run_config.callback_url = callback_url
336
+
177
337
  try:
178
338
  if env_configs:
179
339
  logger.info(f"Creating {len(env_configs)} environments...")
@@ -199,12 +359,66 @@ async def _run_dev(
199
359
  await session.close()
200
360
  await plato.close()
201
361
 
362
+ # Reset logging
363
+ if chronos_url:
364
+ from plato.agents import reset_logging
365
+
366
+ reset_logging()
367
+
368
+
369
+ def _setup_colored_logging(verbose: bool = False) -> None:
370
+ """Setup colored logging with filtered noisy loggers."""
371
+ log_level = logging.DEBUG if verbose else logging.INFO
372
+
373
+ # Define colors for different log levels
374
+ colors = {
375
+ "DEBUG": "\033[36m", # Cyan
376
+ "INFO": "\033[32m", # Green
377
+ "WARNING": "\033[33m", # Yellow
378
+ "ERROR": "\033[31m", # Red
379
+ "CRITICAL": "\033[35m", # Magenta
380
+ }
381
+ reset = "\033[0m"
382
+
383
+ class ColoredFormatter(logging.Formatter):
384
+ def format(self, record: logging.LogRecord) -> str:
385
+ color = colors.get(record.levelname, "")
386
+ record.levelname = f"{color}{record.levelname}{reset}"
387
+ record.name = f"\033[34m{record.name}{reset}" # Blue for logger name
388
+ return super().format(record)
389
+
390
+ # Create handler with colored formatter
391
+ handler = logging.StreamHandler()
392
+ handler.setFormatter(
393
+ ColoredFormatter(
394
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
395
+ datefmt="%H:%M:%S",
396
+ )
397
+ )
398
+
399
+ # Configure root logger
400
+ root_logger = logging.getLogger()
401
+ root_logger.setLevel(log_level)
402
+ root_logger.handlers = [handler]
403
+
404
+ # Silence noisy HTTP loggers
405
+ for noisy_logger in ["httpcore", "httpx", "urllib3", "hpack"]:
406
+ logging.getLogger(noisy_logger).setLevel(logging.WARNING)
407
+
202
408
 
203
409
  @app.command("dev")
204
410
  def dev(
205
411
  world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
206
412
  config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
207
413
  env_timeout: Annotated[int, typer.Option("--env-timeout", help="Timeout for environment creation (seconds)")] = 600,
414
+ chronos_url: Annotated[
415
+ str | None, typer.Option("--chronos-url", help="Chronos base URL for log events (e.g., http://localhost:8000)")
416
+ ] = None,
417
+ api_key: Annotated[str | None, typer.Option("--api-key", help="Plato API key for Chronos authentication")] = None,
418
+ agents_dir: Annotated[
419
+ Path | None,
420
+ typer.Option("--agents-dir", "-a", help="Directory containing agent source code (builds local images)"),
421
+ ] = None,
208
422
  verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
209
423
  ) -> None:
210
424
  """Run a world locally for development/debugging.
@@ -212,30 +426,35 @@ def dev(
212
426
  This creates Plato environments automatically (like Chronos does)
213
427
  and runs the world with the session attached.
214
428
 
429
+ Optionally sends log events to a Chronos server for real-time monitoring.
430
+
215
431
  Example config.json:
216
432
  {
217
- "repository_url": "https://github.com/user/repo",
218
- "prompt": "Fix the bug in main.py",
219
- "agents": {
220
- "coder": {
221
- "image": "openhands:latest",
222
- "config": {"model_name": "claude-sonnet-4"}
223
- }
433
+ "instruction": "Create a git repo and upload files to S3",
434
+ "coder": {
435
+ "image": "openhands:latest",
436
+ "config": {"model_name": "gemini/gemini-3-flash-preview"}
224
437
  },
225
438
  "secrets": {
226
- "anthropic_api_key": "sk-..."
439
+ "gemini_api_key": "..."
227
440
  }
228
441
  }
229
442
 
230
443
  Environment variables:
231
444
  PLATO_API_KEY: API key for Plato (required)
445
+
446
+ Examples:
447
+ # Basic usage
448
+ plato-world-runner dev -w code -c config.json
449
+
450
+ # With local agent builds (from plato-client repo)
451
+ plato-world-runner dev -w code -c config.json --agents-dir ~/plato-client/agents
452
+
453
+ # With Chronos logging
454
+ plato-world-runner dev -w code -c config.json --chronos-url http://localhost:8000
232
455
  """
233
- # Setup logging
234
- log_level = logging.DEBUG if verbose else logging.INFO
235
- logging.basicConfig(
236
- level=log_level,
237
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
238
- )
456
+ # Setup colored logging with filtered noisy loggers
457
+ _setup_colored_logging(verbose)
239
458
 
240
459
  if not config.exists():
241
460
  typer.echo(f"Error: Config file not found: {config}", err=True)
@@ -246,7 +465,7 @@ def dev(
246
465
  raise typer.Exit(1)
247
466
 
248
467
  try:
249
- asyncio.run(_run_dev(world, config, env_timeout))
468
+ asyncio.run(_run_dev(world, config, env_timeout, chronos_url, api_key, agents_dir))
250
469
  except Exception as e:
251
470
  logger.exception(f"World execution failed: {e}")
252
471
  raise typer.Exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -302,7 +302,7 @@ plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
302
302
  plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
303
303
  plato/agents/config.py,sha256=VZVMdCmEQnoR0VkrGdScG8p6zSKVFe7BZPd2h8lKNjI,5460
304
304
  plato/agents/logging.py,sha256=z9rDlGPbrpcTS8PephbK2rDqT7thC1KyLkua4ypUkv4,12210
305
- plato/agents/runner.py,sha256=rOWYTSAhdola_FdrbviP955NBusNtBUy-q_c5fDA9to,5123
305
+ plato/agents/runner.py,sha256=YoqG1QdNScIjSSH0vPgnm42LlqeAeVsFT01VL77ony0,5565
306
306
  plato/agents/trajectory.py,sha256=WdiBmua0KvCrNaM3qgPI7-7B4xmSkfbP4oZ_9_8qHzU,10529
307
307
  plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
308
308
  plato/chronos/client.py,sha256=YcOGtHWERyOD9z8LKt8bRMVL0cEwL2hiAP4qQgdZlUI,5495
@@ -464,11 +464,11 @@ plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
464
464
  plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
465
465
  plato/worlds/README.md,sha256=TgG4aidude0ouJSCfY81Ev45hsUxPkO85HUIiWNqkcc,5463
466
466
  plato/worlds/__init__.py,sha256=crzpXFh4XD8eS4pYFTEUf3XgUf0wapFPT4npAu8sWwk,2078
467
- plato/worlds/base.py,sha256=McV2QAuHYztSMfG8EXQML1kEPbdeADLt8tgiYpH35n4,10751
467
+ plato/worlds/base.py,sha256=5_BAad_w0l4DkgnNYojSDNfOWHXLmD0Q39cLSUuGYJw,10795
468
468
  plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
469
469
  plato/worlds/config.py,sha256=ggDcySspfeFry2KBUwhgnS6Po2KssYzwZNIDmVeGhPQ,9460
470
- plato/worlds/runner.py,sha256=07fKyPYxem1vIKfPRo4le7eKWTkY3euDXugObVyAsf4,7858
471
- plato_sdk_v2-2.2.2.dist-info/METADATA,sha256=1kiU4THegqudfEb-uvUEujXQzCP6EIRL97Y_LsWq1kg,8508
472
- plato_sdk_v2-2.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
473
- plato_sdk_v2-2.2.2.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
474
- plato_sdk_v2-2.2.2.dist-info/RECORD,,
470
+ plato/worlds/runner.py,sha256=RNnWFQ7rfEWE7TQ_tqgLHgLm1a4VxtP0mR7beALx4f0,15781
471
+ plato_sdk_v2-2.2.3.dist-info/METADATA,sha256=18iVpDD44_fE6Sy6U19MKBNCI1zDY-0fk7CZ0SlC8ak,8508
472
+ plato_sdk_v2-2.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
473
+ plato_sdk_v2-2.2.3.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
474
+ plato_sdk_v2-2.2.3.dist-info/RECORD,,