plato-sdk-v2 2.8.7__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/worlds/base.py CHANGED
@@ -2,51 +2,43 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
6
+ import importlib.metadata
5
7
  import logging
6
8
  import os
7
9
  import subprocess
8
10
  from abc import ABC, abstractmethod
9
11
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, get_args, get_origin
11
-
12
- from pydantic import BaseModel, Field
12
+ from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar, get_args, get_origin
13
13
 
14
+ from plato.agents.artifacts import upload_artifact as _upload_artifact_raw
15
+ from plato.agents.otel import get_tracer, init_tracing, shutdown_tracing
16
+ from plato.agents.runner import run_agent as _run_agent_raw
17
+ from plato.v2.async_.session import Session
14
18
  from plato.worlds.config import RunConfig
19
+ from plato.worlds.models import Observation, StepResult
20
+ from plato.worlds.schema import get_world_schema
15
21
 
16
22
  if TYPE_CHECKING:
17
23
  from plato.v2.async_.environment import Environment
18
- from plato.v2.async_.session import Session
19
-
20
- from plato.agents.artifacts import (
21
- upload_artifact as _upload_artifact_raw,
22
- )
23
- from plato.agents.otel import (
24
- get_tracer,
25
- init_tracing,
26
- shutdown_tracing,
27
- )
28
- from plato.agents.runner import run_agent as _run_agent_raw
29
24
 
30
25
  logger = logging.getLogger(__name__)
31
26
 
27
+ # Global registry of worlds
28
+ _WORLD_REGISTRY: dict[str, type[BaseWorld]] = {}
29
+
30
+ # Type variable for config
31
+ ConfigT = TypeVar("ConfigT", bound=RunConfig)
32
+
32
33
 
33
34
  def _get_plato_version() -> str:
34
35
  """Get the installed plato SDK version."""
35
36
  try:
36
- from importlib.metadata import version
37
-
38
- return version("plato")
37
+ return importlib.metadata.version("plato")
39
38
  except Exception:
40
39
  return "unknown"
41
40
 
42
41
 
43
- # Global registry of worlds
44
- _WORLD_REGISTRY: dict[str, type[BaseWorld]] = {}
45
-
46
- # Type variable for config
47
- ConfigT = TypeVar("ConfigT", bound=RunConfig)
48
-
49
-
50
42
  def register_world(name: str | None = None):
51
43
  """Decorator to register a world class.
52
44
 
@@ -75,22 +67,6 @@ def get_world(name: str) -> type[BaseWorld] | None:
75
67
  return _WORLD_REGISTRY.get(name)
76
68
 
77
69
 
78
- class Observation(BaseModel):
79
- """Observation returned from reset/step."""
80
-
81
- data: dict[str, Any] = Field(default_factory=dict)
82
-
83
- model_config = {"extra": "allow"}
84
-
85
-
86
- class StepResult(BaseModel):
87
- """Result of a step."""
88
-
89
- observation: Observation
90
- done: bool = False
91
- info: dict[str, Any] = Field(default_factory=dict)
92
-
93
-
94
70
  class BaseWorld(ABC, Generic[ConfigT]):
95
71
  """Base class for Plato worlds.
96
72
 
@@ -144,8 +120,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
144
120
  @classmethod
145
121
  def get_version(cls) -> str:
146
122
  """Get version from package metadata."""
147
- import importlib.metadata
148
-
149
123
  for pkg_name in [cls.__module__.split(".")[0], f"plato-world-{cls.name}"]:
150
124
  try:
151
125
  return importlib.metadata.version(pkg_name)
@@ -156,28 +130,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
156
130
  @classmethod
157
131
  def get_schema(cls) -> dict:
158
132
  """Get full schema including world config, agents, secrets, and envs."""
159
- config_class = cls.get_config_class()
160
- schema = config_class.get_json_schema()
161
-
162
- result = {
163
- "$schema": "https://json-schema.org/draft/2020-12/schema",
164
- "type": "object",
165
- "properties": schema.get("properties", {}),
166
- "required": schema.get("required", []),
167
- "agents": schema.get("agents", []),
168
- "secrets": schema.get("secrets", []),
169
- "envs": schema.get("envs", []),
170
- }
171
-
172
- # Include env_list if present (for worlds with arbitrary environment lists)
173
- if "env_list" in schema:
174
- result["env_list"] = schema["env_list"]
175
-
176
- # Include $defs if present (for nested type references)
177
- if "$defs" in schema:
178
- result["$defs"] = schema["$defs"]
179
-
180
- return result
133
+ return get_world_schema(cls)
181
134
 
