Zach Wentz commited on
Commit
0d07eb9
·
1 Parent(s): d321359

🤖 Deploy chat_env environment - 2025-10-20 12:09:32

Browse files
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,
@@ -199,6 +274,7 @@ def create_web_interface_app(
199
  env: Environment,
200
  action_cls: Type[Action],
201
  observation_cls: Type[Observation],
 
202
  ) -> FastAPI:
203
  """
204
  Create a FastAPI application with web interface for the given environment.
@@ -207,6 +283,7 @@ def create_web_interface_app(
207
  env: The Environment instance to serve
208
  action_cls: The Action subclass this environment expects
209
  observation_cls: The Observation subclass this environment returns
 
210
 
211
  Returns:
212
  FastAPI application instance with web interface
@@ -216,14 +293,22 @@ def create_web_interface_app(
216
  # Create the base environment app
217
  app = create_fastapi_app(env, action_cls, observation_cls)
218
 
 
 
 
219
  # Create web interface manager
220
- web_manager = WebInterfaceManager(env, action_cls, observation_cls)
221
 
222
  # Add web interface routes
223
  @app.get("/web", response_class=HTMLResponse)
224
  async def web_interface():
225
  """Serve the web interface."""
226
- return get_web_interface_html(action_cls)
 
 
 
 
 
227
 
228
  @app.websocket("/ws")
229
  async def websocket_endpoint(websocket: WebSocket):
@@ -263,7 +348,7 @@ def create_web_interface_app(
263
  return app
264
 
265
 
266
- def get_web_interface_html(action_cls: Type[Action]) -> str:
267
  """Generate the HTML for the web interface."""
268
 
269
  # Check if this is a chat environment by looking for tokens field
@@ -274,30 +359,8 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
274
  is_chat_env = True
275
  break
276
 
277
- # Get action fields for dynamic form generation
278
- action_fields = []
279
- if hasattr(action_cls, '__dataclass_fields__'):
280
- for field_name, field_info in action_cls.__dataclass_fields__.items():
281
- if field_name != 'metadata':
282
- field_type = field_info.type
283
- if field_type == str:
284
- input_type = "text"
285
- elif field_type == int:
286
- input_type = "number"
287
- elif field_type == float:
288
- input_type = "number"
289
- elif field_type == bool:
290
- input_type = "checkbox"
291
- elif hasattr(field_type, '__name__') and 'Tensor' in field_type.__name__:
292
- input_type = "tensor"
293
- else:
294
- input_type = "text"
295
-
296
- action_fields.append({
297
- 'name': field_name,
298
- 'type': input_type,
299
- 'required': field_info.default is field_info.default_factory
300
- })
301
 
302
  return f"""
303
  <!DOCTYPE html>
@@ -613,6 +676,210 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
613
  border-color: #007bff;
614
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
615
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  </style>
617
  </head>
618
  <body>
@@ -624,6 +891,9 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
624
  HumanAgent Interface
625
  </div>
626
  <div class="pane-content">
 
 
 
627
  <!-- Action Form or Chat Interface -->
628
  {_generate_action_interface(action_fields, is_chat_env)}
629
 
@@ -725,6 +995,17 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
725
  }}
726
 
727
  setupEventListeners() {{
 
 
 
 
 
 
 
 
 
 
 
728
  // Check if this is a chat environment
729
  const isChatEnv = document.getElementById('chat-messages') !== null;
730
 
@@ -974,6 +1255,206 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
974
  """.replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
975
 
976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
  def _generate_action_interface(action_fields: List[Dict[str, Any]], is_chat_env: bool) -> str:
978
  """Generate either a chat interface or action form based on environment type."""
979
  if is_chat_env:
@@ -1023,42 +1504,103 @@ def _generate_action_form(action_fields: List[Dict[str, Any]]) -> str:
1023
  '''
1024
 
1025
  def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
1026
- """Generate HTML form fields for action input."""
1027
  if not action_fields:
1028
  return '<p>No action fields available</p>'
1029
 
1030
  fields_html = []
1031
  for field in action_fields:
1032
- if field['type'] == 'checkbox':
1033
- fields_html.append(f'''
1034
- <div class="form-group">
1035
- <label>
1036
- <input type="checkbox" name="{field['name']}" value="true">
1037
- {field['name']}
1038
- </label>
1039
- </div>
1040
- ''')
1041
- elif field['type'] == 'tensor':
1042
- fields_html.append(f'''
1043
- <div class="form-group">
1044
- <label for="{field['name']}">{field['name']} (comma-separated integers):</label>
1045
- <input type="text" name="{field['name']}" id="{field['name']}" placeholder="e.g., 1,2,3,4,5" {"required" if field['required'] else ""}>
1046
- <small>Enter token IDs as comma-separated integers</small>
1047
- </div>
1048
- ''')
1049
- elif field['type'] == 'text' and 'message' in field['name'].lower():
1050
- fields_html.append(f'''
1051
- <div class="form-group">
1052
- <label for="{field['name']}">{field['name']}:</label>
1053
- <textarea name="{field['name']}" id="{field['name']}" rows="3" placeholder="Enter {field['name']}..."></textarea>
1054
- </div>
1055
- ''')
1056
- else:
1057
- fields_html.append(f'''
1058
- <div class="form-group">
1059
- <label for="{field['name']}">{field['name']}:</label>
1060
- <input type="{field['type']}" name="{field['name']}" id="{field['name']}" placeholder="Enter {field['name']}..." {"required" if field['required'] else ""}>
1061
- </div>
1062
- ''')
1063
 
1064
  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,
 
274
  env: Environment,
275
  action_cls: Type[Action],
276
  observation_cls: Type[Observation],
277
+ env_name: Optional[str] = None,
278
  ) -> FastAPI:
279
  """
280
  Create a FastAPI application with web interface for the given environment.
 
283
  env: The Environment instance to serve
284
  action_cls: The Action subclass this environment expects
285
  observation_cls: The Observation subclass this environment returns
286
+ env_name: Optional environment name for README loading
287
 
288
  Returns:
289
  FastAPI application instance with web interface
 
293
  # Create the base environment app
294
  app = create_fastapi_app(env, action_cls, observation_cls)
295
 
296
+ # Load environment metadata
297
+ metadata = load_environment_metadata(env, env_name)
298
+
299
  # Create web interface manager
300
+ web_manager = WebInterfaceManager(env, action_cls, observation_cls, metadata)
301
 
302
  # Add web interface routes
303
  @app.get("/web", response_class=HTMLResponse)
304
  async def web_interface():
305
  """Serve the web interface."""
306
+ return get_web_interface_html(action_cls, web_manager.metadata)
307
+
308
+ @app.get("/web/metadata")
309
+ async def web_metadata():
310
+ """Get environment metadata."""
311
+ return asdict(web_manager.metadata)
312
 
313
  @app.websocket("/ws")
314
  async def websocket_endpoint(websocket: WebSocket):
 
348
  return app
349
 
350
 
351
+ def get_web_interface_html(action_cls: Type[Action], metadata: Optional[EnvironmentMetadata] = None) -> str:
352
  """Generate the HTML for the web interface."""
353
 
354
  # Check if this is a chat environment by looking for tokens field
 
359
  is_chat_env = True
360
  break
361
 
362
+ # Get action fields for dynamic form generation with enhanced metadata
363
+ action_fields = _extract_action_fields(action_cls)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
  return f"""
366
  <!DOCTYPE html>
 
676
  border-color: #007bff;
677
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
678
  }}
679
+
680
+ /* Instructions Section Styles */
681
+ .instructions-section {{
682
+ background: white;
683
+ border: 1px solid #e0e0e0;
684
+ border-radius: 8px;
685
+ padding: 20px;
686
+ margin-bottom: 20px;
687
+ }}
688
+
689
+ .instructions-header {{
690
+ display: flex;
691
+ justify-content: space-between;
692
+ align-items: center;
693
+ margin-bottom: 15px;
694
+ }}
695
+
696
+ .instructions-title {{
697
+ font-size: 18px;
698
+ font-weight: 600;
699
+ color: #333;
700
+ margin: 0;
701
+ }}
702
+
703
+ .instructions-toggle {{
704
+ background: #f8f9fa;
705
+ border: 1px solid #dee2e6;
706
+ border-radius: 4px;
707
+ padding: 5px 10px;
708
+ cursor: pointer;
709
+ font-size: 12px;
710
+ color: #6c757d;
711
+ }}
712
+
713
+ .instructions-toggle:hover {{
714
+ background: #e9ecef;
715
+ }}
716
+
717
+ .instructions-content {{
718
+ display: none;
719
+ max-height: 400px;
720
+ overflow-y: auto;
721
+ border-top: 1px solid #e0e0e0;
722
+ padding-top: 15px;
723
+ }}
724
+
725
+ .instructions-content.expanded {{
726
+ display: block;
727
+ }}
728
+
729
+ .instructions-content h1,
730
+ .instructions-content h2,
731
+ .instructions-content h3 {{
732
+ color: #333;
733
+ margin-top: 20px;
734
+ margin-bottom: 10px;
735
+ }}
736
+
737
+ .instructions-content h1 {{
738
+ font-size: 24px;
739
+ border-bottom: 2px solid #007bff;
740
+ padding-bottom: 10px;
741
+ }}
742
+
743
+ .instructions-content h2 {{
744
+ font-size: 20px;
745
+ }}
746
+
747
+ .instructions-content h3 {{
748
+ font-size: 16px;
749
+ }}
750
+
751
+ .instructions-content p {{
752
+ margin-bottom: 10px;
753
+ line-height: 1.6;
754
+ }}
755
+
756
+ .instructions-content code {{
757
+ background: #f8f9fa;
758
+ padding: 2px 4px;
759
+ border-radius: 3px;
760
+ font-family: monospace;
761
+ font-size: 14px;
762
+ }}
763
+
764
+ .instructions-content pre {{
765
+ background: #f8f9fa;
766
+ border: 1px solid #e9ecef;
767
+ border-radius: 4px;
768
+ padding: 15px;
769
+ overflow-x: auto;
770
+ margin: 10px 0;
771
+ }}
772
+
773
+ .instructions-content pre code {{
774
+ background: none;
775
+ padding: 0;
776
+ }}
777
+
778
+ .instructions-content ul,
779
+ .instructions-content ol {{
780
+ margin: 10px 0;
781
+ padding-left: 20px;
782
+ }}
783
+
784
+ .instructions-content li {{
785
+ margin-bottom: 5px;
786
+ }}
787
+
788
+ .instructions-content table {{
789
+ border-collapse: collapse;
790
+ width: 100%;
791
+ margin: 15px 0;
792
+ }}
793
+
794
+ .instructions-content th,
795
+ .instructions-content td {{
796
+ border: 1px solid #dee2e6;
797
+ padding: 8px 12px;
798
+ text-align: left;
799
+ }}
800
+
801
+ .instructions-content th {{
802
+ background: #f8f9fa;
803
+ font-weight: 600;
804
+ }}
805
+
806
+ /* Enhanced Form Styles */
807
+ .help-text {{
808
+ display: block;
809
+ margin-top: 5px;
810
+ font-size: 12px;
811
+ color: #6c757d;
812
+ font-style: italic;
813
+ }}
814
+
815
+ .form-group label {{
816
+ font-weight: 500;
817
+ color: #333;
818
+ margin-bottom: 5px;
819
+ }}
820
+
821
+ .form-group select {{
822
+ width: 100%;
823
+ padding: 8px 12px;
824
+ border: 1px solid #ddd;
825
+ border-radius: 4px;
826
+ font-size: 14px;
827
+ background-color: white;
828
+ }}
829
+
830
+ .form-group select:focus {{
831
+ outline: none;
832
+ border-color: #007bff;
833
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
834
+ }}
835
+
836
+ .form-group textarea {{
837
+ width: 100%;
838
+ padding: 8px 12px;
839
+ border: 1px solid #ddd;
840
+ border-radius: 4px;
841
+ font-size: 14px;
842
+ font-family: inherit;
843
+ resize: vertical;
844
+ }}
845
+
846
+ .form-group textarea:focus {{
847
+ outline: none;
848
+ border-color: #007bff;
849
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
850
+ }}
851
+
852
+ .form-group input[type="number"] {{
853
+ width: 100%;
854
+ padding: 8px 12px;
855
+ border: 1px solid #ddd;
856
+ border-radius: 4px;
857
+ font-size: 14px;
858
+ }}
859
+
860
+ .form-group input[type="number"]:focus {{
861
+ outline: none;
862
+ border-color: #007bff;
863
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
864
+ }}
865
+
866
+ .form-group input[type="text"]:focus {{
867
+ outline: none;
868
+ border-color: #007bff;
869
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
870
+ }}
871
+
872
+ .required-indicator {{
873
+ color: #dc3545;
874
+ font-weight: bold;
875
+ }}
876
+
877
+ .form-group .field-description {{
878
+ font-size: 11px;
879
+ color: #666;
880
+ margin-top: 2px;
881
+ font-style: italic;
882
+ }}
883
  </style>
884
  </head>
885
  <body>
 
891
  HumanAgent Interface
892
  </div>
893
  <div class="pane-content">
894
+ <!-- Instructions Section -->
895
+ {_generate_instructions_section(metadata)}
896
+
897
  <!-- Action Form or Chat Interface -->
898
  {_generate_action_interface(action_fields, is_chat_env)}
899
 
 
995
  }}
996
 
997
  setupEventListeners() {{
998
+ // Instructions toggle
999
+ const instructionsToggle = document.getElementById('instructions-toggle');
1000
+ const instructionsContent = document.getElementById('instructions-content');
1001
+ if (instructionsToggle && instructionsContent) {{
1002
+ instructionsToggle.addEventListener('click', () => {{
1003
+ instructionsContent.classList.toggle('expanded');
1004
+ instructionsToggle.textContent = instructionsContent.classList.contains('expanded')
1005
+ ? 'Hide Instructions' : 'Show Instructions';
1006
+ }});
1007
+ }}
1008
+
1009
  // Check if this is a chat environment
1010
  const isChatEnv = document.getElementById('chat-messages') !== null;
1011
 
 
1255
  """.replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
1256
 
1257
 
1258
+ def _generate_instructions_section(metadata: Optional[EnvironmentMetadata]) -> str:
1259
+ """Generate the instructions section with environment documentation."""
1260
+ if not metadata or not metadata.readme_content:
1261
+ return ''
1262
+
1263
+ # Convert markdown to HTML (basic conversion)
1264
+ import re
1265
+ html_content = _markdown_to_html(metadata.readme_content)
1266
+
1267
+ return f'''
1268
+ <!-- Instructions Section -->
1269
+ <div class="instructions-section">
1270
+ <div class="instructions-header">
1271
+ <h3 class="instructions-title">{metadata.name}</h3>
1272
+ <button class="instructions-toggle" id="instructions-toggle">Show Instructions</button>
1273
+ </div>
1274
+ <div class="instructions-content" id="instructions-content">
1275
+ <div class="instructions-readme">
1276
+ {html_content}
1277
+ </div>
1278
+ </div>
1279
+ </div>
1280
+ '''
1281
+
1282
+
1283
+ def _extract_action_fields(action_cls: Type[Action]) -> List[Dict[str, Any]]:
1284
+ """Extract enhanced field metadata from Action class for form generation."""
1285
+ import typing
1286
+ from typing import get_origin, get_args
1287
+
1288
+ action_fields = []
1289
+ if not hasattr(action_cls, '__dataclass_fields__'):
1290
+ return action_fields
1291
+
1292
+ for field_name, field_info in action_cls.__dataclass_fields__.items():
1293
+ if field_name == 'metadata':
1294
+ continue
1295
+
1296
+ field_type = field_info.type
1297
+ field_metadata = _extract_field_metadata(field_name, field_info)
1298
+
1299
+ # Determine input type based on field type
1300
+ input_type = _determine_input_type(field_type)
1301
+
1302
+ # Check if field is required
1303
+ is_required = field_info.default is field_info.default_factory
1304
+
1305
+ action_fields.append({
1306
+ 'name': field_name,
1307
+ 'type': input_type,
1308
+ 'required': is_required,
1309
+ 'description': field_metadata.get('description', ''),
1310
+ 'default_value': field_metadata.get('default_value'),
1311
+ 'choices': field_metadata.get('choices', []),
1312
+ 'min_value': field_metadata.get('min_value'),
1313
+ 'max_value': field_metadata.get('max_value'),
1314
+ 'placeholder': field_metadata.get('placeholder', ''),
1315
+ 'help_text': field_metadata.get('help_text', ''),
1316
+ })
1317
+
1318
+ return action_fields
1319
+
1320
+
1321
+ def _extract_field_metadata(field_name: str, field_info) -> Dict[str, Any]:
1322
+ """Extract metadata from dataclass field including docstring and type hints."""
1323
+ import typing
1324
+ from typing import get_origin, get_args, Literal, Union, Optional
1325
+
1326
+ metadata = {}
1327
+
1328
+ # Extract description from field docstring or annotation
1329
+ if hasattr(field_info, 'metadata') and field_info.metadata:
1330
+ # Check for custom metadata
1331
+ for meta in field_info.metadata:
1332
+ if isinstance(meta, dict):
1333
+ metadata.update(meta)
1334
+
1335
+ # Extract type information
1336
+ field_type = field_info.type
1337
+ origin = get_origin(field_type)
1338
+
1339
+ # Handle Literal types for dropdown choices
1340
+ if origin is Literal:
1341
+ args = get_args(field_type)
1342
+ metadata['choices'] = list(args)
1343
+
1344
+ # Handle Optional types
1345
+ if origin is Union:
1346
+ args = get_args(field_type)
1347
+ if len(args) == 2 and type(None) in args:
1348
+ # This is Optional[SomeType]
1349
+ non_none_type = args[0] if args[1] is type(None) else args[1]
1350
+ metadata['optional'] = True
1351
+ # Recursively check the non-None type for choices
1352
+ if get_origin(non_none_type) is Literal:
1353
+ metadata['choices'] = list(get_args(non_none_type))
1354
+ else:
1355
+ # Regular Union type
1356
+ metadata['choices'] = [str(arg) for arg in args if arg is not type(None)]
1357
+
1358
+ # Handle numeric constraints
1359
+ if field_type in (int, float):
1360
+ # Check for common constraint patterns in field name
1361
+ if 'count' in field_name.lower() or 'num' in field_name.lower():
1362
+ metadata['min_value'] = 0
1363
+ if 'id' in field_name.lower():
1364
+ metadata['min_value'] = 0
1365
+
1366
+ # Generate placeholder text
1367
+ if 'message' in field_name.lower():
1368
+ metadata['placeholder'] = f'Enter {field_name.replace("_", " ")}...'
1369
+ elif 'code' in field_name.lower():
1370
+ metadata['placeholder'] = 'Enter Python code here...'
1371
+ elif 'tokens' in field_name.lower():
1372
+ metadata['placeholder'] = 'Enter comma-separated token IDs (e.g., 1,2,3,4,5)'
1373
+ else:
1374
+ metadata['placeholder'] = f'Enter {field_name.replace("_", " ")}...'
1375
+
1376
+ # Generate help text based on field name and type
1377
+ if 'action_id' in field_name.lower():
1378
+ metadata['help_text'] = 'The action ID to execute in the environment'
1379
+ elif 'game_name' in field_name.lower():
1380
+ metadata['help_text'] = 'Name of the game or environment'
1381
+ elif 'tokens' in field_name.lower():
1382
+ metadata['help_text'] = 'Token IDs as a comma-separated list of integers'
1383
+ elif 'code' in field_name.lower():
1384
+ metadata['help_text'] = 'Python code to execute in the environment'
1385
+ elif 'message' in field_name.lower():
1386
+ metadata['help_text'] = 'Text message to send'
1387
+
1388
+ return metadata
1389
+
1390
+
1391
+ def _determine_input_type(field_type) -> str:
1392
+ """Determine the appropriate HTML input type for a field type."""
1393
+ import typing
1394
+ from typing import get_origin, get_args, Literal, Union
1395
+
1396
+ # Handle direct types
1397
+ if field_type == str:
1398
+ return "text"
1399
+ elif field_type == int:
1400
+ return "number"
1401
+ elif field_type == float:
1402
+ return "number"
1403
+ elif field_type == bool:
1404
+ return "checkbox"
1405
+
1406
+ # Handle complex types
1407
+ origin = get_origin(field_type)
1408
+
1409
+ if origin is Literal:
1410
+ return "select"
1411
+ elif origin is Union:
1412
+ args = get_args(field_type)
1413
+ if len(args) == 2 and type(None) in args:
1414
+ # Optional type - use the non-None type
1415
+ non_none_type = args[0] if args[1] is type(None) else args[1]
1416
+ return _determine_input_type(non_none_type)
1417
+ elif all(isinstance(arg, str) for arg in args if arg is not type(None)):
1418
+ return "select"
1419
+ else:
1420
+ return "text"
1421
+ elif hasattr(field_type, '__name__') and 'Tensor' in field_type.__name__:
1422
+ return "tensor"
1423
+ else:
1424
+ return "text"
1425
+
1426
+
1427
+ def _markdown_to_html(markdown: str) -> str:
1428
+ """Convert basic markdown to HTML for README display."""
1429
+ import html
1430
+ import re
1431
+
1432
+ # Escape HTML first
1433
+ html_content = html.escape(markdown)
1434
+
1435
+ # Convert headers
1436
+ html_content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html_content, flags=re.MULTILINE)
1437
+ html_content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html_content, flags=re.MULTILINE)
1438
+ html_content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html_content, flags=re.MULTILINE)
1439
+
1440
+ # Convert code blocks
1441
+ html_content = re.sub(r'```(.*?)\n(.*?)\n```', r'<pre><code>\2</code></pre>', html_content, flags=re.DOTALL)
1442
+ html_content = re.sub(r'`([^`]+)`', r'<code>\1</code>', html_content)
1443
+
1444
+ # Convert bold and italic
1445
+ html_content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html_content)
1446
+ html_content = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html_content)
1447
+
1448
+ # Convert lists
1449
+ html_content = re.sub(r'^- (.*?)$', r'<li>\1</li>', html_content, flags=re.MULTILINE)
1450
+ html_content = re.sub(r'(<li>.*</li>)', r'<ul>\1</ul>', html_content, flags=re.DOTALL)
1451
+
1452
+ # Convert line breaks
1453
+ html_content = html_content.replace('\n', '<br>')
1454
+
1455
+ return html_content
1456
+
1457
+
1458
  def _generate_action_interface(action_fields: List[Dict[str, Any]], is_chat_env: bool) -> str:
