Spaces:
openenv
/
Running on CPU Upgrade

burtenshaw HF Staff commited on
Commit
7d23275
·
verified ·
1 Parent(s): b82bfac

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +10 -5
  2. README.md +59 -28
  3. __init__.py +20 -3
  4. build/lib/build/lib/server/__init__.py +11 -0
  5. build/lib/build/lib/server/app.py +57 -0
  6. build/lib/build/lib/server/echo_environment.py +102 -0
  7. build/lib/echo_env/__init__.py +29 -0
  8. build/lib/echo_env/client.py +81 -0
  9. build/lib/echo_env/server/__init__.py +11 -0
  10. build/lib/echo_env/server/app.py +60 -0
  11. build/lib/echo_env/server/echo_environment.py +196 -0
  12. build/lib/server/__init__.py +11 -0
  13. build/lib/server/app.py +57 -0
  14. build/lib/server/echo_environment.py +102 -0
  15. client.py +54 -84
  16. envs/echo_env/README.md +160 -0
  17. envs/echo_env/__init__.py +29 -0
  18. envs/echo_env/build/lib/build/lib/server/__init__.py +11 -0
  19. envs/echo_env/build/lib/build/lib/server/app.py +57 -0
  20. envs/echo_env/build/lib/build/lib/server/echo_environment.py +102 -0
  21. envs/echo_env/build/lib/echo_env/__init__.py +29 -0
  22. envs/echo_env/build/lib/echo_env/client.py +81 -0
  23. envs/echo_env/build/lib/echo_env/server/__init__.py +11 -0
  24. envs/echo_env/build/lib/echo_env/server/app.py +60 -0
  25. envs/echo_env/build/lib/echo_env/server/echo_environment.py +196 -0
  26. envs/echo_env/build/lib/server/__init__.py +11 -0
  27. envs/echo_env/build/lib/server/app.py +57 -0
  28. envs/echo_env/build/lib/server/echo_environment.py +102 -0
  29. envs/echo_env/client.py +81 -0
  30. envs/echo_env/openenv.yaml +6 -0
  31. envs/echo_env/openenv_echo_env.egg-info/PKG-INFO +13 -0
  32. envs/echo_env/openenv_echo_env.egg-info/SOURCES.txt +15 -0
  33. envs/echo_env/openenv_echo_env.egg-info/dependency_links.txt +1 -0
  34. envs/echo_env/openenv_echo_env.egg-info/entry_points.txt +2 -0
  35. envs/echo_env/openenv_echo_env.egg-info/requires.txt +9 -0
  36. envs/echo_env/openenv_echo_env.egg-info/top_level.txt +1 -0
  37. envs/echo_env/pyproject.toml +40 -0
  38. envs/echo_env/server/Dockerfile +80 -0
  39. envs/echo_env/server/__init__.py +11 -0
  40. envs/echo_env/server/app.py +61 -0
  41. envs/echo_env/server/echo_environment.py +196 -0
  42. openenv.yaml +6 -6
  43. openenv_echo_env.egg-info/PKG-INFO +1 -1
  44. openenv_echo_env.egg-info/SOURCES.txt +6 -5
  45. openenv_echo_env.egg-info/requires.txt +1 -1
  46. pyproject.toml +1 -1
  47. server/Dockerfile +80 -0
  48. server/__init__.py +1 -1
  49. server/app.py +8 -4
  50. server/echo_environment.py +136 -44
Dockerfile CHANGED
@@ -11,7 +11,7 @@
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
 
@@ -40,22 +40,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
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
 
@@ -77,5 +81,6 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
77
 
78
  # Run the FastAPI server
79
  # The module path is constructed to work with the /app/env structure
80
- ENV ENABLE_WEB_INTERFACE=true
81
  CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
 
 
 
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 ghcr.io/meta-pytorch/openenv-base:latest AS builder
15
 
16
  WORKDIR /app
17
 
 
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 curl -LsSf https://astral.sh/uv/install.sh | sh && \
44
+ install -m 0755 /root/.local/bin/uv /usr/local/bin/uv && \
45
+ install -m 0755 /root/.local/bin/uvx /usr/local/bin/uvx
46
+
47
  RUN --mount=type=cache,target=/root/.cache/uv \
48
  if [ -f uv.lock ]; then \
49
+ uv sync --no-install-project --no-editable; \
50
  else \
51
  uv sync --no-install-project --no-editable; \
52
  fi
53
 
54
  RUN --mount=type=cache,target=/root/.cache/uv \
55
  if [ -f uv.lock ]; then \
56
+ uv sync --no-editable; \
57
  else \
58
  uv sync --no-editable; \
59
  fi
60
 
61
  # Final runtime stage
62
+ FROM ghcr.io/meta-pytorch/openenv-base:latest
63
 
64
  WORKDIR /app
65
 
 
81
 
82
  # Run the FastAPI server
83
  # The module path is constructed to work with the /app/env structure
 
84
  CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
85
+
86
+ ENV ENABLE_WEB_INTERFACE=true
README.md CHANGED
@@ -2,54 +2,82 @@
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:
21
 
22
  ```python
23
- from envs.echo_env import EchoAction, EchoEnv
 
24
 
25
- try:
26
  # Create environment from Docker image
27
- echo_env = EchoEnv.from_docker_image("echo-env:latest")
28
 
29
- # Reset
30
- result = echo_env.reset()
31
- print(f"Reset: {result.observation.echoed_message}")
 
32
 
33
- # Send multiple messages
34
- messages = ["Hello, World!", "Testing echo", "Final message"]
35
 
36
- for msg in messages:
37
- result = echo_env.step(EchoAction(message=msg))
38
- print(f"Sent: '{msg}'")
39
- print(f" → Echoed: '{result.observation.echoed_message}'")
40
- print(f" → Length: {result.observation.message_length}")
41
- print(f" → Reward: {result.reward}")
42
 
43
- finally:
44
- # Always clean up
45
- echo_env.close()
46
  ```