182
135
  @abstractmethod
183
136
  async def reset(self) -> Observation:
@@ -201,8 +154,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
201
154
 
202
155
  async def _cleanup_agent_containers(self) -> None:
203
156
  """Stop any agent containers spawned by this world."""
204
- import asyncio
205
-
206
157
  if not self._agent_containers:
207
158
  return
208
159
 
@@ -272,8 +223,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
272
223
  return
273
224
 
274
225
  try:
275
- from plato.v2.async_.session import Session
276
-
277
226
  self.logger.info("Restoring Plato session from serialized data")
278
227
  self.plato_session = await Session.load(self.config.plato_session, start_heartbeat=True)
279
228
  self.logger.info(f"Plato session {self.plato_session.session_id} restored, heartbeat started")
@@ -544,18 +493,18 @@ class BaseWorld(ABC, Generic[ConfigT]):
544
493
  def get_env(self, alias: str) -> Environment | None:
545
494
  """Get an environment by alias.
546
495
 
547
- Use this to access environments defined in the world config (e.g., gitea, localstack).
496
+ Use this to access environments defined in the world config.
548
497
 
549
498
  Args:
550
- alias: The environment alias (e.g., "gitea", "localstack", "runtime")
499
+ alias: The environment alias (e.g., "runtime", "database")
551
500
 
552
501
  Returns:
553
502
  The Environment object or None if not found or session not connected.
554
503
 
555
504
  Example:
556
- gitea = self.get_env("gitea")
557
- if gitea:
558
- result = await gitea.execute("git status")
505
+ env = self.get_env("runtime")
506
+ if env:
507
+ result = await env.execute("ls -la")
559
508
  """
560
509
  if not self.plato_session:
561
510
  self.logger.warning("Cannot get env: Plato session not connected")
@@ -576,8 +525,8 @@ class BaseWorld(ABC, Generic[ConfigT]):
576
525
  def get_sim_env_vars(self) -> dict[str, str]:
577
526
  """Get environment variables from all configured sims.
578
527
 
579
- Automatically discovers and loads env vars from sims like localstack, gitea, etc.
580
- based on the environments configured in the world.
528
+ Automatically discovers and loads env vars from sims based on the
529
+ environments configured in the world.
581
530
 
582
531
  Returns:
583
532
  Dict of environment variable name -> value
@@ -591,88 +540,54 @@ class BaseWorld(ABC, Generic[ConfigT]):
591
540
  """
592
541
  env_vars: dict[str, str] = {}
593
542
 
594
- # Known sim packages and their env aliases
595
- sim_packages = [
596
- ("localstack", "localstack"),
597
- ("gitea", "gitea"),
598
- ]
599
-
600
- for package_name, env_alias in sim_packages:
601
- env = self.get_env(env_alias)
602
- if not env:
603
- continue
604
-
543
+ for env in self.envs:
605
544
  try:
606
- # Dynamically import the sim package
607
- sim_module = __import__(f"plato.sims.{package_name}", fromlist=[package_name])
545
+ sim_module = __import__(f"plato.sims.{env.alias}", fromlist=[env.alias])
546
+
547
+ if not hasattr(sim_module, "get_service_urls") or not hasattr(sim_module, "get_env_vars"):
548
+ continue
608
549
 
609
- # Get service URLs and env vars
610
550
  service_urls = sim_module.get_service_urls(env.job_id)
611
551
  sim_vars = sim_module.get_env_vars(service_urls)
612
552
  env_vars.update(sim_vars)
613
- self.logger.info(f"{package_name} env vars: {list(sim_vars.keys())}")
553
+ self.logger.info(f"{env.alias} env vars: {list(sim_vars.keys())}")
614
554
  except ImportError:
615
- raise ImportError(
616
- f"Environment '{env_alias}' is configured but 'plato.sims.{package_name}' "
617
- f"package is not installed.\n\n"
618
- f"Install sims packages:\n"
619
- f' export INDEX_URL="https://__token__:${{PLATO_API_KEY}}@plato.so/api/v2/pypi/sims/simple/"\n'
620
- f" uv pip install '.[sims]' --extra-index-url $INDEX_URL"
621
- ) from None
555
+ # Sim package not installed for this env - skip silently
556
+ continue
622
557
  except Exception as e:
623
- self.logger.warning(f"Failed to get {package_name} env vars: {e}")
558
+ self.logger.warning(f"Failed to get {env.alias} env vars: {e}")
624
559
 
625
560
  return env_vars
626
561
 
627
562
  def get_sim_instructions(self) -> str:
628
563
  """Get usage instructions from all configured sims.
629
564
 
630
- Returns markdown-formatted instructions for using LocalStack, Gitea, etc.
631
- based on the environments configured in the world.
565
+ Returns markdown-formatted instructions for using configured sims
566
+ based on the environments in the world.
632
567
 
633
568
  Returns:
634
569
  Markdown string with instructions, or empty string if no sims configured.
635
570
 
636
- Raises:
637
- ImportError: If a sim environment is configured but package is not installed.
638
-
639
571
  Example:
640
572
  instructions = self.get_sim_instructions()
641
- # Returns markdown with LocalStack/Gitea setup instructions
573
+ # Returns markdown with sim setup instructions
642
574
  """
643
575
  instructions_parts: list[str] = []
644
576
 
645
- # Known sim packages and their env aliases
646
- sim_packages = [
647
- ("localstack", "localstack"),
648
- ("gitea", "gitea"),
649
- ]
650
-
651
- for package_name, env_alias in sim_packages:
652
- env = self.get_env(env_alias)
653
- if not env:
654
- continue
655
-
577
+ for env in self.envs:
656
578
  try:
657
- # Dynamically import the sim package
658
- sim_module = __import__(f"plato.sims.{package_name}", fromlist=[package_name])
579
+ sim_module = __import__(f"plato.sims.{env.alias}", fromlist=[env.alias])
659
580
 
660
- # Get instructions using the job_id
661
581
  if hasattr(sim_module, "get_instructions_from_job"):
662
582
  instructions = sim_module.get_instructions_from_job(env.job_id)
663
583
  if instructions:
664
584
  instructions_parts.append(instructions)
665
- self.logger.info(f"Added {package_name} instructions to prompt")
585
+ self.logger.info(f"Added {env.alias} instructions to prompt")
666
586
  except ImportError:
667
- raise ImportError(
668
- f"Environment '{env_alias}' is configured but 'plato.sims.{package_name}' "
669
- f"package is not installed.\n\n"
670
- f"Install sims packages:\n"
671
- f' export INDEX_URL="https://__token__:${{PLATO_API_KEY}}@plato.so/api/v2/pypi/sims/simple/"\n'
672
- f" uv pip install '.[sims]' --extra-index-url $INDEX_URL"
673
- ) from None
587
+ # Sim package not installed for this env - skip silently
588
+ continue
674
589
  except Exception as e:
675
- self.logger.warning(f"Failed to get {package_name} instructions: {e}")
590
+ self.logger.warning(f"Failed to get {env.alias} instructions: {e}")
676
591
 
677
592
  if instructions_parts:
678
593
  return "\n\n---\n\n".join(instructions_parts)
@@ -681,8 +596,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
681
596
  def format_instruction_with_sims(self, instruction: str) -> str:
