plato-sdk-v2 2.8.8__py3-none-any.whl → 2.9.0__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/base.py +3 -9
- plato/agents/config.py +7 -64
- plato/agents/markers.py +18 -0
- plato/agents/otel.py +22 -60
- plato/agents/schema.py +96 -0
- plato/cli/chronos.py +2 -100
- plato/worlds/__init__.py +2 -6
- plato/worlds/base.py +44 -130
- plato/worlds/config.py +5 -166
- plato/worlds/markers.py +63 -0
- plato/worlds/models.py +23 -0
- plato/worlds/schema.py +154 -0
- {plato_sdk_v2-2.8.8.dist-info → plato_sdk_v2-2.9.0.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.8.8.dist-info → plato_sdk_v2-2.9.0.dist-info}/RECORD +16 -11
- {plato_sdk_v2-2.8.8.dist-info → plato_sdk_v2-2.9.0.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.8.8.dist-info → plato_sdk_v2-2.9.0.dist-info}/entry_points.txt +0 -0
plato/agents/base.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import importlib.metadata
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
from abc import ABC, abstractmethod
|
|
@@ -9,6 +10,7 @@ from pathlib import Path
|
|
|
9
10
|
from typing import Any, ClassVar, Generic, TypeVar, get_args, get_origin
|
|
10
11
|
|
|
11
12
|
from plato.agents.config import AgentConfig
|
|
13
|
+
from plato.agents.schema import get_agent_schema
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
@@ -91,8 +93,6 @@ class BaseAgent(ABC, Generic[ConfigT]):
|
|
|
91
93
|
@classmethod
|
|
92
94
|
def get_version(cls) -> str:
|
|
93
95
|
"""Get version from package metadata."""
|
|
94
|
-
import importlib.metadata
|
|
95
|
-
|
|
96
96
|
for pkg_name in [cls.__module__.split(".")[0], f"plato-agent-{cls.name}"]:
|
|
97
97
|
try:
|
|
98
98
|
return importlib.metadata.version(pkg_name)
|
|
@@ -103,13 +103,7 @@ class BaseAgent(ABC, Generic[ConfigT]):
|
|
|
103
103
|
@classmethod
|
|
104
104
|
def get_schema(cls) -> dict:
|
|
105
105
|
"""Get full schema for the agent including config and build schemas."""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
config_class = cls.get_config_class()
|
|
109
|
-
return {
|
|
110
|
-
"config": config_class.get_json_schema(),
|
|
111
|
-
"build": BuildConfig.get_json_schema(),
|
|
112
|
-
}
|
|
106
|
+
return get_agent_schema(cls)
|
|
113
107
|
|
|
114
108
|
@abstractmethod
|
|
115
109
|
async def run(self, instruction: str) -> None:
|
plato/agents/config.py
CHANGED
|
@@ -17,25 +17,15 @@ Example:
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import base64
|
|
20
21
|
import json
|
|
22
|
+
import os
|
|
21
23
|
from typing import Any
|
|
22
24
|
|
|
23
25
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"""Annotation marker for secret fields.
|
|
28
|
-
|
|
29
|
-
Fields annotated with Secret are automatically loaded from environment variables.
|
|
30
|
-
The env var name is the uppercase version of the field name (e.g., api_key -> API_KEY).
|
|
31
|
-
|
|
32
|
-
Usage:
|
|
33
|
-
api_key: Annotated[str, Secret(description="API key")]
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(self, description: str = "", required: bool = False):
|
|
37
|
-
self.description = description
|
|
38
|
-
self.required = required
|
|
27
|
+
from plato.agents.markers import Secret
|
|
28
|
+
from plato.agents.schema import get_agent_config_schema, get_field_secrets
|
|
39
29
|
|
|
40
30
|
|
|
41
31
|
class AgentConfig(BaseSettings):
|
|
@@ -68,56 +58,12 @@ class AgentConfig(BaseSettings):
|
|
|
68
58
|
@classmethod
|
|
69
59
|
def get_field_secrets(cls) -> dict[str, Secret]:
|
|
70
60
|
"""Get Secret annotations for each field."""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
for field_name, field_info in cls.model_fields.items():
|
|
74
|
-
for meta in field_info.metadata:
|
|
75
|
-
if isinstance(meta, Secret):
|
|
76
|
-
result[field_name] = meta
|
|
77
|
-
break
|
|
78
|
-
|
|
79
|
-
return result
|
|
61
|
+
return get_field_secrets(cls)
|
|
80
62
|
|
|
81
63
|
@classmethod
|
|
82
64
|
def get_json_schema(cls) -> dict:
|
|
83
65
|
"""Get JSON schema with secrets separated."""
|
|
84
|
-
|
|
85
|
-
full_schema.pop("title", None)
|
|
86
|
-
|
|
87
|
-
secrets_map = cls.get_field_secrets()
|
|
88
|
-
properties = full_schema.get("properties", {})
|
|
89
|
-
|
|
90
|
-
config_properties = {}
|
|
91
|
-
secrets = []
|
|
92
|
-
|
|
93
|
-
# Skip internal fields
|
|
94
|
-
internal_fields = {"logs_dir"}
|
|
95
|
-
|
|
96
|
-
for field_name, prop_schema in properties.items():
|
|
97
|
-
if field_name in internal_fields:
|
|
98
|
-
continue
|
|
99
|
-
|
|
100
|
-
if field_name in secrets_map:
|
|
101
|
-
secret = secrets_map[field_name]
|
|
102
|
-
secrets.append(
|
|
103
|
-
{
|
|
104
|
-
"name": field_name,
|
|
105
|
-
"description": secret.description,
|
|
106
|
-
"required": secret.required,
|
|
107
|
-
}
|
|
108
|
-
)
|
|
109
|
-
else:
|
|
110
|
-
config_properties[field_name] = prop_schema
|
|
111
|
-
|
|
112
|
-
required = [r for r in full_schema.get("required", []) if r not in internal_fields and r not in secrets_map]
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
116
|
-
"type": "object",
|
|
117
|
-
"properties": config_properties,
|
|
118
|
-
"required": required,
|
|
119
|
-
"secrets": secrets,
|
|
120
|
-
}
|
|
66
|
+
return get_agent_config_schema(cls)
|
|
121
67
|
|
|
122
68
|
def get_secrets_dict(self) -> dict[str, str]:
|
|
123
69
|
"""Extract secret values as a dict for environment variables."""
|
|
@@ -137,7 +83,7 @@ class AgentConfig(BaseSettings):
|
|
|
137
83
|
internal_fields = {"logs_dir"}
|
|
138
84
|
|
|
139
85
|
result: dict[str, Any] = {}
|
|
140
|
-
for field_name in self.model_fields:
|
|
86
|
+
for field_name in self.__class__.model_fields:
|
|
141
87
|
if field_name not in secrets_map and field_name not in internal_fields:
|
|
142
88
|
value = getattr(self, field_name, None)
|
|
143
89
|
if value is not None:
|
|
@@ -152,9 +98,6 @@ class AgentConfig(BaseSettings):
|
|
|
152
98
|
The runner passes config as base64-encoded JSON in the
|
|
153
99
|
AGENT_CONFIG_B64 environment variable.
|
|
154
100
|
"""
|
|
155
|
-
import base64
|
|
156
|
-
import os
|
|
157
|
-
|
|
158
101
|
config_b64 = os.environ.get("AGENT_CONFIG_B64")
|
|
159
102
|
if not config_b64:
|
|
160
103
|
raise ValueError("AGENT_CONFIG_B64 environment variable not set")
|
plato/agents/markers.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Annotation markers for Plato agent configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Secret:
|
|
7
|
+
"""Annotation marker for secret fields.
|
|
8
|
+
|
|
9
|
+
Fields annotated with Secret are automatically loaded from environment variables.
|
|
10
|
+
The env var name is the uppercase version of the field name (e.g., api_key -> API_KEY).
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
api_key: Annotated[str, Secret(description="API key")]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, description: str = "", required: bool = False):
|
|
17
|
+
self.description = description
|
|
18
|
+
self.required = required
|
plato/agents/otel.py
CHANGED
|
@@ -26,11 +26,19 @@ Usage:
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
28
|
import logging
|
|
29
|
+
import os
|
|
29
30
|
|
|
31
|
+
from opentelemetry import context as context_api
|
|
30
32
|
from opentelemetry import trace
|
|
31
|
-
from opentelemetry.
|
|
33
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
34
|
+
OTLPSpanExporter,
|
|
35
|
+
)
|
|
36
|
+
from opentelemetry.sdk.resources import Resource
|
|
37
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
38
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
39
|
+
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags, Tracer
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
34
42
|
|
|
35
43
|
# Global state
|
|
36
44
|
_tracer_provider = None
|
|
@@ -39,10 +47,7 @@ _log_handler = None
|
|
|
39
47
|
|
|
40
48
|
|
|
41
49
|
class OTelSpanLogHandler(logging.Handler):
|
|
42
|
-
"""Logging handler that creates OTel spans for log messages.
|
|
43
|
-
|
|
44
|
-
Converts Python log records to OTel spans with log attributes.
|
|
45
|
-
"""
|
|
50
|
+
"""Logging handler that creates OTel spans for log messages."""
|
|
46
51
|
|
|
47
52
|
def __init__(self, tracer: Tracer, level: int = logging.INFO):
|
|
48
53
|
super().__init__(level)
|
|
@@ -51,9 +56,6 @@ class OTelSpanLogHandler(logging.Handler):
|
|
|
51
56
|
def emit(self, record: logging.LogRecord) -> None:
|
|
52
57
|
"""Emit a log record as an OTel span."""
|
|
53
58
|
try:
|
|
54
|
-
# Debug: print that we're emitting a log span
|
|
55
|
-
print(f"[OTel] Emitting log span: {record.name} - {record.getMessage()[:100]}")
|
|
56
|
-
# Create a span for the log message
|
|
57
59
|
with self.tracer.start_as_current_span(f"log.{record.levelname.lower()}") as span:
|
|
58
60
|
span.set_attribute("log.level", record.levelname)
|
|
59
61
|
span.set_attribute("log.message", record.getMessage())
|
|
@@ -66,12 +68,9 @@ class OTelSpanLogHandler(logging.Handler):
|
|
|
66
68
|
if record.lineno:
|
|
67
69
|
span.set_attribute("log.lineno", record.lineno)
|
|
68
70
|
|
|
69
|
-
# Mark errors
|
|
70
71
|
if record.levelno >= logging.ERROR:
|
|
71
72
|
span.set_attribute("error", True)
|
|
72
|
-
|
|
73
73
|
except Exception:
|
|
74
|
-
# Don't let logging errors crash the application
|
|
75
74
|
pass
|
|
76
75
|
|
|
77
76
|
|
|
@@ -94,20 +93,10 @@ def init_tracing(
|
|
|
94
93
|
global _tracer_provider, _initialized, _log_handler
|
|
95
94
|
|
|
96
95
|
if _initialized:
|
|
97
|
-
|
|
96
|
+
logger.debug("Tracing already initialized")
|
|
98
97
|
return
|
|
99
98
|
|
|
100
99
|
try:
|
|
101
|
-
from opentelemetry import context as context_api
|
|
102
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
103
|
-
OTLPSpanExporter,
|
|
104
|
-
)
|
|
105
|
-
from opentelemetry.sdk.resources import Resource
|
|
106
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
107
|
-
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
108
|
-
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
|
|
109
|
-
|
|
110
|
-
# Create resource with session ID
|
|
111
100
|
resource = Resource.create(
|
|
112
101
|
{
|
|
113
102
|
"service.name": service_name,
|
|
@@ -115,53 +104,39 @@ def init_tracing(
|
|
|
115
104
|
}
|
|
116
105
|
)
|
|
117
106
|
|
|
118
|
-
# Create tracer provider
|
|
119
107
|
_tracer_provider = TracerProvider(resource=resource)
|
|
120
108
|
|
|
121
|
-
# Add OTLP exporter pointing to Chronos (use SimpleSpanProcessor for immediate export)
|
|
122
109
|
otlp_exporter = OTLPSpanExporter(endpoint=f"{otlp_endpoint.rstrip('/')}/v1/traces")
|
|
123
110
|
_tracer_provider.add_span_processor(SimpleSpanProcessor(otlp_exporter))
|
|
124
111
|
|
|
125
|
-
# Set as global tracer provider
|
|
126
112
|
trace.set_tracer_provider(_tracer_provider)
|
|
127
113
|
|
|
128
|
-
# If parent context is provided, set it as the current context
|
|
129
|
-
# This allows new spans to automatically link to the parent
|
|
130
114
|
if parent_trace_id and parent_span_id:
|
|
131
115
|
parent_context = SpanContext(
|
|
132
116
|
trace_id=int(parent_trace_id, 16),
|
|
133
117
|
span_id=int(parent_span_id, 16),
|
|
134
118
|
is_remote=True,
|
|
135
|
-
trace_flags=TraceFlags(0x01),
|
|
119
|
+
trace_flags=TraceFlags(0x01),
|
|
136
120
|
)
|
|
137
121
|
parent_span = NonRecordingSpan(parent_context)
|
|
138
122
|
ctx = trace.set_span_in_context(parent_span)
|
|
139
123
|
context_api.attach(ctx)
|
|
140
|
-
|
|
124
|
+
logger.debug(f"Using parent context: trace_id={parent_trace_id}, span_id={parent_span_id}")
|
|
141
125
|
|
|
142
|
-
# Add OTel logging handler to capture logs from plato SDK
|
|
143
126
|
tracer = trace.get_tracer(service_name)
|
|
144
127
|
_log_handler = OTelSpanLogHandler(tracer, level=logging.INFO)
|
|
145
128
|
|
|
146
|
-
# Add handler to plato loggers (worlds and agents)
|
|
147
|
-
# Set level to INFO to ensure logs propagate from child loggers
|
|
148
129
|
plato_logger = logging.getLogger("plato")
|
|
149
130
|
plato_logger.setLevel(logging.INFO)
|
|
150
131
|
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
|
-
)
|
|
154
132
|
|
|
155
133
|
_initialized = True
|
|
156
|
-
|
|
157
|
-
print(f"[OTel] Tracing initialized: service={service_name}, session={session_id}, endpoint={otlp_endpoint}")
|
|
134
|
+
logger.debug(f"Tracing initialized: service={service_name}, session={session_id}")
|
|
158
135
|
|
|
159
136
|
except ImportError as e:
|
|
160
|
-
|
|
161
|
-
_module_logger.warning(f"OpenTelemetry SDK not installed: {e}")
|
|
137
|
+
logger.warning(f"OpenTelemetry SDK not installed: {e}")
|
|
162
138
|
except Exception as e:
|
|
163
|
-
|
|
164
|
-
_module_logger.error(f"Failed to initialize tracing: {e}")
|
|
139
|
+
logger.error(f"Failed to initialize tracing: {e}")
|
|
165
140
|
|
|
166
141
|
|
|
167
142
|
def shutdown_tracing(timeout_millis: int = 30000) -> None:
|
|
@@ -172,7 +147,6 @@ def shutdown_tracing(timeout_millis: int = 30000) -> None:
|
|
|
172
147
|
"""
|
|
173
148
|
global _tracer_provider, _initialized, _log_handler
|
|
174
149
|
|
|
175
|
-
# Remove log handler
|
|
176
150
|
if _log_handler:
|
|
177
151
|
try:
|
|
178
152
|
plato_logger = logging.getLogger("plato")
|
|
@@ -183,19 +157,13 @@ def shutdown_tracing(timeout_millis: int = 30000) -> None:
|
|
|
183
157
|
|
|
184
158
|
if _tracer_provider:
|
|
185
159
|
try:
|
|
186
|
-
# Force flush all pending spans before shutdown
|
|
187
|
-
print(f"[OTel] Flushing spans (timeout={timeout_millis}ms)...")
|
|
188
160
|
flush_success = _tracer_provider.force_flush(timeout_millis=timeout_millis)
|
|
189
|
-
if flush_success:
|
|
190
|
-
|
|
191
|
-
else:
|
|
192
|
-
print("[OTel] Span flush timed out or failed")
|
|
193
|
-
|
|
161
|
+
if not flush_success:
|
|
162
|
+
logger.warning("Span flush timed out")
|
|
194
163
|
_tracer_provider.shutdown()
|
|
195
|
-
|
|
164
|
+
logger.debug("Tracing shutdown complete")
|
|
196
165
|
except Exception as e:
|
|
197
|
-
|
|
198
|
-
_module_logger.warning(f"Error shutting down tracer: {e}")
|
|
166
|
+
logger.warning(f"Error shutting down tracer: {e}")
|
|
199
167
|
|
|
200
168
|
_tracer_provider = None
|
|
201
169
|
_initialized = False
|
|
@@ -235,21 +203,15 @@ def instrument(service_name: str = "plato-agent") -> Tracer:
|
|
|
235
203
|
Returns:
|
|
236
204
|
OpenTelemetry Tracer
|
|
237
205
|
"""
|
|
238
|
-
import os
|
|
239
|
-
|
|
240
206
|
otel_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
241
207
|
session_id = os.environ.get("SESSION_ID", "local")
|
|
242
208
|
parent_trace_id = os.environ.get("OTEL_TRACE_ID")
|
|
243
209
|
parent_span_id = os.environ.get("OTEL_PARENT_SPAN_ID")
|
|
244
210
|
|
|
245
|
-
print(f"[OTel] instrument() called: service={service_name}, endpoint={otel_endpoint}, session={session_id}")
|
|
246
|
-
|
|
247
211
|
if not otel_endpoint:
|
|
248
|
-
|
|
249
|
-
print("[OTel] No OTEL_EXPORTER_OTLP_ENDPOINT set, returning no-op tracer")
|
|
212
|
+
logger.debug("No OTEL_EXPORTER_OTLP_ENDPOINT set, using no-op tracer")
|
|
250
213
|
return trace.get_tracer(service_name)
|
|
251
214
|
|
|
252
|
-
# Initialize tracing with parent context if provided
|
|
253
215
|
init_tracing(
|
|
254
216
|
service_name=service_name,
|
|
255
217
|
session_id=session_id,
|
plato/agents/schema.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""JSON schema generation for Plato agent configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from plato.agents.markers import Secret
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from plato.agents.config import AgentConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_field_secrets(config_cls: type[AgentConfig]) -> dict[str, Secret]:
|
|
14
|
+
"""Get Secret annotations for each field.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config_cls: The AgentConfig subclass to inspect
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Dict mapping field name to Secret annotation
|
|
21
|
+
"""
|
|
22
|
+
result: dict[str, Secret] = {}
|
|
23
|
+
|
|
24
|
+
for field_name, field_info in config_cls.model_fields.items():
|
|
25
|
+
for meta in field_info.metadata:
|
|
26
|
+
if isinstance(meta, Secret):
|
|
27
|
+
result[field_name] = meta
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_agent_config_schema(config_cls: type[AgentConfig]) -> dict[str, Any]:
|
|
34
|
+
"""Get JSON schema for an agent config with secrets separated.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config_cls: The AgentConfig subclass to generate schema for
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
JSON schema dict with properties, required, and secrets fields
|
|
41
|
+
"""
|
|
42
|
+
full_schema = config_cls.model_json_schema()
|
|
43
|
+
full_schema.pop("title", None)
|
|
44
|
+
|
|
45
|
+
secrets_map = get_field_secrets(config_cls)
|
|
46
|
+
properties = full_schema.get("properties", {})
|
|
47
|
+
|
|
48
|
+
config_properties: dict[str, Any] = {}
|
|
49
|
+
secrets: list[dict[str, Any]] = []
|
|
50
|
+
|
|
51
|
+
# Skip internal fields
|
|
52
|
+
internal_fields = {"logs_dir"}
|
|
53
|
+
|
|
54
|
+
for field_name, prop_schema in properties.items():
|
|
55
|
+
if field_name in internal_fields:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if field_name in secrets_map:
|
|
59
|
+
secret = secrets_map[field_name]
|
|
60
|
+
secrets.append(
|
|
61
|
+
{
|
|
62
|
+
"name": field_name,
|
|
63
|
+
"description": secret.description,
|
|
64
|
+
"required": secret.required,
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
config_properties[field_name] = prop_schema
|
|
69
|
+
|
|
70
|
+
required = [r for r in full_schema.get("required", []) if r not in internal_fields and r not in secrets_map]
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": config_properties,
|
|
76
|
+
"required": required,
|
|
77
|
+
"secrets": secrets,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_agent_schema(agent_cls: type) -> dict[str, Any]:
|
|
82
|
+
"""Get full schema for an agent including config and build schemas.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
agent_cls: The BaseAgent subclass to generate schema for
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dict with config and build schema sections
|
|
89
|
+
"""
|
|
90
|
+
from plato.agents.build import BuildConfig
|
|
91
|
+
|
|
92
|
+
config_class = agent_cls.get_config_class()
|
|
93
|
+
return {
|
|
94
|
+
"config": get_agent_config_schema(config_class),
|
|
95
|
+
"build": BuildConfig.get_json_schema(),
|
|
96
|
+
}
|
plato/cli/chronos.py
CHANGED
|
@@ -6,11 +6,13 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
|
+
import platform as plat
|
|
9
10
|
import subprocess
|
|
10
11
|
import tempfile
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Annotated
|
|
13
14
|
|
|
15
|
+
import httpx
|
|
14
16
|
import typer
|
|
15
17
|
|
|
16
18
|
from plato.cli.utils import console
|
|
@@ -64,8 +66,6 @@ def launch(
|
|
|
64
66
|
The config file should contain world.package (required) and optionally world.config,
|
|
65
67
|
runtime.artifact_id, and tags.
|
|
66
68
|
"""
|
|
67
|
-
import httpx
|
|
68
|
-
|
|
69
69
|
# Set defaults
|
|
70
70
|
if not chronos_url:
|
|
71
71
|
chronos_url = "https://chronos.plato.so"
|
|
@@ -126,80 +126,6 @@ def launch(
|
|
|
126
126
|
raise typer.Exit(1)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
@chronos_app.command()
|
|
130
|
-
def example(
|
|
131
|
-
world: str = typer.Argument(
|
|
132
|
-
"structured-execution",
|
|
133
|
-
help="World to generate example config for",
|
|
134
|
-
),
|
|
135
|
-
output: Path = typer.Option(
|
|
136
|
-
None,
|
|
137
|
-
"--output",
|
|
138
|
-
"-o",
|
|
139
|
-
help="Output file path (prints to stdout if not specified)",
|
|
140
|
-
),
|
|
141
|
-
):
|
|
142
|
-
"""Generate an example job config file.
|
|
143
|
-
|
|
144
|
-
Creates a sample JSON configuration for launching Chronos jobs, which can be
|
|
145
|
-
customized for your use case.
|
|
146
|
-
|
|
147
|
-
Arguments:
|
|
148
|
-
world: World type to generate example for (default: "structured-execution")
|
|
149
|
-
|
|
150
|
-
Options:
|
|
151
|
-
-o, --output: Output file path. If not specified, prints to stdout.
|
|
152
|
-
|
|
153
|
-
Available worlds: structured-execution, code-world
|
|
154
|
-
"""
|
|
155
|
-
examples = {
|
|
156
|
-
"structured-execution": {
|
|
157
|
-
"world_package": "plato-world-structured-execution",
|
|
158
|
-
"world_version": "latest",
|
|
159
|
-
"world_config": {
|
|
160
|
-
"sim_name": "my-sim",
|
|
161
|
-
"github_url": "https://github.com/example/repo",
|
|
162
|
-
"max_attempts": 3,
|
|
163
|
-
"use_backtrack": True,
|
|
164
|
-
"skill_runner": {
|
|
165
|
-
"image": "claude-code:2.1.5",
|
|
166
|
-
"config": {"model_name": "anthropic/claude-sonnet-4-20250514", "max_turns": 100},
|
|
167
|
-
},
|
|
168
|
-
"plato_api_key": "pk_xxx",
|
|
169
|
-
"anthropic_api_key": "sk-ant-xxx",
|
|
170
|
-
},
|
|
171
|
-
"_comment": "Agents and secrets are embedded directly in world_config",
|
|
172
|
-
},
|
|
173
|
-
"code-world": {
|
|
174
|
-
"world_package": "plato-world-code",
|
|
175
|
-
"world_config": {
|
|
176
|
-
"task": "Fix the bug in src/main.py",
|
|
177
|
-
"repo_url": "https://github.com/example/repo",
|
|
178
|
-
"coder": {
|
|
179
|
-
"image": "claude-code:latest",
|
|
180
|
-
"config": {"model_name": "anthropic/claude-sonnet-4-20250514"},
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
"_comment": "world_version is optional - uses latest if not specified",
|
|
184
|
-
},
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if world not in examples:
|
|
188
|
-
console.print(f"[red]❌ Unknown world: {world}[/red]")
|
|
189
|
-
console.print(f"Available examples: {list(examples.keys())}")
|
|
190
|
-
raise typer.Exit(1)
|
|
191
|
-
|
|
192
|
-
example_config = examples[world]
|
|
193
|
-
json_output = json.dumps(example_config, indent=2)
|
|
194
|
-
|
|
195
|
-
if output:
|
|
196
|
-
with open(output, "w") as f:
|
|
197
|
-
f.write(json_output)
|
|
198
|
-
console.print(f"[green]✅ Example config written to {output}[/green]")
|
|
199
|
-
else:
|
|
200
|
-
console.print(json_output)
|
|
201
|
-
|
|
202
|
-
|
|
203
129
|
def _get_world_runner_dockerfile() -> Path:
|
|
204
130
|
"""Get the path to the world runner Dockerfile template."""
|
|
205
131
|
return Path(__file__).parent.parent / "v1" / "cli" / "templates" / "world-runner.Dockerfile"
|
|
@@ -253,8 +179,6 @@ def _get_docker_platform(override: str | None = None) -> str:
|
|
|
253
179
|
if override:
|
|
254
180
|
return override
|
|
255
181
|
|
|
256
|
-
import platform as plat
|
|
257
|
-
|
|
258
182
|
system = plat.system()
|
|
259
183
|
machine = plat.machine().lower()
|
|
260
184
|
|
|
@@ -424,8 +348,6 @@ async def _create_chronos_session(
|
|
|
424
348
|
tags: list[str] | None = None,
|
|
425
349
|
) -> dict:
|
|
426
350
|
"""Create a session in Chronos."""
|
|
427
|
-
import httpx
|
|
428
|
-
|
|
429
351
|
url = f"{chronos_url.rstrip('/')}/api/sessions"
|
|
430
352
|
|
|
431
353
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
@@ -443,25 +365,6 @@ async def _create_chronos_session(
|
|
|
443
365
|
return response.json()
|
|
444
366
|
|
|
445
367
|
|
|
446
|
-
async def _close_chronos_session(
|
|
447
|
-
chronos_url: str,
|
|
448
|
-
api_key: str,
|
|
449
|
-
session_id: str,
|
|
450
|
-
) -> None:
|
|
451
|
-
"""Close a Chronos session."""
|
|
452
|
-
import httpx
|
|
453
|
-
|
|
454
|
-
url = f"{chronos_url.rstrip('/')}/api/sessions/{session_id}/close"
|
|
455
|
-
|
|
456
|
-
try:
|
|
457
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
458
|
-
response = await client.post(url, headers={"x-api-key": api_key})
|
|
459
|
-
response.raise_for_status()
|
|
460
|
-
logger.info(f"Closed Chronos session: {session_id}")
|
|
461
|
-
except Exception as e:
|
|
462
|
-
logger.warning(f"Failed to close Chronos session: {e}")
|
|
463
|
-
|
|
464
|
-
|
|
465
368
|
async def _complete_chronos_session(
|
|
466
369
|
chronos_url: str,
|
|
467
370
|
api_key: str,
|
|
@@ -471,7 +374,6 @@ async def _complete_chronos_session(
|
|
|
471
374
|
error_message: str | None = None,
|
|
472
375
|
) -> None:
|
|
473
376
|
"""Complete a Chronos session with final status."""
|
|
474
|
-
import httpx
|
|
475
377
|
|
|
476
378
|
url = f"{chronos_url.rstrip('/')}/api/sessions/{session_id}/complete"
|
|
477
379
|
|
plato/worlds/__init__.py
CHANGED
|
@@ -46,23 +46,19 @@ from plato._generated.models import (
|
|
|
46
46
|
from plato.worlds.base import (
|
|
47
47
|
BaseWorld,
|
|
48
48
|
ConfigT,
|
|
49
|
-
Observation,
|
|
50
|
-
StepResult,
|
|
51
49
|
get_registered_worlds,
|
|
52
50
|
get_world,
|
|
53
51
|
register_world,
|
|
54
52
|
)
|
|
55
53
|
from plato.worlds.config import (
|
|
56
|
-
Agent,
|
|
57
54
|
AgentConfig,
|
|
58
55
|
CheckpointConfig,
|
|
59
|
-
Env,
|
|
60
56
|
EnvConfig,
|
|
61
|
-
EnvList,
|
|
62
57
|
RunConfig,
|
|
63
|
-
Secret,
|
|
64
58
|
StateConfig,
|
|
65
59
|
)
|
|
60
|
+
from plato.worlds.markers import Agent, Env, EnvList, Secret
|
|
61
|
+
from plato.worlds.models import Observation, StepResult
|
|
66
62
|
from plato.worlds.runner import run_world
|
|
67
63
|
|
|
68
64
|
__all__ = [
|