diff --git a/docs/environment-builder.md b/docs/environment-builder.md index 9fefc9ee1..20a793ced 100644 --- a/docs/environment-builder.md +++ b/docs/environment-builder.md @@ -58,33 +58,26 @@ my_env/ └── Dockerfile ``` -Python classes are generated for the action, observation, and state, and a client is generated for the environment. For example, you will find `MyEnvironment`, `MyAction`, `MyObservation`, and `MyState` in the `my_env` directory based on the name of the environment you provided. +Python classes are generated for the action, observation, environment, and client. For example, you will find `MyEnvironment`, `MyAction`, `MyObservation`, and `MyEnv` (client) in the `my_env` directory based on the name you provided. The environment uses the core `State` class from `openenv.core.env_server.types`. ### 2. Define Models -Edit `models.py` to describe your action, observation, and state dataclasses: +Edit `models.py` to describe your action and observation using Pydantic: ```python # models.py -from dataclasses import dataclass -from openenv.core.env_server import Action, Observation, State +from pydantic import Field +from openenv.core.env_server.types import Action, Observation -@dataclass class MyAction(Action): """Your custom action.""" - command: str - parameters: dict + command: str = Field(..., description="Command to execute") + parameters: dict = Field(default_factory=dict, description="Command parameters") -@dataclass class MyObservation(Observation): """Your custom observation.""" - result: str - success: bool - -@dataclass -class MyState(State): - """Custom state fields.""" - custom_field: int = 0 + result: str = Field(..., description="Result of the action") + success: bool = Field(..., description="Whether the action succeeded") ``` ### 3. Implement Environment Logic @@ -93,42 +86,42 @@ Customize `server/my_environment.py` by extending `Environment`: ```python # server/my_environment.py -import uuid -from openenv.core.env_server import Environment -from ..models import MyAction, MyObservation, MyState +from uuid import uuid4 +from openenv.core.env_server.interfaces import Environment +from openenv.core.env_server.types import State +from models import MyAction, MyObservation class MyEnvironment(Environment): def __init__(self): - super().__init__() - self._state = MyState() + self._state = State(episode_id=str(uuid4()), step_count=0) def reset(self) -> MyObservation: - self._state = MyState(episode_id=str(uuid.uuid4())) - return MyObservation(result="Ready", success=True) + self._state = State(episode_id=str(uuid4()), step_count=0) + return MyObservation(result="Ready", success=True, done=False, reward=0.0) def step(self, action: MyAction) -> MyObservation: # Implement your logic here self._state.step_count += 1 result = self._execute_command(action.command) - return MyObservation(result=result, success=True) + return MyObservation(result=result, success=True, done=False, reward=1.0) @property - def state(self) -> MyState: + def state(self) -> State: return self._state ``` ### 4. Create the FastAPI Server -`server/app.py` should expose the environment through `create_fastapi_app`: +`server/app.py` should expose the environment through `create_app`: ```python # server/app.py -from openenv.core.env_server import create_fastapi_app -from ..models import MyAction, MyObservation +from openenv.core.env_server.http_server import create_app +from my_env.models import MyAction, MyObservation from .my_environment import MyEnvironment env = MyEnvironment() -app = create_fastapi_app(env, MyAction, MyObservation) +app = create_app(env, MyAction, MyObservation, env_name="my_env") ``` ### 5. Implement the Client @@ -138,23 +131,33 @@ app = create_fastapi_app(env, MyAction, MyObservation) ```python # client.py from openenv.core.http_env_client import HTTPEnvClient -from openenv.core.types import StepResult -from .models import MyAction, MyObservation, MyState +from openenv.core.client_types import StepResult +from openenv.core.env_server.types import State +from .models import MyAction, MyObservation class MyEnv(HTTPEnvClient[MyAction, MyObservation]): def _step_payload(self, action: MyAction) -> dict: return {"command": action.command, "parameters": action.parameters} def _parse_result(self, payload: dict) -> StepResult[MyObservation]: - obs = MyObservation(**payload["observation"]) + obs_data = payload.get("observation", {}) + obs = MyObservation( + result=obs_data.get("result", ""), + success=obs_data.get("success", False), + done=payload.get("done", False), + reward=payload.get("reward"), + ) return StepResult( observation=obs, reward=payload.get("reward"), done=payload.get("done", False), ) - def _parse_state(self, payload: dict) -> MyState: - return MyState(**payload) + def _parse_state(self, payload: dict) -> State: + return State( + episode_id=payload.get("episode_id"), + step_count=payload.get("step_count", 0), + ) ``` ### 6. Configure Dependencies & Dockerfile diff --git a/envs/echo_env/models.py b/envs/echo_env/models.py index 4cbf1016c..3032b7511 100644 --- a/envs/echo_env/models.py +++ b/envs/echo_env/models.py @@ -10,7 +10,7 @@ The Echo environment is a simple test environment that echoes back messages. """ -from dataclasses import dataclass +from pydantic import Field # Support both in-repo and standalone imports try: @@ -21,16 +21,14 @@ from openenv.core.env_server.types import Action, Observation -@dataclass(kw_only=True) class EchoAction(Action): """Action for the Echo environment - just a message to echo.""" - message: str + message: str = Field(..., min_length=1, description="Message to echo back") -@dataclass(kw_only=True) class EchoObservation(Observation): """Observation from the Echo environment - the echoed message.""" - echoed_message: str - message_length: int = 0 \ No newline at end of file + echoed_message: str = Field(..., description="The echoed message from the environment") + message_length: int = Field(default=0, ge=0, description="Length of the echoed message") \ No newline at end of file diff --git a/src/openenv/cli/templates/openenv_env/models.py b/src/openenv/cli/templates/openenv_env/models.py index 64010449b..57e2d1fca 100644 --- a/src/openenv/cli/templates/openenv_env/models.py +++ b/src/openenv/cli/templates/openenv_env/models.py @@ -10,22 +10,20 @@ The __ENV_NAME__ environment is a simple test environment that echoes back messages. """ -from dataclasses import dataclass +from pydantic import Field from openenv.core.env_server.types import Action, Observation -@dataclass(kw_only=True) class __ENV_CLASS_NAME__Action(Action): """Action for the __ENV_TITLE_NAME__ environment - just a message to echo.""" - message: str + message: str = Field(..., description="Message to echo back") -@dataclass(kw_only=True) class __ENV_CLASS_NAME__Observation(Observation): """Observation from the __ENV_TITLE_NAME__ environment - the echoed message.""" - echoed_message: str - message_length: int = 0 + echoed_message: str = Field(..., description="The echoed message") + message_length: int = Field(default=0, description="Length of the echoed message")