682
597
  """Format an instruction with sim context prepended.
683
598
 
684
- Automatically adds available service instructions (LocalStack, Gitea, etc.)
685
- before the main instruction.
599
+ Automatically adds available service instructions before the main instruction.
686
600
 
687
601
  Args:
688
602
  instruction: The base instruction/task
@@ -695,7 +609,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
695
609
  # Returns:
696
610
  # ## Available Services
697
611
  # The following services are available...
698
- # [LocalStack instructions]
612
+ # [sim instructions]
699
613
  # ---
700
614
  # ## Task
701
615
  # Fix the bug in main.py
plato/worlds/config.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
@@ -13,6 +14,8 @@ from plato._generated.models import (
13
14
  EnvFromSimulator,
14
15
  )
15
16
  from plato.v2.async_.session import SerializedSession
17
+ from plato.worlds.markers import Agent, Env, EnvList, Secret
18
+ from plato.worlds.schema import get_field_annotations, get_world_config_schema
16
19
 
17
20
  # Union type for environment configurations
18
21
  EnvConfig = EnvFromArtifact | EnvFromSimulator | EnvFromResource
@@ -30,66 +33,6 @@ class AgentConfig(BaseModel):
30
33
  config: dict[str, Any] = Field(default_factory=dict)
31
34
 
32
35
 
33
- class Agent:
34
- """Annotation marker for agent fields.
35
-
36
- Usage:
37
- coder: Annotated[AgentConfig, Agent(description="Coding agent")]
38
- """
39
-
40
- def __init__(self, description: str = "", required: bool = True):
41
- self.description = description
42
- self.required = required
43
-
44
-
45
- class Secret:
46
- """Annotation marker for secret fields.
47
-
48
- Usage:
49
- api_key: Annotated[str | None, Secret(description="API key")] = None
50
- """
51
-
52
- def __init__(self, description: str = "", required: bool = False):
53
- self.description = description
54
- self.required = required
55
-
56
-
57
- class Env:
58
- """Annotation marker for single environment fields.
59
-
60
- Environments are VMs that run alongside the world's runtime.
61
- They can be specified by artifact ID, simulator name, or resource config.
62
-
63
- Usage:
64
- gitea: Annotated[EnvConfig, Env(description="Git server")] = EnvFromArtifact(
65
- artifact_id="abc123",
66
- alias="gitea",
67
- )
68
- """
69
-
70
- def __init__(self, description: str = "", required: bool = True):
71
- self.description = description
72
- self.required = required
73
-
74
-
75
- class EnvList:
76
- """Annotation marker for a list of arbitrary environments.
77
-
78
- Use this when the world accepts a dynamic list of environments
79
- rather than named, fixed environment fields.
80
-
81
- Usage:
82
- envs: Annotated[
83
- list[EnvFromSimulator | EnvFromArtifact | EnvFromResource],
84
- EnvList(description="Environments to create for this task"),
85
- Field(discriminator="type"),
86
- ] = []
87
- """
88
-
89
- def __init__(self, description: str = ""):
90
- self.description = description
91
-
92
-
93
36
  class StateConfig(BaseModel):
94
37
  """Configuration for world state persistence.
95
38
 
@@ -169,114 +112,12 @@ class RunConfig(BaseModel):
169
112
  @classmethod
170
113
  def get_field_annotations(cls) -> dict[str, Agent | Secret | Env | EnvList | None]:
171
114
  """Get Agent/Secret/Env/EnvList annotations for each field."""
172
- result: dict[str, Agent | Secret | Env | EnvList | None] = {}
173
-
174
- for field_name, field_info in cls.model_fields.items():
175
- marker = None
176
-
177
- # Pydantic stores Annotated metadata in field_info.metadata
178
- for meta in field_info.metadata:
179
- if isinstance(meta, (Agent, Secret, Env, EnvList)):
180
- marker = meta
181
- break
182
-
183
- result[field_name] = marker
184
-
185
- return result
115
+ return get_field_annotations(cls)
186
116
 
187
117
  @classmethod
188
118
  def get_json_schema(cls) -> dict:
189
119
  """Get JSON schema with agents, secrets, and envs separated."""
