SwapnilPatil28 commited on
Commit
6792e60
·
verified ·
1 Parent(s): d642ecf

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ ENV ENABLE_WEB_INTERFACE=true
7
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,255 @@
1
  ---
2
- title: Support Env
3
- emoji: 😻
4
- colorFrom: gray
5
- colorTo: purple
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: Support Env Environment Server
3
+ emoji: 🔔
4
+ colorFrom: yellow
5
+ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Support Env Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the Support Env environment is through the `SupportEnv` class:
21
+
22
+ ```python
23
+ from support_env import SupportAction, SupportEnv
24
+
25
+ try:
26
+ # Create environment from Docker image
27
+ support_envenv = SupportEnv.from_docker_image("support_env-env:latest")
28
+
29
+ # Reset
30
+ result = support_envenv.reset()
31
+ print(f"Reset: {result.observation.echoed_message}")
32
+
33
+ # Send multiple messages
34
+ messages = ["Hello, World!", "Testing echo", "Final message"]
35
+
36
+ for msg in messages:
37
+ result = support_envenv.step(SupportAction(message=msg))
38
+ print(f"Sent: '{msg}'")
39
+ print(f" → Echoed: '{result.observation.echoed_message}'")
40
+ print(f" → Length: {result.observation.message_length}")
41
+ print(f" → Reward: {result.reward}")
42
+
43
+ finally:
44
+ # Always clean up
45
+ support_envenv.close()
46
+ ```
47
+
48
+ That's it! The `SupportEnv.from_docker_image()` method handles:
49
+ - Starting the Docker container
50
+ - Waiting for the server to be ready
51
+ - Connecting to the environment
52
+ - Container cleanup when you call `close()`
53
+
54
+ ## Building the Docker Image
55
+
56
+ Before using the environment, you need to build the Docker image:
57
+
58
+ ```bash
59
+ # From project root
60
+ docker build -t support_env-env:latest -f server/Dockerfile .
61
+ ```
62
+
63
+ ## Deploying to Hugging Face Spaces
64
+
65
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
+
67
+ ```bash
68
+ # From the environment directory (where openenv.yaml is located)
69
+ openenv push
70
+
71
+ # Or specify options
72
+ openenv push --namespace my-org --private
73
+ ```
74
+
75
+ The `openenv push` command will:
76
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
+ 3. Upload to Hugging Face (ensuring you're logged in)
79
+
80
+ ### Prerequisites
81
+
82
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
83
+
84
+ ### Options
85
+
86
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
+ - `--private`: Deploy the space as private (default: public)
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
+ openenv push
96
+
97
+ # Push to a specific repository
98
+ openenv push --repo-id my-org/my-env
99
+
100
+ # Push with a custom base image
101
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
+
103
+ # Push as a private space
104
+ openenv push --private
105
+
106
+ # Combine options
107
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
108
+ ```
109
+
110
+ After deployment, your space will be available at:
111
+ `https://huggingface.co/spaces/<repo-id>`
112
+
113
+ The deployed space includes:
114
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
+ - **Health Check** at `/health` - Container health monitoring
117
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
118
+
119
+ ## Environment Details
120
+
121
+ ### Action
122
+ **SupportAction**: Contains a single field
123
+ - `message` (str) - The message to echo back
124
+
125
+ ### Observation
126
+ **SupportObservation**: Contains the echo response and metadata
127
+ - `echoed_message` (str) - The message echoed back
128
+ - `message_length` (int) - Length of the message
129
+ - `reward` (float) - Reward based on message length (length × 0.1)
130
+ - `done` (bool) - Always False for echo environment
131
+ - `metadata` (dict) - Additional info like step count
132
+
133
+ ### Reward
134
+ The reward is calculated as: `message_length × 0.1`
135
+ - "Hi" → reward: 0.2
136
+ - "Hello, World!" → reward: 1.3
137
+ - Empty message → reward: 0.0
138
+
139
+ ## Advanced Usage
140
+
141
+ ### Connecting to an Existing Server
142
+
143
+ If you already have a Support Env environment server running, you can connect directly:
144
+
145
+ ```python
146
+ from support_env import SupportEnv
147
+
148
+ # Connect to existing server
149
+ support_envenv = SupportEnv(base_url="<ENV_HTTP_URL_HERE>")
150
+
151
+ # Use as normal
152
+ result = support_envenv.reset()
153
+ result = support_envenv.step(SupportAction(message="Hello!"))
154
+ ```
155
+
156
+ Note: When connecting to an existing server, `support_envenv.close()` will NOT stop the server.
157
+
158
+ ### Using the Context Manager
159
+
160
+ The client supports context manager usage for automatic connection management:
161
+
162
+ ```python
163
+ from support_env import SupportAction, SupportEnv
164
+
165
+ # Connect with context manager (auto-connects and closes)
166
+ with SupportEnv(base_url="http://localhost:8000") as env:
167
+ result = env.reset()
168
+ print(f"Reset: {result.observation.echoed_message}")
169
+ # Multiple steps with low latency
170
+ for msg in ["Hello", "World", "!"]:
171
+ result = env.step(SupportAction(message=msg))
172
+ print(f"Echoed: {result.observation.echoed_message}")
173
+ ```
174
+
175
+ The client uses WebSocket connections for:
176
+ - **Lower latency**: No HTTP connection overhead per request
177
+ - **Persistent session**: Server maintains your environment state
178
+ - **Efficient for episodes**: Better for many sequential steps
179
+
180
+ ### Concurrent WebSocket Sessions
181
+
182
+ The server supports multiple concurrent WebSocket connections. To enable this,
183
+ modify `server/app.py` to use factory mode:
184
+
185
+ ```python
186
+ # In server/app.py - use factory mode for concurrent sessions
187
+ app = create_app(
188
+ SupportEnvironment, # Pass class, not instance
189
+ SupportAction,
190
+ SupportObservation,
191
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
192
+ )
193
+ ```
194
+
195
+ Then multiple clients can connect simultaneously:
196
+
197
+ ```python
198
+ from support_env import SupportAction, SupportEnv
199
+ from concurrent.futures import ThreadPoolExecutor
200
+
201
+ def run_episode(client_id: int):
202
+ with SupportEnv(base_url="http://localhost:8000") as env:
203
+ result = env.reset()
204
+ for i in range(10):
205
+ result = env.step(SupportAction(message=f"Client {client_id}, step {i}"))
206
+ return client_id, result.observation.message_length
207
+
208
+ # Run 4 episodes concurrently
209
+ with ThreadPoolExecutor(max_workers=4) as executor:
210
+ results = list(executor.map(run_episode, range(4)))
211
+ ```
212
+
213
+ ## Development & Testing
214
+
215
+ ### Direct Environment Testing
216
+
217
+ Test the environment logic directly without starting the HTTP server:
218
+
219
+ ```bash
220
+ # From the server directory
221
+ python3 server/support_env_environment.py
222
+ ```
223
+
224
+ This verifies that:
225
+ - Environment resets correctly
226
+ - Step executes actions properly
227
+ - State tracking works
228
+ - Rewards are calculated correctly
229
+
230
+ ### Running Locally
231
+
232
+ Run the server locally for development:
233
+
234
+ ```bash
235
+ uvicorn server.app:app --reload
236
+ ```
237
+
238
+ ## Project Structure
239
+
240
+ ```
241
+ support_env/
242
+ ├── .dockerignore # Docker build exclusions
243
+ ├── __init__.py # Module exports
244
+ ├── README.md # This file
245
+ ├── openenv.yaml # OpenEnv manifest
246
+ ├── pyproject.toml # Project metadata and dependencies
247
+ ├── uv.lock # Locked dependencies (generated)
248
+ ├── client.py # SupportEnv client
249
+ ├── models.py # Action and Observation models
250
+ └── server/
251
+ ├── __init__.py # Server module exports
252
+ ├── support_env_environment.py # Core environment logic
253
+ ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
254
+ └── Dockerfile # Container image definition
255
+ ```
__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
+ """Support Env Environment."""
8
+
9
+ from .client import SupportEnv
10
+ from .models import SupportAction, SupportObservation
11
+
12
+ __all__ = [
13
+ "SupportAction",
14
+ "SupportObservation",
15
+ "SupportEnv",
16
+ ]
client.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openenv.core.env_client import EnvClient
2
+ from openenv.core.client_types import StepResult
3
+ from .models import SupportAction, SupportObservation, SupportState
4
+
5
+ class SupportEnvClient(EnvClient[SupportAction, SupportObservation, SupportState]):
6
+ def _step_payload(self, action: SupportAction) -> dict:
7
+ return {"department": action.department}
8
+
9
+ def _parse_result(self, payload: dict) -> StepResult:
10
+ obs = payload.get("observation", {})
11
+ return StepResult(
12
+ observation=SupportObservation(
13
+ done=payload.get("done", False), reward=payload.get("reward", 0.0),
14
+ ticket_id=obs.get("ticket_id", ""), ticket_text=obs.get("ticket_text", ""),
15
+ message=obs.get("message", "")
16
+ ),
17
+ reward=payload.get("reward", 0.0), done=payload.get("done", False),
18
+ )
19
+
20
+ def _parse_state(self, payload: dict) -> SupportState:
21
+ return SupportState(**payload)
inference.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from client import SupportEnvClient, SupportAction
4
+
5
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
6
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
7
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "dummy-key")
8
+ BENCHMARK = "support_env"
9
+
10
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
11
+
12
+ def run_task(task_name: str):
13
+ env_url = os.getenv("ENV_URL", "http://localhost:8000")
14
+ env = SupportEnvClient(base_url=env_url).sync()
15
+ result = env.reset(task_name=task_name)
16
+
17
+ print(f"[START] task={task_name} env={BENCHMARK} model={MODEL_NAME}")
18
+
19
+ step = 1
20
+ rewards = []
21
+
22
+ while not result.done:
23
+ prompt = f"Ticket: '{result.observation.ticket_text}'. Reply ONLY with one word: Billing, Tech, or Sales."
24
+ try:
25
+ response = client.chat.completions.create(
26
+ model=MODEL_NAME,
27
+ messages=[{"role": "user", "content": prompt}],
28
+ max_tokens=10
29
+ )
30
+ action_str = response.choices[0].message.content.strip()
31
+ except Exception:
32
+ action_str = "Tech" # Fallback if API fails
33
+
34
+ result = env.step(SupportAction(department=action_str))
35
+ reward_val = float(result.reward or 0.0)
36
+ rewards.append(reward_val)
37
+
38
+ done_str = str(result.done).lower()
39
+ print(f"[STEP] step={step} action={action_str} reward={reward_val:.2f} done={done_str} error=null")
40
+ step += 1
41
+
42
+ score = sum(rewards) / len(rewards) if rewards else 0.0
43
+ success = str(score > 0).lower()
44
+ rewards_str = ",".join([f"{r:.2f}" for r in rewards])
45
+
46
+ print(f"[END] success={success} steps={step-1} score={score:.2f} rewards={rewards_str}")
47
+ env.close()
48
+
49
+ if __name__ == "__main__":
50
+ for task in ["easy", "medium", "hard"]:
51
+ run_task(task)
models.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Dict
2
+ from openenv.core.env_server import Action, Observation, State
3
+
4
+ class SupportAction(Action):
5
+ department: str
6
+
7
+ class SupportObservation(Observation):
8
+ ticket_id: str
9
+ ticket_text: str
10
+ message: str
11
+
12
+ class SupportState(State):
13
+ task_name: str = "easy"
14
+ total_tickets: int = 1
15
+ tickets_routed: int = 0
openenv.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "support_env"
2
+ version: "1.0"
3
+ description: "A real-world environment for routing customer support tickets."
4
+ tasks:
5
+ - id: "easy"
6
+ description: "Route 1 obvious ticket."
7
+ - id: "medium"
8
+ description: "Route 2 standard tickets."
9
+ - id: "hard"
10
+ description: "Route 3 complex tickets."
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-support_env"
13
+ version = "0.1.0"
14
+ description = "Support 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.2",
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 support_env.server.app
40
+ server = "support_env.server.app:main"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+ packages = ["support_env", "support_env.server"]
45
+ package-dir = { "support_env" = ".", "support_env.server" = "server" }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ openenv-core
2
+ fastapi
3
+ uvicorn
4
+ pydantic
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
+ """Support Env environment server components."""
8
+
9
+ from .support_env_environment import SupportEnvironment
10
+
11
+ __all__ = ["SupportEnvironment"]
server/app.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from openenv.core.env_server import create_fastapi_app
2
+ from ..models import SupportAction, SupportObservation
3
+ from .environment import SupportEnvironment
4
+
5
+ app = create_fastapi_app(SupportEnvironment, SupportAction, SupportObservation)
server/environment.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from typing import List, Dict
3
+ from openenv.core.env_server import Observation
4
+ from ..models import SupportAction, SupportObservation, SupportState
5
+
6
+ class SupportEnvironment:
7
+ def __init__(self):
8
+ self._state = SupportState()
9
+ self._current_queue: List[Dict[str, str]] = []
10
+ self._current_ticket = None
11
+ self.tasks = {
12
+ "easy": [{"id": "T1", "text": "Can I get a refund for my last invoice?", "dept": "Billing"}],
13
+ "medium": [{"id": "T2", "text": "My screen is flickering.", "dept": "Tech"},
14
+ {"id": "T3", "text": "I want to upgrade.", "dept": "Sales"}],
15
+ "hard": [{"id": "T4", "text": "Update credit card?", "dept": "Billing"},
16
+ {"id": "T5", "text": "Enterprise discount?", "dept": "Sales"},
17
+ {"id": "T6", "text": "API 500 error on POST.", "dept": "Tech"}]
18
+ }
19
+
20
+ def reset(self, task_name: str = "easy") -> SupportObservation:
21
+ if task_name not in self.tasks:
22
+ task_name = "easy"
23
+ self._current_queue = self.tasks[task_name].copy()
24
+ self._state = SupportState(
25
+ episode_id=str(uuid.uuid4()), step_count=0, task_name=task_name,
26
+ total_tickets=len(self._current_queue), tickets_routed=0
27
+ )
28
+ self._current_ticket = self._current_queue.pop(0)
29
+ return SupportObservation(
30
+ done=False, reward=0.0, ticket_id=self._current_ticket["id"],
31
+ ticket_text=self._current_ticket["text"], message="Please route this ticket."
32
+ )
33
+
34
+ def step(self, action: SupportAction) -> SupportObservation:
35
+ self._state.step_count += 1
36
+ is_correct = action.department.lower() == self._current_ticket["dept"].lower()
37
+ reward = 1.0 if is_correct else 0.0
38
+ self._state.tickets_routed += 1
39
+
40
+ if len(self._current_queue) > 0:
41
+ self._current_ticket = self._current_queue.pop(0)
42
+ return SupportObservation(
43
+ done=False, reward=reward, ticket_id=self._current_ticket["id"],
44
+ ticket_text=self._current_ticket["text"], message=f"Routed. Correct: {is_correct}"
45
+ )
46
+ else:
47
+ return SupportObservation(
48
+ done=True, reward=reward, ticket_id="None",
49
+ ticket_text="Queue is empty.", message="All tickets routed."
50
+ )
51
+
52
+ @property
53
+ def state(self) -> SupportState:
54
+ 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
+
server/support_env_environment.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Support 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
+
16
+ from openenv.core.env_server.interfaces import Environment
17
+ from openenv.core.env_server.types import State
18
+
19
+ try:
20
+ from ..models import SupportAction, SupportObservation
21
+ except ImportError:
22
+ from models import SupportAction, SupportObservation
23
+
24
+
25
+ class SupportEnvironment(Environment):
26
+ """
27
+ A simple echo environment that echoes back messages.
28
+
29
+ This environment is designed for testing the HTTP server infrastructure.
30
+ It maintains minimal state and simply echoes back whatever message it receives.
31
+
32
+ Example:
33
+ >>> env = SupportEnvironment()
34
+ >>> obs = env.reset()
35
+ >>> print(obs.echoed_message) # "Support Env environment ready!"
36
+ >>>
37
+ >>> obs = env.step(SupportAction(message="Hello"))
38
+ >>> print(obs.echoed_message) # "Hello"
39
+ >>> print(obs.message_length) # 5
40
+ """
41
+
42
+ # Enable concurrent WebSocket sessions.
43
+ # Set to True if your environment isolates state between instances.
44
+ # When True, multiple WebSocket clients can connect simultaneously, each
45
+ # getting their own environment instance (when using factory mode in app.py).
46
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
47
+
48
+ def __init__(self):
49
+ """Initialize the support_env environment."""
50
+ self._state = State(episode_id=str(uuid4()), step_count=0)
51
+ self._reset_count = 0
52
+
53
+ def reset(self) -> SupportObservation:
54
+ """
55
+ Reset the environment.
56
+
57
+ Returns:
58
+ SupportObservation with a ready message
59
+ """
60
+ self._state = State(episode_id=str(uuid4()), step_count=0)
61
+ self._reset_count += 1
62
+
63
+ return SupportObservation(
64
+ echoed_message="Support Env environment ready!",
65
+ message_length=0,
66
+ done=False,
67
+ reward=0.0,
68
+ )
69
+
70
+ def step(self, action: SupportAction) -> SupportObservation: # type: ignore[override]
71
+ """
72
+ Execute a step in the environment by echoing the message.
73
+
74
+ Args:
75
+ action: SupportAction containing the message to echo
76
+
77
+ Returns:
78
+ SupportObservation with the echoed message and its length
79
+ """
80
+ self._state.step_count += 1
81
+
82
+ message = action.message
83
+ length = len(message)
84
+
85
+ # Simple reward: longer messages get higher rewards
86
+ reward = length * 0.1
87
+
88
+ return SupportObservation(
89
+ echoed_message=message,
90
+ message_length=length,
91
+ done=False,
92
+ reward=reward,
93
+ metadata={"original_message": message, "step": self._state.step_count},
94
+ )
95
+
96
+ @property
97
+ def state(self) -> State:
98
+ """
99
+ Get the current environment state.
100
+
101
+ Returns:
102
+ Current State with episode_id and step_count
103
+ """
104
+ return self._state
uv.lock ADDED
The diff for this file is too large to render. See raw diff