cc4718 commited on
Commit
64c6bf5
·
verified ·
1 Parent(s): 0f6bfe1

Upload folder using huggingface_hub

Browse files
Dockerfile 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
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
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
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=my_env
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
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
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f 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
+ ENV ENABLE_WEB_INTERFACE=true
81
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,10 +1,122 @@
1
  ---
2
- title: My Env
3
- emoji: 🐠
4
  colorFrom: yellow
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
 
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: My Env Environment Server
3
+ emoji: 💿
4
  colorFrom: yellow
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
+ # Maintenance-Bench
14
 
15
+ An **OpenEnv-based reinforcement learning environment for industrial maintenance diagnostics**.
16
+
17
+ This repository provides the initial implementation of a diagnostic decision-making environment designed to evaluate **AI research agents on industrial troubleshooting tasks**.
18
+
19
+ The environment is inspired by real-world industrial maintenance logs and is part of the **Maintenance-Bench benchmark**.
20
+
21
+ ---
22
+
23
+ # Overview
24
+
25
+ The environment simulates an **industrial diagnostic process** where an agent must decide which diagnostic steps to perform to identify a fault.
26
+
27
+ Each episode corresponds to a **maintenance case scenario** stored as a JSON file.
28
+
29
+ A scenario contains:
30
+
31
+ - machine metadata
32
+ - symptoms
33
+ - possible diagnostic steps
34
+ - observations for each step
35
+ - rewards
36
+ - operational costs
37
+
38
+ The agent interacts with the environment through a **step-by-step diagnostic process**, receiving observations, rewards, accumulated costs, and the **current health status of the system**.
39
+
40
+ ---
41
+
42
+ # Environment API
43
+
44
+ The environment follows the **OpenEnv interface**:
45
+
46
+ - `reset()` – loads a new diagnostic scenario and initializes an episode
47
+ - `step(action)` – performs a diagnostic action and returns the observation
48
+
49
+ ---
50
+
51
+ # Episode Flow
52
+
53
+ 1. The environment loads a diagnostic scenario from the `data/` directory.
54
+ 2. The agent receives the **initial symptom description**.
55
+ 3. The agent chooses a diagnostic action.
56
+ 4. The environment returns:
57
+
58
+ - the observation
59
+ - reward
60
+ - accumulated cost
61
+ - **health status of the system**
62
+ - history of performed steps
63
+
64
+ ---
65
+
66
+ # Example Interaction
67
+
68
+ Example sequence of actions:
69
+
70
+ ```python
71
+ reset()
72
+ ```
73
+
74
+ Agent observation:
75
+
76
+ ```
77
+ "High Bearing temperature."
78
+ ```
79
+
80
+ Agent performs diagnostic step:
81
+
82
+ ```python
83
+ action = "diagnose:0"
84
+ ```
85
+
86
+ Environment response:
87
+
88
+ ```
89
+ Observation:
90
+ "Overload relay found tripped at the time of breakdown."
91
+
92
+ Reward: 1
93
+ Total Cost: 5
94
+ done: False #equivalent to health status 0
95
+
96
+ History:
97
+ [
98
+ {
99
+ "action": "Monitored bearing temperatures at DE and NDE using RTD.",
100
+ "observation": "Overload relay found tripped at the time of breakdown."
101
+ }
102
+ ]
103
+ ```
104
+
105
+ ---
106
+
107
+ # Project Goal
108
+
109
+ The goal of this project is to provide a **benchmark environment for evaluating deep research agents on industrial maintenance diagnostics tasks**.
110
+
111
+ This environment supports research in:
112
+
113
+ - autonomous troubleshooting agents
114
+ - cost-aware reasoning
115
+ - industrial decision making
116
+ - long-horizon diagnostic planning
117
+
118
+ ---
119
+
120
+ # License
121
+
122
+ This project builds on **OpenEnv** and follows the license terms of the respective components.
__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """My Env Environment."""
8
+
9
+ from .client import MyEnv
10
+ from .models import MyAction, MyObservation
11
+
12
+ __all__ = [
13
+ "MyAction",
14
+ "MyObservation",
15
+ "MyEnv",
16
+ ]
client.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
+ """My Env Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core import EnvClient
12
+ from openenv.core.client_types import StepResult
13
+ from openenv.core.env_server.types import State
14
+
15
+ from .models import MyAction, MyObservation
16
+
17
+ import sys
18
+
19
+ sys.path.append('.')
20
+
21
+ class MyEnv(
22
+ EnvClient[MyAction, MyObservation, State]
23
+ ):
24
+ """
25
+ Client for the My Env Environment.
26
+
27
+ This client maintains a persistent WebSocket connection to the environment server,
28
+ enabling efficient multi-step interactions with lower latency.
29
+ Each client instance has its own dedicated environment session on the server.
30
+
31
+ Example:
32
+ >>> # Connect to a running server
33
+ >>> with MyEnv(base_url="http://localhost:8000") as client:
34
+ ... result = client.reset()
35
+ ... print(result.observation.echoed_message)
36
+ ...
37
+ ... result = client.step(MyAction(message="Hello!"))
38
+ ... print(result.observation.echoed_message)
39
+
40
+ Example with Docker:
41
+ >>> # Automatically start container and connect
42
+ >>> client = MyEnv.from_docker_image("my_env-env:latest")
43
+ >>> try:
44
+ ... result = client.reset()
45
+ ... result = client.step(MyAction(message="Test"))
46
+ ... finally:
47
+ ... client.close()
48
+ """
49
+
50
+ def _step_payload(self, action: MyAction) -> Dict:
51
+ """
52
+ Convert MyAction to JSON payload for step message.
53
+
54
+ Args:
55
+ action: MyAction instance
56
+
57
+ Returns:
58
+ Dictionary representation suitable for JSON encoding
59
+ """
60
+ return {
61
+ "message": action.message,
62
+ }
63
+
64
+ def _parse_result(self, payload: Dict) -> StepResult[MyObservation]:
65
+ """
66
+ Parse server response into StepResult[MyObservation].
67
+
68
+ Args:
69
+ payload: JSON response data from server
70
+
71
+ Returns:
72
+ StepResult with MyObservation
73
+ """
74
+ obs_data = payload.get("observation", {})
75
+ observation = MyObservation(
76
+ echoed_message=obs_data.get("echoed_message", ""),
77
+ message_length=obs_data.get("message_length", 0),
78
+ done=payload.get("done", False),
79
+ reward=payload.get("reward"),
80
+ metadata=obs_data.get("metadata", {}),
81
+ )
82
+
83
+ return StepResult(
84
+ observation=observation,
85
+ reward=payload.get("reward"),
86
+ done=payload.get("done", False),
87
+ )
88
+
89
+ def _parse_state(self, payload: Dict) -> State:
90
+ """
91
+ Parse server response into State object.
92
+
93
+ Args:
94
+ payload: JSON response from state request
95
+
96
+ Returns:
97
+ State object with episode_id and step_count
98
+ """
99
+ return State(
100
+ episode_id=payload.get("episode_id"),
101
+ step_count=payload.get("step_count", 0),
102
+ )
data/1.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "case_id": "80",
3
+ "motor_id": "<<A068>>",
4
+ "area": "<<HSM>",
5
+ "line": "<<Furnace Area>>",
6
+ "rating_kw": 400,
7
+ "log_book_entry_date": "2024-05-14",
8
+ "failure_type": "<<Breakdown>>",
9
+ "symptom_text": "<<High Bearing temperature.>>",
10
+ "diagnostic_steps": [
11
+ "Monitored bearing temperatures at DE and NDE using RTD.",
12
+ "Monitored vibration severity at DE and NDE using vibration meter / physical assessment.",
13
+ "Checked rotor free movement by manual rotation.",
14
+ "Inspected coupling condition for seizure, wear, or damage.",
15
+ "Verified motor-gearbox alignment condition.",
16
+ "Checked foundation bolts and base plate tightness.",
17
+ "Inspected bearing lubrication condition and grease contamination."
18
+ ],
19
+ "diagnostic_steps_rewards": [
20
+ 1,
21
+ 0,
22
+ 0,
23
+ 0,
24
+ 0,
25
+ 0,
26
+ 0
27
+ ],
28
+ "diagnostic_steps_costs": [
29
+ 5,
30
+ 10,
31
+ 5,
32
+ 15,
33
+ 45,
34
+ 10,
35
+ 15
36
+ ],
37
+ "observations": [
38
+ "Overload relay found tripped at the time of breakdown.",
39
+ "Incoming supply voltage and phase current were normal with no imbalance.",
40
+ "Insulation resistance (IR) values were within acceptable limits, indicating no electrical fault.",
41
+ "Normal vibration observed during motor operation.",
42
+ "NDE bearing temperature was normal when checked using laser thermometer.",
43
+ "Rotor free movement was ok.",
44
+ "NDE Bearing fine."
45
+ ],
46
+ "root_cause": "Bearing seizure at NDE causing overload trip",
47
+ "rectification": [
48
+ "Replaced NDE bearing",
49
+ "Cleaned and greased housings",
50
+ "Checked alignment",
51
+ "Performed no-load and load trials",
52
+ "Reset motor overload relay",
53
+ "Replace motor starter contactor",
54
+ "Reprogram the VFD acceleration ramp time"
55
+ ],
56
+ "rectification_rewards": [
57
+ 1,
58
+ 1,
59
+ 1,
60
+ 1,
61
+ 0,
62
+ 0,
63
+ 0
64
+ ],
65
+ "rectification_costs":[
66
+ 120,
67
+ 30,
68
+ 60,
69
+ 90,
70
+ 10,
71
+ 45,
72
+ 15
73
+ ],
74
+ "verification": "Motor tested OK after repair",
75
+ "reasoning": "<<Electrical causes eliminated as IR and supply were normal. Tight rotor and high bearing temperature indicated mechanical loading. Bearing replacement.>>",
76
+ "failure_bucket": "Bearing Failure",
77
+ "severity": " Medium",
78
+ "repeat_failure": true
79
+ }
80
+
models.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Data models for the My Env Environment.
9
+
10
+ The my_env environment is a simple test environment that echoes back messages.
11
+ """
12
+
13
+ from openenv.core.env_server.types import Action, Observation
14
+ from pydantic import Field
15
+ from typing import List, Dict
16
+ class MyAction(Action):
17
+ """Action for the My Env environment - just a message to echo."""
18
+
19
+ message: str = Field(..., description="Message to echo back")
20
+
21
+
22
+ class MyObservation(Observation):
23
+ """Observation from the My Env environment - the echoed message."""
24
+
25
+ echoed_message: str = Field(default="", description="The echoed message")
26
+ message_length: int = Field(default=0, description="Length of the echoed message")
27
+ total_cost: int = Field(default=0, description="Total cost until now in the episode")
28
+ history: List[Dict] = Field(default_factory=list)
openenv.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: my_env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
openenv_my_env.egg-info/PKG-INFO ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-my_env
3
+ Version: 0.1.0
4
+ Summary: My Env environment for OpenEnv
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core[core]>=0.2.1
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
9
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
openenv_my_env.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ models.py
5
+ pyproject.toml
6
+ ./__init__.py
7
+ ./client.py
8
+ ./models.py
9
+ openenv_my_env.egg-info/PKG-INFO
10
+ openenv_my_env.egg-info/SOURCES.txt
11
+ openenv_my_env.egg-info/dependency_links.txt
12
+ openenv_my_env.egg-info/entry_points.txt
13
+ openenv_my_env.egg-info/requires.txt
14
+ openenv_my_env.egg-info/top_level.txt
15
+ server/__init__.py
16
+ server/app.py
17
+ server/my_env_environment.py
openenv_my_env.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
openenv_my_env.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = my_env.server.app:main
openenv_my_env.egg-info/requires.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.1
2
+
3
+ [dev]
4
+ pytest>=8.0.0
5
+ pytest-cov>=4.0.0
openenv_my_env.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ my_env
pyproject.toml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-my_env"
13
+ version = "0.1.0"
14
+ description = "My Env environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
+ # install from github
19
+ # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
+ "openenv-core[core]>=0.2.1",
21
+ # Environment-specific dependencies
22
+ # Add all dependencies needed for your environment here
23
+ # Examples:
24
+ # "numpy>=1.19.0",
25
+ # "torch>=2.0.0",
26
+ # "gymnasium>=0.29.0",
27
+ # "openspiel>=1.0.0",
28
+ # "smolagents>=1.22.0,<2",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0.0",
34
+ "pytest-cov>=4.0.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ # Server entry point - enables running via: uv run --project . server
39
+ # or: python -m my_env.server.app
40
+ server = "my_env.server.app:main"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+ packages = ["my_env", "my_env.server"]
45
+ package-dir = { "my_env" = ".", "my_env.server" = "server" }
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
+ """My Env environment server components."""
8
+
9
+ from .my_env_environment import MyEnvironment
10
+
11
+ __all__ = ["MyEnvironment"]
server/app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 My Env Environment.
9
+
10
+ This module creates an HTTP server that exposes the MyEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Endpoints:
14
+ - POST /reset: Reset the environment
15
+ - POST /step: Execute an action
16
+ - GET /state: Get current environment state
17
+ - GET /schema: Get action/observation schemas
18
+ - WS /ws: WebSocket endpoint for persistent sessions
19
+
20
+ Usage:
21
+ # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
+
24
+ # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
+
27
+ # Or run directly:
28
+ python -m server.app
29
+ """
30
+
31
+ try:
32
+ from openenv.core.env_server.http_server import create_app
33
+ except Exception as e: # pragma: no cover
34
+ raise ImportError(
35
+ "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
+ ) from e
37
+
38
+ # Import from local models.py (PYTHONPATH includes /app/env in Docker)
39
+ from models import MyAction, MyObservation
40
+
41
+ from .my_env_environment import MyEnvironment
42
+
43
+
44
+ # Create the app with web interface and README integration
45
+ app = create_app(
46
+ MyEnvironment,
47
+ MyAction,
48
+ MyObservation,
49
+ env_name="my_env",
50
+ max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
51
+ )
52
+
53
+
54
+ def main(host: str = "0.0.0.0", port: int = 8000):
55
+ """
56
+ Entry point for direct execution via uv run or python -m.
57
+
58
+ This function enables running the server without Docker:
59
+ uv run --project . server
60
+ uv run --project . server --port 8001
61
+ python -m my_env.server.app
62
+
63
+ Args:
64
+ host: Host address to bind to (default: "0.0.0.0")
65
+ port: Port number to listen on (default: 8000)
66
+
67
+ For production deployments, consider using uvicorn directly with
68
+ multiple workers:
69
+ uvicorn my_env.server.app:app --workers 4
70
+ """
71
+ import uvicorn
72
+
73
+ uvicorn.run(app, host=host, port=port)
74
+
75
+
76
+ if __name__ == "__main__":
77
+ import argparse
78
+
79
+ parser = argparse.ArgumentParser()
80
+ parser.add_argument("--port", type=int, default=8000)
81
+ args = parser.parse_args()
82
+ main(port=args.port)
server/my_env_environment.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ My Env 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
+ import os
16
+ import json
17
+ from models import MyAction, MyObservation
18
+ from openenv.core.env_server.interfaces import Environment
19
+ from openenv.core.env_server.types import State
20
+
21
+ EPISODES = {}
22
+ class MyEnvironment(Environment):
23
+ """
24
+ A simple echo environment that echoes back messages.
25
+
26
+ This environment is designed for testing the HTTP server infrastructure.
27
+ It maintains minimal state and simply echoes back whatever message it receives.
28
+
29
+ Example:
30
+ >>> env = MyEnvironment()
31
+ >>> obs = env.reset()
32
+ >>> print(obs.echoed_message) # "My Env environment ready!"
33
+ >>>
34
+ >>> obs = env.step(MyAction(message="Hello"))
35
+ >>> print(obs.echoed_message) # "Hello"
36
+ >>> print(obs.message_length) # 5
37
+ """
38
+
39
+ # Enable concurrent WebSocket sessions.
40
+ # Set to True if your environment isolates state between instances.
41
+ # When True, multiple WebSocket clients can connect simultaneously, each
42
+ # getting their own environment instance (when using factory mode in app.py).
43
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
44
+
45
+ def __init__(self):
46
+
47
+ self.data_folder = "data"
48
+
49
+ self.files = sorted([
50
+ f for f in os.listdir(self.data_folder)
51
+ if f.endswith(".json")
52
+ ])
53
+
54
+ self.current_index = -1
55
+
56
+ # persistent environment state
57
+ self._state = State(
58
+ episode_id=str(uuid4()),
59
+ step_count=0,
60
+ data={}
61
+ )
62
+ def reset(self, episode_id: str = "default"):
63
+
64
+ self.current_index += 1
65
+ if self.current_index >= len(self.files):
66
+ self.current_index = 0
67
+
68
+ file_path = os.path.join(self.data_folder, self.files[self.current_index])
69
+
70
+ with open(file_path) as f:
71
+ scenario = json.load(f)
72
+
73
+ # create episode memory
74
+ EPISODES[episode_id] = {
75
+ "scenario": scenario,
76
+ "history": [],
77
+ "total_cost": 0,
78
+ "total_reward": 0,
79
+ "step_count": 0
80
+ }
81
+
82
+ # reset state metadata
83
+ self._state = State(
84
+ episode_id=episode_id,
85
+ step_count=0,
86
+ data={} # not used anymore
87
+ )
88
+
89
+ return MyObservation(
90
+ echoed_message=scenario["symptom_text"],
91
+ message_length=len(scenario["symptom_text"]),
92
+ total_cost=0,
93
+ history=[],
94
+ reward=0,
95
+ done=False
96
+ )
97
+ def step(self, action: MyAction, episode_id: str = "default") -> MyObservation:
98
+
99
+ data = EPISODES[episode_id]
100
+
101
+ scenario = data["scenario"]
102
+ history = data["history"]
103
+ total_cost = data["total_cost"]
104
+ total_reward = data["total_reward"]
105
+
106
+ self._state.step_count += 1
107
+ data["step_count"] += 1
108
+
109
+ message = action.message
110
+
111
+ if message.startswith("diagnose:"):
112
+
113
+ idx = int(message.split(":")[1])
114
+
115
+ step = scenario["diagnostic_steps"][idx]
116
+ obs = scenario["observations"][idx]
117
+
118
+ reward = scenario["diagnostic_steps_rewards"][idx]
119
+ cost = scenario["diagnostic_steps_costs"][idx]
120
+
121
+ total_reward += reward
122
+ total_cost += cost
123
+
124
+ history.append({
125
+ "action": step,
126
+ "observation": obs
127
+ })
128
+
129
+ # save updates
130
+ data["history"] = history
131
+ data["total_cost"] = total_cost
132
+ data["total_reward"] = total_reward
133
+
134
+ return MyObservation(
135
+ echoed_message=obs,
136
+ message_length=len(obs),
137
+ total_cost=total_cost,
138
+ history=history,
139
+ reward=reward,
140
+ done=False
141
+ )
142
+ @property
143
+ def state(self) -> State:
144
+ """
145
+ Get the current environment state.
146
+
147
+ Returns:
148
+ Current State with episode_id and step_count
149
+ """
150
+ return self._state
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openenv[core]>=0.2.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+
5
+
6
+
uv.lock ADDED
The diff for this file is too large to render. See raw diff