plato-sdk-v2 2.1.11__py3-none-any.whl → 2.2.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/_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 +15 -6
- plato/agents/logging.py +401 -0
- plato/agents/runner.py +98 -302
- plato/agents/trajectory.py +4 -4
- plato/chronos/models/__init__.py +1 -1
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +10 -0
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +84 -44
- plato/v1/cli/sandbox.py +47 -9
- plato/v1/cli/sim.py +11 -0
- plato/v1/cli/verify.py +1269 -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/async_/session.py +4 -4
- plato/v2/sync/session.py +4 -4
- plato/worlds/__init__.py +21 -2
- plato/worlds/base.py +222 -2
- plato/worlds/config.py +97 -7
- plato/worlds/runner.py +339 -1
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +34 -29
- plato/agents/callback.py +0 -246
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +0 -0
plato/agents/logging.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Chronos logging utilities.
|
|
2
|
+
|
|
3
|
+
Simple singleton-based logging for Chronos events and spans.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from plato.agents.logging import init_logging, span, log_event, upload_logs
|
|
7
|
+
|
|
8
|
+
# Initialize once (typically in world setup)
|
|
9
|
+
init_logging(
|
|
10
|
+
callback_url="http://chronos/api/callback",
|
|
11
|
+
session_id="session-123",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Use spans anywhere - parent tracking is automatic
|
|
15
|
+
async with span("clone_repo") as s:
|
|
16
|
+
s.log("Cloning...")
|
|
17
|
+
|
|
18
|
+
async with span("checkout"): # Automatically nested under clone_repo
|
|
19
|
+
s.log("Checking out...")
|
|
20
|
+
|
|
21
|
+
# Log events - parent is automatically the current span
|
|
22
|
+
await log_event(span_type="my_event", content="Something happened")
|
|
23
|
+
|
|
24
|
+
# Upload artifacts (zip of directory)
|
|
25
|
+
await upload_artifacts(dir_path="/path/to/logs")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import base64
|
|
31
|
+
import io
|
|
32
|
+
import logging
|
|
33
|
+
import zipfile
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
from uuid import uuid4
|
|
38
|
+
|
|
39
|
+
import httpx
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _ChronosLogger:
|
|
45
|
+
"""Internal singleton logger for Chronos events."""
|
|
46
|
+
|
|
47
|
+
_instance: _ChronosLogger | None = None
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
callback_url: str,
|
|
52
|
+
session_id: str,
|
|
53
|
+
parent_event_id: str | None = None,
|
|
54
|
+
):
|
|
55
|
+
self.callback_url = callback_url.rstrip("/")
|
|
56
|
+
self.session_id = session_id
|
|
57
|
+
self.parent_event_id = parent_event_id
|
|
58
|
+
self._current_span_id: str | None = None
|
|
59
|
+
self._enabled = bool(callback_url and session_id)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def enabled(self) -> bool:
|
|
63
|
+
return self._enabled
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def current_parent_id(self) -> str | None:
|
|
67
|
+
"""Get the current parent ID (current span or root parent)."""
|
|
68
|
+
return self._current_span_id or self.parent_event_id
|
|
69
|
+
|
|
70
|
+
async def log_event(
|
|
71
|
+
self,
|
|
72
|
+
span_type: str,
|
|
73
|
+
content: str = "",
|
|
74
|
+
source: str = "agent",
|
|
75
|
+
log_type: str | None = None,
|
|
76
|
+
step_number: int | None = None,
|
|
77
|
+
extra: dict[str, Any] | None = None,
|
|
78
|
+
event_id: str | None = None,
|
|
79
|
+
parent_id: str | None = None,
|
|
80
|
+
started_at: str | None = None,
|
|
81
|
+
ended_at: str | None = None,
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Log an event to Chronos."""
|
|
84
|
+
if not self._enabled:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
89
|
+
response = await client.post(
|
|
90
|
+
f"{self.callback_url}/event",
|
|
91
|
+
json={
|
|
92
|
+
"session_id": self.session_id,
|
|
93
|
+
"span_type": span_type,
|
|
94
|
+
"content": content,
|
|
95
|
+
"source": source,
|
|
96
|
+
"log_type": log_type,
|
|
97
|
+
"step_number": step_number,
|
|
98
|
+
"extra": extra,
|
|
99
|
+
"event_id": event_id,
|
|
100
|
+
"parent_id": parent_id,
|
|
101
|
+
"started_at": started_at,
|
|
102
|
+
"ended_at": ended_at,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
if response.status_code == 200:
|
|
106
|
+
return True
|
|
107
|
+
else:
|
|
108
|
+
logger.warning(f"Failed to log event: {response.status_code} {response.text}")
|
|
109
|
+
return False
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.warning(f"Failed to log event to Chronos: {e}")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SpanContext:
|
|
116
|
+
"""Context manager for logging spans with automatic timing and nesting."""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
span_type: str = "span",
|
|
122
|
+
source: str = "agent",
|
|
123
|
+
extra: dict[str, Any] | None = None,
|
|
124
|
+
):
|
|
125
|
+
self._name = name
|
|
126
|
+
self._span_type = span_type
|
|
127
|
+
self._source = source
|
|
128
|
+
self._extra = extra or {}
|
|
129
|
+
self._event_id: str | None = None
|
|
130
|
+
self._started_at: datetime | None = None
|
|
131
|
+
self._child_logs: list[str] = []
|
|
132
|
+
self._previous_span_id: str | None = None
|
|
133
|
+
|
|
134
|
+
async def __aenter__(self) -> SpanContext:
|
|
135
|
+
"""Log span start and set as current span."""
|
|
136
|
+
self._started_at = datetime.now(timezone.utc)
|
|
137
|
+
self._event_id = str(uuid4())
|
|
138
|
+
|
|
139
|
+
chronos = _ChronosLogger._instance
|
|
140
|
+
if chronos and chronos.enabled:
|
|
141
|
+
# Save previous span and set self as current
|
|
142
|
+
self._previous_span_id = chronos._current_span_id
|
|
143
|
+
parent_id = chronos.current_parent_id
|
|
144
|
+
|
|
145
|
+
await chronos.log_event(
|
|
146
|
+
span_type=self._span_type,
|
|
147
|
+
content=f"{self._name} started",
|
|
148
|
+
source=self._source,
|
|
149
|
+
event_id=self._event_id,
|
|
150
|
+
parent_id=parent_id,
|
|
151
|
+
started_at=self._started_at.isoformat(),
|
|
152
|
+
extra=self._extra,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Set self as the current span for child events
|
|
156
|
+
chronos._current_span_id = self._event_id
|
|
157
|
+
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
161
|
+
"""Log span end and restore previous span."""
|
|
162
|
+
ended_at = datetime.now(timezone.utc)
|
|
163
|
+
status = "failed" if exc_type else "completed"
|
|
164
|
+
|
|
165
|
+
chronos = _ChronosLogger._instance
|
|
166
|
+
if chronos and chronos.enabled:
|
|
167
|
+
# Restore previous span
|
|
168
|
+
chronos._current_span_id = self._previous_span_id
|
|
169
|
+
parent_id = self._previous_span_id or chronos.parent_event_id
|
|
170
|
+
|
|
171
|
+
# Update extra with any child logs
|
|
172
|
+
extra = {**self._extra}
|
|
173
|
+
if self._child_logs:
|
|
174
|
+
extra["logs"] = self._child_logs
|
|
175
|
+
if exc_type:
|
|
176
|
+
extra["error"] = str(exc_val)
|
|
177
|
+
|
|
178
|
+
await chronos.log_event(
|
|
179
|
+
span_type=self._span_type,
|
|
180
|
+
content=f"{self._name} {status}",
|
|
181
|
+
source=self._source,
|
|
182
|
+
event_id=self._event_id,
|
|
183
|
+
parent_id=parent_id,
|
|
184
|
+
started_at=self._started_at.isoformat() if self._started_at else None,
|
|
185
|
+
ended_at=ended_at.isoformat(),
|
|
186
|
+
extra=extra,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def log(self, message: str) -> None:
|
|
190
|
+
"""Add a log message to the span."""
|
|
191
|
+
self._child_logs.append(message)
|
|
192
|
+
logger.info(f"[{self._name}] {message}")
|
|
193
|
+
|
|
194
|
+
def set_extra(self, data: dict[str, Any]) -> None:
|
|
195
|
+
"""Update the span's extra data."""
|
|
196
|
+
self._extra.update(data)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def event_id(self) -> str | None:
|
|
200
|
+
"""Get the span's event ID (available after entering context)."""
|
|
201
|
+
return self._event_id
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class _NoOpSpanContext:
|
|
205
|
+
"""No-op span context when logging is not initialized."""
|
|
206
|
+
|
|
207
|
+
def __init__(self, name: str, **kwargs):
|
|
208
|
+
self._name = name
|
|
209
|
+
self._event_id = str(uuid4())
|
|
210
|
+
|
|
211
|
+
async def __aenter__(self) -> _NoOpSpanContext:
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
def log(self, message: str) -> None:
|
|
218
|
+
logger.debug(f"[{self._name}] {message}")
|
|
219
|
+
|
|
220
|
+
def set_extra(self, data: dict[str, Any]) -> None:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def event_id(self) -> str | None:
|
|
225
|
+
return self._event_id
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# =============================================================================
|
|
229
|
+
# Public API
|
|
230
|
+
# =============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def init_logging(
|
|
234
|
+
callback_url: str,
|
|
235
|
+
session_id: str,
|
|
236
|
+
parent_event_id: str | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Initialize Chronos logging.
|
|
239
|
+
|
|
240
|
+
Call this once at startup (e.g., in world.run()).
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
callback_url: Chronos callback URL (e.g., http://chronos/api/callback)
|
|
244
|
+
session_id: Chronos session ID
|
|
245
|
+
parent_event_id: Optional root parent event ID (e.g., step ID)
|
|
246
|
+
"""
|
|
247
|
+
_ChronosLogger._instance = _ChronosLogger(
|
|
248
|
+
callback_url=callback_url,
|
|
249
|
+
session_id=session_id,
|
|
250
|
+
parent_event_id=parent_event_id,
|
|
251
|
+
)
|
|
252
|
+
logger.info(f"Chronos logging initialized: session={session_id}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def set_parent_event_id(parent_event_id: str | None) -> None:
|
|
256
|
+
"""Set the root parent event ID.
|
|
257
|
+
|
|
258
|
+
Useful for updating the parent when entering a new step.
|
|
259
|
+
"""
|
|
260
|
+
if _ChronosLogger._instance:
|
|
261
|
+
_ChronosLogger._instance.parent_event_id = parent_event_id
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def span(
|
|
265
|
+
name: str,
|
|
266
|
+
span_type: str = "span",
|
|
267
|
+
source: str = "agent",
|
|
268
|
+
extra: dict[str, Any] | None = None,
|
|
269
|
+
) -> SpanContext | _NoOpSpanContext:
|
|
270
|
+
"""Create a span context manager.
|
|
271
|
+
|
|
272
|
+
Usage:
|
|
273
|
+
async with span("my_operation") as s:
|
|
274
|
+
s.log("Starting...")
|
|
275
|
+
# nested spans work automatically
|
|
276
|
+
async with span("sub_operation"):
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
name: Span name/description
|
|
281
|
+
span_type: Event type for the span
|
|
282
|
+
source: Event source (agent, world, system)
|
|
283
|
+
extra: Additional data
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
SpanContext (or NoOpSpanContext if logging not initialized)
|
|
287
|
+
"""
|
|
288
|
+
if _ChronosLogger._instance and _ChronosLogger._instance.enabled:
|
|
289
|
+
return SpanContext(
|
|
290
|
+
name=name,
|
|
291
|
+
span_type=span_type,
|
|
292
|
+
source=source,
|
|
293
|
+
extra=extra,
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
return _NoOpSpanContext(name=name)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def log_event(
|
|
300
|
+
span_type: str,
|
|
301
|
+
content: str = "",
|
|
302
|
+
source: str = "agent",
|
|
303
|
+
log_type: str | None = None,
|
|
304
|
+
extra: dict[str, Any] | None = None,
|
|
305
|
+
parent_id: str | None = None,
|
|
306
|
+
) -> bool:
|
|
307
|
+
"""Log a single event to Chronos.
|
|
308
|
+
|
|
309
|
+
For spans with timing, use span() instead.
|
|
310
|
+
"""
|
|
311
|
+
chronos = _ChronosLogger._instance
|
|
312
|
+
if not chronos or not chronos.enabled:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
return await chronos.log_event(
|
|
316
|
+
span_type=span_type,
|
|
317
|
+
content=content,
|
|
318
|
+
source=source,
|
|
319
|
+
log_type=log_type,
|
|
320
|
+
extra=extra,
|
|
321
|
+
parent_id=parent_id or chronos.current_parent_id,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def is_logging_enabled() -> bool:
|
|
326
|
+
"""Check if Chronos logging is enabled."""
|
|
327
|
+
return _ChronosLogger._instance is not None and _ChronosLogger._instance.enabled
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def reset_logging() -> None:
|
|
331
|
+
"""Reset the logger (mainly for testing)."""
|
|
332
|
+
_ChronosLogger._instance = None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# =============================================================================
|
|
336
|
+
# Logs Upload
|
|
337
|
+
# =============================================================================
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def zip_directory(dir_path: str) -> bytes:
|
|
341
|
+
"""Zip an entire directory.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
dir_path: Path to the directory
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Zip file contents as bytes.
|
|
348
|
+
"""
|
|
349
|
+
path = Path(dir_path)
|
|
350
|
+
buffer = io.BytesIO()
|
|
351
|
+
|
|
352
|
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
353
|
+
for file_path in path.rglob("*"):
|
|
354
|
+
if file_path.is_file():
|
|
355
|
+
arcname = file_path.relative_to(path)
|
|
356
|
+
zf.write(file_path, arcname)
|
|
357
|
+
|
|
358
|
+
buffer.seek(0)
|
|
359
|
+
return buffer.read()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def upload_artifacts(dir_path: str) -> str | None:
|
|
363
|
+
"""Upload a directory as a zip to Chronos.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
dir_path: Path to the directory to upload
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
URL if successful, None otherwise.
|
|
370
|
+
"""
|
|
371
|
+
chronos = _ChronosLogger._instance
|
|
372
|
+
if not chronos or not chronos.enabled:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
zip_data = zip_directory(dir_path)
|
|
377
|
+
zip_base64 = base64.b64encode(zip_data).decode("utf-8")
|
|
378
|
+
logger.info(f"Zipped directory: {len(zip_data)} bytes")
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.warning(f"Failed to zip directory: {e}")
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
385
|
+
response = await client.post(
|
|
386
|
+
f"{chronos.callback_url}/logs-upload",
|
|
387
|
+
json={
|
|
388
|
+
"session_id": chronos.session_id,
|
|
389
|
+
"logs_base64": zip_base64,
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
if response.status_code == 200:
|
|
393
|
+
result = response.json()
|
|
394
|
+
logger.info(f"Uploaded artifacts: {result}")
|
|
395
|
+
return result.get("logs_url")
|
|
396
|
+
else:
|
|
397
|
+
logger.warning(f"Failed to upload artifacts: {response.status_code} {response.text}")
|
|
398
|
+
return None
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.warning(f"Failed to upload artifacts: {e}")
|
|
401
|
+
return None
|