190
- # Get full Pydantic schema
191
- full_schema = cls.model_json_schema()
192
- full_schema.pop("title", None)
193
-
194
- # Separate fields by annotation type
195
- annotations = cls.get_field_annotations()
196
- properties = full_schema.get("properties", {})
197
-
198
- world_properties = {}
199
- agents = []
200
- secrets = []
201
- envs = []
202
- env_list_field: dict | None = None # For EnvList marker (arbitrary envs)
203
-
204
- # Skip runtime fields
205
- runtime_fields = {"session_id", "otel_url", "upload_url", "plato_session", "checkpoint", "state"}
206
-
207
- for field_name, prop_schema in properties.items():
208
- if field_name in runtime_fields:
209
- continue
210
-
211
- marker = annotations.get(field_name)
212
-
213
- if isinstance(marker, Agent):
214
- agents.append(
215
- {
216
- "name": field_name,
217
- "description": marker.description,
218
- "required": marker.required,
219
- }
220
- )
221
- elif isinstance(marker, Secret):
222
- secrets.append(
223
- {
224
- "name": field_name,
225
- "description": marker.description,
226
- "required": marker.required,
227
- }
228
- )
229
- elif isinstance(marker, Env):
230
- # Get default value for this env field
231
- field_info = cls.model_fields.get(field_name)
232
- default_value = None
233
- if field_info and field_info.default is not None:
234
- # Serialize the default EnvConfig to dict
235
- default_env = field_info.default
236
- if hasattr(default_env, "model_dump"):
237
- default_value = default_env.model_dump()
238
- elif isinstance(default_env, dict):
239
- default_value = default_env
240
-
241
- envs.append(
242
- {
243
- "name": field_name,
244
- "description": marker.description,
245
- "required": marker.required,
246
- "default": default_value,
247
- }
248
- )
249
- elif isinstance(marker, EnvList):
250
- # Field marked as a list of arbitrary environments
251
- env_list_field = {
252
- "name": field_name,
253
- "description": marker.description,
254
- }
255
- else:
256
- world_properties[field_name] = prop_schema
257
-
258
- # Compute required fields (excluding runtime and annotated fields)
259
- required = [
260
- r for r in full_schema.get("required", []) if r not in runtime_fields and annotations.get(r) is None
261
- ]
262
-
263
- result = {
264
- "properties": world_properties,
265
- "required": required,
266
- "agents": agents,
267
- "secrets": secrets,
268
- "envs": envs,
269
- }
270
-
271
- # Include $defs if present (for nested type references)
272
- if "$defs" in full_schema:
273
- result["$defs"] = full_schema["$defs"]
274
-
275
- # Add env_list if present (for worlds with arbitrary environment lists)
276
- if env_list_field:
277
- result["env_list"] = env_list_field
278
-
279
- return result
120
+ return get_world_config_schema(cls)
280
121
 
281
122
  def get_envs(self) -> list[EnvConfig]:
282
123
  """Get all environment configurations from this config.
@@ -298,8 +139,6 @@ class RunConfig(BaseModel):
298
139
  @classmethod
299
140
  def from_file(cls, path: str | Path) -> RunConfig:
300
141
  """Load config from a JSON file."""
301
- import json
302
-
303
142
  path = Path(path)
304
143
  with open(path) as f:
305
144
  data = json.load(f)
@@ -0,0 +1,63 @@
1
+ """Annotation markers for Plato world configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Agent:
7
+ """Annotation marker for agent fields.
8
+
9
+ Usage:
10
+ coder: Annotated[AgentConfig, Agent(description="Coding agent")]
11
+ """
12
+
13
+ def __init__(self, description: str = "", required: bool = True):
14
+ self.description = description
15
+ self.required = required
16
+
17
+
18
+ class Secret:
19
+ """Annotation marker for secret fields.
20
+
21
+ Usage:
22
+ api_key: Annotated[str | None, Secret(description="API key")] = None
23
+ """
24
+
25
+ def __init__(self, description: str = "", required: bool = False):
26
+ self.description = description
27
+ self.required = required
28
+
29
+
30
+ class Env:
31
+ """Annotation marker for single environment fields.
32
+
33
+ Environments are VMs that run alongside the world's runtime.
34
+ They can be specified by artifact ID, simulator name, or resource config.
35
+
36
+ Usage:
37
+ database: Annotated[EnvConfig, Env(description="Database server")] = EnvFromArtifact(
38
+ artifact_id="abc123",
39
+ alias="database",
40
+ )
41
+ """
42
+
43
+ def __init__(self, description: str = "", required: bool = True):
44
+ self.description = description
45
+ self.required = required
46
+
47
+
48
+ class EnvList:
49
+ """Annotation marker for a list of arbitrary environments.
50
+
51
+ Use this when the world accepts a dynamic list of environments
52
+ rather than named, fixed environment fields.
53
+
54
+ Usage:
55
+ envs: Annotated[
56
+ list[EnvFromSimulator | EnvFromArtifact | EnvFromResource],
57
+ EnvList(description="Environments to create for this task"),
58
+ Field(discriminator="type"),
59
+ ] = []
60
+ """
61
+
62
+ def __init__(self, description: str = ""):
63
+ self.description = description
plato/worlds/models.py ADDED
@@ -0,0 +1,23 @@
1
+ """Data models for Plato worlds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Observation(BaseModel):
11
+ """Observation returned from reset/step."""
12
+
13
+ data: dict[str, Any] = Field(default_factory=dict)
14
+
15
+ model_config = {"extra": "allow"}
16
+
17
+
18
+ class StepResult(BaseModel):
19
+ """Result of a step."""
20
+
21
+ observation: Observation
22
+ done: bool = False
23
+ info: dict[str, Any] = Field(default_factory=dict)