thomasm6m6 commited on
Commit
94ed1e7
·
verified ·
1 Parent(s): 55ba410

Add official OpenEnv echo_env sample

Browse files
Files changed (9) hide show
  1. README.md +154 -4
  2. __init__.py +29 -0
  3. client.py +81 -0
  4. openenv.yaml +6 -0
  5. pyproject.toml +40 -0
  6. server/Dockerfile +80 -0
  7. server/__init__.py +11 -0
  8. server/app.py +61 -0
  9. server/echo_environment.py +196 -0
README.md CHANGED
@@ -1,10 +1,160 @@
1
  ---
2
- title: Openenv Echo Docs
3
- emoji: 😻
4
- colorFrom: yellow
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Echo Environment Server
3
+ emoji: 🔊
4
+ colorFrom: blue
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Echo Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the Echo environment is through the `EchoEnv` class. The client is **async by default**:
21
+
22
+ ```python
23
+ import asyncio
24
+ from echo_env import EchoAction, EchoEnv
25
+
26
+ async def main():
27
+ # Create environment from Docker image
28
+ client = await EchoEnv.from_docker_image("echo-env:latest")
29
+
30
+ async with client:
31
+ # Reset
32
+ result = await client.reset()
33
+ print(f"Reset: {result.observation.echoed_message}")
34
+
35
+ # Send multiple messages
36
+ messages = ["Hello, World!", "Testing echo", "Final message"]
37
+
38
+ for msg in messages:
39
+ result = await client.step(EchoAction(message=msg))
40
+ print(f"Sent: '{msg}'")
41
+ print(f" → Echoed: '{result.observation.echoed_message}'")
42
+ print(f" → Length: {result.observation.message_length}")
43
+ print(f" → Reward: {result.reward}")
44
+
45
+ asyncio.run(main())
46
+ ```
47
+
48
+ For **synchronous usage**, use the `.sync()` wrapper:
49
+
50
+ ```python
51
+ from echo_env import EchoAction, EchoEnv
52
+
53
+ with EchoEnv(base_url="http://localhost:8000").sync() as client:
54
+ result = client.reset()
55
+ result = client.step(EchoAction(message="Hello!"))
56
+ print(result.observation.echoed_message)
57
+ ```
58
+
59
+ The `EchoEnv.from_docker_image()` method handles:
60
+ - Starting the Docker container
61
+ - Waiting for the server to be ready
62
+ - Connecting to the environment
63
+ - Container cleanup when the context manager exits
64
+
65
+ ## Building the Docker Image
66
+
67
+ Before using the environment, you need to build the Docker image:
68
+
69
+ ```bash
70
+ # From project root
71
+ docker build -t echo-env:latest -f envs/echo_env/server/Dockerfile .
72
+ ```
73
+
74
+ ## Environment Details
75
+
76
+ ### Action
77
+ **EchoAction**: Contains a single field
78
+ - `message` (str) - The message to echo back
79
+
80
+ ### Observation
81
+ **EchoObservation**: Contains the echo response and metadata
82
+ - `echoed_message` (str) - The message echoed back
83
+ - `message_length` (int) - Length of the message
84
+ - `reward` (float) - Reward based on message length (length × 0.1)
85
+ - `done` (bool) - Always False for echo environment
86
+ - `metadata` (dict) - Additional info like step count
87
+
88
+ ### Reward
89
+ The reward is calculated as: `message_length × 0.1`
90
+ - "Hi" → reward: 0.2
91
+ - "Hello, World!" → reward: 1.3
92
+ - Empty message → reward: 0.0
93
+
94
+ ## Advanced Usage
95
+
96
+ ### Connecting to an Existing Server
97
+
98
+ If you already have an Echo environment server running, you can connect directly:
99
+
100
+ ```python
101
+ from echo_env import EchoAction, EchoEnv
102
+
103
+ # Async usage
104
+ async with EchoEnv(base_url="http://localhost:8000") as client:
105
+ result = await client.reset()
106
+ result = await client.step(EchoAction(message="Hello!"))
107
+
108
+ # Sync usage
109
+ with EchoEnv(base_url="http://localhost:8000").sync() as client:
110
+ result = client.reset()
111
+ result = client.step(EchoAction(message="Hello!"))
112
+ ```
113
+
114
+ Note: When connecting to an existing server, closing the client will NOT stop the server.
115
+
116
+ ## Development & Testing
117
+
118
+ ### Direct Environment Testing
119
+
120
+ Test the environment logic directly without starting the HTTP server:
121
+
122
+ ```bash
123
+ # From the server directory
124
+ python3 envs/echo_env/server/test_echo_env.py
125
+ ```
126
+
127
+ This verifies that:
128
+ - Environment resets correctly
129
+ - Step executes actions properly
130
+ - State tracking works
131
+ - Rewards are calculated correctly
132
+
133
+ ### Running the Full Example
134
+
135
+ Run the complete example that demonstrates the full workflow:
136
+
137
+ ```bash
138
+ python3 examples/local_echo_env.py
139
+ ```
140
+
141
+ This example shows:
142
+ - Creating an environment from a Docker image
143
+ - Resetting and stepping through the environment
144
+ - Automatic cleanup with `close()`
145
+
146
+ ## Project Structure
147
+
148
+ ```
149
+ echo_env/
150
+ ├── __init__.py # Module exports
151
+ ├── README.md # This file
152
+ ├── client.py # EchoEnv client implementation
153
+ ├── models.py # Action and Observation models
154
+ └── server/
155
+ ├── __init__.py # Server module exports
156
+ ├── echo_environment.py # Core environment logic
157
+ ├── app.py # FastAPI application
158
+ ├── test_echo_env.py # Direct environment tests
159
+ └── Dockerfile # Container image definition
160
+ ```
__init__.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Echo Environment - A pure MCP environment for testing and demonstration.
9
+
10
+ This environment exposes all functionality through MCP tools:
11
+ - `echo_message(message)`: Echo back the provided message
12
+ - `echo_with_length(message)`: Echo back the message with its length
13
+
14
+ Example:
15
+ >>> from echo_env import EchoEnv
16
+ >>>
17
+ >>> with EchoEnv(base_url="http://localhost:8000") as env:
18
+ ... env.reset()
19
+ ... tools = env.list_tools()
20
+ ... result = env.call_tool("echo_message", message="Hello!")
21
+ ... print(result) # "Hello!"
22
+ """
23
+
24
+ # Re-export MCP types for convenience
25
+ from openenv.core.env_server.mcp_types import CallToolAction, ListToolsAction
26
+
27
+ from .client import EchoEnv
28
+
29
+ __all__ = ["EchoEnv", "CallToolAction", "ListToolsAction"]
client.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Echo Environment Client.
9
+
10
+ This module provides the client for connecting to an Echo Environment server.
11
+ EchoEnv extends MCPToolClient to provide tool-calling style interactions.
12
+
13
+ Example:
14
+ >>> with EchoEnv(base_url="http://localhost:8000") as env:
15
+ ... env.reset()
16
+ ...
17
+ ... # Discover tools
18
+ ... tools = env.list_tools()
19
+ ... print([t.name for t in tools]) # ['echo_message', 'echo_with_length']
20
+ ...
21
+ ... # Call tools
22
+ ... result = env.call_tool("echo_message", message="Hello!")
23
+ ... print(result) # "Hello!"
24
+ ...
25
+ ... result = env.call_tool("echo_with_length", message="Test")
26
+ ... print(result) # {"message": "Test", "length": 4}
27
+ """
28
+
29
+ from openenv.core.mcp_client import MCPToolClient
30
+
31
+
32
+ class EchoEnv(MCPToolClient):
33
+ """
34
+ Client for the Echo Environment.
35
+
36
+ This client provides a simple interface for interacting with the Echo
37
+ Environment via MCP tools. It inherits all functionality from MCPToolClient:
38
+ - `list_tools()`: Discover available tools
39
+ - `call_tool(name, **kwargs)`: Call a tool by name
40
+ - `reset(**kwargs)`: Reset the environment
41
+ - `step(action)`: Execute an action (for advanced use)
42
+
43
+ Example:
44
+ >>> # Connect to a running server
45
+ >>> with EchoEnv(base_url="http://localhost:8000") as env:
46
+ ... env.reset()
47
+ ...
48
+ ... # List available tools
49
+ ... tools = env.list_tools()
50
+ ... for tool in tools:
51
+ ... print(f"{tool.name}: {tool.description}")
52
+ ...
53
+ ... # Echo a message
54
+ ... result = env.call_tool("echo_message", message="Hello!")
55
+ ... print(result) # "Hello!"
56
+ ...
57
+ ... # Echo with length
58
+ ... result = env.call_tool("echo_with_length", message="Test")
59
+ ... print(result) # {"message": "Test", "length": 4}
60
+
61
+ Example with Docker:
62
+ >>> # Automatically start container and connect
63
+ >>> env = EchoEnv.from_docker_image("echo-env:latest")
64
+ >>> try:
65
+ ... env.reset()
66
+ ... tools = env.list_tools()
67
+ ... result = env.call_tool("echo_message", message="Hello!")
68
+ ... finally:
69
+ ... env.close()
70
+
71
+ Example with HuggingFace Space:
72
+ >>> # Run from HuggingFace Space
73
+ >>> env = EchoEnv.from_env("openenv/echo-env")
74
+ >>> try:
75
+ ... env.reset()
76
+ ... result = env.call_tool("echo_message", message="Hello!")
77
+ ... finally:
78
+ ... env.close()
79
+ """
80
+
81
+ pass # MCPToolClient provides all needed functionality
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: echo_env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
pyproject.toml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-echo-env"
13
+ version = "0.1.0"
14
+ description = "Echo Environment for OpenEnv - simple test environment that echoes back messages"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv dependencies (required for server functionality)
18
+ "openenv-core[core]>=0.2.1",
19
+ "fastapi>=0.115.0",
20
+ "pydantic>=2.0.0",
21
+ "uvicorn>=0.24.0",
22
+ "requests>=2.31.0",
23
+ # No additional environment-specific dependencies needed for echo_env
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest>=8.0.0",
29
+ "pytest-cov>=4.0.0",
30
+ ]
31
+
32
+ [project.scripts]
33
+ # Server entry point - enables running via: uv run --project . server
34
+ # or: python -m echo_env.server.app
35
+ server = "echo_env.server.app:main"
36
+
37
+ [tool.setuptools]
38
+ include-package-data = true
39
+ packages = ["echo_env", "echo_env.server"]
40
+ package-dir = { "echo_env" = ".", "echo_env.server" = "server" }
server/Dockerfile ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local src/core)
10
+ # - Standalone environments (with openenv-core from pip)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Build argument to control whether we're building standalone or in-repo
19
+ ARG BUILD_MODE=in-repo
20
+
21
+ # Copy environment code (always at root of build context)
22
+ COPY . /app/env
23
+
24
+ # For in-repo builds, openenv-core is already in the pyproject.toml dependencies
25
+ # For standalone builds, openenv-core will be installed from pip via pyproject.toml
26
+ WORKDIR /app/env
27
+
28
+ # Ensure uv is available (for local builds where base image lacks it)
29
+ RUN if ! command -v uv >/dev/null 2>&1; then \
30
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
31
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
32
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
33
+ fi
34
+
35
+ # Install git for building from git repos (build-time only)
36
+ RUN apt-get update && apt-get install -y --no-install-recommends \
37
+ git \
38
+ && rm -rf /var/lib/apt/lists/*
39
+
40
+ # Install dependencies using uv sync
41
+ # First pass: install dependencies without the project (for better caching)
42
+ # Second pass: install the project itself
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check using Python (more portable than curl/wget)
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Echo environment server components."""
8
+
9
+ from .echo_environment import EchoEnvironment
10
+
11
+ __all__ = ["EchoEnvironment"]
server/app.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the Echo Environment.
9
+
10
+ This module creates an HTTP server that exposes the EchoEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with MCPToolClient.
12
+
13
+ Usage:
14
+ # Development (with auto-reload):
15
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
16
+
17
+ # Production:
18
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
19
+
20
+ # Or run directly:
21
+ uv run --project . server
22
+ """
23
+
24
+ # Support both in-repo and standalone imports
25
+ try:
26
+ # In-repo imports (when running from OpenEnv repository)
27
+ from openenv.core.env_server.http_server import create_app
28
+ from openenv.core.env_server.mcp_types import CallToolAction, CallToolObservation
29
+
30
+ from .echo_environment import EchoEnvironment
31
+ except ImportError:
32
+ # Standalone imports (when environment is standalone with openenv from pip)
33
+ from openenv.core.env_server.http_server import create_app
34
+ from openenv.core.env_server.mcp_types import CallToolAction, CallToolObservation
35
+ from server.echo_environment import EchoEnvironment
36
+
37
+ # Create the app with web interface and README integration
38
+ # Pass the class (factory) instead of an instance for WebSocket session support
39
+ # Use MCP types for action/observation since this is a pure MCP environment
40
+ app = create_app(
41
+ EchoEnvironment, CallToolAction, CallToolObservation, env_name="echo_env"
42
+ )
43
+
44
+
45
+ def main():
46
+ """
47
+ Entry point for direct execution via uv run or python -m.
48
+
49
+ This function enables running the server without Docker:
50
+ uv run --project . server
51
+ python -m envs.echo_env.server.app
52
+ openenv serve echo_env
53
+
54
+ """
55
+ import uvicorn
56
+
57
+ uvicorn.run(app, host="0.0.0.0", port=8000)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
server/echo_environment.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Echo Environment Implementation.
9
+
10
+ A pure MCP environment that echoes back messages sent to it.
11
+ This demonstrates how to build an MCPEnvironment with inline FastMCP tools.
12
+
13
+ All interactions happen through MCP tools:
14
+ - `echo_message(message)`: Echo back the provided message
15
+ - `echo_with_length(message)`: Echo back the message with its length
16
+
17
+ Example:
18
+ >>> from openenv.core.env_server.mcp_types import ListToolsAction, CallToolAction
19
+ >>> env = EchoEnvironment()
20
+ >>> env.reset()
21
+ >>>
22
+ >>> # List available tools
23
+ >>> obs = env.step(ListToolsAction())
24
+ >>> print([t.name for t in obs.tools]) # ["echo_message", "echo_with_length"]
25
+ >>>
26
+ >>> # Call a tool
27
+ >>> obs = env.step(CallToolAction(tool_name="echo_message", arguments={"message": "Hi!"}))
28
+ >>> print(obs.result) # "Hi!"
29
+ """
30
+
31
+ from typing import Any, Optional
32
+ from uuid import uuid4
33
+
34
+ # Support both in-repo and standalone imports
35
+ try:
36
+ # In-repo imports (when running from OpenEnv repository)
37
+ from openenv.core.env_server.mcp_environment import MCPEnvironment
38
+ from openenv.core.env_server.types import Action, Observation, State
39
+ except ImportError:
40
+ # Standalone imports (when environment is standalone with openenv from pip)
41
+ from openenv.core.env_server.mcp_environment import MCPEnvironment
42
+ from openenv.core.env_server.types import Action, Observation, State
43
+
44
+ from fastmcp import FastMCP
45
+
46
+
47
+ class EchoEnvironment(MCPEnvironment):
48
+ """
49
+ A pure MCP echo environment that echoes back messages.
50
+
51
+ This environment exposes all functionality through MCP tools:
52
+ - `echo_message`: Echo back the provided message
53
+ - `echo_with_length`: Echo back the message with its length
54
+
55
+ The environment inherits MCP support (ListToolsAction, CallToolAction)
56
+ from the MCPEnvironment base class. No legacy action types are supported.
57
+
58
+ Example using MCPToolClient:
59
+ >>> from openenv.core.mcp_client import MCPToolClient
60
+ >>>
61
+ >>> with MCPToolClient(base_url="http://localhost:8000") as env:
62
+ ... env.reset()
63
+ ... tools = env.list_tools()
64
+ ... print([t.name for t in tools])
65
+ ... result = env.call_tool("echo_message", message="Hello!")
66
+ ... print(result)
67
+ """
68
+
69
+ def __init__(self):
70
+ """Initialize the echo environment with MCP server and tools."""
71
+ # Create MCP server and define tools inline
72
+ mcp = FastMCP("echo_env")
73
+
74
+ @mcp.tool
75
+ def echo_message(message: str) -> str:
76
+ """
77
+ Echo back the provided message.
78
+
79
+ Args:
80
+ message: The message to echo back
81
+
82
+ Returns:
83
+ The same message that was provided
84
+ """
85
+ return message
86
+
87
+ @mcp.tool
88
+ def echo_with_length(message: str) -> dict:
89
+ """
90
+ Echo back the message with its length.
91
+
92
+ Args:
93
+ message: The message to echo back
94
+
95
+ Returns:
96
+ Dictionary with the message and its length
97
+ """
98
+ return {"message": message, "length": len(message)}
99
+
100
+ # Pass the MCP server to the base class
101
+ super().__init__(mcp)
102
+ self._state = State(episode_id=str(uuid4()), step_count=0)
103
+ self._reset_count = 0
104
+
105
+ def reset(
106
+ self,
107
+ seed: Optional[int] = None,
108
+ episode_id: Optional[str] = None,
109
+ **kwargs: Any,
110
+ ) -> Observation:
111
+ """
112
+ Reset the environment.
113
+
114
+ Args:
115
+ seed: Optional random seed (unused in echo env)
116
+ episode_id: Optional episode ID to use
117
+ **kwargs: Additional reset options
118
+
119
+ Returns:
120
+ Observation indicating the environment is ready
121
+ """
122
+ self._state = State(
123
+ episode_id=episode_id or str(uuid4()),
124
+ step_count=0,
125
+ )
126
+ self._reset_count += 1
127
+
128
+ return Observation(
129
+ done=False,
130
+ reward=0.0,
131
+ metadata={"status": "ready", "message": "Echo environment ready!"},
132
+ )
133
+
134
+ def _step_impl(
135
+ self,
136
+ action: Action,
137
+ timeout_s: Optional[float] = None,
138
+ **kwargs: Any,
139
+ ) -> Observation:
140
+ """
141
+ Handle non-MCP actions.
142
+
143
+ This environment only supports MCP actions (ListToolsAction, CallToolAction).
144
+ Any other action type returns an error observation.
145
+
146
+ Args:
147
+ action: The action to execute
148
+ timeout_s: Optional timeout (unused)
149
+ **kwargs: Additional arguments
150
+
151
+ Returns:
152
+ Observation with error for unknown action types
153
+ """
154
+ return Observation(
155
+ done=False,
156
+ reward=0.0,
157
+ metadata={
158
+ "error": f"Unknown action type: {type(action).__name__}. "
159
+ "Use ListToolsAction or CallToolAction for MCP interactions."
160
+ },
161
+ )
162
+
163
+ def step(
164
+ self,
165
+ action: Action,
166
+ timeout_s: Optional[float] = None,
167
+ **kwargs: Any,
168
+ ) -> Observation:
169
+ """
170
+ Execute a step in the environment.
171
+
172
+ Delegates to base class for MCP actions. Increments step count for all actions.
173
+
174
+ Args:
175
+ action: The MCP action to execute (ListToolsAction or CallToolAction)
176
+ timeout_s: Optional timeout for the action
177
+ **kwargs: Additional arguments
178
+
179
+ Returns:
180
+ Observation from the action execution
181
+ """
182
+ # Increment step count for all actions
183
+ self._state.step_count += 1
184
+
185
+ # Let the base class handle MCP actions and non-MCP routing
186
+ return super().step(action, timeout_s=timeout_s, **kwargs)
187
+
188
+ @property
189
+ def state(self) -> State:
190
+ """
191
+ Get the current environment state.
192
+
193
+ Returns:
194
+ Current State with episode_id and step_count
195
+ """
196
+ return self._state