plato-sdk-v2 2.3.7__py3-none-any.whl → 2.3.8__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/otel.py CHANGED
@@ -51,6 +51,8 @@ class OTelSpanLogHandler(logging.Handler):
51
51
  def emit(self, record: logging.LogRecord) -> None:
52
52
  """Emit a log record as an OTel span."""
53
53
  try:
54
+ # Debug: print that we're emitting a log span
55
+ print(f"[OTel] Emitting log span: {record.name} - {record.getMessage()[:100]}")
54
56
  # Create a span for the log message
55
57
  with self.tracer.start_as_current_span(f"log.{record.levelname.lower()}") as span:
56
58
  span.set_attribute("log.level", record.levelname)
@@ -137,13 +139,18 @@ def init_tracing(
137
139
  context_api.attach(ctx)
138
140
  print(f"[OTel] Using parent context: trace_id={parent_trace_id}, span_id={parent_span_id}")
139
141
 
140
- # Add OTel logging handler to capture world logs
142
+ # Add OTel logging handler to capture logs from plato SDK
141
143
  tracer = trace.get_tracer(service_name)
142
144
  _log_handler = OTelSpanLogHandler(tracer, level=logging.INFO)
143
145
 
144
- # Add handler to plato.worlds loggers
145
- plato_worlds_logger = logging.getLogger("plato.worlds")
146
- plato_worlds_logger.addHandler(_log_handler)
146
+ # Add handler to plato loggers (worlds and agents)
147
+ # Set level to INFO to ensure logs propagate from child loggers
148
+ plato_logger = logging.getLogger("plato")
149
+ plato_logger.setLevel(logging.INFO)
150
+ plato_logger.addHandler(_log_handler)
151
+ print(
152
+ f"[OTel] Added log handler to 'plato' logger (level={plato_logger.level}, handlers={len(plato_logger.handlers)})"
153
+ )
147
154
 
148
155
  _initialized = True
149
156
 
@@ -164,8 +171,8 @@ def shutdown_tracing() -> None:
164
171
  # Remove log handler
165
172
  if _log_handler:
166
173
  try:
167
- plato_worlds_logger = logging.getLogger("plato.worlds")
168
- plato_worlds_logger.removeHandler(_log_handler)
174
+ plato_logger = logging.getLogger("plato")
175
+ plato_logger.removeHandler(_log_handler)
169
176
  except Exception:
170
177
  pass
171
178
  _log_handler = None
@@ -222,8 +229,11 @@ def instrument(service_name: str = "plato-agent") -> Tracer:
222
229
  parent_trace_id = os.environ.get("OTEL_TRACE_ID")
223
230
  parent_span_id = os.environ.get("OTEL_PARENT_SPAN_ID")
224
231
 
232
+ print(f"[OTel] instrument() called: service={service_name}, endpoint={otel_endpoint}, session={session_id}")
233
+
225
234
  if not otel_endpoint:
226
235
  # Return default tracer (no-op if no provider configured)
236
+ print("[OTel] No OTEL_EXPORTER_OTLP_ENDPOINT set, returning no-op tracer")
227
237
  return trace.get_tracer(service_name)
228
238
 
229
239
  # Initialize tracing with parent context if provided
plato/v1/cli/sandbox.py CHANGED
@@ -131,6 +131,9 @@ def sandbox_start(
131
131
  timeout: int = typer.Option(1800, "--timeout", help="VM lifetime in seconds (default: 30 minutes)"),
132
132
  no_reset: bool = typer.Option(False, "--no-reset", help="Skip initial reset after ready"),
133
133
  json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
134
+ working_dir: Path = typer.Option(
135
+ None, "--working-dir", "-w", help="Working directory for .sandbox.yaml and .plato/"
136
+ ),
134
137
  ):
135
138
  """
136
139
  Start a sandbox environment.
@@ -377,7 +380,7 @@ def sandbox_start(
377
380
  console.print("[cyan] Generating SSH key pair...[/cyan]")
378
381
 
379
382
  base_url = os.getenv("PLATO_BASE_URL", "https://plato.so")
380
- ssh_info = setup_ssh_for_sandbox(base_url, job_id, username=ssh_username)
383
+ ssh_info = setup_ssh_for_sandbox(base_url, job_id, username=ssh_username, working_dir=working_dir)
381
384
  ssh_host = ssh_info["ssh_host"]
382
385
  ssh_config_path = ssh_info["config_path"]
383
386
  ssh_private_key_path = ssh_info["private_key_path"]
@@ -489,7 +492,7 @@ def sandbox_start(
489
492
  # Add heartbeat PID
490
493
  if heartbeat_pid:
491
494
  state["heartbeat_pid"] = heartbeat_pid
492
- save_sandbox_state(state)
495
+ save_sandbox_state(state, working_dir)
493
496
 
494
497
  # Close the plato client (heartbeat process keeps session alive)
495
498
  plato.close()
plato/v1/cli/ssh.py CHANGED
@@ -9,21 +9,21 @@ from cryptography.hazmat.primitives import serialization
9
9
  from cryptography.hazmat.primitives.asymmetric import ed25519
10
10
 
11
11
 
12
- def get_plato_dir() -> Path:
12
+ def get_plato_dir(working_dir: Path | str | None = None) -> Path:
13
13
  """Get the directory for plato config/SSH files.
14
14
 
15
- Uses /workspace/.plato if /workspace exists (container environment),
16
- otherwise uses ~/.plato (local development).
15
+ Args:
16
+ working_dir: If provided, returns working_dir/.plato (for container/agent use).
17
+ If None, returns ~/.plato (local development).
17
18
  """
18
- workspace = Path("/workspace")
19
- if workspace.exists() and workspace.is_dir():
20
- return workspace / ".plato"
19
+ if working_dir is not None:
20
+ return Path(working_dir) / ".plato"
21
21
  return Path.home() / ".plato"
22
22
 
23
23
 
24
- def get_next_sandbox_number() -> int:
24
+ def get_next_sandbox_number(working_dir: Path | str | None = None) -> int:
25
25
  """Find next available sandbox number by checking existing config files."""
26
- plato_dir = get_plato_dir()
26
+ plato_dir = get_plato_dir(working_dir)
27
27
  if not plato_dir.exists():
28
28
  return 1
29
29
 
@@ -41,13 +41,13 @@ def get_next_sandbox_number() -> int:
41
41
  return max_num + 1
42
42
 
43
43
 
44
- def generate_ssh_key_pair(sandbox_num: int) -> tuple[str, str]:
44
+ def generate_ssh_key_pair(sandbox_num: int, working_dir: Path | str | None = None) -> tuple[str, str]:
45
45
  """
46
46
  Generate a new ed25519 SSH key pair for a specific sandbox.
47
47
 
48
48
  Returns (public_key_str, private_key_path).
49
49
  """
50
- plato_dir = get_plato_dir()
50
+ plato_dir = get_plato_dir(working_dir)
51
51
  plato_dir.mkdir(mode=0o700, exist_ok=True)
52
52
 
53
53
  private_key_path = plato_dir / f"ssh_{sandbox_num}_key"
@@ -136,6 +136,7 @@ def create_ssh_config(
136
136
  username: str,
137
137
  private_key_path: str,
138
138
  sandbox_num: int,
139
+ working_dir: Path | str | None = None,
139
140
  ) -> str:
140
141
  """
141
142
  Create a temporary SSH config file for a specific sandbox.
@@ -172,7 +173,7 @@ def create_ssh_config(
172
173
  TCPKeepAlive yes
173
174
  """
174
175
 
175
- plato_dir = get_plato_dir()
176
+ plato_dir = get_plato_dir(working_dir)
176
177
  plato_dir.mkdir(mode=0o700, exist_ok=True)
177
178
 
178
179
  config_path = plato_dir / f"ssh_{sandbox_num}.conf"
@@ -182,7 +183,12 @@ def create_ssh_config(
182
183
  return str(config_path)
183
184
 
184
185
 
185
- def setup_ssh_for_sandbox(base_url: str, job_public_id: str, username: str = "plato") -> dict:
186
+ def setup_ssh_for_sandbox(
187
+ base_url: str,
188
+ job_public_id: str,
189
+ username: str = "plato",
190
+ working_dir: Path | str | None = None,
191
+ ) -> dict:
186
192
  """
187
193
  Set up SSH access for a sandbox - generates keys and creates config.
188
194
 
@@ -190,14 +196,14 @@ def setup_ssh_for_sandbox(base_url: str, job_public_id: str, username: str = "pl
190
196
 
191
197
  Returns dict with: ssh_host, config_path, public_key, private_key_path
192
198
  """
193
- sandbox_num = get_next_sandbox_number()
199
+ sandbox_num = get_next_sandbox_number(working_dir)
194
200
  ssh_host = f"sandbox-{sandbox_num}"
195
201
 
196
202
  # Choose random port between 2200 and 2299
197
203
  local_port = random.randint(2200, 2299)
198
204
 
199
205
  # Generate SSH key pair
200
- public_key, private_key_path = generate_ssh_key_pair(sandbox_num)
206
+ public_key, private_key_path = generate_ssh_key_pair(sandbox_num, working_dir)
201
207
 
202
208
  # Create SSH config file
203
209
  config_path = create_ssh_config(
@@ -208,6 +214,7 @@ def setup_ssh_for_sandbox(base_url: str, job_public_id: str, username: str = "pl
208
214
  username=username,
209
215
  private_key_path=private_key_path,
210
216
  sandbox_num=sandbox_num,
217
+ working_dir=working_dir,
211
218
  )
212
219
 
213
220
  return {
plato/v1/cli/utils.py CHANGED
@@ -15,32 +15,52 @@ console = Console()
15
15
  SANDBOX_FILE = ".sandbox.yaml"
16
16
 
17
17
 
18
- def get_sandbox_state() -> dict | None:
19
- """Read sandbox state from .sandbox.yaml in current directory."""
20
- sandbox_file = Path.cwd() / SANDBOX_FILE
18
+ def get_sandbox_state(working_dir: Path | str | None = None) -> dict | None:
19
+ """Read sandbox state from .sandbox.yaml.
20
+
21
+ Args:
22
+ working_dir: Directory containing .sandbox.yaml. If None, uses cwd.
23
+ """
24
+ base_dir = Path(working_dir) if working_dir else Path.cwd()
25
+ sandbox_file = base_dir / SANDBOX_FILE
21
26
  if not sandbox_file.exists():
22
27
  return None
23
28
  with open(sandbox_file) as f:
24
29
  return yaml.safe_load(f)
25
30
 
26
31
 
27
- def save_sandbox_state(state: dict) -> None:
28
- """Save sandbox state to .sandbox.yaml in current directory."""
29
- sandbox_file = Path.cwd() / SANDBOX_FILE
32
+ def save_sandbox_state(state: dict, working_dir: Path | str | None = None) -> None:
33
+ """Save sandbox state to .sandbox.yaml.
34
+
35
+ Args:
36
+ state: State dict to save.
37
+ working_dir: Directory to save .sandbox.yaml in. If None, uses cwd.
38
+ """
39
+ base_dir = Path(working_dir) if working_dir else Path.cwd()
40
+ sandbox_file = base_dir / SANDBOX_FILE
30
41
  with open(sandbox_file, "w") as f:
31
42
  yaml.dump(state, f, default_flow_style=False)
32
43
 
33
44
 
34
- def remove_sandbox_state() -> None:
35
- """Remove .sandbox.yaml from current directory."""
36
- sandbox_file = Path.cwd() / SANDBOX_FILE
45
+ def remove_sandbox_state(working_dir: Path | str | None = None) -> None:
46
+ """Remove .sandbox.yaml.
47
+
48
+ Args:
49
+ working_dir: Directory containing .sandbox.yaml. If None, uses cwd.
50
+ """
51
+ base_dir = Path(working_dir) if working_dir else Path.cwd()
52
+ sandbox_file = base_dir / SANDBOX_FILE
37
53
  if sandbox_file.exists():
38
54
  sandbox_file.unlink()
39
55
 
40
56
 
41
- def require_sandbox_state() -> dict:
42
- """Get sandbox state or exit with error."""
43
- state = get_sandbox_state()
57
+ def require_sandbox_state(working_dir: Path | str | None = None) -> dict:
58
+ """Get sandbox state or exit with error.
59
+
60
+ Args:
61
+ working_dir: Directory containing .sandbox.yaml. If None, uses cwd.
62
+ """
63
+ state = get_sandbox_state(working_dir)
44
64
  if not state:
45
65
  console.print("[red]No sandbox found in current directory[/red]")
46
66
  console.print("\n[yellow]Start a sandbox with:[/yellow]")
plato/worlds/base.py CHANGED
@@ -28,6 +28,17 @@ from plato.agents.otel import (
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
31
+
32
+ def _get_plato_version() -> str:
33
+ """Get the installed plato SDK version."""
34
+ try:
35
+ from importlib.metadata import version
36
+
37
+ return version("plato")
38
+ except Exception:
39
+ return "unknown"
40
+
41
+
31
42
  # Global registry of worlds
32
43
  _WORLD_REGISTRY: dict[str, type[BaseWorld]] = {}
33
44
 
@@ -673,6 +684,11 @@ The following services are available for your use:
673
684
  else:
674
685
  logger.debug("No otel_url in config - OTel tracing disabled")
675
686
 
687
+ # Log version info (goes to OTel after init_tracing)
688
+ plato_version = _get_plato_version()
689
+ world_version = self.get_version()
690
+ self.logger.info(f"World version: {world_version}, Plato SDK version: {plato_version}")
691
+
676
692
  # Connect to Plato session if configured (for heartbeats)
677
693
  await self._connect_plato_session()
678
694
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.3.7
3
+ Version: 2.3.8
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -47,7 +47,6 @@ Description-Content-Type: text/markdown
47
47
 
48
48
  # Plato Python SDK
49
49
 
50
-
51
50
  Python SDK for the Plato platform. Uses [Harbor](https://harborframework.com) for agent execution.
52
51
 
53
52
  ## Installation
@@ -302,7 +302,7 @@ plato/agents/artifacts.py,sha256=ljeI0wzsp7Q6uKqMb-k7kTb680Vizs54ohtM-d7zvOg,292
302
302
  plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
303
303
  plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
304
304
  plato/agents/config.py,sha256=CmRS6vOAg7JeqX4Hgp_KpA1YWBX_LuMicHm7SBjQEbs,5077
305
- plato/agents/otel.py,sha256=LI5ZK4lwoDD2AnXhSubbv6ONP2VayOsNIk-F1hQ6968,7991
305
+ plato/agents/otel.py,sha256=UOEBeMyyfbffsC3tRXlHAydoj9bXwJupA_UeLZ5h97w,8585
306
306
  plato/agents/runner.py,sha256=Ei20Ib-Fn5XOaS6V1Rtw0UEw34XflEWaXMpazPjmnrE,6061
307
307
  plato/agents/trajectory.py,sha256=WdiBmua0KvCrNaM3qgPI7-7B4xmSkfbP4oZ_9_8qHzU,10529
308
308
  plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
@@ -389,9 +389,9 @@ plato/v1/cli/__init__.py,sha256=om4b7PxgsoI7rEwuQelmQkqPdhMVn53_5qEN8kvksYw,105
389
389
  plato/v1/cli/agent.py,sha256=G6TV3blG_BqMDBWS-CG7GwzqoqcJTMsIKQ88jvLXb4k,43745
390
390
  plato/v1/cli/main.py,sha256=ktPtBvMwykR7AjXmTQ6bmZkHdzpAjhX5Fq66cDbGSzA,6844
391
391
  plato/v1/cli/pm.py,sha256=uLM6WszKqxq9Czg1FraDyWb9_INUuHZq63imvRYfRLw,49734
392
- plato/v1/cli/sandbox.py,sha256=5rth_jL73L72GC0VJ0meXRgZo2EpsJ_qI3ipFjfXzJY,95185
393
- plato/v1/cli/ssh.py,sha256=10ag6S1sxMcAmvcg24qy-yYwXb1miWfqxkXOs4QX8u0,6623
394
- plato/v1/cli/utils.py,sha256=be-llK6T6NHnIQl_Kfs-8EPu9JhIuZ_k9tJ3Ts-AKt4,3887
392
+ plato/v1/cli/sandbox.py,sha256=N7DIpXsxExtZB47tWKxp-3cV0_gLnI7C-mTKW3tTi8Y,95360
393
+ plato/v1/cli/ssh.py,sha256=enrf7Y01ZeRIyHDEX0Yt7up5zEe7MCvE9u8SP4Oqiz4,6926
394
+ plato/v1/cli/utils.py,sha256=ba7Crv4OjDmgCv4SeB8UeZDin-iOdQw_3N6fd-g5XVk,4572
395
395
  plato/v1/cli/verify.py,sha256=7QmQwfOOkr8a51f8xfVIr2zif7wGl2E8HOZTbOaIoV0,20671
396
396
  plato/v1/cli/world.py,sha256=yBUadOJs1QYm6Jmx_ACDzogybRq5x4B-BnTvGO_ulQk,9757
397
397
  plato/v1/examples/doordash_tasks.py,sha256=8Sz9qx-vTmiOAiCAbrDRvZGsA1qQQBr1KHbxXdjr7OI,23233
@@ -458,11 +458,11 @@ plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
458
458
  plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
459
459
  plato/worlds/README.md,sha256=XFOkEA3cNNcrWkk-Cxnsl-zn-y0kvUENKQRSqFKpdqw,5479
460
460
  plato/worlds/__init__.py,sha256=ALoou3l5lXvs_YZc5eH6HdMHpvhnpzKWqz__aSC1jFc,2152
461
- plato/worlds/base.py,sha256=_svL9RBp3dTIhHqcvZB1F7qEFrZvAuQ-XjZkTa3L6zo,27750
461
+ plato/worlds/base.py,sha256=kIX02gMQR43ZsZK25vZvCoNkYtICVRMeofNk-im5IRM,28215
462
462
  plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
463
463
  plato/worlds/config.py,sha256=a5frj3mt06rSlT25kE-L8Q2b2MTWkR-8cUoBKpC8tG4,11036
464
464
  plato/worlds/runner.py,sha256=2H5EV77bTYrMyI7qez0kwxOp9EApQxG19Ob9a_GTdbw,19383
465
- plato_sdk_v2-2.3.7.dist-info/METADATA,sha256=7T1hf9Y8o0lFSrSx35VozfobEdwM097kfZQT6rEIn68,8653
466
- plato_sdk_v2-2.3.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
467
- plato_sdk_v2-2.3.7.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
468
- plato_sdk_v2-2.3.7.dist-info/RECORD,,
465
+ plato_sdk_v2-2.3.8.dist-info/METADATA,sha256=GnoZAAlPmFfWxSwHFjkab7s6MmWfk8rXOvOynTb2r28,8652
466
+ plato_sdk_v2-2.3.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
467
+ plato_sdk_v2-2.3.8.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
468
+ plato_sdk_v2-2.3.8.dist-info/RECORD,,