47
 
48
- That's it! The `EchoEnv.from_docker_image()` method handles:
 
 
 
 
 
 
 
 
 
 
 
49
  - Starting the Docker container
50
  - Waiting for the server to be ready
51
  - Connecting to the environment
52
- - Container cleanup when you call `close()`
53
 
54
  ## Building the Docker Image
55
 
@@ -87,17 +115,20 @@ The reward is calculated as: `message_length × 0.1`
87
  If you already have an Echo environment server running, you can connect directly:
88
 
89
  ```python
90
- from envs.echo_env import EchoEnv
91
 
92
- # Connect to existing server
93
- echo_env = EchoEnv(base_url="<ENV_HTTP_URL_HERE>")
 
 
94
 
95
- # Use as normal
96
- result = echo_env.reset()
97
- result = echo_env.step(EchoAction(message="Hello!"))
 
98
  ```
99
 
100
- Note: When connecting to an existing server, `echo_env.close()` will NOT stop the server.
101
 
102
  ## Development & Testing
103
 
 
2
  title: Echo Environment Server
3
  emoji: 🔊
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  app_port: 8000
9
  base_path: /web
10
  tags:
11
+ - openenv-main
12
  - openenv
13
  ---
14
 
15
+ ## Hugging Face Space Deployment
16
+
17
+ This Space is built from OpenEnv environment `echo_env`.
18
+
19
+ - Space URL: `https://huggingface.co/spaces/openenv/echo_env`
20
+ - OpenEnv pinned ref: `main`
21
+ - Hub tag: `openenv`
22
+
23
+ ### Connecting from Code
24
+
25
+ ```python
26
+ from envs.echo_env import EchoEnv
27
+
28
+ env = EchoEnv(base_url="https://huggingface.co/spaces/openenv/echo_env")
29
+ ```
30
+
31
  # Echo Environment
32
 
33
  A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
34
 
35
  ## Quick Start
36
 
37
+ The simplest way to use the Echo environment is through the `EchoEnv` class. The client is **async by default**:
38
 
39
  ```python
40
+ import asyncio
41
+ from echo_env import EchoAction, EchoEnv
42
 
43
+ async def main():
44
  # Create environment from Docker image
45
+ client = await EchoEnv.from_docker_image("echo-env:latest")
46
 
47
+ async with client:
48
+ # Reset
49
+ result = await client.reset()
50
+ print(f"Reset: {result.observation.echoed_message}")
51
 
52
+ # Send multiple messages
53
+ messages = ["Hello, World!", "Testing echo", "Final message"]
54
 
55
+ for msg in messages:
56
+ result = await client.step(EchoAction(message=msg))
57
+ print(f"Sent: '{msg}'")
58
+ print(f" → Echoed: '{result.observation.echoed_message}'")
59
+ print(f" → Length: {result.observation.message_length}")
60
+ print(f" → Reward: {result.reward}")
61
 
62
+ asyncio.run(main())
 
 
63
  ```
64
 
65
+ For **synchronous usage**, use the `.sync()` wrapper:
66
+
67
+ ```python
68
+ from echo_env import EchoAction, EchoEnv
69
+
70
+ with EchoEnv(base_url="http://localhost:8000").sync() as client:
71
+ result = client.reset()
72
+ result = client.step(EchoAction(message="Hello!"))
73
+ print(result.observation.echoed_message)
74
+ ```
75
+
76
+ The `EchoEnv.from_docker_image()` method handles:
77
  - Starting the Docker container
78
  - Waiting for the server to be ready
79
  - Connecting to the environment
80
+ - Container cleanup when the context manager exits
81
 
82
  ## Building the Docker Image
83
 
 
115
  If you already have an Echo environment server running, you can connect directly:
116
 
117
  ```python
118
+ from echo_env import EchoAction, EchoEnv
119
 
120
+ # Async usage
121
+ async with EchoEnv(base_url="http://localhost:8000") as client:
122
+ result = await client.reset()
123
+ result = await client.step(EchoAction(message="Hello!"))
124
 
125
+ # Sync usage
126
+ with EchoEnv(base_url="http://localhost:8000").sync() as client:
127
+ result = client.reset()
128
+ result = client.step(EchoAction(message="Hello!"))
129
  ```
130
 
131
+ Note: When connecting to an existing server, closing the client will NOT stop the server.
132
 
133
  ## Development & Testing
134
 
__init__.py CHANGED
@@ -4,9 +4,26 @@
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 - A simple test environment for HTTP server."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  from .client import EchoEnv
10
- from .models import EchoAction, EchoObservation
11
 
12
- __all__ = ["EchoAction", "EchoObservation", "EchoEnv"]
 
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"]
build/lib/build/lib/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"]
build/lib/build/lib/server/app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 EnvClient.
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 ..models import EchoAction, EchoObservation
29
+ from .echo_environment import EchoEnvironment
30
+ except ImportError:
31
+ # Standalone imports (when environment is standalone with openenv from pip)
32
+ from openenv.core.env_server.http_server import create_app
33
+ from models import EchoAction, EchoObservation
34
+ from server.echo_environment import EchoEnvironment
35
+
36
+ # Create the app with web interface and README integration
37
+ # Pass the class (factory) instead of an instance for WebSocket session support
38
+ app = create_app(EchoEnvironment, EchoAction, EchoObservation, env_name="echo_env")
39
+
40
+
41
+ def main():
42
+ """
43
+ Entry point for direct execution via uv run or python -m.
44
+
45
+ This function enables running the server without Docker:
46
+ uv run --project . server
47
+ python -m envs.echo_env.server.app
48
+ openenv serve echo_env
49
+
50
+ """
51
+ import uvicorn
52
+
53
+ uvicorn.run(app, host="0.0.0.0", port=8000)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
build/lib/build/lib/server/echo_environment.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 simple test environment that echoes back messages sent to it.
11
+ Perfect for testing HTTP server infrastructure.
12
+ """
13
+
14
+ from uuid import uuid4
15
+
16
+ # Support both in-repo and standalone imports
17
+ try:
18
+ # In-repo imports (when running from OpenEnv repository)
19
+ from openenv.core.env_server.interfaces import Environment
20
+ from openenv.core.env_server.types import State
21
+ from ..models import EchoAction, EchoObservation
22
+ except ImportError:
23
+ # Standalone imports (when environment is standalone with openenv from pip)
24
+ from openenv.core.env_server.interfaces import Environment
25
+ from openenv.core.env_server.types import State
26
+ from models import EchoAction, EchoObservation
27
+
28
+
29
+ class EchoEnvironment(Environment):
30
+ """
31
+ A simple echo environment that echoes back messages.
32
+
33
+ This environment is designed for testing the HTTP server infrastructure.
34
+ It maintains minimal state and simply echoes back whatever message it receives.
35
+
36
+ Example:
37
+ >>> env = EchoEnvironment()
38
+ >>> obs = env.reset()
39
+ >>> print(obs.echoed_message) # "Echo environment ready!"
40
+ >>>
41
+ >>> obs = env.step(EchoAction(message="Hello"))
42
+ >>> print(obs.echoed_message) # "Hello"
43
+ >>> print(obs.message_length) # 5
44
+ """
45
+
46
+ def __init__(self):
47
+ """Initialize the echo environment."""
48
+ self._state = State(episode_id=str(uuid4()), step_count=0)
49
+ self._reset_count = 0
50
+
51
+ def reset(self) -> EchoObservation:
52
+ """
53
+ Reset the environment.
54
+
55
+ Returns:
56
+ EchoObservation with a ready message
57
+ """
58
+ self._state = State(episode_id=str(uuid4()), step_count=0)
59
+ self._reset_count += 1
60
+
61
+ return EchoObservation(
62
+ echoed_message="Echo environment ready!",
63
+ message_length=0,
64
+ done=False,
65
+ reward=0.0,
66
+ )
67
+
68
+ def step(self, action: EchoAction) -> EchoObservation: # type: ignore[override]
69
+ """
70
+ Execute a step in the environment by echoing the message.
71
+
72
+ Args:
73
+ action: EchoAction containing the message to echo
74
+
75
+ Returns:
76
+ EchoObservation with the echoed message and its length
77
+ """
78
+ self._state.step_count += 1
79
+
80
+ message = action.message
81
+ length = len(message)
82
+
83
+ # Simple reward: longer messages get higher rewards
84
+ reward = length * 0.1
85
+
86
+ return EchoObservation(
87
+ echoed_message=message,
88
+ message_length=length,
89
+ done=False,
90
+ reward=reward,
91
+ metadata={"original_message": message, "step": self._state.step_count},
92
+ )
93
+
94
+ @property
95
+ def state(self) -> State:
96
+ """
97
+ Get the current environment state.
98
+
99
+ Returns:
100
+ Current State with episode_id and step_count
101
+ """
102
+ return self._state
build/lib/echo_env/__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
+ from .client import EchoEnv
25
+
26
+ # Re-export MCP types for convenience
27
+ from openenv.core.env_server.mcp_types import CallToolAction, ListToolsAction
28
+
29
+ __all__ = ["EchoEnv", "CallToolAction", "ListToolsAction"]
build/lib/echo_env/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
build/lib/echo_env/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"]
build/lib/echo_env/server/app.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from .echo_environment import EchoEnvironment
30
+ except ImportError:
31
+ # Standalone imports (when environment is standalone with openenv from pip)
32
+ from openenv.core.env_server.http_server import create_app
33
+ from openenv.core.env_server.mcp_types import CallToolAction, CallToolObservation
34
+ from server.echo_environment import EchoEnvironment
35
+
36
+ # Create the app with web interface and README integration
37
+ # Pass the class (factory) instead of an instance for WebSocket session support
38
+ # Use MCP types for action/observation since this is a pure MCP environment
39
+ app = create_app(
40
+ EchoEnvironment, CallToolAction, CallToolObservation, env_name="echo_env"
41
+ )
42
+
43
+
44
+ def main():
45
+ """
46
+ Entry point for direct execution via uv run or python -m.
47
+
48
+ This function enables running the server without Docker:
49
+ uv run --project . server
50
+ python -m envs.echo_env.server.app
51
+ openenv serve echo_env
52
+
53
+ """
54
+ import uvicorn
55
+
56
+ uvicorn.run(app, host="0.0.0.0", port=8000)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
build/lib/echo_env/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
build/lib/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"]
build/lib/server/app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 EnvClient.
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 ..models import EchoAction, EchoObservation
29
+ from .echo_environment import EchoEnvironment
30
+ except ImportError:
31
+ # Standalone imports (when environment is standalone with openenv from pip)
32
+ from openenv.core.env_server.http_server import create_app
33
+ from models import EchoAction, EchoObservation
34
+ from server.echo_environment import EchoEnvironment
35
+
36
+ # Create the app with web interface and README integration
37
+ # Pass the class (factory) instead of an instance for WebSocket session support
38
+ app = create_app(EchoEnvironment, EchoAction, EchoObservation, env_name="echo_env")
39
+
40
+
41
+ def main():
42
+ """
43
+ Entry point for direct execution via uv run or python -m.
44
+
45
+ This function enables running the server without Docker:
46
+ uv run --project . server
47
+ python -m envs.echo_env.server.app
48
+ openenv serve echo_env
49
+
50
+ """
51
+ import uvicorn
52
+
53
+ uvicorn.run(app, host="0.0.0.0", port=8000)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
build/lib/server/echo_environment.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 simple test environment that echoes back messages sent to it.
11
+ Perfect for testing HTTP server infrastructure.
12
+ """
13
+
14
+ from uuid import uuid4
15
+
16
+ # Support both in-repo and standalone imports
17
+ try:
18
+ # In-repo imports (when running from OpenEnv repository)
19
+ from openenv.core.env_server.interfaces import Environment
20
+ from openenv.core.env_server.types import State
21
+ from ..models import EchoAction, EchoObservation
22
+ except ImportError:
23
+ # Standalone imports (when environment is standalone with openenv from pip)
24
+ from openenv.core.env_server.interfaces import Environment
25
+ from openenv.core.env_server.types import State
26
+ from models import EchoAction, EchoObservation
27
+
28
+
29
+ class EchoEnvironment(Environment):
30
+ """
31
+ A simple echo environment that echoes back messages.
32
+
33
+ This environment is designed for testing the HTTP server infrastructure.
34
+ It maintains minimal state and simply echoes back whatever message it receives.
35
+
36
+ Example:
37
+ >>> env = EchoEnvironment()
38
+ >>> obs = env.reset()
39
+ >>> print(obs.echoed_message) # "Echo environment ready!"
40
+ >>>
41
+ >>> obs = env.step(EchoAction(message="Hello"))
42
+ >>> print(obs.echoed_message) # "Hello"
43
+ >>> print(obs.message_length) # 5
44
+ """
45
+
46
+ def __init__(self):
47
+ """Initialize the echo environment."""
48
+ self._state = State(episode_id=str(uuid4()), step_count=0)
49
+ self._reset_count = 0
50
+
51
+ def reset(self) -> EchoObservation:
52
+ """
53
+ Reset the environment.
54
+
55
+ Returns:
56
+ EchoObservation with a ready message
57
+ """
58
+ self._state = State(episode_id=str(uuid4()), step_count=0)
59
+ self._reset_count += 1
60
+
61
+ return EchoObservation(
62
+ echoed_message="Echo environment ready!",
63
+ message_length=0,
64
+ done=False,
65
+ reward=0.0,
66
+ )
67
+
68
+ def step(self, action: EchoAction) -> EchoObservation: # type: ignore[override]
69
+ """
70
+ Execute a step in the environment by echoing the message.
71
+
72
+ Args:
73
+ action: EchoAction containing the message to echo
74
+
75
+ Returns:
76
+ EchoObservation with the echoed message and its length
77
+ """
78
+ self._state.step_count += 1
79
+
80
+ message = action.message
81
+ length = len(message)
82
+
83
+ # Simple reward: longer messages get higher rewards
84
+ reward = length * 0.1
85
+
86
+ return EchoObservation(
87
+ echoed_message=message,
88
+ message_length=length,
89
+ done=False,
90
+ reward=reward,
91
+ metadata={"original_message": message, "step": self._state.step_count},
92
+ )
93
+
94
+ @property
95
+ def state(self) -> State:
96
+ """
97
+ Get the current environment state.
98
+
99
+ Returns:
100
+ Current State with episode_id and step_count
101
+ """
102
+ return self._state
client.py CHANGED
@@ -7,105 +7,75 @@
7
  """
8
  Echo Environment Client.
9
 
10
- This module provides the client for connecting to an Echo Environment server
11
- via WebSocket for persistent sessions.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  """
13
 
14
- from typing import Any, Dict
15
 
16
- # Support both in-repo and standalone imports
17
- try:
18
- # In-repo imports (when running from OpenEnv repository)
19
- from openenv.core.client_types import StepResult
20
- from openenv.core.env_server.types import State
21
- from openenv.core.env_client import EnvClient
22
- from .models import EchoAction, EchoObservation
23
- except ImportError:
24
- # Standalone imports (when environment is standalone with openenv from pip)
25
- from openenv.core.client_types import StepResult
26
- from openenv.core.env_server.types import State
27
- from openenv.core.env_client import EnvClient
28
- from models import EchoAction, EchoObservation
29
 
30
-
31
- class EchoEnv(EnvClient[EchoAction, EchoObservation, State]):
32
  """
33
  Client for the Echo Environment.
34
 
35
- This client maintains a persistent WebSocket connection to the environment
36
- server, enabling efficient multi-step interactions with lower latency.
37
- Each client instance has its own dedicated environment session on the server.
 
 
 
38
 
39
  Example:
40
  >>> # Connect to a running server
41
- >>> with EchoEnv(base_url="http://localhost:8000") as client:
42
- ... result = client.reset()
43
- ... print(result.observation.echoed_message)
 
 
 
 
44
  ...
45
- ... result = client.step(EchoAction(message="Hello!"))
46
- ... print(result.observation.echoed_message)
47
- ... print(result.reward)
 
 
 
 
48
 
49
  Example with Docker:
50
  >>> # Automatically start container and connect
51
- >>> client = EchoEnv.from_docker_image("echo-env:latest")
52
  >>> try:
53
- ... result = client.reset()
54
- ... result = client.step(EchoAction(message="Test"))
 
55
  ... finally:
56
- ... client.close()
57
- """
58
-
59
- def _step_payload(self, action: EchoAction) -> Dict:
60
- """
61
- Convert EchoAction to JSON payload for step request.
62
-
63
- Args:
64
- action: EchoAction instance
65
-
66
- Returns:
67
- Dictionary representation suitable for JSON encoding
68
- """
69
- return {
70
- "message": action.message,
71
- }
72
-
73
- def _parse_result(self, payload: Dict) -> StepResult[EchoObservation]:
74
- """
75
- Parse server response into StepResult[EchoObservation].
76
 
77
- Args:
78
- payload: JSON response from server
79
-
80
- Returns:
81
- StepResult with EchoObservation
82
- """
83
- obs_data = payload.get("observation", {})
84
- observation = EchoObservation(
85
- echoed_message=obs_data.get("echoed_message", ""),
86
- message_length=obs_data.get("message_length", 0),
87
- done=payload.get("done", False),
88
- reward=payload.get("reward"),
89
- metadata=obs_data.get("metadata", {}),
90
- )
91
-
92
- return StepResult(
93
- observation=observation,
94
- reward=payload.get("reward"),
95
- done=payload.get("done", False),
96
- )
97
-
98
- def _parse_state(self, payload: Dict) -> State:
99
- """
100
- Parse server response into State object.
101
-
102
- Args:
103
- payload: JSON response from /state endpoint
104
 
105
- Returns:
106
- State object with episode_id and step_count
107
- """
108
- return State(
109
- episode_id=payload.get("episode_id"),
110
- step_count=payload.get("step_count", 0),
111
- )
 
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
 
 
 
 
 
 
envs/echo_env/README.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ```
envs/echo_env/__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"]
envs/echo_env/build/lib/build/lib/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"]
envs/echo_env/build/lib/build/lib/server/app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 EnvClient.
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 ..models import EchoAction, EchoObservation
29
+ from .echo_environment import EchoEnvironment
30
+ except ImportError:
31
+ # Standalone imports (when environment is standalone with openenv from pip)
32
+ from openenv.core.env_server.http_server import create_app
33
+ from models import EchoAction, EchoObservation
34
+ from server.echo_environment import EchoEnvironment
35
+
36
+ # Create the app with web interface and README integration
37
+ # Pass the class (factory) instead of an instance for WebSocket session support
38
+ app = create_app(EchoEnvironment, EchoAction, EchoObservation, env_name="echo_env")
39
+
40
+
41
+ def main():
42
+ """
43
+ Entry point for direct execution via uv run or python -m.
44
+
45
+ This function enables running the server without Docker:
46
+ uv run --project . server
47
+ python -m envs.echo_env.server.app
48
+ openenv serve echo_env
49
+
50
+ """
51
+ import uvicorn
52
+
53
+ uvicorn.run(app, host="0.0.0.0", port=8000)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
envs/echo_env/build/lib/build/lib/server/echo_environment.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 simple test environment that echoes back messages sent to it.
11
+ Perfect for testing HTTP server infrastructure.
12
+ """
13
+
14
+ from uuid import uuid4
15
+
16
+ # Support both in-repo and standalone imports
17
+ try:
18
+ # In-repo imports (when running from OpenEnv repository)
19
+ from openenv.core.env_server.interfaces import Environment
20
+ from openenv.core.env_server.types import State
21
+ from ..models import EchoAction, EchoObservation
22
+ except ImportError:
23
+ # Standalone imports (when environment is standalone with openenv from pip)
24
+ from openenv.core.env_server.interfaces import Environment
25
+ from openenv.core.env_server.types import State
26
+ from models import EchoAction, EchoObservation
27
+
28
+
29
+ class EchoEnvironment(Environment):
30
+ """
31
+ A simple echo environment that echoes back messages.
32
+
33
+ This environment is designed for testing the HTTP server infrastructure.
34
+ It maintains minimal state and simply echoes back whatever message it receives.
35
+
36
+ Example:
37
+ >>> env = EchoEnvironment()
38
+ >>> obs = env.reset()
39
+ >>> print(obs.echoed_message) # "Echo environment ready!"
40
+ >>>
41
+ >>> obs = env.step(EchoAction(message="Hello"))
42
+ >>> print(obs.echoed_message) # "Hello"
43
+ >>> print(obs.message_length) # 5
44
+ """
45
+
46
+ def __init__(self):
47
+ """Initialize the echo environment."""
48
+ self._state = State(episode_id=str(uuid4()), step_count=0)
49
+ self._reset_count = 0
50
+
51
+ def reset(self) -> EchoObservation:
52
+ """
53
+ Reset the environment.
54
+
55
+ Returns:
56
+ EchoObservation with a ready message
57
+ """
58
+ self._state = State(episode_id=str(uuid4()), step_count=0)
59
+ self._reset_count += 1
60
+
61
+ return EchoObservation(
62
+ echoed_message="Echo environment ready!",
63
+ message_length=0,
64
+ done=False,
65
+ reward=0.0,
66
+ )
67
+
68
+ def step(self, action: EchoAction) -> EchoObservation: # type: ignore[override]
69
+ """
70
+ Execute a step in the environment by echoing the message.
71
+
72
+ Args:
73
+ action: EchoAction containing the message to echo
74
+
75
+ Returns:
76
+ EchoObservation with the echoed message and its length
77
+ """
78
+ self._state.step_count += 1
79
+
80
+ message = action.message
81
+ length = len(message)
82
+
83
+ # Simple reward: longer messages get higher rewards
84
+ reward = length * 0.1
85
+
86
+ return EchoObservation(
87
+ echoed_message=message,
88
+ message_length=length,
89
+ done=False,
90
+ reward=reward,
91
+ metadata={"original_message": message, "step": self._state.step_count},
92
+ )
93
+
94
+ @property
95
+ def state(self) -> State:
96
+ """
97
+ Get the current environment state.
98
+
99
+ Returns:
100
+ Current State with episode_id and step_count
101
+ """
102
+ return self._state
envs/echo_env/build/lib/echo_env/__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
+ from .client import EchoEnv
25
+
26
+ # Re-export MCP types for convenience
27
+ from openenv.core.env_server.mcp_types import CallToolAction, ListToolsAction
28
+
29
+ __all__ = ["EchoEnv", "CallToolAction", "ListToolsAction"]
envs/echo_env/build/lib/echo_env/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
envs/echo_env/build/lib/echo_env/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"]
envs/echo_env/build/lib/echo_env/server/app.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from .echo_environment import EchoEnvironment
30
+ except ImportError:
31
+ # Standalone imports (when environment is standalone with openenv from pip)
32
+ from openenv.core.env_server.http_server import create_app
33
+ from openenv.core.env_server.mcp_types import CallToolAction, CallToolObservation
34
+ from server.echo_environment import EchoEnvironment
35
+
36
+ # Create the app with web interface and README integration
37
+ # Pass the class (factory) instead of an instance for WebSocket session support
38
+ # Use MCP types for action/observation since this is a pure MCP environment
39
+ app = create_app(
40
+ EchoEnvironment, CallToolAction, CallToolObservation, env_name="echo_env"
41
+ )
42
+
43
+
44
+ def main():
45
+ """
46
+ Entry point for direct execution via uv run or python -m.
47
+
48
+ This function enables running the server without Docker:
49
+ uv run --project . server
50
+ python -m envs.echo_env.server.app
51
+ openenv serve echo_env
52
+
53
+ """
54
+ import uvicorn
55
+
56
+ uvicorn.run(app, host="0.0.0.0", port=8000)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
envs/echo_env/build/lib/echo_env/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
envs/echo_env/build/lib/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"]
envs/echo_env/build/lib/server/app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 EnvClient.
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 ..models import EchoAction, EchoObservation
29
+ from .echo_environment import EchoEnvironment
30
+ except ImportError:
31
+ # Standalone imports (when environment is standalone with openenv from pip)
32
+ from openenv.core.env_server.http_server import create_app
33
+ from models import EchoAction, EchoObservation
34
+ from server.echo_environment import EchoEnvironment
35
+
36
+ # Create the app with web interface and README integration
37
+ # Pass the class (factory) instead of an instance for WebSocket session support
38
+ app = create_app(EchoEnvironment, EchoAction, EchoObservation, env_name="echo_env")
39
+
40
+
41
+ def main():
42
+ """
43
+ Entry point for direct execution via uv run or python -m.
44
+
45
+ This function enables running the server without Docker:
46
+ uv run --project . server
47
+ python -m envs.echo_env.server.app
48
+ openenv serve echo_env
49
+
50
+ """
51
+ import uvicorn
52
+
53
+ uvicorn.run(app, host="0.0.0.0", port=8000)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
envs/echo_env/build/lib/server/echo_environment.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 simple test environment that echoes back messages sent to it.
11
+ Perfect for testing HTTP server infrastructure.
12
+ """
13
+
14
+ from uuid import uuid4
15
+
16
+ # Support both in-repo and standalone imports
17
+ try:
18
+ # In-repo imports (when running from OpenEnv repository)
19
+ from openenv.core.env_server.interfaces import Environment
20
+ from openenv.core.env_server.types import State
21
+ from ..models import EchoAction, EchoObservation
22
+ except ImportError:
23
+ # Standalone imports (when environment is standalone with openenv from pip)
24
+ from openenv.core.env_server.interfaces import Environment
25
+ from openenv.core.env_server.types import State
26
+ from models import EchoAction, EchoObservation
27
+
28
+
29
+ class EchoEnvironment(Environment):
30
+ """
31
+ A simple echo environment that echoes back messages.
32
+
33
+ This environment is designed for testing the HTTP server infrastructure.
34
+ It maintains minimal state and simply echoes back whatever message it receives.
35
+
36
+ Example:
37
+ >>> env = EchoEnvironment()
38
+ >>> obs = env.reset()
39
+ >>> print(obs.echoed_message) # "Echo environment ready!"
40
+ >>>
41
+ >>> obs = env.step(EchoAction(message="Hello"))
42
+ >>> print(obs.echoed_message) # "Hello"
43
+ >>> print(obs.message_length) # 5
44
+ """
45
+
46
+ def __init__(self):
47
+ """Initialize the echo environment."""
48
+ self._state = State(episode_id=str(uuid4()), step_count=0)
49
+ self._reset_count = 0
50
+
51
+ def reset(self) -> EchoObservation:
52
+ """
53
+ Reset the environment.
54
+
55
+ Returns:
56
+ EchoObservation with a ready message
57
+ """
58
+ self._state = State(episode_id=str(uuid4()), step_count=0)
59
+ self._reset_count += 1
60
+
61
+ return EchoObservation(
62
+ echoed_message="Echo environment ready!",
63
+ message_length=0,
64
+ done=False,
65
+ reward=0.0,
66
+ )
67
+
68
+ def step(self, action: EchoAction) -> EchoObservation: # type: ignore[override]
69
+ """
70
+ Execute a step in the environment by echoing the message.
71
+
72
+ Args:
73
+ action: EchoAction containing the message to echo
74
+
75
+ Returns:
76
+ EchoObservation with the echoed message and its length
77
+ """
78
+ self._state.step_count += 1
79
+
80
+ message = action.message
81
+ length = len(message)
82
+
83
+ # Simple reward: longer messages get higher rewards
84
+ reward = length * 0.1
85
+
86
+ return EchoObservation(
87
+ echoed_message=message,
88
+ message_length=length,
89
+ done=False,
90
+ reward=reward,
91
+ metadata={"original_message": message, "step": self._state.step_count},
92
+ )
93
+
94
+ @property
95
+ def state(self) -> State:
96
+ """
97
+ Get the current environment state.
98
+
99
+ Returns:
100
+ Current State with episode_id and step_count
101
+ """
102
+ return self._state
envs/echo_env/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
envs/echo_env/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
envs/echo_env/openenv_echo_env.egg-info/PKG-INFO ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-echo-env
3
+ Version: 0.1.0
4
+ Summary: Echo Environment for OpenEnv - simple test environment that echoes back messages
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core[core]>=0.2.1
7
+ Requires-Dist: fastapi>=0.115.0
8
+ Requires-Dist: pydantic>=2.0.0
9
+ Requires-Dist: uvicorn>=0.24.0
10
+ Requires-Dist: requests>=2.31.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
13
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
envs/echo_env/openenv_echo_env.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ pyproject.toml
5
+ ./__init__.py
6
+ ./client.py
7
+ openenv_echo_env.egg-info/PKG-INFO
8
+ openenv_echo_env.egg-info/SOURCES.txt
9
+ openenv_echo_env.egg-info/dependency_links.txt
10
+ openenv_echo_env.egg-info/entry_points.txt
11
+ openenv_echo_env.egg-info/requires.txt
12
+ openenv_echo_env.egg-info/top_level.txt
13
+ server/__init__.py
14
+ server/app.py
15
+ server/echo_environment.py
envs/echo_env/openenv_echo_env.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
envs/echo_env/openenv_echo_env.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = echo_env.server.app:main
envs/echo_env/openenv_echo_env.egg-info/requires.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.1
2
+ fastapi>=0.115.0
3
+ pydantic>=2.0.0
4
+ uvicorn>=0.24.0
5
+ requests>=2.31.0
6
+
7
+ [dev]
8
+ pytest>=8.0.0
9
+ pytest-cov>=4.0.0
envs/echo_env/openenv_echo_env.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ echo_env
envs/echo_env/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] @ git+https://github.com/meta-pytorch/OpenEnv.git@main",
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" }
envs/echo_env/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"]
envs/echo_env/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"]
envs/echo_env/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()
envs/echo_env/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
openenv.yaml CHANGED
@@ -1,6 +1,6 @@
1
- spec_version: 1
2
- name: echo_env
3
- type: space
4
- runtime: fastapi
5
- app: server.app:app
6
- port: 8000
 
1
+ spec_version: 1
2
+ name: echo_env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
openenv_echo_env.egg-info/PKG-INFO CHANGED
@@ -3,7 +3,7 @@ Name: openenv-echo-env
3
  Version: 0.1.0
4
  Summary: Echo Environment for OpenEnv - simple test environment that echoes back messages
5
  Requires-Python: >=3.10
6
- Requires-Dist: openenv-core>=0.2.0
7
  Requires-Dist: fastapi>=0.115.0
8
  Requires-Dist: pydantic>=2.0.0
9
  Requires-Dist: uvicorn>=0.24.0
 
3
  Version: 0.1.0
4
  Summary: Echo Environment for OpenEnv - simple test environment that echoes back messages
5
  Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core[core]>=0.2.1
7
  Requires-Dist: fastapi>=0.115.0
8
  Requires-Dist: pydantic>=2.0.0
9
  Requires-Dist: uvicorn>=0.24.0
openenv_echo_env.egg-info/SOURCES.txt CHANGED
@@ -1,14 +1,15 @@
1
  README.md
 
 
2
  pyproject.toml
3
  ./__init__.py
4
  ./client.py
5
- ./models.py
6
- ./server/__init__.py
7
- ./server/app.py
8
- ./server/echo_environment.py
9
  openenv_echo_env.egg-info/PKG-INFO
10
  openenv_echo_env.egg-info/SOURCES.txt
11
  openenv_echo_env.egg-info/dependency_links.txt
12
  openenv_echo_env.egg-info/entry_points.txt
13
  openenv_echo_env.egg-info/requires.txt
14
- openenv_echo_env.egg-info/top_level.txt
 
 
 
 
1
  README.md
2
+ __init__.py
3
+ client.py
4
  pyproject.toml
5
  ./__init__.py
6
  ./client.py
 
 
 
 
7
  openenv_echo_env.egg-info/PKG-INFO
8
  openenv_echo_env.egg-info/SOURCES.txt
9
  openenv_echo_env.egg-info/dependency_links.txt
10
  openenv_echo_env.egg-info/entry_points.txt
11
  openenv_echo_env.egg-info/requires.txt
12
+ openenv_echo_env.egg-info/top_level.txt
13
+ server/__init__.py
14
+ server/app.py
15
+ server/echo_environment.py
openenv_echo_env.egg-info/requires.txt CHANGED
@@ -1,4 +1,4 @@
1
- openenv-core>=0.2.0
2
  fastapi>=0.115.0
3
  pydantic>=2.0.0
4
  uvicorn>=0.24.0
 
1
+ openenv-core[core]>=0.2.1
2
  fastapi>=0.115.0
3
  pydantic>=2.0.0
4
  uvicorn>=0.24.0
pyproject.toml CHANGED
@@ -15,7 +15,7 @@ description = "Echo Environment for OpenEnv - simple test environment that echoe
15
  requires-python = ">=3.10"
16
  dependencies = [
17
  # Core OpenEnv dependencies (required for server functionality)
18
- "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.1",
19
  "fastapi>=0.115.0",
20
  "pydantic>=2.0.0",
21
  "uvicorn>=0.24.0",
 
15
  requires-python = ">=3.10"
16
  dependencies = [
17
  # Core OpenEnv dependencies (required for server functionality)
18
+ "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@main",
19
  "fastapi>=0.115.0",
20
  "pydantic>=2.0.0",
21
  "uvicorn>=0.24.0",
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 CHANGED
@@ -8,4 +8,4 @@
8
 
9
  from .echo_environment import EchoEnvironment
10
 
11
- __all__ = ["EchoEnvironment"]
 
8
 
9
  from .echo_environment import EchoEnvironment
10
 
11
+ __all__ = ["EchoEnvironment"]
server/app.py CHANGED
@@ -8,7 +8,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 EnvClient.
12
 
13
  Usage:
14
  # Development (with auto-reload):
@@ -25,17 +25,21 @@ Usage:
25
  try:
26
  # In-repo imports (when running from OpenEnv repository)
27
  from openenv.core.env_server.http_server import create_app
28
- from ..models import EchoAction, EchoObservation
 
29
  from .echo_environment import EchoEnvironment
30
  except ImportError:
31
  # Standalone imports (when environment is standalone with openenv from pip)
32
  from openenv.core.env_server.http_server import create_app
33
- from models import EchoAction, EchoObservation
34
  from server.echo_environment import EchoEnvironment
35
 
36
  # Create the app with web interface and README integration
37
  # Pass the class (factory) instead of an instance for WebSocket session support
38
- app = create_app(EchoEnvironment, EchoAction, EchoObservation, env_name="echo_env")
 
 
 
39
 
40
 
41
  def main():
 
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):
 
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():
server/echo_environment.py CHANGED
@@ -7,91 +7,183 @@
7
  """
8
  Echo Environment Implementation.
9
 
10
- A simple test environment that echoes back messages sent to it.
11
- Perfect for testing HTTP server infrastructure.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  """
13
 
 
14
  from uuid import uuid4
15
 
16
  # Support both in-repo and standalone imports
17
  try:
18
  # In-repo imports (when running from OpenEnv repository)
19
- from openenv.core.env_server.interfaces import Environment
20
- from openenv.core.env_server.types import State
21
- from ..models import EchoAction, EchoObservation
22
  except ImportError:
23
  # Standalone imports (when environment is standalone with openenv from pip)
24
- from openenv.core.env_server.interfaces import Environment
25
- from openenv.core.env_server.types import State
26
- from models import EchoAction, EchoObservation
27
 
 
28
 
29
- class EchoEnvironment(Environment):
 
30
  """
31
- A simple echo environment that echoes back messages.
 
 
 
 
32
 
33
- This environment is designed for testing the HTTP server infrastructure.
34
- It maintains minimal state and simply echoes back whatever message it receives.
35
 
36
- Example:
37
- >>> env = EchoEnvironment()
38
- >>> obs = env.reset()
39
- >>> print(obs.echoed_message) # "Echo environment ready!"
40
  >>>
41
- >>> obs = env.step(EchoAction(message="Hello"))
42
- >>> print(obs.echoed_message) # "Hello"
43
- >>> print(obs.message_length) # 5
 
 
 
44
  """
45
 
46
- SUPPORTS_CONCURRENT_SESSIONS: bool = True
47
-
48
  def __init__(self):
49
- """Initialize the echo environment."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  self._state = State(episode_id=str(uuid4()), step_count=0)
51
  self._reset_count = 0
52
 
53
- def reset(self) -> EchoObservation:
 
 
 
 
 
54
  """
55
  Reset the environment.
56
 
 
 
 
 
 
57
  Returns:
58
- EchoObservation with a ready message
59
  """
60
- self._state = State(episode_id=str(uuid4()), step_count=0)
 
 
 
61
  self._reset_count += 1
62
 
63
- return EchoObservation(
64
- echoed_message="Echo environment ready!",
65
- message_length=0,
66
  done=False,
67
  reward=0.0,
 
68
  )
69
 
70
- def step(self, action: EchoAction) -> EchoObservation: # type: ignore[override]
 
 
 
 
 
71
  """
72
- Execute a step in the environment by echoing the message.
 
 
 
73
 
74
  Args:
75
- action: EchoAction containing the message to echo
 
 
76
 
77
  Returns:
78
- EchoObservation with the echoed message and its length
79
  """
80
- self._state.step_count += 1
 
 
 
 
 
 
 
81
 
82
- message = action.message
83
- length = len(message)
 
 
 
 
 
 
84
 
85
- # Simple reward: longer messages get higher rewards
86
- reward = length * 0.1
87
 
88
- return EchoObservation(
89
- echoed_message=message,
90
- message_length=length,
91
- done=False,
92
- reward=reward,
93
- metadata={"original_message": message, "step": self._state.step_count},
94
- )
 
 
 
 
 
 
95
 
96
  @property
97
  def state(self) -> State:
 
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: