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.
- plato/_generated/__init__.py +1 -1
- plato/_generated/api/v1/evals/get_scores_by_user.py +7 -0
- plato/_generated/api/v1/testcases/get_testcases.py +14 -0
- plato/_generated/api/v2/sessions/setup_sandbox.py +2 -2
- plato/_generated/models/__init__.py +42 -2
- plato/agents/runner.py +2 -33
- plato/chronos/api/admin/__init__.py +17 -0
- plato/chronos/api/admin/clear_database_api_admin_clear_db_post.py +57 -0
- plato/chronos/api/admin/sync_agents_api_admin_sync_agents_post.py +67 -0
- plato/chronos/api/admin/sync_all_api_admin_sync_all_post.py +67 -0
- plato/chronos/api/admin/sync_runtimes_api_admin_sync_runtimes_post.py +67 -0
- plato/chronos/api/admin/sync_worlds_api_admin_sync_worlds_post.py +67 -0
- plato/chronos/api/agents/list_agents.py +5 -5
- plato/chronos/api/checkpoints/__init__.py +8 -0
- plato/chronos/api/checkpoints/list_checkpoints.py +52 -0
- plato/chronos/api/checkpoints/preview_checkpoint.py +74 -0
- plato/chronos/api/events/__init__.py +8 -0
- plato/chronos/api/events/get_session_event.py +68 -0
- plato/chronos/api/events/list_session_events.py +62 -0
- plato/chronos/api/jobs/launch_job.py +8 -2
- plato/chronos/api/otel/__init__.py +8 -0
- plato/chronos/api/otel/get_session_traces_api_otel_sessions__session_id__traces_get.py +56 -0
- plato/chronos/api/otel/receive_traces_api_otel_v1_traces_post.py +49 -0
- plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +5 -5
- plato/chronos/api/runtimes/__init__.py +2 -1
- plato/chronos/api/runtimes/get_runtime_logs.py +61 -0
- plato/chronos/api/sessions/__init__.py +24 -1
- plato/chronos/api/sessions/close_session.py +66 -0
- plato/chronos/api/sessions/complete_session.py +74 -0
- plato/chronos/api/sessions/create_session.py +69 -0
- plato/chronos/api/sessions/get_session.py +20 -2
- plato/chronos/api/sessions/get_session_bash_logs_download.py +61 -0
- plato/chronos/api/sessions/get_session_envs.py +62 -0
- plato/chronos/api/sessions/get_session_live_logs.py +62 -0
- plato/chronos/api/sessions/get_session_logs.py +3 -3
- plato/chronos/api/sessions/get_session_status.py +62 -0
- plato/chronos/api/sessions/list_sessions.py +20 -2
- plato/chronos/api/sessions/list_tags.py +80 -0
- plato/chronos/api/sessions/update_session_tags.py +68 -0
- plato/chronos/models/__init__.py +241 -196
- plato/v1/cli/chronos.py +66 -4
- plato/v2/__init__.py +8 -0
- plato/v2/async_/__init__.py +4 -0
- plato/v2/async_/chronos.py +419 -0
- plato/v2/sync/__init__.py +3 -0
- plato/v2/sync/chronos.py +419 -0
- plato/worlds/base.py +3 -3
- plato/worlds/config.py +3 -7
- {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/RECORD +52 -25
- {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.5.1.dist-info → plato_sdk_v2-2.6.1.dist-info}/entry_points.txt +0 -0
plato/v1/cli/chronos.py
CHANGED
|
@@ -98,9 +98,13 @@ def launch(
|
|
|
98
98
|
raise typer.Exit(1)
|
|
99
99
|
|
|
100
100
|
# Build request
|
|
101
|
+
# Normalize tags for ltree: replace '-' with '_', ':' with '.'
|
|
102
|
+
raw_tags = job_config.get("tags", [])
|
|
103
|
+
normalized_tags = [tag.replace("-", "_").replace(":", ".").replace(" ", "_") for tag in raw_tags]
|
|
101
104
|
request_body = {
|
|
102
105
|
"world": job_config["world"],
|
|
103
106
|
"runtime": job_config.get("runtime", {}),
|
|
107
|
+
"tags": normalized_tags,
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
world_package = job_config["world"]["package"]
|
|
@@ -371,6 +375,7 @@ async def _create_chronos_session(
|
|
|
371
375
|
world_name: str,
|
|
372
376
|
world_config: dict,
|
|
373
377
|
plato_session_id: str | None = None,
|
|
378
|
+
tags: list[str] | None = None,
|
|
374
379
|
) -> dict:
|
|
375
380
|
"""Create a session in Chronos."""
|
|
376
381
|
import httpx
|
|
@@ -384,6 +389,7 @@ async def _create_chronos_session(
|
|
|
384
389
|
"world_name": world_name,
|
|
385
390
|
"world_config": world_config,
|
|
386
391
|
"plato_session_id": plato_session_id,
|
|
392
|
+
"tags": tags or [],
|
|
387
393
|
},
|
|
388
394
|
headers={"x-api-key": api_key},
|
|
389
395
|
)
|
|
@@ -541,12 +547,14 @@ async def _run_dev_impl(
|
|
|
541
547
|
|
|
542
548
|
# Create Chronos session
|
|
543
549
|
console.print("[blue]Creating Chronos session...[/blue]")
|
|
550
|
+
tags = raw_config.get("tags", [])
|
|
544
551
|
chronos_session = await _create_chronos_session(
|
|
545
552
|
chronos_url=chronos_url,
|
|
546
553
|
api_key=api_key,
|
|
547
554
|
world_name=world_name,
|
|
548
555
|
world_config=config_data,
|
|
549
556
|
plato_session_id=plato_session_id,
|
|
557
|
+
tags=tags,
|
|
550
558
|
)
|
|
551
559
|
chronos_session_id = chronos_session["public_id"]
|
|
552
560
|
console.print(f"[green]✅ Created Chronos session: {chronos_session_id}[/green]")
|
|
@@ -634,10 +642,6 @@ async def _run_dev_impl(
|
|
|
634
642
|
f"UPLOAD_URL={chronos_session.get('upload_url', '')}",
|
|
635
643
|
]
|
|
636
644
|
|
|
637
|
-
# Add secrets as env vars
|
|
638
|
-
for key, value in config_data.get("secrets", {}).items():
|
|
639
|
-
docker_cmd.extend(["-e", f"{key.upper()}={value}"])
|
|
640
|
-
|
|
641
645
|
# Use world runner image
|
|
642
646
|
docker_cmd.append(world_runner_image)
|
|
643
647
|
|
|
@@ -699,6 +703,64 @@ async def _run_dev_impl(
|
|
|
699
703
|
await plato.close()
|
|
700
704
|
|
|
701
705
|
|
|
706
|
+
@chronos_app.command()
|
|
707
|
+
def stop(
|
|
708
|
+
session_id: Annotated[
|
|
709
|
+
str,
|
|
710
|
+
typer.Argument(help="Session ID to stop"),
|
|
711
|
+
],
|
|
712
|
+
chronos_url: str = typer.Option(
|
|
713
|
+
None,
|
|
714
|
+
"--url",
|
|
715
|
+
"-u",
|
|
716
|
+
envvar="CHRONOS_URL",
|
|
717
|
+
help="Chronos API URL (default: https://chronos.plato.so)",
|
|
718
|
+
),
|
|
719
|
+
api_key: str = typer.Option(
|
|
720
|
+
None,
|
|
721
|
+
"--api-key",
|
|
722
|
+
"-k",
|
|
723
|
+
envvar="PLATO_API_KEY",
|
|
724
|
+
help="Plato API key for authentication",
|
|
725
|
+
),
|
|
726
|
+
):
|
|
727
|
+
"""
|
|
728
|
+
Stop a running Chronos session.
|
|
729
|
+
|
|
730
|
+
This marks the session as cancelled with status reason "User cancelled".
|
|
731
|
+
|
|
732
|
+
Examples:
|
|
733
|
+
plato chronos stop abc123
|
|
734
|
+
plato chronos stop abc123 --url https://chronos.plato.so
|
|
735
|
+
"""
|
|
736
|
+
# Set defaults
|
|
737
|
+
if not chronos_url:
|
|
738
|
+
chronos_url = "https://chronos.plato.so"
|
|
739
|
+
|
|
740
|
+
if not api_key:
|
|
741
|
+
console.print("[red]❌ No API key provided[/red]")
|
|
742
|
+
console.print("Set PLATO_API_KEY environment variable or use --api-key")
|
|
743
|
+
raise typer.Exit(1)
|
|
744
|
+
|
|
745
|
+
console.print(f"[yellow]⏹ Stopping session {session_id}...[/yellow]")
|
|
746
|
+
|
|
747
|
+
async def _stop():
|
|
748
|
+
await _complete_chronos_session(
|
|
749
|
+
chronos_url=chronos_url,
|
|
750
|
+
api_key=api_key,
|
|
751
|
+
session_id=session_id,
|
|
752
|
+
status="cancelled",
|
|
753
|
+
error_message="User cancelled",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
asyncio.run(_stop())
|
|
758
|
+
console.print(f"[green]✅ Session {session_id} stopped[/green]")
|
|
759
|
+
except Exception as e:
|
|
760
|
+
console.print(f"[red]❌ Failed to stop session: {e}[/red]")
|
|
761
|
+
raise typer.Exit(1)
|
|
762
|
+
|
|
763
|
+
|
|
702
764
|
@chronos_app.command()
|
|
703
765
|
def dev(
|
|
704
766
|
config: Annotated[
|
plato/v2/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Usage:
|
|
4
4
|
# from plato.v2 import Plato, Session, Environment # Sync
|
|
5
5
|
# from plato.v2 import AsyncPlato, AsyncSession, AsyncEnvironment # Async
|
|
6
|
+
# from plato.v2 import Chronos, AsyncChronos # Chronos job management
|
|
6
7
|
# from plato.v2 import Env, SimConfigCompute, Flow # Helpers
|
|
7
8
|
|
|
8
9
|
# Models
|
|
@@ -11,12 +12,15 @@ from plato._generated.models import ArtifactInfoResponse, Flow
|
|
|
11
12
|
from plato.v2 import async_, sync
|
|
12
13
|
|
|
13
14
|
# Async exports (prefixed with Async)
|
|
15
|
+
from plato.v2.async_.chronos import AsyncChronos
|
|
16
|
+
from plato.v2.async_.chronos import ChronosSession as AsyncChronosSession
|
|
14
17
|
from plato.v2.async_.client import AsyncPlato
|
|
15
18
|
from plato.v2.async_.environment import Environment as AsyncEnvironment
|
|
16
19
|
from plato.v2.async_.flow_executor import FlowExecutionError as AsyncFlowExecutionError
|
|
17
20
|
from plato.v2.async_.flow_executor import FlowExecutor as AsyncFlowExecutor
|
|
18
21
|
from plato.v2.async_.session import SerializedSession
|
|
19
22
|
from plato.v2.async_.session import Session as AsyncSession
|
|
23
|
+
from plato.v2.sync.chronos import Chronos, ChronosSession
|
|
20
24
|
from plato.v2.sync.client import Plato
|
|
21
25
|
from plato.v2.sync.environment import Environment
|
|
22
26
|
from plato.v2.sync.flow_executor import FlowExecutionError, FlowExecutor
|
|
@@ -40,6 +44,8 @@ __all__ = [
|
|
|
40
44
|
"FlowExecutor",
|
|
41
45
|
"FlowExecutionError",
|
|
42
46
|
"ArtifactInfoResponse",
|
|
47
|
+
"Chronos",
|
|
48
|
+
"ChronosSession",
|
|
43
49
|
# Async
|
|
44
50
|
"AsyncPlato",
|
|
45
51
|
"AsyncSession",
|
|
@@ -47,6 +53,8 @@ __all__ = [
|
|
|
47
53
|
"AsyncFlowExecutor",
|
|
48
54
|
"AsyncFlowExecutionError",
|
|
49
55
|
"SerializedSession",
|
|
56
|
+
"AsyncChronos",
|
|
57
|
+
"AsyncChronosSession",
|
|
50
58
|
# Models
|
|
51
59
|
"Flow",
|
|
52
60
|
# Helpers
|
plato/v2/async_/__init__.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Plato SDK v2 - Async API."""
|
|
2
2
|
|
|
3
3
|
from plato._generated.models import ArtifactInfoResponse
|
|
4
|
+
from plato.v2.async_.chronos import AsyncChronos
|
|
5
|
+
from plato.v2.async_.chronos import ChronosSession as AsyncChronosSession
|
|
4
6
|
from plato.v2.async_.client import AsyncPlato as Plato
|
|
5
7
|
from plato.v2.async_.environment import Environment
|
|
6
8
|
from plato.v2.async_.flow_executor import FlowExecutionError, FlowExecutor
|
|
@@ -14,4 +16,6 @@ __all__ = [
|
|
|
14
16
|
"LoginResult",
|
|
15
17
|
"FlowExecutor",
|
|
16
18
|
"FlowExecutionError",
|
|
19
|
+
"AsyncChronos",
|
|
20
|
+
"AsyncChronosSession",
|
|
17
21
|
]
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Plato SDK v2 - Asynchronous 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 asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
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.AsyncClient,
|
|
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
|
+
async 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 await get_session_status.asyncio(
|
|
86
|
+
client=self._http,
|
|
87
|
+
public_id=self._session_id,
|
|
88
|
+
x_api_key=self._api_key,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async 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 await get_session.asyncio(
|
|
98
|
+
client=self._http,
|
|
99
|
+
public_id=self._session_id,
|
|
100
|
+
x_api_key=self._api_key,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def get_envs(self) -> SessionEnvsResponse:
|
|
104
|
+
"""Get environment information for this session.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
SessionEnvsResponse with list of environments.
|
|
108
|
+
"""
|
|
109
|
+
return await get_session_envs.asyncio(
|
|
110
|
+
client=self._http,
|
|
111
|
+
public_id=self._session_id,
|
|
112
|
+
x_api_key=self._api_key,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async 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 await get_session_logs.asyncio(
|
|
128
|
+
client=self._http,
|
|
129
|
+
public_id=self._session_id,
|
|
130
|
+
limit=limit,
|
|
131
|
+
x_api_key=self._api_key,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async 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 await update_session_tags.asyncio(
|
|
147
|
+
client=self._http,
|
|
148
|
+
public_id=self._session_id,
|
|
149
|
+
body=request,
|
|
150
|
+
x_api_key=self._api_key,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async 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 await complete_session.asyncio(
|
|
167
|
+
client=self._http,
|
|
168
|
+
public_id=self._session_id,
|
|
169
|
+
body=request,
|
|
170
|
+
x_api_key=self._api_key,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async 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 await complete_session.asyncio(
|
|
195
|
+
client=self._http,
|
|
196
|
+
public_id=self._session_id,
|
|
197
|
+
body=request,
|
|
198
|
+
x_api_key=self._api_key,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async 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
|
+
await close_session.asyncio(
|
|
207
|
+
client=self._http,
|
|
208
|
+
public_id=self._session_id,
|
|
209
|
+
x_api_key=self._api_key,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
async 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 = await self.get_status()
|
|
234
|
+
if status_response.status in terminal_statuses:
|
|
235
|
+
return await self.get_details()
|
|
236
|
+
|
|
237
|
+
await asyncio.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 AsyncChronos:
|
|
247
|
+
"""Asynchronous 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.async_ import AsyncChronos
|
|
254
|
+
|
|
255
|
+
async with AsyncChronos() as chronos:
|
|
256
|
+
# Launch a job
|
|
257
|
+
session = await 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 = await session.wait_until_complete()
|
|
265
|
+
print(f"Status: {result.status}")
|
|
266
|
+
|
|
267
|
+
# Or poll manually
|
|
268
|
+
while True:
|
|
269
|
+
status = await session.get_status()
|
|
270
|
+
if status.status in ("completed", "failed"):
|
|
271
|
+
break
|
|
272
|
+
await asyncio.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.AsyncClient(
|
|
298
|
+
base_url=self.base_url,
|
|
299
|
+
timeout=httpx.Timeout(timeout),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async 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 = await 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 = await launch_job.asyncio(
|
|
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
|
+
async 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
|
+
await get_session_status.asyncio(
|
|
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
|
+
async 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 await list_sessions.asyncio(
|
|
395
|
+
client=self._http,
|
|
396
|
+
tag=tag,
|
|
397
|
+
x_api_key=self._api_key,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
async def list_tags(self) -> TagsListResponse:
|
|
401
|
+
"""List all unique tags across sessions.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
TagsListResponse with list of tags.
|
|
405
|
+
"""
|
|
406
|
+
return await list_tags.asyncio(
|
|
407
|
+
client=self._http,
|
|
408
|
+
x_api_key=self._api_key,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
async def close(self) -> None:
|
|
412
|
+
"""Close the underlying HTTP client."""
|
|
413
|
+
await self._http.aclose()
|
|
414
|
+
|
|
415
|
+
async def __aenter__(self) -> AsyncChronos:
|
|
416
|
+
return self
|
|
417
|
+
|
|
418
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
419
|
+
await self.close()
|
plato/v2/sync/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Plato SDK v2 - Sync API."""
|
|
2
2
|
|
|
3
3
|
from plato._generated.models import ArtifactInfoResponse
|
|
4
|
+
from plato.v2.sync.chronos import Chronos, ChronosSession
|
|
4
5
|
from plato.v2.sync.client import Plato
|
|
5
6
|
from plato.v2.sync.environment import Environment
|
|
6
7
|
from plato.v2.sync.session import Session
|
|
@@ -10,4 +11,6 @@ __all__ = [
|
|
|
10
11
|
"Plato",
|
|
11
12
|
"Session",
|
|
12
13
|
"Environment",
|
|
14
|
+
"Chronos",
|
|
15
|
+
"ChronosSession",
|
|
13
16
|
]
|