plato-sdk-v2 2.5.1__py3-none-any.whl → 2.6.1__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.
Files changed (52) hide show
  1. plato/_generated/__init__.py +1 -1
  2. plato/_generated/api/v1/evals/get_scores_by_user.py +7 -0
  3. plato/_generated/api/v1/testcases/get_testcases.py +14 -0
  4. plato/_generated/api/v2/sessions/setup_sandbox.py +2 -2
  5. plato/_generated/models/__init__.py +42 -2
  6. plato/agents/runner.py +2 -33
  7. plato/chronos/api/admin/__init__.py +17 -0
  8. plato/chronos/api/admin/clear_database_api_admin_clear_db_post.py +57 -0
  9. plato/chronos/api/admin/sync_agents_api_admin_sync_agents_post.py +67 -0
  10. plato/chronos/api/admin/sync_all_api_admin_sync_all_post.py +67 -0
  11. plato/chronos/api/admin/sync_runtimes_api_admin_sync_runtimes_post.py +67 -0
  12. plato/chronos/api/admin/sync_worlds_api_admin_sync_worlds_post.py +67 -0
  13. plato/chronos/api/agents/list_agents.py +5 -5
  14. plato/chronos/api/checkpoints/__init__.py +8 -0
  15. plato/chronos/api/checkpoints/list_checkpoints.py +52 -0
  16. plato/chronos/api/checkpoints/preview_checkpoint.py +74 -0
  17. plato/chronos/api/events/__init__.py +8 -0
  18. plato/chronos/api/events/get_session_event.py +68 -0
  19. plato/chronos/api/events/list_session_events.py +62 -0
  20. plato/chronos/api/jobs/launch_job.py +8 -2
  21. plato/chronos/api/otel/__init__.py +8 -0
  22. plato/chronos/api/otel/get_session_traces_api_otel_sessions__session_id__traces_get.py +56 -0
  23. plato/chronos/api/otel/receive_traces_api_otel_v1_traces_post.py +49 -0
  24. plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +5 -5
  25. plato/chronos/api/runtimes/__init__.py +2 -1
  26. plato/chronos/api/runtimes/get_runtime_logs.py +61 -0
  27. plato/chronos/api/sessions/__init__.py +24 -1
  28. plato/chronos/api/sessions/close_session.py +66 -0
  29. plato/chronos/api/sessions/complete_session.py +74 -0
  30. plato/chronos/api/sessions/create_session.py +69 -0
  31. plato/chronos/api/sessions/get_session.py +20 -2
  32. plato/chronos/api/sessions/get_session_bash_logs_download.py +61 -0
  33. plato/chronos/api/sessions/get_session_envs.py +62 -0
  34. plato/chronos/api/sessions/get_session_live_logs.py +62 -0
  35. plato/chronos/api/sessions/get_session_logs.py +3 -3
  36. plato/chronos/api/sessions/get_session_status.py +62 -0
  37. plato/chronos/api/sessions/list_sessions.py +20 -2
  38. plato/chronos/api/sessions/list_tags.py +80 -0
  39. plato/chronos/api/sessions/update_session_tags.py +68 -0
  40. plato/chronos/models/__init__.py +241 -196
  41. plato/v1/cli/chronos.py +66 -4
  42. plato/v2/__init__.py +8 -0
  43. plato/v2/async_/__init__.py +4 -0
  44. plato/v2/async_/chronos.py +419 -0
  45. plato/v2/sync/__init__.py +3 -0
  46. plato/v2/sync/chronos.py +419 -0
  47. plato/worlds/base.py +3 -3
  48. plato/worlds/config.py +3 -7
  49. {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/METADATA +1 -1
  50. {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/RECORD +52 -25
  51. {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/WHEEL +0 -0
  52. {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,419 @@
1
+ """Plato SDK v2 - Synchronous Chronos Client.
2
+
3
+ Provides high-level APIs for managing Chronos sessions and jobs programmatically.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import os
10
+ import time
11
+ from typing import Any
12
+
13
+ import httpx
14
+ from dotenv import load_dotenv
15
+
16
+ from plato.chronos.api.jobs import launch_job
17
+ from plato.chronos.api.sessions import (
18
+ close_session,
19
+ complete_session,
20
+ get_session,
21
+ get_session_envs,
22
+ get_session_logs,
23
+ get_session_status,
24
+ list_sessions,
25
+ list_tags,
26
+ update_session_tags,
27
+ )
28
+ from plato.chronos.models import (
29
+ CompleteSessionRequest,
30
+ LaunchJobRequest,
31
+ LaunchJobResponse,
32
+ RuntimeConfig,
33
+ SessionEnvsResponse,
34
+ SessionListResponse,
35
+ SessionLogsResponse,
36
+ SessionResponse,
37
+ SessionStatusResponse,
38
+ TagsListResponse,
39
+ UpdateTagsRequest,
40
+ WorldConfig,
41
+ )
42
+
43
+ load_dotenv()
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ DEFAULT_CHRONOS_URL = "https://chronos.plato.so"
48
+ DEFAULT_TIMEOUT = 120.0
49
+
50
+
51
+ class ChronosSession:
52
+ """Wrapper for a Chronos session with convenient methods.
53
+
54
+ Provides methods to check status, get logs, update tags, and stop the session.
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ http_client: httpx.Client,
60
+ api_key: str,
61
+ session_id: str,
62
+ plato_session_id: str | None = None,
63
+ ):
64
+ self._http = http_client
65
+ self._api_key = api_key
66
+ self._session_id = session_id
67
+ self._plato_session_id = plato_session_id
68
+
69
+ @property
70
+ def session_id(self) -> str:
71
+ """Get the Chronos session public ID."""
72
+ return self._session_id
73
+
74
+ @property
75
+ def plato_session_id(self) -> str | None:
76
+ """Get the underlying Plato session ID."""
77
+ return self._plato_session_id
78
+
79
+ def get_status(self) -> SessionStatusResponse:
80
+ """Get the current status of the session (lightweight).
81
+
82
+ Returns:
83
+ SessionStatusResponse with status and status_reason.
84
+ """
85
+ return get_session_status.sync(
86
+ client=self._http,
87
+ public_id=self._session_id,
88
+ x_api_key=self._api_key,
89
+ )
90
+
91
+ def get_details(self) -> SessionResponse:
92
+ """Get full session details including trajectory and world config.
93
+
94
+ Returns:
95
+ SessionResponse with full session details.
96
+ """
97
+ return get_session.sync(
98
+ client=self._http,
99
+ public_id=self._session_id,
100
+ x_api_key=self._api_key,
101
+ )
102
+
103
+ def get_envs(self) -> SessionEnvsResponse:
104
+ """Get environment information for this session.
105
+
106
+ Returns:
107
+ SessionEnvsResponse with list of environments.
108
+ """
109
+ return get_session_envs.sync(
110
+ client=self._http,
111
+ public_id=self._session_id,
112
+ x_api_key=self._api_key,
113
+ )
114
+
115
+ def get_logs(
116
+ self,
117
+ limit: int = 10000,
118
+ ) -> SessionLogsResponse:
119
+ """Get logs for this session.
120
+
121
+ Args:
122
+ limit: Maximum number of log entries to return.
123
+
124
+ Returns:
125
+ SessionLogsResponse with log entries.
126
+ """
127
+ return get_session_logs.sync(
128
+ client=self._http,
129
+ public_id=self._session_id,
130
+ limit=limit,
131
+ x_api_key=self._api_key,
132
+ )
133
+
134
+ def update_tags(self, tags: list[str]) -> SessionResponse:
135
+ """Update tags for this session.
136
+
137
+ Args:
138
+ tags: List of tags (use '.' for hierarchy, '_' instead of '-').
139
+
140
+ Returns:
141
+ Updated SessionResponse.
142
+ """
143
+ # Normalize tags
144
+ normalized = [tag.replace("-", "_").replace(":", ".").replace(" ", "_") for tag in tags]
145
+ request = UpdateTagsRequest(tags=normalized)
146
+ return update_session_tags.sync(
147
+ client=self._http,
148
+ public_id=self._session_id,
149
+ body=request,
150
+ x_api_key=self._api_key,
151
+ )
152
+
153
+ def stop(self, error_message: str = "Cancelled by user") -> SessionResponse:
154
+ """Stop/cancel this session.
155
+
156
+ Args:
157
+ error_message: Reason for stopping.
158
+
159
+ Returns:
160
+ Updated SessionResponse.
161
+ """
162
+ request = CompleteSessionRequest(
163
+ status="cancelled",
164
+ error_message=error_message,
165
+ )
166
+ return complete_session.sync(
167
+ client=self._http,
168
+ public_id=self._session_id,
169
+ body=request,
170
+ x_api_key=self._api_key,
171
+ )
172
+
173
+ def complete(
174
+ self,
175
+ status: str = "completed",
176
+ exit_code: int | None = None,
177
+ error_message: str | None = None,
178
+ ) -> SessionResponse:
179
+ """Mark session as completed or failed.
180
+
181
+ Args:
182
+ status: Final status ('completed', 'failed', or 'cancelled').
183
+ exit_code: Optional exit code from world runner.
184
+ error_message: Error message if failed.
185
+
186
+ Returns:
187
+ Updated SessionResponse.
188
+ """
189
+ request = CompleteSessionRequest(
190
+ status=status,
191
+ exit_code=exit_code,
192
+ error_message=error_message,
193
+ )
194
+ return complete_session.sync(
195
+ client=self._http,
196
+ public_id=self._session_id,
197
+ body=request,
198
+ x_api_key=self._api_key,
199
+ )
200
+
201
+ def close(self) -> None:
202
+ """Close the Plato session (release VM resources).
203
+
204
+ Note: This closes the underlying Plato session, not just the Chronos record.
205
+ """
206
+ close_session.sync(
207
+ client=self._http,
208
+ public_id=self._session_id,
209
+ x_api_key=self._api_key,
210
+ )
211
+
212
+ def wait_until_complete(
213
+ self,
214
+ timeout: float = 3600.0,
215
+ poll_interval: float = 5.0,
216
+ ) -> SessionResponse:
217
+ """Wait until the session reaches a terminal state.
218
+
219
+ Args:
220
+ timeout: Maximum time to wait in seconds.
221
+ poll_interval: Time between status checks.
222
+
223
+ Returns:
224
+ Final SessionResponse.
225
+
226
+ Raises:
227
+ TimeoutError: If session doesn't complete within timeout.
228
+ """
229
+ terminal_statuses = {"completed", "failed", "cancelled", "error"}
230
+ elapsed = 0.0
231
+
232
+ while elapsed < timeout:
233
+ status_response = self.get_status()
234
+ if status_response.status in terminal_statuses:
235
+ return self.get_details()
236
+
237
+ time.sleep(poll_interval)
238
+ elapsed += poll_interval
239
+
240
+ raise TimeoutError(f"Session {self._session_id} did not complete within {timeout} seconds")
241
+
242
+ def __repr__(self) -> str:
243
+ return f"ChronosSession(session_id={self._session_id!r})"
244
+
245
+
246
+ class Chronos:
247
+ """Synchronous client for Chronos job management API.
248
+
249
+ Provides high-level methods for launching jobs, managing sessions,
250
+ and monitoring job status.
251
+
252
+ Usage:
253
+ from plato.v2.sync import Chronos
254
+
255
+ with Chronos() as chronos:
256
+ # Launch a job
257
+ session = chronos.launch(
258
+ world_package="plato-world-computer-use",
259
+ world_config={"task": "Navigate to google.com"},
260
+ tags=["test", "project.my_project"],
261
+ )
262
+
263
+ # Wait for completion
264
+ result = session.wait_until_complete()
265
+ print(f"Status: {result.status}")
266
+
267
+ # Or poll manually
268
+ while True:
269
+ status = session.get_status()
270
+ if status.status in ("completed", "failed"):
271
+ break
272
+ time.sleep(5)
273
+ """
274
+
275
+ def __init__(
276
+ self,
277
+ api_key: str | None = None,
278
+ base_url: str | None = None,
279
+ timeout: float = DEFAULT_TIMEOUT,
280
+ ):
281
+ """Initialize the Chronos client.
282
+
283
+ Args:
284
+ api_key: Plato API key. Falls back to PLATO_API_KEY env var.
285
+ base_url: Chronos API base URL. Falls back to CHRONOS_URL env var.
286
+ timeout: Request timeout in seconds.
287
+ """
288
+ resolved_api_key = api_key or os.environ.get("PLATO_API_KEY")
289
+ if not resolved_api_key:
290
+ raise ValueError("API key required. Set PLATO_API_KEY or pass api_key=")
291
+ self._api_key: str = resolved_api_key
292
+
293
+ url = base_url or os.environ.get("CHRONOS_URL", DEFAULT_CHRONOS_URL)
294
+ self.base_url = url.rstrip("/")
295
+ self.timeout = timeout
296
+
297
+ self._http = httpx.Client(
298
+ base_url=self.base_url,
299
+ timeout=httpx.Timeout(timeout),
300
+ )
301
+
302
+ def launch(
303
+ self,
304
+ world_package: str,
305
+ world_config: dict[str, Any] | None = None,
306
+ runtime_artifact_id: str | None = None,
307
+ tags: list[str] | None = None,
308
+ ) -> ChronosSession:
309
+ """Launch a new Chronos job.
310
+
311
+ Args:
312
+ world_package: World package name with optional version
313
+ (e.g., "plato-world-computer-use:0.1.0").
314
+ world_config: Configuration passed to the world runner.
315
+ runtime_artifact_id: Optional runtime artifact ID for cached environment.
316
+ tags: Optional tags for organizing sessions (use '.' for hierarchy).
317
+
318
+ Returns:
319
+ ChronosSession for monitoring and managing the job.
320
+
321
+ Example:
322
+ session = chronos.launch(
323
+ world_package="plato-world-computer-use",
324
+ world_config={
325
+ "task": "Navigate to google.com",
326
+ "agents": [{"name": "computer-use", "version": "latest"}],
327
+ },
328
+ tags=["project.my_project", "env.dev"],
329
+ )
330
+ """
331
+ # Normalize tags
332
+ normalized_tags = None
333
+ if tags:
334
+ normalized_tags = [tag.replace("-", "_").replace(":", ".").replace(" ", "_") for tag in tags]
335
+
336
+ request = LaunchJobRequest(
337
+ world=WorldConfig(
338
+ package=world_package,
339
+ config=world_config,
340
+ ),
341
+ runtime=RuntimeConfig(artifact_id=runtime_artifact_id) if runtime_artifact_id else None,
342
+ tags=normalized_tags,
343
+ )
344
+
345
+ response: LaunchJobResponse = launch_job.sync(
346
+ client=self._http,
347
+ body=request,
348
+ x_api_key=self._api_key,
349
+ )
350
+
351
+ logger.info(f"Launched Chronos job: {response.session_id}")
352
+
353
+ return ChronosSession(
354
+ http_client=self._http,
355
+ api_key=self.api_key,
356
+ session_id=response.session_id,
357
+ plato_session_id=response.plato_session_id,
358
+ )
359
+
360
+ def get_session(self, session_id: str) -> ChronosSession:
361
+ """Get a ChronosSession wrapper for an existing session.
362
+
363
+ Args:
364
+ session_id: The Chronos session public ID.
365
+
366
+ Returns:
367
+ ChronosSession for the specified session.
368
+ """
369
+ # Verify the session exists by fetching status
370
+ get_session_status.sync(
371
+ client=self._http,
372
+ public_id=session_id,
373
+ x_api_key=self._api_key,
374
+ )
375
+
376
+ return ChronosSession(
377
+ http_client=self._http,
378
+ api_key=self.api_key,
379
+ session_id=session_id,
380
+ )
381
+
382
+ def list_sessions(
383
+ self,
384
+ tag: str | None = None,
385
+ ) -> SessionListResponse:
386
+ """List Chronos sessions.
387
+
388
+ Args:
389
+ tag: Filter by tag (fuzzy substring match).
390
+
391
+ Returns:
392
+ SessionListResponse with list of sessions.
393
+ """
394
+ return list_sessions.sync(
395
+ client=self._http,
396
+ tag=tag,
397
+ x_api_key=self._api_key,
398
+ )
399
+
400
+ def list_tags(self) -> TagsListResponse:
401
+ """List all unique tags across sessions.
402
+
403
+ Returns:
404
+ TagsListResponse with list of tags.
405
+ """
406
+ return list_tags.sync(
407
+ client=self._http,
408
+ x_api_key=self._api_key,
409
+ )
410
+
411
+ def close(self) -> None:
412
+ """Close the underlying HTTP client."""
413
+ self._http.close()
414
+
415
+ def __enter__(self) -> Chronos:
416
+ return self
417
+
418
+ def __exit__(self, *args: Any) -> None:
419
+ self.close()
plato/worlds/base.py CHANGED
@@ -227,7 +227,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
227
227
  self,
228
228
  image: str,
229
229
  config: dict,
230
- secrets: dict[str, str],
231
230
  instruction: str,
232
231
  workspace: str | None = None,
233
232
  logs_dir: str | None = None,
@@ -241,7 +240,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
241
240
  Args:
242
241
  image: Docker image URI
243
242
  config: Agent configuration dict
244
- secrets: Secret values (API keys, etc.)
245
243
  instruction: Task instruction for the agent
246
244
  workspace: Docker volume name for workspace
247
245
  logs_dir: Ignored (kept for backwards compatibility)
@@ -249,11 +247,13 @@ class BaseWorld(ABC, Generic[ConfigT]):
249
247
 
250
248
  Returns:
251
249
  The container name that was created
250
+
251
+ Note: Common API key environment variables (ANTHROPIC_API_KEY, etc.)
252
+ are automatically forwarded to the agent container.
252
253
  """
253
254
  container_name = await _run_agent_raw(
254
255
  image=image,
255
256
  config=config,
256
- secrets=secrets,
257
257
  instruction=instruction,
258
258
  workspace=workspace,
259
259
  logs_dir=logs_dir,
plato/worlds/config.py CHANGED
@@ -153,7 +153,6 @@ class RunConfig(BaseModel):
153
153
  session_id: str = ""
154
154
  otel_url: str = "" # OTel endpoint URL
155
155
  upload_url: str = "" # Presigned S3 URL for uploads
156
- all_secrets: dict[str, str] = Field(default_factory=dict) # All secrets (world + agent)
157
156
 
158
157
  # Serialized Plato session for connecting to VM and sending heartbeats
159
158
  # This is the output of Session.dump() - used to restore session with Session.load()
@@ -203,7 +202,7 @@ class RunConfig(BaseModel):
203
202
  env_list_field: dict | None = None # For EnvList marker (arbitrary envs)
204
203
 
205
204
  # Skip runtime fields
206
- runtime_fields = {"session_id", "otel_url", "upload_url", "all_secrets", "plato_session", "checkpoint", "state"}
205
+ runtime_fields = {"session_id", "otel_url", "upload_url", "plato_session", "checkpoint", "state"}
207
206
 
208
207
  for field_name, prop_schema in properties.items():
209
208
  if field_name in runtime_fields:
@@ -319,8 +318,8 @@ class RunConfig(BaseModel):
319
318
  # Handle agents dict -> individual agent fields
320
319
  agents_dict = data.pop("agents", {})
321
320
 
322
- # Handle secrets dict -> individual secret fields
323
- secrets_dict = data.pop("secrets", {})
321
+ # Handle secrets dict -> individual secret fields (for schema validation)
322
+ secrets_dict = data.pop("secrets", {}) # Pop but don't store separately
324
323
 
325
324
  # Check if there's an EnvList field - if so, don't pop envs as a dict
326
325
  has_env_list = any(isinstance(m, EnvList) for m in annotations.values())
@@ -356,9 +355,6 @@ class RunConfig(BaseModel):
356
355
  if isinstance(env_list, list):
357
356
  parsed[field_name] = [_parse_env_config(e) if isinstance(e, dict) else e for e in env_list]
358
357
 
359
- # Store all secrets for agent use
360
- parsed["all_secrets"] = secrets_dict
361
-
362
358
  return cls(**parsed)
363
359
 
364
360
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.5.1
3
+ Version: 2.6.1
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT