github-actions[bot] commited on
Commit
538f1d3
·
1 Parent(s): d0ae716

🤖 Deploy atari_env environment - 2025-10-21 02:05:10

Browse files
Dockerfile CHANGED
@@ -4,38 +4,14 @@
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: First stage builds the base image
8
- FROM python:3.11-slim as base-builder
9
-
10
- # Install system dependencies
11
- RUN apt-get update && apt-get install -y --no-install-recommends \
12
- curl \
13
- && rm -rf /var/lib/apt/lists/*
14
-
15
- # Install Python dependencies that all environments need
16
- RUN pip install --no-cache-dir \
17
- fastapi>=0.104.0 \
18
- "uvicorn[standard]>=0.24.0" \
19
- requests>=2.25.0 \
20
- wsproto>=1.0.0
21
-
22
- # Set working directory
23
- WORKDIR /app
24
-
25
- # Default environment variables
26
- ENV PYTHONPATH=/app/src
27
- ENV PYTHONUNBUFFERED=1
28
-
29
- # Second stage: Use the built base image and add environment-specific dependencies
30
- FROM base-builder
31
-
32
  # Install ALE-specific dependencies
33
  RUN pip install --no-cache-dir \
34
  gymnasium>=0.29.0 \
35
  ale-py>=0.8.0 \
36
  numpy>=1.24.0
37
 
38
-
39
  # Copy only what's needed for this environment
40
  COPY src/core/ /app/src/core/
41
  COPY src/envs/atari_env/ /app/src/envs/atari_env/
 
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
+ # Use the specified openenv-base image
8
+ FROM openenv-base:sha-7dd8148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  # Install ALE-specific dependencies
10
  RUN pip install --no-cache-dir \
11
  gymnasium>=0.29.0 \
12
  ale-py>=0.8.0 \
13
  numpy>=1.24.0
14
 
 
15
  # Copy only what's needed for this environment
16
  COPY src/core/ /app/src/core/
17
  COPY src/envs/atari_env/ /app/src/envs/atari_env/
src/core/__pycache__/__init__.cpython-311.pyc DELETED
Binary file (400 Bytes)
 
src/core/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (383 Bytes)
 
src/core/__pycache__/http_env_client.cpython-311.pyc DELETED
Binary file (7.68 kB)
 
src/core/__pycache__/types.cpython-311.pyc DELETED
Binary file (1.09 kB)
 
src/core/containers/__pycache__/__init__.cpython-311.pyc DELETED
Binary file (206 Bytes)
 
src/core/containers/runtime/__pycache__/__init__.cpython-311.pyc DELETED
Binary file (389 Bytes)
 
src/core/containers/runtime/__pycache__/providers.cpython-311.pyc DELETED
Binary file (10.9 kB)
 
src/core/env_server/__pycache__/__init__.cpython-311.pyc DELETED
Binary file (898 Bytes)
 
src/core/env_server/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (940 Bytes)
 
src/core/env_server/__pycache__/base_transforms.cpython-311.pyc DELETED
Binary file (1.67 kB)
 
src/core/env_server/__pycache__/base_transforms.cpython-313.pyc DELETED
Binary file (1.57 kB)
 
src/core/env_server/__pycache__/http_server.cpython-311.pyc DELETED
Binary file (9.2 kB)
 
src/core/env_server/__pycache__/http_server.cpython-313.pyc DELETED
Binary file (7.14 kB)
 
src/core/env_server/__pycache__/interfaces.cpython-311.pyc DELETED
Binary file (5.22 kB)
 
src/core/env_server/__pycache__/interfaces.cpython-313.pyc DELETED
Binary file (4.68 kB)
 
src/core/env_server/__pycache__/types.cpython-311.pyc DELETED
Binary file (2.39 kB)
 
src/core/env_server/__pycache__/types.cpython-313.pyc DELETED
Binary file (2.1 kB)
 
src/core/env_server/__pycache__/web_interface.cpython-311.pyc DELETED
Binary file (29.9 kB)
 
src/core/env_server/http_server.py CHANGED
@@ -163,20 +163,22 @@ def create_app(
163
  env: Environment,
164
  action_cls: Type[Action],
165
  observation_cls: Type[Observation],
 
166
  ) -> Any:
167
  """
168
- Create a FastAPI application with web interface enabled for Hugging Face deployments.
169
 
170
- This function checks for the ENABLE_WEB_INTERFACE environment variable to determine
171
- whether to enable the web interface.
172
 
173
  Args:
174
  env: The Environment instance to serve
175
  action_cls: The Action subclass this environment expects
176
  observation_cls: The Observation subclass this environment returns
 
177
 
178
  Returns:
179
- FastAPI application instance with or without web interface based on environment
180
  """
181
  # Check if web interface should be enabled
182
  # This can be controlled via environment variable or build argument
@@ -187,7 +189,7 @@ def create_app(
187
  if enable_web:
188
  # Import web interface only when needed
189
  from .web_interface import create_web_interface_app
190
- return create_web_interface_app(env, action_cls, observation_cls)
191
  else:
192
  # Use standard FastAPI app without web interface
193
  return create_fastapi_app(env, action_cls, observation_cls)
 
163
  env: Environment,
164
  action_cls: Type[Action],
165
  observation_cls: Type[Observation],
166
+ env_name: Optional[str] = None,
167
  ) -> Any:
168
  """
169
+ Create a FastAPI application with or without web interface.
170
 
171
+ This function creates a FastAPI app with the web interface enabled by default,
172
+ including README integration for better user experience.
173
 
174
  Args:
175
  env: The Environment instance to serve
176
  action_cls: The Action subclass this environment expects
177
  observation_cls: The Observation subclass this environment returns
178
+ env_name: Optional environment name for README loading
179
 
180
  Returns:
181
+ FastAPI application instance with or without web interface and README integration
182
  """
183
  # Check if web interface should be enabled
184
  # This can be controlled via environment variable or build argument
 
189
  if enable_web:
190
  # Import web interface only when needed
191
  from .web_interface import create_web_interface_app
192
+ return create_web_interface_app(env, action_cls, observation_cls, env_name)
193
  else:
194
  # Use standard FastAPI app without web interface
195
  return create_fastapi_app(env, action_cls, observation_cls)
src/core/env_server/types.py CHANGED
@@ -43,3 +43,15 @@ class CodeExecResult:
43
  stdout: str
44
  stderr: str
45
  exit_code: int
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  stdout: str
44
  stderr: str
45
  exit_code: int
46
+
47
+
48
+ @dataclass
49
+ class EnvironmentMetadata:
50
+ """Metadata about an environment for documentation and UI purposes."""
51
+
52
+ name: str
53
+ description: str
54
+ readme_content: Optional[str] = None
55
+ version: Optional[str] = None
56
+ author: Optional[str] = None
57
+ documentation_url: Optional[str] = None
src/core/env_server/web_interface.py CHANGED
@@ -25,7 +25,77 @@ from fastapi.staticfiles import StaticFiles
25
  from pydantic import BaseModel
26
 
27
  from .interfaces import Environment
28
- from .types import Action, Observation, State
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
 
31
  @dataclass
@@ -57,10 +127,15 @@ class WebInterfaceManager:
57
  env: Environment,
58
  action_cls: Type[Action],
59
  observation_cls: Type[Observation],
 
60
  ):
61
  self.env = env
62
  self.action_cls = action_cls
63
  self.observation_cls = observation_cls
 
 
 
 
64
  self.episode_state = EpisodeState(
65
  episode_id=None,
66
  step_count=0,
@@ -168,7 +243,36 @@ class WebInterfaceManager:
168
  def _deserialize_action(self, action_data: Dict[str, Any]) -> Action:
169
  """Convert JSON dict to Action instance."""
170
  metadata = action_data.pop("metadata", {})
171
- action = self.action_cls(**action_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  action.metadata = metadata
173
  return action
174
 
@@ -177,6 +281,7 @@ def create_web_interface_app(
177
  env: Environment,
178
  action_cls: Type[Action],
179
  observation_cls: Type[Observation],
 
180
  ) -> FastAPI:
181
  """
182
  Create a FastAPI application with web interface for the given environment.
@@ -185,6 +290,7 @@ def create_web_interface_app(
185
  env: The Environment instance to serve
186
  action_cls: The Action subclass this environment expects
187
  observation_cls: The Observation subclass this environment returns
 
188
 
189
  Returns:
190
  FastAPI application instance with web interface
@@ -194,14 +300,22 @@ def create_web_interface_app(
194
  # Create the base environment app
195
  app = create_fastapi_app(env, action_cls, observation_cls)
196
 
 
 
 
197
  # Create web interface manager
198
- web_manager = WebInterfaceManager(env, action_cls, observation_cls)
199
 
200
  # Add web interface routes
201
  @app.get("/web", response_class=HTMLResponse)
202
  async def web_interface():
203
  """Serve the web interface."""
204
- return get_web_interface_html(action_cls)
 
 
 
 
 
205
 
206
  @app.websocket("/ws")
207
  async def websocket_endpoint(websocket: WebSocket):
@@ -222,7 +336,15 @@ def create_web_interface_app(
222
  @app.post("/web/step")
223
  async def web_step(request: Dict[str, Any]):
224
  """Step endpoint for web interface."""
225
- action_data = request.get("action", {})
 
 
 
 
 
 
 
 
226
  return await web_manager.step_environment(action_data)
227
 
228
  @app.get("/web/state")
@@ -233,31 +355,19 @@ def create_web_interface_app(
233
  return app
234
 
235
 
236
- def get_web_interface_html(action_cls: Type[Action]) -> str:
237
  """Generate the HTML for the web interface."""
238
 
239
- # Get action fields for dynamic form generation
240
- action_fields = []
241
  if hasattr(action_cls, '__dataclass_fields__'):
242
  for field_name, field_info in action_cls.__dataclass_fields__.items():
243
- if field_name != 'metadata':
244
- field_type = field_info.type
245
- if field_type == str:
246
- input_type = "text"
247
- elif field_type == int:
248
- input_type = "number"
249
- elif field_type == float:
250
- input_type = "number"
251
- elif field_type == bool:
252
- input_type = "checkbox"
253
- else:
254
- input_type = "text"
255
-
256
- action_fields.append({
257
- 'name': field_name,
258
- 'type': input_type,
259
- 'required': field_info.default is field_info.default_factory
260
- })
261
 
262
  return f"""
263
  <!DOCTYPE html>
@@ -476,6 +586,307 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
476
  max-height: 200px;
477
  overflow-y: auto;
478
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  </style>
480
  </head>
481
  <body>
@@ -487,14 +898,11 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
487
  HumanAgent Interface
488
  </div>
489
  <div class="pane-content">
490
- <!-- Action Form -->
491
- <div class="action-form">
492
- <h3>Take Action</h3>
493
- <form id="action-form">
494
- {_generate_action_form_fields(action_fields)}
495
- <button type="submit" class="btn" id="step-btn">Step</button>
496
- </form>
497
- </div>
498
 
499
  <!-- Control Buttons -->
500
  <div style="margin-bottom: 20px;">
@@ -594,11 +1002,43 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
594
  }}
595
 
596
  setupEventListeners() {{
597
- // Action form submission
598
- document.getElementById('action-form').addEventListener('submit', (e) => {{
599
- e.preventDefault();
600
- this.submitAction();
601
- }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
 
603
  // Reset button
604
  document.getElementById('reset-btn').addEventListener('click', () => {{
@@ -611,6 +1051,61 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
611
  }});
612
  }}
613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  async submitAction() {{
615
  const formData = new FormData(document.getElementById('action-form'));
616
  const action = {{}};
@@ -618,7 +1113,17 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
618
  // Collect form data
619
  for (const [key, value] of formData.entries()) {{
620
  if (value !== '') {{
621
- action[key] = value;
 
 
 
 
 
 
 
 
 
 
622
  }}
623
  }}
624
 
@@ -682,6 +1187,9 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
682
  }}
683
 
684
  updateUI(episodeState) {{
 
 
 
685
  // Update current state
686
  document.getElementById('env-status').textContent =
687
  episodeState.is_reset ? 'Reset' : 'Running';
@@ -690,14 +1198,19 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
690
  document.getElementById('step-count').textContent =
691
  episodeState.step_count.toString();
692
 
693
- // Update current observation
694
- const observationDiv = document.getElementById('current-observation');
695
- if (episodeState.current_observation) {{
696
- observationDiv.textContent = JSON.stringify(
697
- episodeState.current_observation, null, 2
698
- );
699
  }} else {{
700
- observationDiv.textContent = 'No observation yet';
 
 
 
 
 
 
 
 
701
  }}
702
 
703
  // Update action logs
@@ -718,6 +1231,25 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
718
  `).join('');
719
  }}
720
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  }}
722
 
723
  // Initialize the web interface when the page loads
@@ -730,35 +1262,352 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
730
  """.replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
731
 
732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
734
- """Generate HTML form fields for action input."""
735
  if not action_fields:
736
  return '<p>No action fields available</p>'
737
 
738
  fields_html = []
739
  for field in action_fields:
740
- if field['type'] == 'checkbox':
741
- fields_html.append(f'''
742
- <div class="form-group">
743
- <label>
744
- <input type="checkbox" name="{field['name']}" value="true">
745
- {field['name']}
746
- </label>
747
- </div>
748
- ''')
749
- elif field['type'] == 'text' and 'message' in field['name'].lower():
750
- fields_html.append(f'''
751
- <div class="form-group">
752
- <label for="{field['name']}">{field['name']}:</label>
753
- <textarea name="{field['name']}" id="{field['name']}" rows="3" placeholder="Enter {field['name']}..."></textarea>
754
- </div>
755
- ''')
756
- else:
757
- fields_html.append(f'''
758
- <div class="form-group">
759
- <label for="{field['name']}">{field['name']}:</label>
760
- <input type="{field['type']}" name="{field['name']}" id="{field['name']}" placeholder="Enter {field['name']}..." {"required" if field['required'] else ""}>
761
- </div>
762
- ''')
763
 
764
  return '\n'.join(fields_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  from pydantic import BaseModel
26
 
27
  from .interfaces import Environment
28
+ from .types import Action, Observation, State, EnvironmentMetadata
29
+
30
+
31
+ def load_environment_metadata(env: Environment, env_name: Optional[str] = None) -> EnvironmentMetadata:
32
+ """
33
+ Load environment metadata including README content.
34
+
35
+ Args:
36
+ env: The environment instance
37
+ env_name: Optional environment name for README file lookup
38
+
39
+ Returns:
40
+ EnvironmentMetadata with loaded information
41
+ """
42
+ # Try to get metadata from environment if it has a method for it
43
+ if hasattr(env, 'get_metadata'):
44
+ return env.get_metadata()
45
+
46
+ # Default metadata
47
+ metadata = EnvironmentMetadata(
48
+ name=env_name or env.__class__.__name__,
49
+ description=f"{env.__class__.__name__} environment",
50
+ version="1.0.0"
51
+ )
52
+
53
+ # Try to load README from file system
54
+ readme_content = _load_readme_from_filesystem(env_name)
55
+ if readme_content:
56
+ metadata.readme_content = readme_content
57
+
58
+ return metadata
59
+
60
+
61
+ def _load_readme_from_filesystem(env_name: Optional[str]) -> Optional[str]:
62
+ """
63
+ Load README content from the filesystem.
64
+
65
+ Tries multiple locations:
66
+ 1. Container filesystem: /app/README.md
67
+ 2. Local development: src/envs/{env_name}/README.md
68
+ 3. Environment variable: ENV_README_PATH
69
+ """
70
+ import os
71
+ from pathlib import Path
72
+
73
+ # Try container filesystem first
74
+ container_readme = Path("/app/README.md")
75
+ if container_readme.exists():
76
+ try:
77
+ return container_readme.read_text(encoding='utf-8')
78
+ except Exception:
79
+ pass
80
+
81
+ # Try environment variable path
82
+ custom_path = os.environ.get("ENV_README_PATH")
83
+ if custom_path and Path(custom_path).exists():
84
+ try:
85
+ return Path(custom_path).read_text(encoding='utf-8')
86
+ except Exception:
87
+ pass
88
+
89
+ # Try local development path
90
+ if env_name:
91
+ local_readme = Path(f"src/envs/{env_name}/README.md")
92
+ if local_readme.exists():
93
+ try:
94
+ return local_readme.read_text(encoding='utf-8')
95
+ except Exception:
96
+ pass
97
+
98
+ return None
99
 
100
 
101
  @dataclass
 
127
  env: Environment,
128
  action_cls: Type[Action],
129
  observation_cls: Type[Observation],
130
+ metadata: Optional[EnvironmentMetadata] = None,
131
  ):
132
  self.env = env
133
  self.action_cls = action_cls
134
  self.observation_cls = observation_cls
135
+ self.metadata = metadata or EnvironmentMetadata(
136
+ name=env.__class__.__name__,
137
+ description=f"{env.__class__.__name__} environment"
138
+ )
139
  self.episode_state = EpisodeState(
140
  episode_id=None,
141
  step_count=0,
 
243
  def _deserialize_action(self, action_data: Dict[str, Any]) -> Action:
244
  """Convert JSON dict to Action instance."""
245
  metadata = action_data.pop("metadata", {})
246
+
247
+ # Handle tensor fields that come from JSON as lists
248
+ processed_data = {}
249
+ for key, value in action_data.items():
250
+ if key == "tokens" and isinstance(value, (list, str)):
251
+ # Convert list or string to tensor
252
+ if isinstance(value, str):
253
+ # If it's a string, try to parse it as a list of numbers
254
+ try:
255
+ import json
256
+ value = json.loads(value)
257
+ except:
258
+ # If parsing fails, treat as empty list
259
+ value = []
260
+ if isinstance(value, list):
261
+ import torch
262
+ processed_data[key] = torch.tensor(value, dtype=torch.long)
263
+ else:
264
+ processed_data[key] = value
265
+ elif key == "action_id" and isinstance(value, str):
266
+ # Convert action_id from string to int
267
+ try:
268
+ processed_data[key] = int(value)
269
+ except ValueError:
270
+ # If conversion fails, keep original value
271
+ processed_data[key] = value
272
+ else:
273
+ processed_data[key] = value
274
+
275
+ action = self.action_cls(**processed_data)
276
  action.metadata = metadata
277
  return action
278
 
 
281
  env: Environment,
282
  action_cls: Type[Action],
283
  observation_cls: Type[Observation],
284
+ env_name: Optional[str] = None,
285
  ) -> FastAPI:
286
  """
287
  Create a FastAPI application with web interface for the given environment.
 
290
  env: The Environment instance to serve
291
  action_cls: The Action subclass this environment expects
292
  observation_cls: The Observation subclass this environment returns
293
+ env_name: Optional environment name for README loading
294
 
295
  Returns:
296
  FastAPI application instance with web interface
 
300
  # Create the base environment app
301
  app = create_fastapi_app(env, action_cls, observation_cls)
302
 
303
+ # Load environment metadata
304
+ metadata = load_environment_metadata(env, env_name)
305
+
306
  # Create web interface manager
307
+ web_manager = WebInterfaceManager(env, action_cls, observation_cls, metadata)
308
 
309
  # Add web interface routes
310
  @app.get("/web", response_class=HTMLResponse)
311
  async def web_interface():
312
  """Serve the web interface."""
313
+ return get_web_interface_html(action_cls, web_manager.metadata)
314
+
315
+ @app.get("/web/metadata")
316
+ async def web_metadata():
317
+ """Get environment metadata."""
318
+ return asdict(web_manager.metadata)
319
 
320
  @app.websocket("/ws")
321
  async def websocket_endpoint(websocket: WebSocket):
 
336
  @app.post("/web/step")
337
  async def web_step(request: Dict[str, Any]):
338
  """Step endpoint for web interface."""
339
+ # Check if this is a message-based request (chat environment)
340
+ if "message" in request:
341
+ message = request["message"]
342
+ # Convert message to action using the environment's message_to_action method
343
+ action = web_manager.env.message_to_action(message)
344
+ action_data = {"tokens": action.tokens.tolist()}
345
+ else:
346
+ action_data = request.get("action", {})
347
+
348
  return await web_manager.step_environment(action_data)
349
 
350
  @app.get("/web/state")
 
355
  return app
356
 
357
 
358
+ def get_web_interface_html(action_cls: Type[Action], metadata: Optional[EnvironmentMetadata] = None) -> str:
359
  """Generate the HTML for the web interface."""
360
 
361
+ # Check if this is a chat environment by looking for tokens field
362
+ is_chat_env = False
363
  if hasattr(action_cls, '__dataclass_fields__'):
364
  for field_name, field_info in action_cls.__dataclass_fields__.items():
365
+ if field_name == 'tokens' and hasattr(field_info.type, '__name__') and 'Tensor' in field_info.type.__name__:
366
+ is_chat_env = True
367
+ break
368
+
369
+ # Get action fields for dynamic form generation with enhanced metadata
370
+ action_fields = _extract_action_fields(action_cls)
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
  return f"""
373
  <!DOCTYPE html>
 
586
  max-height: 200px;
587
  overflow-y: auto;
588
  }}
589
+
590
+ /* Chat Interface Styles */
591
+ .chat-interface {{
592
+ background: white;
593
+ border: 1px solid #e0e0e0;
594
+ border-radius: 8px;
595
+ padding: 20px;
596
+ margin-bottom: 20px;
597
+ }}
598
+
599
+ .chat-messages {{
600
+ background: #f8f9fa;
601
+ border: 1px solid #e0e0e0;
602
+ border-radius: 8px;
603
+ padding: 15px;
604
+ margin-bottom: 15px;
605
+ max-height: 400px;
606
+ overflow-y: auto;
607
+ }}
608
+
609
+ .chat-message {{
610
+ margin-bottom: 15px;
611
+ padding: 10px;
612
+ border-radius: 8px;
613
+ }}
614
+
615
+ .chat-message:last-child {{
616
+ margin-bottom: 0;
617
+ }}
618
+
619
+ .chat-message.user {{
620
+ background: #e3f2fd;
621
+ margin-left: 20px;
622
+ }}
623
+
624
+ .chat-message.assistant {{
625
+ background: #f3e5f5;
626
+ margin-right: 20px;
627
+ }}
628
+
629
+ .chat-message.system {{
630
+ background: #e8f5e8;
631
+ font-style: italic;
632
+ }}
633
+
634
+ .message-role {{
635
+ font-weight: 600;
636
+ font-size: 12px;
637
+ color: #666;
638
+ margin-bottom: 5px;
639
+ }}
640
+
641
+ .message-content {{
642
+ font-size: 14px;
643
+ line-height: 1.4;
644
+ }}
645
+
646
+ .chat-input-container {{
647
+ border-top: 1px solid #e0e0e0;
648
+ padding-top: 15px;
649
+ }}
650
+
651
+ .role-selector {{
652
+ margin-bottom: 10px;
653
+ }}
654
+
655
+ .role-selector label {{
656
+ font-weight: 500;
657
+ margin-right: 10px;
658
+ }}
659
+
660
+ .role-selector select {{
661
+ padding: 5px 10px;
662
+ border: 1px solid #ddd;
663
+ border-radius: 4px;
664
+ }}
665
+
666
+ .message-input {{
667
+ display: flex;
668
+ gap: 10px;
669
+ align-items: flex-end;
670
+ }}
671
+
672
+ .message-input textarea {{
673
+ flex: 1;
674
+ padding: 10px;
675
+ border: 1px solid #ddd;
676
+ border-radius: 4px;
677
+ resize: vertical;
678
+ font-family: inherit;
679
+ }}
680
+
681
+ .message-input textarea:focus {{
682
+ outline: none;
683
+ border-color: #007bff;
684
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
685
+ }}
686
+
687
+ /* Instructions Section Styles */
688
+ .instructions-section {{
689
+ background: white;
690
+ border: 1px solid #e0e0e0;
691
+ border-radius: 8px;
692
+ padding: 20px;
693
+ margin-bottom: 20px;
694
+ }}
695
+
696
+ .instructions-header {{
697
+ display: flex;
698
+ justify-content: space-between;
699
+ align-items: center;
700
+ margin-bottom: 15px;
701
+ }}
702
+
703
+ .instructions-title {{
704
+ font-size: 18px;
705
+ font-weight: 600;
706
+ color: #333;
707
+ margin: 0;
708
+ }}
709
+
710
+ .instructions-toggle {{
711
+ background: #f8f9fa;
712
+ border: 1px solid #dee2e6;
713
+ border-radius: 4px;
714
+ padding: 5px 10px;
715
+ cursor: pointer;
716
+ font-size: 12px;
717
+ color: #6c757d;
718
+ }}
719
+
720
+ .instructions-toggle:hover {{
721
+ background: #e9ecef;
722
+ }}
723
+
724
+ .instructions-content {{
725
+ display: none;
726
+ max-height: 400px;
727
+ overflow-y: auto;
728
+ border-top: 1px solid #e0e0e0;
729
+ padding-top: 15px;
730
+ }}
731
+
732
+ .instructions-content.expanded {{
733
+ display: block;
734
+ }}
735
+
736
+ .instructions-content h1,
737
+ .instructions-content h2,
738
+ .instructions-content h3 {{
739
+ color: #333;
740
+ margin-top: 20px;
741
+ margin-bottom: 10px;
742
+ }}
743
+
744
+ .instructions-content h1 {{
745
+ font-size: 24px;
746
+ border-bottom: 2px solid #007bff;
747
+ padding-bottom: 10px;
748
+ }}
749
+
750
+ .instructions-content h2 {{
751
+ font-size: 20px;
752
+ }}
753
+
754
+ .instructions-content h3 {{
755
+ font-size: 16px;
756
+ }}
757
+
758
+ .instructions-content p {{
759
+ margin-bottom: 10px;
760
+ line-height: 1.6;
761
+ }}
762
+
763
+ .instructions-content code {{
764
+ background: #f8f9fa;
765
+ padding: 2px 4px;
766
+ border-radius: 3px;
767
+ font-family: monospace;
768
+ font-size: 14px;
769
+ }}
770
+
771
+ .instructions-content pre {{
772
+ background: #f8f9fa;
773
+ border: 1px solid #e9ecef;
774
+ border-radius: 4px;
775
+ padding: 15px;
776
+ overflow-x: auto;
777
+ margin: 10px 0;
778
+ }}
779
+
780
+ .instructions-content pre code {{
781
+ background: none;
782
+ padding: 0;
783
+ }}
784
+
785
+ .instructions-content ul,
786
+ .instructions-content ol {{
787
+ margin: 10px 0;
788
+ padding-left: 20px;
789
+ }}
790
+
791
+ .instructions-content li {{
792
+ margin-bottom: 5px;
793
+ }}
794
+
795
+ .instructions-content table {{
796
+ border-collapse: collapse;
797
+ width: 100%;
798
+ margin: 15px 0;
799
+ }}
800
+
801
+ .instructions-content th,
802
+ .instructions-content td {{
803
+ border: 1px solid #dee2e6;
804
+ padding: 8px 12px;
805
+ text-align: left;
806
+ }}
807
+
808
+ .instructions-content th {{
809
+ background: #f8f9fa;
810
+ font-weight: 600;
811
+ }}
812
+
813
+ /* Enhanced Form Styles */
814
+ .help-text {{
815
+ display: block;
816
+ margin-top: 5px;
817
+ font-size: 12px;
818
+ color: #6c757d;
819
+ font-style: italic;
820
+ }}
821
+
822
+ .form-group label {{
823
+ font-weight: 500;
824
+ color: #333;
825
+ margin-bottom: 5px;
826
+ }}
827
+
828
+ .form-group select {{
829
+ width: 100%;
830
+ padding: 8px 12px;
831
+ border: 1px solid #ddd;
832
+ border-radius: 4px;
833
+ font-size: 14px;
834
+ background-color: white;
835
+ }}
836
+
837
+ .form-group select:focus {{
838
+ outline: none;
839
+ border-color: #007bff;
840
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
841
+ }}
842
+
843
+ .form-group textarea {{
844
+ width: 100%;
845
+ padding: 8px 12px;
846
+ border: 1px solid #ddd;
847
+ border-radius: 4px;
848
+ font-size: 14px;
849
+ font-family: inherit;
850
+ resize: vertical;
851
+ }}
852
+
853
+ .form-group textarea:focus {{
854
+ outline: none;
855
+ border-color: #007bff;
856
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
857
+ }}
858
+
859
+ .form-group input[type="number"] {{
860
+ width: 100%;
861
+ padding: 8px 12px;
862
+ border: 1px solid #ddd;
863
+ border-radius: 4px;
864
+ font-size: 14px;
865
+ }}
866
+
867
+ .form-group input[type="number"]:focus {{
868
+ outline: none;
869
+ border-color: #007bff;
870
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
871
+ }}
872
+
873
+ .form-group input[type="text"]:focus {{
874
+ outline: none;
875
+ border-color: #007bff;
876
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
877
+ }}
878
+
879
+ .required-indicator {{
880
+ color: #dc3545;
881
+ font-weight: bold;
882
+ }}
883
+
884
+ .form-group .field-description {{
885
+ font-size: 11px;
886
+ color: #666;
887
+ margin-top: 2px;
888
+ font-style: italic;
889
+ }}
890
  </style>
891
  </head>
892
  <body>
 
898
  HumanAgent Interface
899
  </div>
900
  <div class="pane-content">
901
+ <!-- Instructions Section -->
902
+ {_generate_instructions_section(metadata)}
903
+
904
+ <!-- Action Form or Chat Interface -->
905
+ {_generate_action_interface(action_fields, is_chat_env)}
 
 
 
906
 
907
  <!-- Control Buttons -->
908
  <div style="margin-bottom: 20px;">
 
1002
  }}
1003
 
1004
  setupEventListeners() {{
1005
+ // Instructions toggle
1006
+ const instructionsToggle = document.getElementById('instructions-toggle');
1007
+ const instructionsContent = document.getElementById('instructions-content');
1008
+ if (instructionsToggle && instructionsContent) {{
1009
+ instructionsToggle.addEventListener('click', () => {{
1010
+ instructionsContent.classList.toggle('expanded');
1011
+ instructionsToggle.textContent = instructionsContent.classList.contains('expanded')
1012
+ ? 'Hide Instructions' : 'Show Instructions';
1013
+ }});
1014
+ }}
1015
+
1016
+ // Check if this is a chat environment
1017
+ const isChatEnv = document.getElementById('chat-messages') !== null;
1018
+
1019
+ if (isChatEnv) {{
1020
+ // Chat environment event listeners
1021
+ document.getElementById('send-message-btn').addEventListener('click', () => {{
1022
+ this.sendMessage();
1023
+ }});
1024
+
1025
+ // Send message on Enter (but allow Shift+Enter for new lines)
1026
+ document.getElementById('message-input').addEventListener('keydown', (e) => {{
1027
+ if (e.key === 'Enter' && !e.shiftKey) {{
1028
+ e.preventDefault();
1029
+ this.sendMessage();
1030
+ }}
1031
+ }});
1032
+ }} else {{
1033
+ // Traditional action form submission
1034
+ const actionForm = document.getElementById('action-form');
1035
+ if (actionForm) {{
1036
+ actionForm.addEventListener('submit', (e) => {{
1037
+ e.preventDefault();
1038
+ this.submitAction();
1039
+ }});
1040
+ }}
1041
+ }}
1042
 
1043
  // Reset button
1044
  document.getElementById('reset-btn').addEventListener('click', () => {{
 
1051
  }});
1052
  }}
