fleet-python 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fleet-python might be problematic. Click here for more details.
- examples/browser_control_example.py +51 -0
- fleet/__init__.py +4 -16
- fleet/base.py +74 -0
- fleet/client.py +115 -279
- fleet/env/__init__.py +3 -25
- fleet/env/base.py +50 -318
- fleet/env/client.py +241 -0
- fleet/env/models.py +127 -0
- fleet/models.py +109 -0
- fleet/resources/base.py +23 -0
- fleet/resources/browser.py +18 -0
- fleet/resources/sqlite.py +41 -0
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/METADATA +2 -1
- fleet_python-0.2.0.dist-info/RECORD +19 -0
- fleet/config.py +0 -125
- fleet/env/factory.py +0 -446
- fleet/facets/__init__.py +0 -7
- fleet/facets/base.py +0 -223
- fleet/facets/factory.py +0 -29
- fleet/manager_client.py +0 -177
- fleet_python-0.1.0.dist-info/RECORD +0 -17
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/WHEEL +0 -0
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/top_level.txt +0 -0
fleet/env/base.py
CHANGED
|
@@ -1,328 +1,60 @@
|
|
|
1
|
-
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
2
3
|
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
import asyncio
|
|
7
|
-
import time
|
|
8
|
-
import logging
|
|
9
|
-
from pydantic import BaseModel, Field
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
from ..manager_client import FleetManagerClient, ManagerHealthResponse
|
|
15
|
-
from ..config import FleetConfig, get_config
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
5
|
+
class BaseWrapper:
|
|
6
|
+
def __init__(self, *, url: str):
|
|
7
|
+
self.url = url
|
|
19
8
|
|
|
9
|
+
def get_headers(self) -> Dict[str, str]:
|
|
10
|
+
headers: Dict[str, str] = {
|
|
11
|
+
"X-Fleet-SDK-Language": "Python",
|
|
12
|
+
"X-Fleet-SDK-Version": "1.0.0",
|
|
13
|
+
}
|
|
14
|
+
return headers
|
|
20
15
|
|
|
21
|
-
class EnvironmentConfig(BaseModel):
|
|
22
|
-
"""Configuration for Fleet environments."""
|
|
23
|
-
|
|
24
|
-
environment_type: str = Field(..., description="Type of environment (e.g., 'chrome-desktop-v1')")
|
|
25
|
-
api_key: Optional[str] = Field(None, description="Fleet API key")
|
|
26
|
-
base_url: str = Field(default="https://fleet.new", description="Fleet API base URL")
|
|
27
|
-
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional configuration")
|
|
28
|
-
|
|
29
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
30
|
-
"""Convert config to dictionary."""
|
|
31
|
-
return self.model_dump()
|
|
32
16
|
|
|
17
|
+
class SyncWrapper(BaseWrapper):
|
|
18
|
+
def __init__(self, *, httpx_client: httpx.Client, **kwargs):
|
|
19
|
+
super().__init__(**kwargs)
|
|
20
|
+
self.httpx_client = httpx_client
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
"""Base class for all Fleet environments."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, config: EnvironmentConfig):
|
|
38
|
-
self.config = config
|
|
39
|
-
self._facets: Dict[str, Facet] = {}
|
|
40
|
-
self._session_id: Optional[str] = None
|
|
41
|
-
self._instance_id: Optional[str] = None
|
|
42
|
-
self._step_count: int = 0
|
|
43
|
-
|
|
44
|
-
@abstractmethod
|
|
45
|
-
async def reset(
|
|
22
|
+
def request(
|
|
46
23
|
self,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
|
|
62
|
-
"""Execute one step in the environment.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
action: The action to execute as a dictionary
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
Tuple of (state, reward, done)
|
|
69
|
-
"""
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
@abstractmethod
|
|
73
|
-
async def close(self) -> None:
|
|
74
|
-
"""Close the environment and clean up resources."""
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
@abstractmethod
|
|
78
|
-
def state(self, facet_uri: str) -> Facet:
|
|
79
|
-
"""Get a facet for accessing environment state.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
facet_uri: URI identifying the facet (e.g., 'sqlite://crm', 'browser://dom')
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
Facet instance for the requested state
|
|
86
|
-
"""
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
@property
|
|
90
|
-
def session_id(self) -> Optional[str]:
|
|
91
|
-
"""Get the current session ID."""
|
|
92
|
-
return self._session_id
|
|
93
|
-
|
|
94
|
-
@property
|
|
95
|
-
def instance_id(self) -> Optional[str]:
|
|
96
|
-
"""Get the current instance ID."""
|
|
97
|
-
return self._instance_id
|
|
98
|
-
|
|
99
|
-
@property
|
|
100
|
-
def step_count(self) -> int:
|
|
101
|
-
"""Get the current step count."""
|
|
102
|
-
return self._step_count
|
|
103
|
-
|
|
104
|
-
def _increment_step(self) -> None:
|
|
105
|
-
"""Increment the step counter."""
|
|
106
|
-
self._step_count += 1
|
|
107
|
-
|
|
108
|
-
def _reset_step_count(self) -> None:
|
|
109
|
-
"""Reset the step counter."""
|
|
110
|
-
self._step_count = 0
|
|
111
|
-
|
|
112
|
-
def _register_facet(self, uri: str, facet: Facet) -> None:
|
|
113
|
-
"""Register a facet for this environment."""
|
|
114
|
-
self._facets[uri] = facet
|
|
115
|
-
|
|
116
|
-
def _get_facet(self, uri: str) -> Optional[Facet]:
|
|
117
|
-
"""Get a registered facet."""
|
|
118
|
-
return self._facets.get(uri)
|
|
24
|
+
method: str,
|
|
25
|
+
path: str,
|
|
26
|
+
params: Optional[Dict[str, Any]] = None,
|
|
27
|
+
json: Optional[Any] = None,
|
|
28
|
+
**kwargs,
|
|
29
|
+
) -> httpx.Response:
|
|
30
|
+
return self.httpx_client.request(
|
|
31
|
+
method,
|
|
32
|
+
f"{self.url}{path}",
|
|
33
|
+
headers=self.get_headers(),
|
|
34
|
+
params=params,
|
|
35
|
+
json=json,
|
|
36
|
+
**kwargs,
|
|
37
|
+
)
|
|
119
38
|
|
|
120
39
|
|
|
121
|
-
class
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# Create Fleet config from environment config
|
|
128
|
-
self._fleet_config = FleetConfig(
|
|
129
|
-
api_key=config.api_key,
|
|
130
|
-
base_url=config.base_url,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Initialize API client
|
|
134
|
-
self._client = FleetAPIClient(self._fleet_config)
|
|
135
|
-
|
|
136
|
-
# Set instance details
|
|
137
|
-
if instance_response:
|
|
138
|
-
self._instance_response = instance_response
|
|
139
|
-
self._instance_id = instance_response.instance_id
|
|
140
|
-
else:
|
|
141
|
-
self._instance_id = instance_id
|
|
142
|
-
self._instance_response = None
|
|
143
|
-
|
|
144
|
-
# Initialize manager client (will be set when instance URLs are available)
|
|
145
|
-
self._manager_client: Optional[FleetManagerClient] = None
|
|
146
|
-
|
|
147
|
-
async def reset(
|
|
40
|
+
class AsyncWrapper(BaseWrapper):
|
|
41
|
+
def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
|
|
42
|
+
super().__init__(**kwargs)
|
|
43
|
+
self.httpx_client = httpx_client
|
|
44
|
+
|
|
45
|
+
async def request(
|
|
148
46
|
self,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"""Execute one step in the environment."""
|
|
164
|
-
if not self._instance_id:
|
|
165
|
-
raise FleetEnvironmentError("Environment not initialized. Call reset() first.")
|
|
166
|
-
|
|
167
|
-
try:
|
|
168
|
-
# Increment step count
|
|
169
|
-
self._increment_step()
|
|
170
|
-
|
|
171
|
-
# Execute action through instance manager API
|
|
172
|
-
# This is a placeholder - actual implementation depends on the manager API spec
|
|
173
|
-
state, reward, done = await self._execute_action(action)
|
|
174
|
-
|
|
175
|
-
return state, reward, done
|
|
176
|
-
|
|
177
|
-
except Exception as e:
|
|
178
|
-
raise FleetEnvironmentError(f"Failed to execute step: {e}")
|
|
179
|
-
|
|
180
|
-
async def close(self) -> None:
|
|
181
|
-
"""Close the environment and clean up resources."""
|
|
182
|
-
try:
|
|
183
|
-
# Delete instance if it exists
|
|
184
|
-
if self._instance_id:
|
|
185
|
-
try:
|
|
186
|
-
await self._client.delete_instance(self._instance_id)
|
|
187
|
-
logger.info(f"Deleted instance: {self._instance_id}")
|
|
188
|
-
except FleetAPIError as e:
|
|
189
|
-
logger.warning(f"Failed to delete instance: {e}")
|
|
190
|
-
finally:
|
|
191
|
-
self._instance_id = None
|
|
192
|
-
self._instance_response = None
|
|
193
|
-
|
|
194
|
-
# Close manager client
|
|
195
|
-
if self._manager_client:
|
|
196
|
-
await self._manager_client.close()
|
|
197
|
-
self._manager_client = None
|
|
198
|
-
|
|
199
|
-
# Close API client
|
|
200
|
-
await self._client.close()
|
|
201
|
-
|
|
202
|
-
except Exception as e:
|
|
203
|
-
logger.error(f"Error closing environment: {e}")
|
|
204
|
-
|
|
205
|
-
def state(self, facet_uri: str) -> Facet:
|
|
206
|
-
"""Get a facet for accessing environment state."""
|
|
207
|
-
# Check if facet is already registered
|
|
208
|
-
facet = self._get_facet(facet_uri)
|
|
209
|
-
if facet:
|
|
210
|
-
return facet
|
|
211
|
-
|
|
212
|
-
# Create new facet based on URI
|
|
213
|
-
from ..facets.factory import create_facet
|
|
214
|
-
facet = create_facet(facet_uri, self)
|
|
215
|
-
self._register_facet(facet_uri, facet)
|
|
216
|
-
return facet
|
|
217
|
-
|
|
218
|
-
async def manager_health_check(self) -> Optional[ManagerHealthResponse]:
|
|
219
|
-
"""Check the health of the manager API.
|
|
220
|
-
|
|
221
|
-
Returns:
|
|
222
|
-
ManagerHealthResponse if manager is available, None otherwise
|
|
223
|
-
"""
|
|
224
|
-
await self._ensure_manager_client()
|
|
225
|
-
if not self._manager_client:
|
|
226
|
-
return None
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
return await self._manager_client.health_check()
|
|
230
|
-
except Exception as e:
|
|
231
|
-
logger.warning(f"Manager health check failed: {e}")
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
async def _ensure_manager_client(self) -> None:
|
|
235
|
-
"""Ensure manager client is initialized if instance URLs are available."""
|
|
236
|
-
if self._manager_client is not None:
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
# Need instance response to get manager URLs
|
|
240
|
-
if not self._instance_response and self._instance_id:
|
|
241
|
-
try:
|
|
242
|
-
self._instance_response = await self._client.get_instance(self._instance_id)
|
|
243
|
-
except Exception as e:
|
|
244
|
-
logger.warning(f"Failed to get instance details for manager client: {e}")
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
if self._instance_response and self._instance_response.urls.manager:
|
|
248
|
-
manager_base_url = self._instance_response.urls.manager.api
|
|
249
|
-
self._manager_client = FleetManagerClient(manager_base_url)
|
|
250
|
-
logger.debug(f"Initialized manager client for {manager_base_url}")
|
|
251
|
-
|
|
252
|
-
async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
|
|
253
|
-
"""Wait for instance to be ready.
|
|
254
|
-
|
|
255
|
-
Args:
|
|
256
|
-
timeout: Maximum time to wait in seconds
|
|
257
|
-
"""
|
|
258
|
-
start_time = time.time()
|
|
259
|
-
|
|
260
|
-
while time.time() - start_time < timeout:
|
|
261
|
-
try:
|
|
262
|
-
instance = await self._client.get_instance(self._instance_id)
|
|
263
|
-
self._instance_response = instance
|
|
264
|
-
|
|
265
|
-
if instance.status == "running":
|
|
266
|
-
logger.info(f"Instance {self._instance_id} is ready")
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
elif instance.status == "error":
|
|
270
|
-
raise FleetEnvironmentError(f"Instance {self._instance_id} failed to start")
|
|
271
|
-
|
|
272
|
-
# Wait before checking again
|
|
273
|
-
await asyncio.sleep(5)
|
|
274
|
-
|
|
275
|
-
except FleetAPIError as e:
|
|
276
|
-
if time.time() - start_time >= timeout:
|
|
277
|
-
raise FleetEnvironmentError(f"Timeout waiting for instance to be ready: {e}")
|
|
278
|
-
await asyncio.sleep(5)
|
|
279
|
-
|
|
280
|
-
raise FleetEnvironmentError(f"Timeout waiting for instance {self._instance_id} to be ready")
|
|
281
|
-
|
|
282
|
-
async def _execute_action(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
|
|
283
|
-
"""Execute an action through the instance manager API.
|
|
284
|
-
|
|
285
|
-
This is a placeholder implementation that should be extended based on
|
|
286
|
-
the actual manager API specification.
|
|
287
|
-
|
|
288
|
-
Args:
|
|
289
|
-
action: The action to execute as a dictionary
|
|
290
|
-
|
|
291
|
-
Returns:
|
|
292
|
-
Tuple of (state, reward, done)
|
|
293
|
-
"""
|
|
294
|
-
# Ensure manager client is available
|
|
295
|
-
await self._ensure_manager_client()
|
|
296
|
-
|
|
297
|
-
# TODO: In the future, this would use the manager API to execute actions
|
|
298
|
-
# For example: await self._manager_client.log_action(action)
|
|
299
|
-
# For now, return placeholder values
|
|
300
|
-
|
|
301
|
-
# Create a placeholder state
|
|
302
|
-
state = self._create_state_from_action(action)
|
|
303
|
-
|
|
304
|
-
# Create a placeholder reward
|
|
305
|
-
reward = 0.0
|
|
306
|
-
|
|
307
|
-
# Determine if episode is done (placeholder logic)
|
|
308
|
-
done = self._step_count >= 100 # Example: done after 100 steps
|
|
309
|
-
|
|
310
|
-
return state, reward, done
|
|
311
|
-
|
|
312
|
-
def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
|
313
|
-
"""Create state based on executed action."""
|
|
314
|
-
return {
|
|
315
|
-
"instance_id": self._instance_id,
|
|
316
|
-
"step": self._step_count,
|
|
317
|
-
"last_action": action,
|
|
318
|
-
"timestamp": time.time(),
|
|
319
|
-
"status": "running"
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async def __aenter__(self):
|
|
323
|
-
"""Async context manager entry."""
|
|
324
|
-
return self
|
|
325
|
-
|
|
326
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
327
|
-
"""Async context manager exit."""
|
|
328
|
-
await self.close()
|
|
47
|
+
method: str,
|
|
48
|
+
path: str,
|
|
49
|
+
params: Optional[Dict[str, Any]] = None,
|
|
50
|
+
json: Optional[Any] = None,
|
|
51
|
+
**kwargs,
|
|
52
|
+
) -> httpx.Response:
|
|
53
|
+
return await self.httpx_client.request(
|
|
54
|
+
method,
|
|
55
|
+
f"{self.url}{path}",
|
|
56
|
+
headers=self.get_headers(),
|
|
57
|
+
params=params,
|
|
58
|
+
json=json,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
fleet/env/client.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Fleet SDK Base Environment Classes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
import asyncio
|
|
5
|
+
import httpx
|
|
6
|
+
import time
|
|
7
|
+
import logging
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from ..resources.sqlite import AsyncSQLiteResource
|
|
11
|
+
from ..resources.browser import AsyncBrowserResource
|
|
12
|
+
from ..resources.base import Resource
|
|
13
|
+
|
|
14
|
+
from ..exceptions import FleetEnvironmentError, FleetAPIError
|
|
15
|
+
|
|
16
|
+
from .base import SyncWrapper, AsyncWrapper
|
|
17
|
+
from .models import (
|
|
18
|
+
ResetResponse,
|
|
19
|
+
Resource as ResourceModel,
|
|
20
|
+
ResourceType,
|
|
21
|
+
HealthResponse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RESOURCE_TYPES = {
|
|
29
|
+
ResourceType.sqlite: AsyncSQLiteResource,
|
|
30
|
+
ResourceType.cdp: AsyncBrowserResource,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Environment:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
url: str,
|
|
38
|
+
httpx_client: Optional[httpx.Client] = None,
|
|
39
|
+
):
|
|
40
|
+
self.base_url = url
|
|
41
|
+
self.client = SyncWrapper(
|
|
42
|
+
url=self.base_url, httpx_client=httpx_client or httpx.Client()
|
|
43
|
+
)
|
|
44
|
+
raise NotImplementedError("SyncManager is not implemented")
|
|
45
|
+
|
|
46
|
+
def reset(self) -> ResetResponse:
|
|
47
|
+
response = self.client.request("POST", "/reset")
|
|
48
|
+
return ResetResponse(**response.json())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AsyncEnvironment:
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
url: str,
|
|
55
|
+
httpx_client: Optional[httpx.AsyncClient] = None,
|
|
56
|
+
):
|
|
57
|
+
self.base_url = url
|
|
58
|
+
self.client = AsyncWrapper(
|
|
59
|
+
url=self.base_url, httpx_client=httpx_client or httpx.AsyncClient()
|
|
60
|
+
)
|
|
61
|
+
self._resources: Optional[List[ResourceModel]] = None
|
|
62
|
+
self._resources_state: Dict[str, Dict[str, Resource]] = {
|
|
63
|
+
resource_type.value: {} for resource_type in ResourceType
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async def load(self) -> None:
|
|
67
|
+
await self._load_resources()
|
|
68
|
+
|
|
69
|
+
async def reset(self) -> ResetResponse:
|
|
70
|
+
response = await self.client.request("POST", "/reset")
|
|
71
|
+
return ResetResponse(**response.json())
|
|
72
|
+
|
|
73
|
+
def state(self, uri: str) -> Resource:
|
|
74
|
+
url = urlparse(uri)
|
|
75
|
+
return self._resources_state[url.scheme][url.netloc]
|
|
76
|
+
|
|
77
|
+
def sqlite(self, name: str) -> AsyncSQLiteResource:
|
|
78
|
+
return AsyncSQLiteResource(
|
|
79
|
+
self._resources_state[ResourceType.sqlite.value][name], self.client
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def browser(self, name: str) -> AsyncBrowserResource:
|
|
83
|
+
return AsyncBrowserResource(
|
|
84
|
+
self._resources_state[ResourceType.cdp.value][name], self.client
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def resources(self) -> List[ResourceModel]:
|
|
88
|
+
await self._load_resources()
|
|
89
|
+
return self._resources
|
|
90
|
+
|
|
91
|
+
async def _load_resources(self) -> None:
|
|
92
|
+
if self._resources is None:
|
|
93
|
+
response = await self.client.request("GET", "/resources")
|
|
94
|
+
if response.status_code != 200:
|
|
95
|
+
self._resources = []
|
|
96
|
+
return
|
|
97
|
+
data = response.json()
|
|
98
|
+
self._resources = [
|
|
99
|
+
ResourceModel(**resource) for resource in data["resources"]
|
|
100
|
+
]
|
|
101
|
+
for resource in self._resources:
|
|
102
|
+
if resource.type not in self._resources_state:
|
|
103
|
+
self._resources_state[resource.type.value] = {}
|
|
104
|
+
self._resources_state[resource.type.value][resource.name] = (
|
|
105
|
+
RESOURCE_TYPES[resource.type](resource, self.client)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
|
|
109
|
+
"""Execute one step in the environment."""
|
|
110
|
+
if not self._instance_id:
|
|
111
|
+
raise FleetEnvironmentError(
|
|
112
|
+
"Environment not initialized. Call reset() first."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Increment step count
|
|
117
|
+
self._increment_step()
|
|
118
|
+
|
|
119
|
+
# Execute action through instance manager API
|
|
120
|
+
# This is a placeholder - actual implementation depends on the manager API spec
|
|
121
|
+
state, reward, done = await self._execute_action(action)
|
|
122
|
+
|
|
123
|
+
return state, reward, done
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise FleetEnvironmentError(f"Failed to execute step: {e}")
|
|
127
|
+
|
|
128
|
+
async def close(self) -> None:
|
|
129
|
+
"""Close the environment and clean up resources."""
|
|
130
|
+
try:
|
|
131
|
+
# Delete instance if it exists
|
|
132
|
+
if self._instance_id:
|
|
133
|
+
try:
|
|
134
|
+
await self._client.delete_instance(self._instance_id)
|
|
135
|
+
logger.info(f"Deleted instance: {self._instance_id}")
|
|
136
|
+
except FleetAPIError as e:
|
|
137
|
+
logger.warning(f"Failed to delete instance: {e}")
|
|
138
|
+
finally:
|
|
139
|
+
self._instance_id = None
|
|
140
|
+
self._instance_response = None
|
|
141
|
+
|
|
142
|
+
# Close manager client
|
|
143
|
+
if self._manager_client:
|
|
144
|
+
await self._manager_client.close()
|
|
145
|
+
self._manager_client = None
|
|
146
|
+
|
|
147
|
+
# Close API client
|
|
148
|
+
await self._client.close()
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Error closing environment: {e}")
|
|
152
|
+
|
|
153
|
+
async def manager_health_check(self) -> Optional[HealthResponse]:
|
|
154
|
+
response = await self.client.request("GET", "/health")
|
|
155
|
+
return HealthResponse(**response.json())
|
|
156
|
+
|
|
157
|
+
async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
|
|
158
|
+
"""Wait for instance to be ready.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
timeout: Maximum time to wait in seconds
|
|
162
|
+
"""
|
|
163
|
+
start_time = time.time()
|
|
164
|
+
|
|
165
|
+
while time.time() - start_time < timeout:
|
|
166
|
+
try:
|
|
167
|
+
instance = await self._client.get_instance(self._instance_id)
|
|
168
|
+
self._instance_response = instance
|
|
169
|
+
|
|
170
|
+
if instance.status == "running":
|
|
171
|
+
logger.info(f"Instance {self._instance_id} is ready")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
elif instance.status == "error":
|
|
175
|
+
raise FleetEnvironmentError(
|
|
176
|
+
f"Instance {self._instance_id} failed to start"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Wait before checking again
|
|
180
|
+
await asyncio.sleep(5)
|
|
181
|
+
|
|
182
|
+
except FleetAPIError as e:
|
|
183
|
+
if time.time() - start_time >= timeout:
|
|
184
|
+
raise FleetEnvironmentError(
|
|
185
|
+
f"Timeout waiting for instance to be ready: {e}"
|
|
186
|
+
)
|
|
187
|
+
await asyncio.sleep(5)
|
|
188
|
+
|
|
189
|
+
raise FleetEnvironmentError(
|
|
190
|
+
f"Timeout waiting for instance {self._instance_id} to be ready"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def _execute_action(
|
|
194
|
+
self, action: Dict[str, Any]
|
|
195
|
+
) -> Tuple[Dict[str, Any], float, bool]:
|
|
196
|
+
"""Execute an action through the instance manager API.
|
|
197
|
+
|
|
198
|
+
This is a placeholder implementation that should be extended based on
|
|
199
|
+
the actual manager API specification.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
action: The action to execute as a dictionary
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (state, reward, done)
|
|
206
|
+
"""
|
|
207
|
+
# Ensure manager client is available
|
|
208
|
+
await self._ensure_manager_client()
|
|
209
|
+
|
|
210
|
+
# TODO: In the future, this would use the manager API to execute actions
|
|
211
|
+
# For example: await self._manager_client.log_action(action)
|
|
212
|
+
# For now, return placeholder values
|
|
213
|
+
|
|
214
|
+
# Create a placeholder state
|
|
215
|
+
state = self._create_state_from_action(action)
|
|
216
|
+
|
|
217
|
+
# Create a placeholder reward
|
|
218
|
+
reward = 0.0
|
|
219
|
+
|
|
220
|
+
# Determine if episode is done (placeholder logic)
|
|
221
|
+
done = self._step_count >= 100 # Example: done after 100 steps
|
|
222
|
+
|
|
223
|
+
return state, reward, done
|
|
224
|
+
|
|
225
|
+
def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
|
226
|
+
"""Create state based on executed action."""
|
|
227
|
+
return {
|
|
228
|
+
"instance_id": self._instance_id,
|
|
229
|
+
"step": self._step_count,
|
|
230
|
+
"last_action": action,
|
|
231
|
+
"timestamp": time.time(),
|
|
232
|
+
"status": "running",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async def __aenter__(self):
|
|
236
|
+
"""Async context manager entry."""
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
240
|
+
"""Async context manager exit."""
|
|
241
|
+
await self.close()
|