plato-sdk-v2 2.1.14__py3-none-any.whl → 2.1.16__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/__init__.py +15 -6
- plato/agents/logging.py +401 -0
- plato/agents/runner.py +88 -312
- plato/worlds/base.py +43 -174
- plato/worlds/config.py +1 -1
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/RECORD +9 -9
- plato/agents/callback.py +0 -332
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.1.14.dist-info → plato_sdk_v2-2.1.16.dist-info}/entry_points.txt +0 -0
plato/worlds/base.py
CHANGED
|
@@ -4,11 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
-
from datetime import datetime, timezone
|
|
8
7
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, get_args, get_origin
|
|
9
|
-
from uuid import uuid4
|
|
10
8
|
|
|
11
|
-
import httpx
|
|
12
9
|
from pydantic import BaseModel, Field
|
|
13
10
|
|
|
14
11
|
from plato.worlds.config import RunConfig
|
|
@@ -16,6 +13,11 @@ from plato.worlds.config import RunConfig
|
|
|
16
13
|
if TYPE_CHECKING:
|
|
17
14
|
from plato.v2.async_.session import Session
|
|
18
15
|
|
|
16
|
+
from plato.agents.logging import init_logging as _init_chronos_logging
|
|
17
|
+
from plato.agents.logging import log_event as _log_event
|
|
18
|
+
from plato.agents.logging import reset_logging as _reset_chronos_logging
|
|
19
|
+
from plato.agents.logging import span as _span
|
|
20
|
+
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
23
|
# Global registry of worlds
|
|
@@ -104,7 +106,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
104
106
|
self._step_count: int = 0
|
|
105
107
|
self.plato_session = None
|
|
106
108
|
self._current_step_id: str | None = None
|
|
107
|
-
self._session_span_id: str | None = None
|
|
108
109
|
|
|
109
110
|
@classmethod
|
|
110
111
|
def get_config_class(cls) -> type[RunConfig]:
|
|
@@ -171,6 +172,9 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
171
172
|
This is called automatically during run() to restore the session
|
|
172
173
|
and start sending heartbeats while the world runs.
|
|
173
174
|
"""
|
|
175
|
+
if not self.config.plato_session:
|
|
176
|
+
return
|
|
177
|
+
|
|
174
178
|
try:
|
|
175
179
|
from plato.v2.async_.session import Session
|
|
176
180
|
|
|
@@ -189,111 +193,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
189
193
|
except Exception as e:
|
|
190
194
|
self.logger.warning(f"Error stopping Plato heartbeat: {e}")
|
|
191
195
|
|
|
192
|
-
async def _log_event(
|
|
193
|
-
self,
|
|
194
|
-
span_type: str,
|
|
195
|
-
content: str = "",
|
|
196
|
-
source: str = "world",
|
|
197
|
-
log_type: str | None = None,
|
|
198
|
-
event_id: str | None = None,
|
|
199
|
-
parent_id: str | None = None,
|
|
200
|
-
step_number: int | None = None,
|
|
201
|
-
extra: dict[str, Any] | None = None,
|
|
202
|
-
started_at: datetime | None = None,
|
|
203
|
-
ended_at: datetime | None = None,
|
|
204
|
-
) -> str | None:
|
|
205
|
-
"""Log an event to the callback API.
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
span_type: Type of event (reset, step, trajectory, agent_action, etc.)
|
|
209
|
-
content: Event description
|
|
210
|
-
source: Event source (world, agent, system)
|
|
211
|
-
log_type: Log type for rendering (atif, step, etc.)
|
|
212
|
-
event_id: Unique ID for this event (generated if not provided)
|
|
213
|
-
parent_id: Parent event ID for nesting
|
|
214
|
-
step_number: Step number for ordering
|
|
215
|
-
extra: Additional structured data
|
|
216
|
-
started_at: When the event started
|
|
217
|
-
ended_at: When the event ended
|
|
218
|
-
|
|
219
|
-
Returns:
|
|
220
|
-
The event ID if successfully logged, None otherwise
|
|
221
|
-
"""
|
|
222
|
-
if not self.config.callback_url or not self.config.session_id:
|
|
223
|
-
return None
|
|
224
|
-
|
|
225
|
-
event_id = event_id or str(uuid4())
|
|
226
|
-
|
|
227
|
-
try:
|
|
228
|
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
229
|
-
response = await client.post(
|
|
230
|
-
f"{self.config.callback_url}/event",
|
|
231
|
-
json={
|
|
232
|
-
"session_id": self.config.session_id,
|
|
233
|
-
"event_id": event_id,
|
|
234
|
-
"parent_id": parent_id,
|
|
235
|
-
"source": source,
|
|
236
|
-
"span_type": span_type,
|
|
237
|
-
"log_type": log_type,
|
|
238
|
-
"step_number": step_number,
|
|
239
|
-
"content": content,
|
|
240
|
-
"extra": extra,
|
|
241
|
-
"started_at": started_at.isoformat() if started_at else None,
|
|
242
|
-
"ended_at": ended_at.isoformat() if ended_at else None,
|
|
243
|
-
},
|
|
244
|
-
)
|
|
245
|
-
if response.status_code == 200:
|
|
246
|
-
return event_id
|
|
247
|
-
else:
|
|
248
|
-
self.logger.warning(f"Failed to log event: {response.status_code} {response.text}")
|
|
249
|
-
except Exception as e:
|
|
250
|
-
self.logger.warning(f"Failed to log event: {e}")
|
|
251
|
-
|
|
252
|
-
return None
|
|
253
|
-
|
|
254
|
-
async def log_agent_action(
|
|
255
|
-
self,
|
|
256
|
-
agent_name: str,
|
|
257
|
-
action: str,
|
|
258
|
-
content: str,
|
|
259
|
-
extra: dict[str, Any] | None = None,
|
|
260
|
-
) -> str | None:
|
|
261
|
-
"""Log an agent action during a step.
|
|
262
|
-
|
|
263
|
-
Args:
|
|
264
|
-
agent_name: Name of the agent
|
|
265
|
-
action: Action type (e.g., tool_call, response)
|
|
266
|
-
content: Action content/description
|
|
267
|
-
extra: Additional data (tool input, output, etc.)
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
The event ID if successfully logged
|
|
271
|
-
"""
|
|
272
|
-
return await self._log_event(
|
|
273
|
-
span_type="agent_action",
|
|
274
|
-
content=f"[{agent_name}] {action}: {content}",
|
|
275
|
-
source="agent",
|
|
276
|
-
parent_id=self._current_step_id,
|
|
277
|
-
step_number=self._step_count,
|
|
278
|
-
extra={"agent": agent_name, "action": action, **(extra or {})},
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
async def log_trajectory(self, trajectory: dict[str, Any]) -> str | None:
|
|
282
|
-
"""Log an ATIF trajectory.
|
|
283
|
-
|
|
284
|
-
Args:
|
|
285
|
-
trajectory: ATIF trajectory dict
|
|
286
|
-
|
|
287
|
-
Returns:
|
|
288
|
-
The event ID if successfully logged
|
|
289
|
-
"""
|
|
290
|
-
return await self._log_event(
|
|
291
|
-
span_type="trajectory",
|
|
292
|
-
log_type="atif",
|
|
293
|
-
source="agent",
|
|
294
|
-
extra=trajectory,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
196
|
async def run(self, config: ConfigT) -> None:
|
|
298
197
|
"""Run the world: reset -> step until done -> close.
|
|
299
198
|
|
|
@@ -305,84 +204,55 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
305
204
|
|
|
306
205
|
self.logger.info(f"Starting world '{self.name}'")
|
|
307
206
|
|
|
207
|
+
# Initialize the logging singleton for agents to use
|
|
208
|
+
if config.callback_url and config.session_id:
|
|
209
|
+
_init_chronos_logging(
|
|
210
|
+
callback_url=config.callback_url,
|
|
211
|
+
session_id=config.session_id,
|
|
212
|
+
)
|
|
213
|
+
|
|
308
214
|
# Connect to Plato session if configured (for heartbeats)
|
|
309
215
|
await self._connect_plato_session()
|
|
310
216
|
|
|
311
217
|
# Log session start
|
|
312
|
-
|
|
313
|
-
self._session_span_id = await self._log_event(
|
|
218
|
+
await _log_event(
|
|
314
219
|
span_type="session_start",
|
|
315
220
|
content=f"World '{self.name}' started",
|
|
316
221
|
source="world",
|
|
317
|
-
started_at=session_start,
|
|
318
222
|
extra={"world_name": self.name, "world_version": self.get_version()},
|
|
319
223
|
)
|
|
320
224
|
|
|
321
225
|
try:
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
content="World reset started",
|
|
328
|
-
source="world",
|
|
329
|
-
event_id=reset_id,
|
|
330
|
-
parent_id=self._session_span_id,
|
|
331
|
-
started_at=reset_start,
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
obs = await self.reset()
|
|
335
|
-
reset_end = datetime.now(timezone.utc)
|
|
336
|
-
|
|
337
|
-
await self._log_event(
|
|
338
|
-
span_type="reset",
|
|
339
|
-
content="World reset complete",
|
|
340
|
-
source="world",
|
|
341
|
-
event_id=reset_id,
|
|
342
|
-
parent_id=self._session_span_id,
|
|
343
|
-
started_at=reset_start,
|
|
344
|
-
ended_at=reset_end,
|
|
345
|
-
extra={"observation": obs.model_dump() if hasattr(obs, "model_dump") else str(obs)},
|
|
346
|
-
)
|
|
226
|
+
# Execute reset with automatic span tracking
|
|
227
|
+
async with _span("reset", span_type="reset", source="world") as reset_span:
|
|
228
|
+
reset_span.log(f"Resetting world '{self.name}'")
|
|
229
|
+
obs = await self.reset()
|
|
230
|
+
reset_span.set_extra({"observation": obs.model_dump() if hasattr(obs, "model_dump") else str(obs)})
|
|
347
231
|
self.logger.info(f"World reset complete: {obs}")
|
|
348
232
|
|
|
349
233
|
while True:
|
|
350
234
|
self._step_count += 1
|
|
351
|
-
step_start = datetime.now(timezone.utc)
|
|
352
|
-
self._current_step_id = str(uuid4())
|
|
353
235
|
|
|
354
|
-
#
|
|
355
|
-
|
|
236
|
+
# Execute step with automatic span tracking
|
|
237
|
+
# The span automatically sets itself as the current parent,
|
|
238
|
+
# so agent trajectories will nest under this step
|
|
239
|
+
async with _span(
|
|
240
|
+
f"step_{self._step_count}",
|
|
356
241
|
span_type="step",
|
|
357
|
-
content=f"Step {self._step_count} started",
|
|
358
242
|
source="world",
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
source="world",
|
|
373
|
-
event_id=self._current_step_id,
|
|
374
|
-
parent_id=self._session_span_id,
|
|
375
|
-
step_number=self._step_count,
|
|
376
|
-
started_at=step_start,
|
|
377
|
-
ended_at=step_end,
|
|
378
|
-
extra={
|
|
379
|
-
"done": result.done,
|
|
380
|
-
"observation": result.observation.model_dump()
|
|
381
|
-
if hasattr(result.observation, "model_dump")
|
|
382
|
-
else str(result.observation),
|
|
383
|
-
"info": result.info,
|
|
384
|
-
},
|
|
385
|
-
)
|
|
243
|
+
) as step_span:
|
|
244
|
+
self._current_step_id = step_span.event_id
|
|
245
|
+
step_span.log(f"Step {self._step_count} started")
|
|
246
|
+
result = await self.step()
|
|
247
|
+
step_span.set_extra(
|
|
248
|
+
{
|
|
249
|
+
"done": result.done,
|
|
250
|
+
"observation": result.observation.model_dump()
|
|
251
|
+
if hasattr(result.observation, "model_dump")
|
|
252
|
+
else str(result.observation),
|
|
253
|
+
"info": result.info,
|
|
254
|
+
}
|
|
255
|
+
)
|
|
386
256
|
|
|
387
257
|
self.logger.info(f"Step {self._step_count}: done={result.done}")
|
|
388
258
|
|
|
@@ -394,15 +264,14 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
394
264
|
await self._disconnect_plato_session()
|
|
395
265
|
|
|
396
266
|
# Log session end
|
|
397
|
-
|
|
398
|
-
await self._log_event(
|
|
267
|
+
await _log_event(
|
|
399
268
|
span_type="session_end",
|
|
400
269
|
content=f"World '{self.name}' completed after {self._step_count} steps",
|
|
401
270
|
source="world",
|
|
402
|
-
parent_id=self._session_span_id,
|
|
403
|
-
started_at=session_start,
|
|
404
|
-
ended_at=session_end,
|
|
405
271
|
extra={"total_steps": self._step_count},
|
|
406
272
|
)
|
|
407
273
|
|
|
274
|
+
# Reset the logging singleton
|
|
275
|
+
_reset_chronos_logging()
|
|
276
|
+
|
|
408
277
|
self.logger.info(f"World '{self.name}' completed after {self._step_count} steps")
|
plato/worlds/config.py
CHANGED
|
@@ -74,7 +74,7 @@ class RunConfig(BaseModel):
|
|
|
74
74
|
|
|
75
75
|
# Serialized Plato session for connecting to VM and sending heartbeats
|
|
76
76
|
# This is the output of Session.dump() - used to restore session with Session.load()
|
|
77
|
-
plato_session: SerializedSession
|
|
77
|
+
plato_session: SerializedSession | None = None
|
|
78
78
|
|
|
79
79
|
model_config = {"extra": "allow"}
|
|
80
80
|
|
|
@@ -294,12 +294,12 @@ plato/_sims_generator/templates/python/errors.py.jinja,sha256=8L_FbHczBNLXJrbSlN
|
|
|
294
294
|
plato/_sims_generator/templates/python/package_init.py.jinja,sha256=sOcJxUT0LuOWu5jOMGGKYxfCEjcYQv1hGF3n0iOA4hQ,986
|
|
295
295
|
plato/_sims_generator/templates/python/tag_init.py.jinja,sha256=WB_9cv0JKIVg5TOXeSolET3tAfVg7sExjboh5jbCXz4,170
|
|
296
296
|
plato/_sims_generator/templates/python/version_init.py.jinja,sha256=sGvFcYVfzXFyQDAe0PSOrg9yys93KE0XInFQNb1TvCY,179
|
|
297
|
-
plato/agents/__init__.py,sha256=
|
|
297
|
+
plato/agents/__init__.py,sha256=qslIFTVSe1yFeTRCKr8Z-mInWarj2HDbNZV4u6AiXek,2755
|
|
298
298
|
plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
|
|
299
299
|
plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
|
|
300
|
-
plato/agents/callback.py,sha256=3BdQfAx_nVDRimx9FiuhpaDFiLnvMNB0thCKeszz3D8,11188
|
|
301
300
|
plato/agents/config.py,sha256=VZVMdCmEQnoR0VkrGdScG8p6zSKVFe7BZPd2h8lKNjI,5460
|
|
302
|
-
plato/agents/
|
|
301
|
+
plato/agents/logging.py,sha256=z9rDlGPbrpcTS8PephbK2rDqT7thC1KyLkua4ypUkv4,12210
|
|
302
|
+
plato/agents/runner.py,sha256=rOWYTSAhdola_FdrbviP955NBusNtBUy-q_c5fDA9to,5123
|
|
303
303
|
plato/agents/trajectory.py,sha256=ayXEMCfYvIuXU2JkQWfPOVO9JywNSx8Kf6Ztrgsdh-I,10441
|
|
304
304
|
plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
|
|
305
305
|
plato/chronos/client.py,sha256=YcOGtHWERyOD9z8LKt8bRMVL0cEwL2hiAP4qQgdZlUI,5495
|
|
@@ -459,11 +459,11 @@ plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
|
|
|
459
459
|
plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
|
|
460
460
|
plato/worlds/README.md,sha256=TgG4aidude0ouJSCfY81Ev45hsUxPkO85HUIiWNqkcc,5463
|
|
461
461
|
plato/worlds/__init__.py,sha256=0ogDVw5GEk4OT7FSD0Z7SrafTn_TBdo8DOU_v6ncbd4,1478
|
|
462
|
-
plato/worlds/base.py,sha256=
|
|
462
|
+
plato/worlds/base.py,sha256=vLAHkYpYg4kl7LgftQ0uC6xXhASVqREogn6msNWA6Hk,9650
|
|
463
463
|
plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
|
|
464
|
-
plato/worlds/config.py,sha256
|
|
464
|
+
plato/worlds/config.py,sha256=-K1dnlRL5I-DNYEoc1I4cKIsTw53X_1QrRY2SWJvRiQ,6256
|
|
465
465
|
plato/worlds/runner.py,sha256=r9B2BxBae8_dM7y5cJf9xhThp_I1Qvf_tlPq2rs8qC8,4013
|
|
466
|
-
plato_sdk_v2-2.1.
|
|
467
|
-
plato_sdk_v2-2.1.
|
|
468
|
-
plato_sdk_v2-2.1.
|
|
469
|
-
plato_sdk_v2-2.1.
|
|
466
|
+
plato_sdk_v2-2.1.16.dist-info/METADATA,sha256=5S_mx4Iij8UNv8od9y6b3n8T00t6gFzM01-1wS-rLdg,8509
|
|
467
|
+
plato_sdk_v2-2.1.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
468
|
+
plato_sdk_v2-2.1.16.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
|
|
469
|
+
plato_sdk_v2-2.1.16.dist-info/RECORD,,
|
plato/agents/callback.py
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
"""Chronos callback utilities for agents.
|
|
2
|
-
|
|
3
|
-
Simple API:
|
|
4
|
-
- log_event() - Log any event (trajectory, logs, status changes)
|
|
5
|
-
- upload_logs() - Upload logs to S3
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import base64
|
|
11
|
-
import io
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
import zipfile
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
|
-
import httpx
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ChronosCallback:
|
|
24
|
-
"""Utility class for communicating with Chronos server.
|
|
25
|
-
|
|
26
|
-
Example:
|
|
27
|
-
callback = ChronosCallback(
|
|
28
|
-
callback_url="http://chronos.example.com/api/callback",
|
|
29
|
-
session_id="abc123",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
# Log a trajectory event
|
|
33
|
-
await callback.log_event(
|
|
34
|
-
span_type="trajectory",
|
|
35
|
-
log_type="atif",
|
|
36
|
-
extra=trajectory_dict,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
# Upload logs to S3
|
|
40
|
-
await callback.upload_logs(logs_dir="/path/to/logs")
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(self, callback_url: str, session_id: str):
|
|
44
|
-
"""Initialize the callback client.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
callback_url: Full callback base URL (e.g., http://server/api/callback)
|
|
48
|
-
session_id: The Chronos session ID for this run
|
|
49
|
-
"""
|
|
50
|
-
self.callback_url = callback_url.rstrip("/")
|
|
51
|
-
self.session_id = session_id
|
|
52
|
-
self._enabled = bool(callback_url and session_id)
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def enabled(self) -> bool:
|
|
56
|
-
"""Check if callbacks are enabled (both server and session_id set)."""
|
|
57
|
-
return self._enabled
|
|
58
|
-
|
|
59
|
-
async def log_event(
|
|
60
|
-
self,
|
|
61
|
-
span_type: str,
|
|
62
|
-
content: str = "",
|
|
63
|
-
source: str = "agent",
|
|
64
|
-
log_type: str | None = None,
|
|
65
|
-
step_number: int | None = None,
|
|
66
|
-
extra: dict[str, Any] | None = None,
|
|
67
|
-
event_id: str | None = None,
|
|
68
|
-
parent_id: str | None = None,
|
|
69
|
-
started_at: str | None = None,
|
|
70
|
-
ended_at: str | None = None,
|
|
71
|
-
) -> bool:
|
|
72
|
-
"""Log an event to Chronos.
|
|
73
|
-
|
|
74
|
-
Use this for all event types:
|
|
75
|
-
- Trajectory: span_type='trajectory', log_type='atif', extra={...atif data...}
|
|
76
|
-
- Steps: span_type='step', extra={observation, done, info}
|
|
77
|
-
- Status: span_type='status_change', extra={status: '...'}
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
span_type: Event type (trajectory, step, reset, agent_action, etc.)
|
|
81
|
-
content: Event description
|
|
82
|
-
source: Event source (agent, world, system)
|
|
83
|
-
log_type: Log type for rendering (atif, step, etc.)
|
|
84
|
-
step_number: Step number for ordering
|
|
85
|
-
extra: Structured data (trajectory, step info, etc.)
|
|
86
|
-
event_id: Unique event ID (generated if not provided)
|
|
87
|
-
parent_id: Parent event ID for nesting
|
|
88
|
-
started_at: ISO timestamp when event started
|
|
89
|
-
ended_at: ISO timestamp when event ended
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
True if event was logged successfully, False otherwise.
|
|
93
|
-
"""
|
|
94
|
-
if not self._enabled:
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
99
|
-
response = await client.post(
|
|
100
|
-
f"{self.callback_url}/event",
|
|
101
|
-
json={
|
|
102
|
-
"session_id": self.session_id,
|
|
103
|
-
"span_type": span_type,
|
|
104
|
-
"content": content,
|
|
105
|
-
"source": source,
|
|
106
|
-
"log_type": log_type,
|
|
107
|
-
"step_number": step_number,
|
|
108
|
-
"extra": extra,
|
|
109
|
-
"event_id": event_id,
|
|
110
|
-
"parent_id": parent_id,
|
|
111
|
-
"started_at": started_at,
|
|
112
|
-
"ended_at": ended_at,
|
|
113
|
-
},
|
|
114
|
-
)
|
|
115
|
-
if response.status_code == 200:
|
|
116
|
-
return True
|
|
117
|
-
else:
|
|
118
|
-
logger.warning(f"Failed to log event: {response.status_code} {response.text}")
|
|
119
|
-
return False
|
|
120
|
-
except Exception as e:
|
|
121
|
-
logger.warning(f"Failed to log event to Chronos: {e}")
|
|
122
|
-
return False
|
|
123
|
-
|
|
124
|
-
async def update_status(
|
|
125
|
-
self,
|
|
126
|
-
status: str,
|
|
127
|
-
message: str | None = None,
|
|
128
|
-
extra: dict[str, Any] | None = None,
|
|
129
|
-
api_key: str | None = None,
|
|
130
|
-
) -> bool:
|
|
131
|
-
"""Update the session status in Chronos.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
status: New status (running, completed, failed)
|
|
135
|
-
message: Optional status message
|
|
136
|
-
extra: Optional additional data
|
|
137
|
-
api_key: API key for closing Plato session on terminal status
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
True if status was updated successfully, False otherwise.
|
|
141
|
-
"""
|
|
142
|
-
if not self._enabled:
|
|
143
|
-
return False
|
|
144
|
-
|
|
145
|
-
try:
|
|
146
|
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
147
|
-
response = await client.post(
|
|
148
|
-
f"{self.callback_url}/status",
|
|
149
|
-
json={
|
|
150
|
-
"session_id": self.session_id,
|
|
151
|
-
"status": status,
|
|
152
|
-
"message": message,
|
|
153
|
-
"extra": extra,
|
|
154
|
-
"api_key": api_key,
|
|
155
|
-
},
|
|
156
|
-
)
|
|
157
|
-
return response.status_code == 200
|
|
158
|
-
except Exception as e:
|
|
159
|
-
logger.warning(f"Failed to update status in Chronos: {e}")
|
|
160
|
-
return False
|
|
161
|
-
|
|
162
|
-
def find_trajectory(self, logs_dir: str) -> dict[str, Any] | None:
|
|
163
|
-
"""Find and load ATIF trajectory from logs directory.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
logs_dir: Path to the logs directory
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
Parsed ATIF trajectory dict if found, None otherwise.
|
|
170
|
-
"""
|
|
171
|
-
trajectory_path = Path(logs_dir) / "agent" / "trajectory.json"
|
|
172
|
-
|
|
173
|
-
if not trajectory_path.exists():
|
|
174
|
-
logger.info(f"No trajectory file at {trajectory_path}")
|
|
175
|
-
return None
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
with open(trajectory_path) as f:
|
|
179
|
-
data = json.load(f)
|
|
180
|
-
if isinstance(data, dict) and "schema_version" in data:
|
|
181
|
-
logger.info(f"Found ATIF trajectory: {trajectory_path}")
|
|
182
|
-
return data
|
|
183
|
-
logger.info(f"Trajectory file exists but no schema_version: {trajectory_path}")
|
|
184
|
-
return None
|
|
185
|
-
except Exception as e:
|
|
186
|
-
logger.warning(f"Failed to load trajectory from {trajectory_path}: {e}")
|
|
187
|
-
return None
|
|
188
|
-
|
|
189
|
-
def zip_logs_dir(self, logs_dir: str) -> bytes:
|
|
190
|
-
"""Zip the entire logs directory.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
logs_dir: Path to the logs directory
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
Zip file contents as bytes.
|
|
197
|
-
"""
|
|
198
|
-
logs_path = Path(logs_dir)
|
|
199
|
-
buffer = io.BytesIO()
|
|
200
|
-
|
|
201
|
-
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
202
|
-
for file_path in logs_path.rglob("*"):
|
|
203
|
-
if file_path.is_file():
|
|
204
|
-
arcname = file_path.relative_to(logs_path)
|
|
205
|
-
zf.write(file_path, arcname)
|
|
206
|
-
|
|
207
|
-
buffer.seek(0)
|
|
208
|
-
return buffer.read()
|
|
209
|
-
|
|
210
|
-
async def upload_logs(self, logs_dir: str) -> str | None:
|
|
211
|
-
"""Upload logs directory to S3 via Chronos.
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
logs_dir: Path to the logs directory
|
|
215
|
-
|
|
216
|
-
Returns:
|
|
217
|
-
S3 URL if successful, None otherwise.
|
|
218
|
-
"""
|
|
219
|
-
if not self._enabled:
|
|
220
|
-
return None
|
|
221
|
-
|
|
222
|
-
try:
|
|
223
|
-
logs_zip = self.zip_logs_dir(logs_dir)
|
|
224
|
-
logs_base64 = base64.b64encode(logs_zip).decode("utf-8")
|
|
225
|
-
logger.info(f"Zipped logs: {len(logs_zip)} bytes")
|
|
226
|
-
except Exception as e:
|
|
227
|
-
logger.warning(f"Failed to zip logs: {e}")
|
|
228
|
-
return None
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
232
|
-
response = await client.post(
|
|
233
|
-
f"{self.callback_url}/logs-upload",
|
|
234
|
-
json={
|
|
235
|
-
"session_id": self.session_id,
|
|
236
|
-
"logs_base64": logs_base64,
|
|
237
|
-
},
|
|
238
|
-
)
|
|
239
|
-
if response.status_code == 200:
|
|
240
|
-
result = response.json()
|
|
241
|
-
logger.info(f"Uploaded logs to Chronos: {result}")
|
|
242
|
-
return result.get("logs_url")
|
|
243
|
-
else:
|
|
244
|
-
logger.warning(f"Failed to upload logs: {response.status_code} {response.text}")
|
|
245
|
-
return None
|
|
246
|
-
except Exception as e:
|
|
247
|
-
logger.warning(f"Failed to upload logs to Chronos: {e}")
|
|
248
|
-
return None
|
|
249
|
-
|
|
250
|
-
async def upload_artifacts(
|
|
251
|
-
self,
|
|
252
|
-
logs_dir: str,
|
|
253
|
-
trajectory: dict[str, Any] | None = None,
|
|
254
|
-
agent_image: str | None = None,
|
|
255
|
-
) -> bool:
|
|
256
|
-
"""Upload trajectory and logs to Chronos.
|
|
257
|
-
|
|
258
|
-
If trajectory is not provided, attempts to find it in the logs directory.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
logs_dir: Path to the logs directory
|
|
262
|
-
trajectory: Optional pre-loaded trajectory dict.
|
|
263
|
-
agent_image: Optional agent Docker image URI to include in trajectory.
|
|
264
|
-
|
|
265
|
-
Returns:
|
|
266
|
-
True if successful, False otherwise.
|
|
267
|
-
"""
|
|
268
|
-
if not self._enabled:
|
|
269
|
-
return False
|
|
270
|
-
|
|
271
|
-
success = False
|
|
272
|
-
|
|
273
|
-
# Find and upload trajectory
|
|
274
|
-
if trajectory is None:
|
|
275
|
-
trajectory = self.find_trajectory(logs_dir)
|
|
276
|
-
|
|
277
|
-
if trajectory:
|
|
278
|
-
# Merge agent image into trajectory
|
|
279
|
-
if agent_image:
|
|
280
|
-
agent = trajectory.get("agent", {})
|
|
281
|
-
extra = agent.get("extra") or {}
|
|
282
|
-
extra["image"] = agent_image
|
|
283
|
-
agent["extra"] = extra
|
|
284
|
-
trajectory["agent"] = agent
|
|
285
|
-
|
|
286
|
-
logger.info("Uploading trajectory to Chronos")
|
|
287
|
-
if await self.log_event(
|
|
288
|
-
span_type="trajectory",
|
|
289
|
-
log_type="atif",
|
|
290
|
-
extra=trajectory,
|
|
291
|
-
source="agent",
|
|
292
|
-
):
|
|
293
|
-
logger.info("Trajectory uploaded successfully")
|
|
294
|
-
success = True
|
|
295
|
-
else:
|
|
296
|
-
logger.warning("Failed to upload trajectory")
|
|
297
|
-
|
|
298
|
-
# Upload logs
|
|
299
|
-
logs_url = await self.upload_logs(logs_dir)
|
|
300
|
-
if logs_url:
|
|
301
|
-
logger.info(f"Logs uploaded: {logs_url}")
|
|
302
|
-
success = True
|
|
303
|
-
|
|
304
|
-
return success
|
|
305
|
-
|
|
306
|
-
# Legacy methods for backwards compatibility
|
|
307
|
-
|
|
308
|
-
async def push_logs(self, logs: list[dict[str, Any]]) -> bool:
|
|
309
|
-
"""Legacy: Push log entries (just acknowledges now)."""
|
|
310
|
-
if not self._enabled:
|
|
311
|
-
return False
|
|
312
|
-
try:
|
|
313
|
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
314
|
-
response = await client.post(
|
|
315
|
-
f"{self.callback_url}/logs",
|
|
316
|
-
json={"session_id": self.session_id, "logs": logs},
|
|
317
|
-
)
|
|
318
|
-
return response.status_code == 200
|
|
319
|
-
except Exception:
|
|
320
|
-
return False
|
|
321
|
-
|
|
322
|
-
async def push_log(
|
|
323
|
-
self,
|
|
324
|
-
message: str,
|
|
325
|
-
level: str = "info",
|
|
326
|
-
extra: dict[str, Any] | None = None,
|
|
327
|
-
) -> bool:
|
|
328
|
-
"""Legacy: Push a single log entry."""
|
|
329
|
-
log_entry: dict[str, Any] = {"level": level, "message": message}
|
|
330
|
-
if extra:
|
|
331
|
-
log_entry["extra"] = extra
|
|
332
|
-
return await self.push_logs([log_entry])
|
|
File without changes
|
|
File without changes
|