fleet-python 0.2.0__py3-none-any.whl → 0.2.2__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/dsl_example.py +112 -0
- examples/example.py +38 -0
- examples/nova_act_example.py +180 -0
- examples/openai_example.py +448 -0
- examples/quickstart.py +5 -5
- fleet/__init__.py +24 -3
- fleet/base.py +1 -1
- fleet/client.py +60 -28
- fleet/env/__init__.py +2 -7
- fleet/env/client.py +9 -235
- fleet/manager/__init__.py +22 -0
- fleet/manager/client.py +258 -0
- fleet/{env → manager}/models.py +15 -14
- fleet/resources/base.py +5 -2
- fleet/resources/browser.py +32 -6
- fleet/resources/sqlite.py +5 -5
- fleet/verifiers/__init__.py +4 -0
- fleet/verifiers/database_snapshot.py +666 -0
- fleet/verifiers/sql_differ.py +187 -0
- {fleet_python-0.2.0.dist-info → fleet_python-0.2.2.dist-info}/METADATA +1 -1
- fleet_python-0.2.2.dist-info/RECORD +27 -0
- examples/browser_control_example.py +0 -51
- fleet_python-0.2.0.dist-info/RECORD +0 -19
- /fleet/{env → manager}/base.py +0 -0
- {fleet_python-0.2.0.dist-info → fleet_python-0.2.2.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.0.dist-info → fleet_python-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.0.dist-info → fleet_python-0.2.2.dist-info}/top_level.txt +0 -0
fleet/client.py
CHANGED
|
@@ -20,38 +20,61 @@ import httpx
|
|
|
20
20
|
import logging
|
|
21
21
|
from typing import Optional, List
|
|
22
22
|
|
|
23
|
-
from .base import
|
|
23
|
+
from .base import EnvironmentBase, AsyncWrapper, SyncWrapper
|
|
24
24
|
from .models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
|
|
25
25
|
|
|
26
|
-
from .
|
|
26
|
+
from .manager import InstanceClient, AsyncInstanceClient, ResetRequest, ResetResponse
|
|
27
|
+
from .resources.base import Resource
|
|
28
|
+
from .resources.sqlite import AsyncSQLiteResource
|
|
29
|
+
from .resources.browser import AsyncBrowserResource
|
|
27
30
|
|
|
28
31
|
logger = logging.getLogger(__name__)
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
class
|
|
34
|
+
class Environment(EnvironmentBase):
|
|
32
35
|
def __init__(self, httpx_client: Optional[httpx.Client] = None, **kwargs):
|
|
33
36
|
super().__init__(**kwargs)
|
|
34
37
|
self._httpx_client = httpx_client or httpx.Client()
|
|
35
|
-
self.
|
|
38
|
+
self._instance: Optional[InstanceClient] = None
|
|
36
39
|
|
|
37
40
|
@property
|
|
38
|
-
def
|
|
39
|
-
if self.
|
|
40
|
-
self.
|
|
41
|
-
return self.
|
|
41
|
+
def instance(self) -> InstanceClient:
|
|
42
|
+
if self._instance is None:
|
|
43
|
+
self._instance = InstanceClient(self.manager_url, self._httpx_client)
|
|
44
|
+
return self._instance
|
|
42
45
|
|
|
43
46
|
|
|
44
|
-
class
|
|
47
|
+
class AsyncEnvironment(EnvironmentBase):
|
|
45
48
|
def __init__(self, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
|
|
46
49
|
super().__init__(**kwargs)
|
|
47
50
|
self._httpx_client = httpx_client or httpx.AsyncClient()
|
|
48
|
-
self.
|
|
51
|
+
self._instance: Optional[AsyncInstanceClient] = None
|
|
49
52
|
|
|
50
53
|
@property
|
|
51
|
-
def
|
|
52
|
-
if self.
|
|
53
|
-
self.
|
|
54
|
-
return self.
|
|
54
|
+
def instance(self) -> AsyncInstanceClient:
|
|
55
|
+
if self._instance is None:
|
|
56
|
+
self._instance = AsyncInstanceClient(self.manager_url, self._httpx_client)
|
|
57
|
+
return self._instance
|
|
58
|
+
|
|
59
|
+
async def reset(
|
|
60
|
+
self, seed: Optional[int] = None, timestamp: Optional[int] = None
|
|
61
|
+
) -> ResetResponse:
|
|
62
|
+
return await self.instance.reset(ResetRequest(seed=seed, timestamp=timestamp))
|
|
63
|
+
|
|
64
|
+
def db(self, name: str = "current") -> AsyncSQLiteResource:
|
|
65
|
+
return self.instance.db(name)
|
|
66
|
+
|
|
67
|
+
def browser(self, name: str = "cdp") -> AsyncBrowserResource:
|
|
68
|
+
return self.instance.browser(name)
|
|
69
|
+
|
|
70
|
+
def state(self, uri: str) -> Resource:
|
|
71
|
+
return self.instance.state(uri)
|
|
72
|
+
|
|
73
|
+
async def resources(self) -> List[Resource]:
|
|
74
|
+
return await self.instance.resources()
|
|
75
|
+
|
|
76
|
+
async def close(self) -> InstanceRecord:
|
|
77
|
+
return await AsyncFleet().delete(self.instance_id)
|
|
55
78
|
|
|
56
79
|
|
|
57
80
|
class Fleet:
|
|
@@ -76,23 +99,23 @@ class Fleet:
|
|
|
76
99
|
response = self.client.request("GET", f"/v1/env/{env_key}")
|
|
77
100
|
return EnvironmentModel(**response.json())
|
|
78
101
|
|
|
79
|
-
def make(self, request: InstanceRequest) ->
|
|
102
|
+
def make(self, request: InstanceRequest) -> Environment:
|
|
80
103
|
response = self.client.request(
|
|
81
104
|
"POST", "/v1/env/instances", json=request.model_dump()
|
|
82
105
|
)
|
|
83
|
-
return
|
|
106
|
+
return Environment(**response.json())
|
|
84
107
|
|
|
85
|
-
def instances(self, status: Optional[str] = None) -> List[
|
|
108
|
+
def instances(self, status: Optional[str] = None) -> List[Environment]:
|
|
86
109
|
params = {}
|
|
87
110
|
if status:
|
|
88
111
|
params["status"] = status
|
|
89
112
|
|
|
90
113
|
response = self.client.request("GET", "/v1/env/instances", params=params)
|
|
91
|
-
return [
|
|
114
|
+
return [Environment(**instance_data) for instance_data in response.json()]
|
|
92
115
|
|
|
93
|
-
def instance(self, instance_id: str) ->
|
|
116
|
+
def instance(self, instance_id: str) -> Environment:
|
|
94
117
|
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
95
|
-
return
|
|
118
|
+
return Environment(**response.json())
|
|
96
119
|
|
|
97
120
|
def delete(self, instance_id: str) -> InstanceRecord:
|
|
98
121
|
response = self.client.request("DELETE", f"/v1/env/instances/{instance_id}")
|
|
@@ -121,29 +144,38 @@ class AsyncFleet:
|
|
|
121
144
|
response = await self.client.request("GET", f"/v1/env/{env_key}")
|
|
122
145
|
return EnvironmentModel(**response.json())
|
|
123
146
|
|
|
124
|
-
async def make(self,
|
|
147
|
+
async def make(self, env_key: str) -> AsyncEnvironment:
|
|
148
|
+
if ":" in env_key:
|
|
149
|
+
env_key_part, version = env_key.split(":", 1)
|
|
150
|
+
if not version.startswith("v"):
|
|
151
|
+
version = f"v{version}"
|
|
152
|
+
else:
|
|
153
|
+
env_key_part = env_key
|
|
154
|
+
version = None
|
|
155
|
+
|
|
156
|
+
request = InstanceRequest(env_key=env_key_part, version=version)
|
|
125
157
|
response = await self.client.request(
|
|
126
158
|
"POST", "/v1/env/instances", json=request.model_dump()
|
|
127
159
|
)
|
|
128
|
-
instance =
|
|
129
|
-
await instance.
|
|
160
|
+
instance = AsyncEnvironment(**response.json())
|
|
161
|
+
await instance.instance.load()
|
|
130
162
|
return instance
|
|
131
163
|
|
|
132
|
-
async def instances(self, status: Optional[str] = None) -> List[
|
|
164
|
+
async def instances(self, status: Optional[str] = None) -> List[AsyncEnvironment]:
|
|
133
165
|
params = {}
|
|
134
166
|
if status:
|
|
135
167
|
params["status"] = status
|
|
136
168
|
|
|
137
169
|
response = await self.client.request("GET", "/v1/env/instances", params=params)
|
|
138
170
|
instances = [
|
|
139
|
-
|
|
171
|
+
AsyncEnvironment(**instance_data) for instance_data in response.json()
|
|
140
172
|
]
|
|
141
|
-
await asyncio.gather(*[instance.
|
|
173
|
+
await asyncio.gather(*[instance.instance.load() for instance in instances])
|
|
142
174
|
return instances
|
|
143
175
|
|
|
144
|
-
async def instance(self, instance_id: str) ->
|
|
176
|
+
async def instance(self, instance_id: str) -> AsyncEnvironment:
|
|
145
177
|
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
146
|
-
instance =
|
|
178
|
+
instance = AsyncEnvironment(**response.json())
|
|
147
179
|
await instance.env.load()
|
|
148
180
|
return instance
|
|
149
181
|
|
fleet/env/__init__.py
CHANGED
fleet/env/client.py
CHANGED
|
@@ -1,241 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
from ..client import AsyncFleet, AsyncEnvironment
|
|
2
|
+
from ..models import Environment as EnvironmentModel
|
|
3
|
+
from typing import List
|
|
2
4
|
|
|
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
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from ..resources.base import Resource
|
|
6
|
+
async def make(env_key: str) -> AsyncEnvironment:
|
|
7
|
+
return await AsyncFleet().make(env_key)
|
|
13
8
|
|
|
14
|
-
from ..exceptions import FleetEnvironmentError, FleetAPIError
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
ResetResponse,
|
|
19
|
-
Resource as ResourceModel,
|
|
20
|
-
ResourceType,
|
|
21
|
-
HealthResponse,
|
|
22
|
-
)
|
|
10
|
+
async def list_envs() -> List[EnvironmentModel]:
|
|
11
|
+
return await AsyncFleet().list_envs()
|
|
23
12
|
|
|
24
13
|
|
|
25
|
-
|
|
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()
|
|
14
|
+
async def get(instance_id: str) -> AsyncEnvironment:
|
|
15
|
+
return await AsyncFleet().instance(instance_id)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Fleet SDK Environment Module."""
|
|
2
|
+
|
|
3
|
+
from .client import InstanceClient, AsyncInstanceClient
|
|
4
|
+
from .models import (
|
|
5
|
+
ResetRequest,
|
|
6
|
+
ResetResponse,
|
|
7
|
+
CDPDescribeResponse,
|
|
8
|
+
ChromeStartRequest,
|
|
9
|
+
ChromeStartResponse,
|
|
10
|
+
ChromeStatusResponse,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"InstanceClient",
|
|
15
|
+
"AsyncInstanceClient",
|
|
16
|
+
"ResetRequest",
|
|
17
|
+
"ResetResponse",
|
|
18
|
+
"CDPDescribeResponse",
|
|
19
|
+
"ChromeStartRequest",
|
|
20
|
+
"ChromeStartResponse",
|
|
21
|
+
"ChromeStatusResponse",
|
|
22
|
+
]
|
fleet/manager/client.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
ResetRequest,
|
|
19
|
+
ResetResponse,
|
|
20
|
+
Resource as ResourceModel,
|
|
21
|
+
ResourceType,
|
|
22
|
+
HealthResponse,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
RESOURCE_TYPES = {
|
|
30
|
+
ResourceType.db: AsyncSQLiteResource,
|
|
31
|
+
ResourceType.cdp: AsyncBrowserResource,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InstanceClient:
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
url: str,
|
|
39
|
+
httpx_client: Optional[httpx.Client] = None,
|
|
40
|
+
):
|
|
41
|
+
self.base_url = url
|
|
42
|
+
self.client = SyncWrapper(
|
|
43
|
+
url=self.base_url, httpx_client=httpx_client or httpx.Client()
|
|
44
|
+
)
|
|
45
|
+
raise NotImplementedError("SyncManager is not implemented")
|
|
46
|
+
|
|
47
|
+
def reset(self) -> ResetResponse:
|
|
48
|
+
response = self.client.request("POST", "/reset")
|
|
49
|
+
return ResetResponse(**response.json())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AsyncInstanceClient:
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
url: str,
|
|
56
|
+
httpx_client: Optional[httpx.AsyncClient] = None,
|
|
57
|
+
):
|
|
58
|
+
self.base_url = url
|
|
59
|
+
self.client = AsyncWrapper(
|
|
60
|
+
url=self.base_url, httpx_client=httpx_client or httpx.AsyncClient()
|
|
61
|
+
)
|
|
62
|
+
self._resources: Optional[List[ResourceModel]] = None
|
|
63
|
+
self._resources_state: Dict[str, Dict[str, Resource]] = {
|
|
64
|
+
resource_type.value: {} for resource_type in ResourceType
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async def load(self) -> None:
|
|
68
|
+
await self._load_resources()
|
|
69
|
+
|
|
70
|
+
async def reset(
|
|
71
|
+
self, reset_request: Optional[ResetRequest] = None
|
|
72
|
+
) -> ResetResponse:
|
|
73
|
+
response = await self.client.request(
|
|
74
|
+
"POST", "/reset", json=reset_request.model_dump() if reset_request else None
|
|
75
|
+
)
|
|
76
|
+
return ResetResponse(**response.json())
|
|
77
|
+
|
|
78
|
+
def state(self, uri: str) -> Resource:
|
|
79
|
+
url = urlparse(uri)
|
|
80
|
+
return self._resources_state[url.scheme][url.netloc]
|
|
81
|
+
|
|
82
|
+
def db(self, name: str) -> AsyncSQLiteResource:
|
|
83
|
+
"""
|
|
84
|
+
Returns an AsyncSQLiteResource object for the given SQLite database name.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: The name of the SQLite database to return
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
An AsyncSQLiteResource object for the given SQLite database name
|
|
91
|
+
"""
|
|
92
|
+
return AsyncSQLiteResource(
|
|
93
|
+
self._resources_state[ResourceType.db.value][name], self.client
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def browser(self, name: str) -> AsyncBrowserResource:
|
|
97
|
+
return AsyncBrowserResource(
|
|
98
|
+
self._resources_state[ResourceType.cdp.value][name], self.client
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def resources(self) -> List[Resource]:
|
|
102
|
+
await self._load_resources()
|
|
103
|
+
return [
|
|
104
|
+
resource
|
|
105
|
+
for resources_by_name in self._resources_state.values()
|
|
106
|
+
for resource in resources_by_name.values()
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
async def _load_resources(self) -> None:
|
|
110
|
+
if self._resources is None:
|
|
111
|
+
response = await self.client.request("GET", "/resources")
|
|
112
|
+
if response.status_code != 200:
|
|
113
|
+
self._resources = []
|
|
114
|
+
return
|
|
115
|
+
self._resources = [
|
|
116
|
+
ResourceModel(**resource) for resource in response.json()
|
|
117
|
+
]
|
|
118
|
+
for resource in self._resources:
|
|
119
|
+
if resource.type not in self._resources_state:
|
|
120
|
+
self._resources_state[resource.type.value] = {}
|
|
121
|
+
self._resources_state[resource.type.value][resource.name] = (
|
|
122
|
+
RESOURCE_TYPES[resource.type](resource, self.client)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
|
|
126
|
+
"""Execute one step in the environment."""
|
|
127
|
+
if not self._instance_id:
|
|
128
|
+
raise FleetEnvironmentError(
|
|
129
|
+
"Environment not initialized. Call reset() first."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Increment step count
|
|
134
|
+
self._increment_step()
|
|
135
|
+
|
|
136
|
+
# Execute action through instance manager API
|
|
137
|
+
# This is a placeholder - actual implementation depends on the manager API spec
|
|
138
|
+
state, reward, done = await self._execute_action(action)
|
|
139
|
+
|
|
140
|
+
return state, reward, done
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
raise FleetEnvironmentError(f"Failed to execute step: {e}")
|
|
144
|
+
|
|
145
|
+
async def close(self) -> None:
|
|
146
|
+
"""Close the environment and clean up resources."""
|
|
147
|
+
try:
|
|
148
|
+
# Delete instance if it exists
|
|
149
|
+
if self._instance_id:
|
|
150
|
+
try:
|
|
151
|
+
await self._client.delete_instance(self._instance_id)
|
|
152
|
+
logger.info(f"Deleted instance: {self._instance_id}")
|
|
153
|
+
except FleetAPIError as e:
|
|
154
|
+
logger.warning(f"Failed to delete instance: {e}")
|
|
155
|
+
finally:
|
|
156
|
+
self._instance_id = None
|
|
157
|
+
self._instance_response = None
|
|
158
|
+
|
|
159
|
+
# Close manager client
|
|
160
|
+
if self._manager_client:
|
|
161
|
+
await self._manager_client.close()
|
|
162
|
+
self._manager_client = None
|
|
163
|
+
|
|
164
|
+
# Close API client
|
|
165
|
+
await self._client.close()
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error closing environment: {e}")
|
|
169
|
+
|
|
170
|
+
async def manager_health_check(self) -> Optional[HealthResponse]:
|
|
171
|
+
response = await self.client.request("GET", "/health")
|
|
172
|
+
return HealthResponse(**response.json())
|
|
173
|
+
|
|
174
|
+
async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
|
|
175
|
+
"""Wait for instance to be ready.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
timeout: Maximum time to wait in seconds
|
|
179
|
+
"""
|
|
180
|
+
start_time = time.time()
|
|
181
|
+
|
|
182
|
+
while time.time() - start_time < timeout:
|
|
183
|
+
try:
|
|
184
|
+
instance = await self._client.get_instance(self._instance_id)
|
|
185
|
+
self._instance_response = instance
|
|
186
|
+
|
|
187
|
+
if instance.status == "running":
|
|
188
|
+
logger.info(f"Instance {self._instance_id} is ready")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
elif instance.status == "error":
|
|
192
|
+
raise FleetEnvironmentError(
|
|
193
|
+
f"Instance {self._instance_id} failed to start"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Wait before checking again
|
|
197
|
+
await asyncio.sleep(5)
|
|
198
|
+
|
|
199
|
+
except FleetAPIError as e:
|
|
200
|
+
if time.time() - start_time >= timeout:
|
|
201
|
+
raise FleetEnvironmentError(
|
|
202
|
+
f"Timeout waiting for instance to be ready: {e}"
|
|
203
|
+
)
|
|
204
|
+
await asyncio.sleep(5)
|
|
205
|
+
|
|
206
|
+
raise FleetEnvironmentError(
|
|
207
|
+
f"Timeout waiting for instance {self._instance_id} to be ready"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def _execute_action(
|
|
211
|
+
self, action: Dict[str, Any]
|
|
212
|
+
) -> Tuple[Dict[str, Any], float, bool]:
|
|
213
|
+
"""Execute an action through the instance manager API.
|
|
214
|
+
|
|
215
|
+
This is a placeholder implementation that should be extended based on
|
|
216
|
+
the actual manager API specification.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
action: The action to execute as a dictionary
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Tuple of (state, reward, done)
|
|
223
|
+
"""
|
|
224
|
+
# Ensure manager client is available
|
|
225
|
+
await self._ensure_manager_client()
|
|
226
|
+
|
|
227
|
+
# TODO: In the future, this would use the manager API to execute actions
|
|
228
|
+
# For example: await self._manager_client.log_action(action)
|
|
229
|
+
# For now, return placeholder values
|
|
230
|
+
|
|
231
|
+
# Create a placeholder state
|
|
232
|
+
state = self._create_state_from_action(action)
|
|
233
|
+
|
|
234
|
+
# Create a placeholder reward
|
|
235
|
+
reward = 0.0
|
|
236
|
+
|
|
237
|
+
# Determine if episode is done (placeholder logic)
|
|
238
|
+
done = self._step_count >= 100 # Example: done after 100 steps
|
|
239
|
+
|
|
240
|
+
return state, reward, done
|
|
241
|
+
|
|
242
|
+
def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
+
"""Create state based on executed action."""
|
|
244
|
+
return {
|
|
245
|
+
"instance_id": self._instance_id,
|
|
246
|
+
"step": self._step_count,
|
|
247
|
+
"last_action": action,
|
|
248
|
+
"timestamp": time.time(),
|
|
249
|
+
"status": "running",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async def __aenter__(self):
|
|
253
|
+
"""Async context manager entry."""
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
257
|
+
"""Async context manager exit."""
|
|
258
|
+
await self.close()
|