1053
 
1054
+ async sendMessage() {{
1055
+ const messageInput = document.getElementById('message-input');
1056
+ const roleSelect = document.getElementById('message-role');
1057
+ const message = messageInput.value.trim();
1058
+ const role = roleSelect.value;
1059
+
1060
+ if (!message) {{
1061
+ return;
1062
+ }}
1063
+
1064
+ // Add message to chat display immediately
1065
+ this.addMessageToChat(role, message);
1066
+
1067
+ // Clear input
1068
+ messageInput.value = '';
1069
+
1070
+ try {{
1071
+ // Send message to server to convert to action and step
1072
+ const response = await fetch('/web/step', {{
1073
+ method: 'POST',
1074
+ headers: {{ 'Content-Type': 'application/json' }},
1075
+ body: JSON.stringify({{
1076
+ message: {{
1077
+ role: role,
1078
+ content: message
1079
+ }}
1080
+ }})
1081
+ }});
1082
+
1083
+ if (!response.ok) {{
1084
+ throw new Error(`HTTP error! status: ${{response.status}}`);
1085
+ }}
1086
+
1087
+ const result = await response.json();
1088
+ console.log('Message sent:', result);
1089
+ }} catch (error) {{
1090
+ console.error('Error sending message:', error);
1091
+ alert('Error sending message: ' + error.message);
1092
+ }}
1093
+ }}
1094
+
1095
+ addMessageToChat(role, content) {{
1096
+ const chatMessages = document.getElementById('chat-messages');
1097
+ const messageDiv = document.createElement('div');
1098
+ messageDiv.className = `chat-message ${{role}}`;
1099
+
1100
+ messageDiv.innerHTML = `
1101
+ <div class="message-role">${{role.charAt(0).toUpperCase() + role.slice(1)}}</div>
1102
+ <div class="message-content">${{content}}</div>
1103
+ `;
1104
+
1105
+ chatMessages.appendChild(messageDiv);
1106
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1107
+ }}
1108
+
1109
  async submitAction() {{
1110
  const formData = new FormData(document.getElementById('action-form'));
1111
  const action = {{}};
 
1113
  // Collect form data
1114
  for (const [key, value] of formData.entries()) {{
1115
  if (value !== '') {{
1116
+ // Handle tensor fields (tokens) - convert comma-separated string to array
1117
+ if (key === 'tokens') {{
1118
+ try {{
1119
+ action[key] = value.split(',').map(x => parseInt(x.trim())).filter(x => !isNaN(x));
1120
+ }} catch (e) {{
1121
+ console.error('Error parsing tokens:', e);
1122
+ action[key] = [];
1123
+ }}
1124
+ }} else {{
1125
+ action[key] = value;
1126
+ }}
1127
  }}
1128
  }}
1129
 
 
1187
  }}
1188
 
1189
  updateUI(episodeState) {{
1190
+ // Check if this is a chat environment
1191
+ const isChatEnv = document.getElementById('chat-messages') !== null;
1192
+
1193
  // Update current state
1194
  document.getElementById('env-status').textContent =
1195
  episodeState.is_reset ? 'Reset' : 'Running';
 
1198
  document.getElementById('step-count').textContent =
1199
  episodeState.step_count.toString();
1200
 
1201
+ if (isChatEnv) {{
1202
+ // Update chat interface
1203
+ this.updateChatInterface(episodeState);
 
 
 
1204
  }} else {{
1205
+ // Update traditional observation display
1206
+ const observationDiv = document.getElementById('current-observation');
1207
+ if (episodeState.current_observation) {{
1208
+ observationDiv.textContent = JSON.stringify(
1209
+ episodeState.current_observation, null, 2
1210
+ );
1211
+ }} else {{
1212
+ observationDiv.textContent = 'No observation yet';
1213
+ }}
1214
  }}
1215
 
1216
  // Update action logs
 
1231
  `).join('');
1232
  }}
1233
  }}
1234
+
1235
+ updateChatInterface(episodeState) {{
1236
+ const chatMessages = document.getElementById('chat-messages');
1237
+ if (!chatMessages) return;
1238
+
1239
+ // Clear existing messages (except system message)
1240
+ const systemMessage = chatMessages.querySelector('.chat-message.system');
1241
+ chatMessages.innerHTML = '';
1242
+ if (systemMessage) {{
1243
+ chatMessages.appendChild(systemMessage);
1244
+ }}
1245
+
1246
+ // Add messages from current observation
1247
+ if (episodeState.current_observation && episodeState.current_observation.messages) {{
1248
+ episodeState.current_observation.messages.forEach(msg => {{
1249
+ this.addMessageToChat(msg.role, msg.content);
1250
+ }});
1251
+ }}
1252
+ }}
1253
  }}
1254
 
1255
  // Initialize the web interface when the page loads
 
1262
  """.replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
1263
 
1264
 
1265
+ def _generate_instructions_section(metadata: Optional[EnvironmentMetadata]) -> str:
1266
+ """Generate the instructions section with environment documentation."""
1267
+ if not metadata or not metadata.readme_content:
1268
+ return ''
1269
+
1270
+ # Convert markdown to HTML (basic conversion)
1271
+ import re
1272
+ html_content = _markdown_to_html(metadata.readme_content)
1273
+
1274
+ return f'''
1275
+ <!-- Instructions Section -->
1276
+ <div class="instructions-section">
1277
+ <div class="instructions-header">
1278
+ <h3 class="instructions-title">{metadata.name}</h3>
1279
+ <button class="instructions-toggle" id="instructions-toggle">Show Instructions</button>
1280
+ </div>
1281
+ <div class="instructions-content" id="instructions-content">
1282
+ <div class="instructions-readme">
1283
+ {html_content}
1284
+ </div>
1285
+ </div>
1286
+ </div>
1287
+ '''
1288
+
1289
+
1290
+ def _extract_action_fields(action_cls: Type[Action]) -> List[Dict[str, Any]]:
1291
+ """Extract enhanced field metadata from Action class for form generation."""
1292
+ import typing
1293
+ from typing import get_origin, get_args
1294
+
1295
+ action_fields = []
1296
+ if not hasattr(action_cls, '__dataclass_fields__'):
1297
+ return action_fields
1298
+
1299
+ for field_name, field_info in action_cls.__dataclass_fields__.items():
1300
+ if field_name == 'metadata':
1301
+ continue
1302
+
1303
+ field_type = field_info.type
1304
+ field_metadata = _extract_field_metadata(field_name, field_info)
1305
+
1306
+ # Determine input type based on field type
1307
+ input_type = _determine_input_type(field_type)
1308
+
1309
+ # Check if field is required
1310
+ is_required = field_info.default is field_info.default_factory
1311
+
1312
+ action_fields.append({
1313
+ 'name': field_name,
1314
+ 'type': input_type,
1315
+ 'required': is_required,
1316
+ 'description': field_metadata.get('description', ''),
1317
+ 'default_value': field_metadata.get('default_value'),
1318
+ 'choices': field_metadata.get('choices', []),
1319
+ 'min_value': field_metadata.get('min_value'),
1320
+ 'max_value': field_metadata.get('max_value'),
1321
+ 'placeholder': field_metadata.get('placeholder', ''),
1322
+ 'help_text': field_metadata.get('help_text', ''),
1323
+ })
1324
+
1325
+ return action_fields
1326
+
1327
+
1328
+ def _extract_field_metadata(field_name: str, field_info) -> Dict[str, Any]:
1329
+ """Extract metadata from dataclass field including docstring and type hints."""
1330
+ import typing
1331
+ from typing import get_origin, get_args, Literal, Union, Optional
1332
+
1333
+ metadata = {}
1334
+
1335
+ # Extract description from field docstring or annotation
1336
+ if hasattr(field_info, 'metadata') and field_info.metadata:
1337
+ # Check for custom metadata
1338
+ for meta in field_info.metadata:
1339
+ if isinstance(meta, dict):
1340
+ metadata.update(meta)
1341
+
1342
+ # Extract type information
1343
+ field_type = field_info.type
1344
+ origin = get_origin(field_type)
1345
+
1346
+ # Handle Literal types for dropdown choices
1347
+ if origin is Literal:
1348
+ args = get_args(field_type)
1349
+ metadata['choices'] = list(args)
1350
+
1351
+ # Handle Optional types
1352
+ if origin is Union:
1353
+ args = get_args(field_type)
1354
+ if len(args) == 2 and type(None) in args:
1355
+ # This is Optional[SomeType]
1356
+ non_none_type = args[0] if args[1] is type(None) else args[1]
1357
+ metadata['optional'] = True
1358
+ # Recursively check the non-None type for choices
1359
+ if get_origin(non_none_type) is Literal:
1360
+ metadata['choices'] = list(get_args(non_none_type))
1361
+ else:
1362
+ # Regular Union type
1363
+ metadata['choices'] = [str(arg) for arg in args if arg is not type(None)]
1364
+
1365
+ # Handle numeric constraints
1366
+ if field_type in (int, float):
1367
+ # Check for common constraint patterns in field name
1368
+ if 'count' in field_name.lower() or 'num' in field_name.lower():
1369
+ metadata['min_value'] = 0
1370
+ if 'id' in field_name.lower():
1371
+ metadata['min_value'] = 0
1372
+
1373
+ # Generate placeholder text
1374
+ if 'message' in field_name.lower():
1375
+ metadata['placeholder'] = f'Enter {field_name.replace("_", " ")}...'
1376
+ elif 'code' in field_name.lower():
1377
+ metadata['placeholder'] = 'Enter Python code here...'
1378
+ elif 'tokens' in field_name.lower():
1379
+ metadata['placeholder'] = 'Enter comma-separated token IDs (e.g., 1,2,3,4,5)'
1380
+ else:
1381
+ metadata['placeholder'] = f'Enter {field_name.replace("_", " ")}...'
1382
+
1383
+ # Generate help text based on field name and type
1384
+ if 'action_id' in field_name.lower():
1385
+ metadata['help_text'] = 'The action ID to execute in the environment'
1386
+ elif 'game_name' in field_name.lower():
1387
+ metadata['help_text'] = 'Name of the game or environment'
1388
+ elif 'tokens' in field_name.lower():
1389
+ metadata['help_text'] = 'Token IDs as a comma-separated list of integers'
1390
+ elif 'code' in field_name.lower():
1391
+ metadata['help_text'] = 'Python code to execute in the environment'
1392
+ elif 'message' in field_name.lower():
1393
+ metadata['help_text'] = 'Text message to send'
1394
+
1395
+ return metadata
1396
+
1397
+
1398
+ def _determine_input_type(field_type) -> str:
1399
+ """Determine the appropriate HTML input type for a field type."""
1400
+ import typing
1401
+ from typing import get_origin, get_args, Literal, Union
1402
+
1403
+ # Handle direct types
1404
+ if field_type == str:
1405
+ return "text"
1406
+ elif field_type == int:
1407
+ return "number"
1408
+ elif field_type == float:
1409
+ return "number"
1410
+ elif field_type == bool:
1411
+ return "checkbox"
1412
+
1413
+ # Handle complex types
1414
+ origin = get_origin(field_type)
1415
+
1416
+ if origin is Literal:
1417
+ return "select"
1418
+ elif origin is Union:
1419
+ args = get_args(field_type)
1420
+ if len(args) == 2 and type(None) in args:
1421
+ # Optional type - use the non-None type
1422
+ non_none_type = args[0] if args[1] is type(None) else args[1]
1423
+ return _determine_input_type(non_none_type)
1424
+ elif all(isinstance(arg, str) for arg in args if arg is not type(None)):
1425
+ return "select"
1426
+ else:
1427
+ return "text"
1428
+ elif hasattr(field_type, '__name__') and 'Tensor' in field_type.__name__:
1429
+ return "tensor"
1430
+ else:
1431
+ return "text"
1432
+
1433
+
1434
+ def _markdown_to_html(markdown: str) -> str:
1435
+ """Convert basic markdown to HTML for README display."""
1436
+ import html
1437
+ import re
1438
+
1439
+ # Escape HTML first
1440
+ html_content = html.escape(markdown)
1441
+
1442
+ # Convert headers
1443
+ html_content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html_content, flags=re.MULTILINE)
1444
+ html_content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html_content, flags=re.MULTILINE)
1445
+ html_content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html_content, flags=re.MULTILINE)
1446
+
1447
+ # Convert code blocks
1448
+ html_content = re.sub(r'```(.*?)\n(.*?)\n```', r'<pre><code>\2</code></pre>', html_content, flags=re.DOTALL)
1449
+ html_content = re.sub(r'`([^`]+)`', r'<code>\1</code>', html_content)
1450
+
1451
+ # Convert bold and italic
1452
+ html_content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html_content)
1453
+ html_content = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html_content)
1454
+
1455
+ # Convert lists
1456
+ html_content = re.sub(r'^- (.*?)$', r'<li>\1</li>', html_content, flags=re.MULTILINE)
1457
+ html_content = re.sub(r'(<li>.*</li>)', r'<ul>\1</ul>', html_content, flags=re.DOTALL)
1458
+
1459
+ # Convert line breaks
1460
+ html_content = html_content.replace('\n', '<br>')
1461
+
1462
+ return html_content
1463
+
1464
+
1465
+ def _generate_action_interface(action_fields: List[Dict[str, Any]], is_chat_env: bool) -> str:
1466
+ """Generate either a chat interface or action form based on environment type."""
1467
+ if is_chat_env:
1468
+ return _generate_chat_interface()
1469
+ else:
1470
+ return _generate_action_form(action_fields)
1471
+
1472
+ def _generate_chat_interface() -> str:
1473
+ """Generate a chat-style interface for chat environments."""
1474
+ return '''
1475
+ <!-- Chat Interface -->
1476
+ <div class="chat-interface">
1477
+ <h3>Chat Interface</h3>
1478
+ <div class="chat-messages" id="chat-messages">
1479
+ <div class="chat-message system">
1480
+ <div class="message-role">System</div>
1481
+ <div class="message-content">Chat environment ready. Send a message to start the conversation.</div>
1482
+ </div>
1483
+ </div>
1484
+ <div class="chat-input-container">
1485
+ <div class="role-selector">
1486
+ <label for="message-role">Role:</label>
1487
+ <select id="message-role">
1488
+ <option value="user">User</option>
1489
+ <option value="assistant">Assistant</option>
1490
+ </select>
1491
+ </div>
1492
+ <div class="message-input">
1493
+ <textarea id="message-input" placeholder="Type your message here..." rows="3"></textarea>
1494
+ <button class="btn" id="send-message-btn">Send Message</button>
1495
+ </div>
1496
+ </div>
1497
+ </div>
1498
+ '''
1499
+
1500
+ def _generate_action_form(action_fields: List[Dict[str, Any]]) -> str:
1501
+ """Generate a traditional action form for non-chat environments."""
1502
+ return f'''
1503
+ <!-- Action Form -->
1504
+ <div class="action-form">
1505
+ <h3>Take Action</h3>
1506
+ <form id="action-form">
1507
+ {_generate_action_form_fields(action_fields)}
1508
+ <button type="submit" class="btn" id="step-btn">Step</button>
1509
+ </form>
1510
+ </div>
1511
+ '''
1512
+
1513
  def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
1514
+ """Generate HTML form fields for action input with enhanced metadata."""
1515
  if not action_fields:
1516
  return '<p>No action fields available</p>'
1517
 
1518
  fields_html = []
1519
  for field in action_fields:
1520
+ field_html = _generate_single_field(field)
1521
+ fields_html.append(field_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522
 
1523
  return '\n'.join(fields_html)
1524
+
1525
+
1526
+ def _generate_single_field(field: Dict[str, Any]) -> str:
1527
+ """Generate HTML for a single form field with enhanced metadata."""
1528
+ field_name = field['name']
1529
+ field_type = field['type']
1530
+ required = field['required']
1531
+ placeholder = field.get('placeholder', '')
1532
+ help_text = field.get('help_text', '')
1533
+ choices = field.get('choices', [])
1534
+ min_value = field.get('min_value')
1535
+ max_value = field.get('max_value')
1536
+ default_value = field.get('default_value')
1537
+
1538
+ # Build label with required indicator
1539
+ label_text = field_name.replace('_', ' ').title()
1540
+ if required:
1541
+ label_text += ' <span style="color: red;">*</span>'
1542
+
1543
+ # Build input attributes
1544
+ input_attrs = []
1545
+ if required:
1546
+ input_attrs.append('required')
1547
+ if placeholder:
1548
+ input_attrs.append(f'placeholder="{placeholder}"')
1549
+ if min_value is not None:
1550
+ input_attrs.append(f'min="{min_value}"')
1551
+ if max_value is not None:
1552
+ input_attrs.append(f'max="{max_value}"')
1553
+ if default_value is not None:
1554
+ input_attrs.append(f'value="{default_value}"')
1555
+
1556
+ attrs_str = ' '.join(input_attrs)
1557
+
1558
+ if field_type == 'checkbox':
1559
+ return f'''
1560
+ <div class="form-group">
1561
+ <label>
1562
+ <input type="checkbox" name="{field_name}" value="true" {attrs_str}>
1563
+ {label_text}
1564
+ </label>
1565
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1566
+ </div>
1567
+ '''
1568
+
1569
+ elif field_type == 'select':
1570
+ options_html = []
1571
+ if not required:
1572
+ options_html.append(f'<option value="">-- Select {label_text} --</option>')
1573
+
1574
+ for choice in choices:
1575
+ selected = 'selected' if str(choice) == str(default_value) else ''
1576
+ options_html.append(f'<option value="{choice}" {selected}>{choice}</option>')
1577
+
1578
+ return f'''
1579
+ <div class="form-group">
1580
+ <label for="{field_name}">{label_text}:</label>
1581
+ <select name="{field_name}" id="{field_name}" {attrs_str}>
1582
+ {''.join(options_html)}
1583
+ </select>
1584
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1585
+ </div>
1586
+ '''
1587
+
1588
+ elif field_type == 'tensor':
1589
+ return f'''
1590
+ <div class="form-group">
1591
+ <label for="{field_name}">{label_text} (comma-separated integers):</label>
1592
+ <input type="text" name="{field_name}" id="{field_name}" {attrs_str}>
1593
+ <small class="help-text">{help_text or 'Enter token IDs as comma-separated integers (e.g., 1,2,3,4,5)'}</small>
1594
+ </div>
1595
+ '''
1596
+
1597
+ elif field_type == 'text' and ('message' in field_name.lower() or 'code' in field_name.lower()):
1598
+ return f'''
1599
+ <div class="form-group">
1600
+ <label for="{field_name}">{label_text}:</label>
1601
+ <textarea name="{field_name}" id="{field_name}" rows="3" {attrs_str}></textarea>
1602
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1603
+ </div>
1604
+ '''
1605
+
1606
+ else:
1607
+ return f'''
1608
+ <div class="form-group">
1609
+ <label for="{field_name}">{label_text}:</label>
1610
+ <input type="{field_type}" name="{field_name}" id="{field_name}" {attrs_str}>
1611
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1612
+ </div>
1613
+ '''
src/envs/atari_env/server/Dockerfile CHANGED
@@ -9,7 +9,7 @@
9
  #
10
  # CI/CD build: docker build --build-arg BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest \
11
  # -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
12
- ARG BASE_IMAGE=envtorch-base:latest
13
  FROM ${BASE_IMAGE}
14
 
15
  # Install ALE-specific dependencies
@@ -25,6 +25,9 @@ COPY src/core/ /app/src/core/
25
  # Copy Atari environment code
26
  COPY src/envs/atari_env/ /app/src/envs/atari_env/
27
 
 
 
 
28
  # Atari-specific environment variables (can be overridden at runtime)
29
  ENV ATARI_GAME=pong
30
  ENV ATARI_OBS_TYPE=rgb
 
9
  #
10
  # CI/CD build: docker build --build-arg BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest \
11
  # -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
12
+ ARG BASE_IMAGE=openenv-base:latest
13
  FROM ${BASE_IMAGE}
14
 
15
  # Install ALE-specific dependencies
 
25
  # Copy Atari environment code
26
  COPY src/envs/atari_env/ /app/src/envs/atari_env/
27
 
28
+ # Copy README for web interface documentation
29
+ COPY src/envs/atari_env/README.md /app/README.md
30
+
31
  # Atari-specific environment variables (can be overridden at runtime)
32
  ENV ATARI_GAME=pong
33
  ENV ATARI_OBS_TYPE=rgb
src/envs/atari_env/server/app.py CHANGED
@@ -63,8 +63,8 @@ env = AtariEnvironment(
63
  frameskip=frameskip,
64
  )
65
 
66
- # Create the FastAPI app with routes
67
- app = create_app(env, AtariAction, AtariObservation)
68
 
69
 
70
  if __name__ == "__main__":
 
63
  frameskip=frameskip,
64
  )
65
 
66
+ # Create the FastAPI app with web interface and README integration
67
+ app = create_app(env, AtariAction, AtariObservation, env_name="atari_env")
68
 
69
 
70
  if __name__ == "__main__":