1459
  """Generate either a chat interface or action form based on environment type."""
1460
  if is_chat_env:
 
1504
  '''
1505
 
1506
  def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
1507
+ """Generate HTML form fields for action input with enhanced metadata."""
1508
  if not action_fields:
1509
  return '<p>No action fields available</p>'
1510
 
1511
  fields_html = []
1512
  for field in action_fields:
1513
+ field_html = _generate_single_field(field)
1514
+ fields_html.append(field_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1515
 
1516
  return '\n'.join(fields_html)
1517
+
1518
+
1519
+ def _generate_single_field(field: Dict[str, Any]) -> str:
1520
+ """Generate HTML for a single form field with enhanced metadata."""
1521
+ field_name = field['name']
1522
+ field_type = field['type']
1523
+ required = field['required']
1524
+ placeholder = field.get('placeholder', '')
1525
+ help_text = field.get('help_text', '')
1526
+ choices = field.get('choices', [])
1527
+ min_value = field.get('min_value')
1528
+ max_value = field.get('max_value')
1529
+ default_value = field.get('default_value')
1530
+
1531
+ # Build label with required indicator
1532
+ label_text = field_name.replace('_', ' ').title()
1533
+ if required:
1534
+ label_text += ' <span style="color: red;">*</span>'
1535
+
1536
+ # Build input attributes
1537
+ input_attrs = []
1538
+ if required:
1539
+ input_attrs.append('required')
1540
+ if placeholder:
1541
+ input_attrs.append(f'placeholder="{placeholder}"')
1542
+ if min_value is not None:
1543
+ input_attrs.append(f'min="{min_value}"')
1544
+ if max_value is not None:
1545
+ input_attrs.append(f'max="{max_value}"')
1546
+ if default_value is not None:
1547
+ input_attrs.append(f'value="{default_value}"')
1548
+
1549
+ attrs_str = ' '.join(input_attrs)
1550
+
1551
+ if field_type == 'checkbox':
1552
+ return f'''
1553
+ <div class="form-group">
1554
+ <label>
1555
+ <input type="checkbox" name="{field_name}" value="true" {attrs_str}>
1556
+ {label_text}
1557
+ </label>
1558
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1559
+ </div>
1560
+ '''
1561
+
1562
+ elif field_type == 'select':
1563
+ options_html = []
1564
+ if not required:
1565
+ options_html.append(f'<option value="">-- Select {label_text} --</option>')
1566
+
1567
+ for choice in choices:
1568
+ selected = 'selected' if str(choice) == str(default_value) else ''
1569
+ options_html.append(f'<option value="{choice}" {selected}>{choice}</option>')
1570
+
1571
+ return f'''
1572
+ <div class="form-group">
1573
+ <label for="{field_name}">{label_text}:</label>
1574
+ <select name="{field_name}" id="{field_name}" {attrs_str}>
1575
+ {''.join(options_html)}
1576
+ </select>
1577
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1578
+ </div>
1579
+ '''
1580
+
1581
+ elif field_type == 'tensor':
1582
+ return f'''
1583
+ <div class="form-group">
1584
+ <label for="{field_name}">{label_text} (comma-separated integers):</label>
1585
+ <input type="text" name="{field_name}" id="{field_name}" {attrs_str}>
1586
+ <small class="help-text">{help_text or 'Enter token IDs as comma-separated integers (e.g., 1,2,3,4,5)'}</small>
1587
+ </div>
1588
+ '''
1589
+
1590
+ elif field_type == 'text' and ('message' in field_name.lower() or 'code' in field_name.lower()):
1591
+ return f'''
1592
+ <div class="form-group">
1593
+ <label for="{field_name}">{label_text}:</label>
1594
+ <textarea name="{field_name}" id="{field_name}" rows="3" {attrs_str}></textarea>
1595
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1596
+ </div>
1597
+ '''
1598
+
1599
+ else:
1600
+ return f'''
1601
+ <div class="form-group">
1602
+ <label for="{field_name}">{label_text}:</label>
1603
+ <input type="{field_type}" name="{field_name}" id="{field_name}" {attrs_str}>
1604
+ {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1605
+ </div>
1606
+ '''
src/envs/chat_env/server/Dockerfile CHANGED
@@ -17,6 +17,9 @@ RUN pip install torch transformers
17
  COPY src/core/ /app/src/core/
18
  COPY src/envs/chat_env/ /app/src/envs/chat_env/
19
 
 
 
 
20
  # Environment variables that can be overridden at runtime
21
  ENV TOKENIZER_NAME=gpt2
22
  ENV SYSTEM_PROMPT="You are a helpful AI assistant."
 
17
  COPY src/core/ /app/src/core/
18
  COPY src/envs/chat_env/ /app/src/envs/chat_env/
19
 
20
+ # Copy README for web interface documentation
21
+ COPY src/envs/chat_env/README.md /app/README.md
22
+
23
  # Environment variables that can be overridden at runtime
24
  ENV TOKENIZER_NAME=gpt2
25
  ENV SYSTEM_PROMPT="You are a helpful AI assistant."
src/envs/chat_env/server/app.py CHANGED
@@ -27,6 +27,7 @@ Usage:
27
  import os
28
 
29
  from core.env_server import create_app
 
30
 
31
  from ..models import ChatAction, ChatObservation
32
  from .chat_environment import ChatEnvironment
@@ -67,8 +68,8 @@ system_prompt = os.environ.get("SYSTEM_PROMPT", None)
67
  tokenizer = get_tokenizer()
68
  env = ChatEnvironment(tokenizer=tokenizer, system_prompt=system_prompt)
69
 
70
- # Create the FastAPI app with routes
71
- app = create_app(env, ChatAction, ChatObservation)
72
 
73
 
74
  if __name__ == "__main__":
 
27
  import os
28
 
29
  from core.env_server import create_app
30
+ from core.env_server.web_interface import create_web_interface_app
31
 
32
  from ..models import ChatAction, ChatObservation
33
  from .chat_environment import ChatEnvironment
 
68
  tokenizer = get_tokenizer()
69
  env = ChatEnvironment(tokenizer=tokenizer, system_prompt=system_prompt)
70
 
71
+ # Create the FastAPI app with web interface and README integration
72
+ app = create_app(env, ChatAction, ChatObservation, env_name="chat_env")
73
 
74
 
75
  if __name__ == "__main__":