fleet-python 0.2.2__py3-none-any.whl → 0.2.3__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 +107 -92
- examples/json_tasks_example.py +82 -0
- examples/nova_act_example.py +18 -169
- examples/openai_example.py +83 -298
- examples/openai_simple_example.py +61 -0
- examples/quickstart.py +5 -5
- fleet/__init__.py +15 -1
- fleet/client.py +18 -3
- fleet/{manager → instance}/__init__.py +4 -1
- fleet/{manager → instance}/client.py +42 -5
- fleet/{manager → instance}/models.py +13 -0
- fleet/playwright.py +291 -0
- fleet/resources/base.py +1 -1
- fleet/resources/browser.py +6 -9
- fleet/resources/sqlite.py +3 -3
- fleet/verifiers/__init__.py +15 -3
- fleet/verifiers/code.py +132 -0
- fleet/verifiers/{database_snapshot.py → db.py} +62 -22
- fleet/verifiers/sql_differ.py +1 -1
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/METADATA +3 -1
- fleet_python-0.2.3.dist-info/RECORD +31 -0
- fleet_python-0.2.2.dist-info/RECORD +0 -27
- /fleet/{manager → instance}/base.py +0 -0
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Fleet SDK Base Environment Classes."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
4
4
|
import asyncio
|
|
5
5
|
import httpx
|
|
6
|
+
import inspect
|
|
6
7
|
import time
|
|
7
8
|
import logging
|
|
8
9
|
from urllib.parse import urlparse
|
|
@@ -11,6 +12,8 @@ from ..resources.sqlite import AsyncSQLiteResource
|
|
|
11
12
|
from ..resources.browser import AsyncBrowserResource
|
|
12
13
|
from ..resources.base import Resource
|
|
13
14
|
|
|
15
|
+
from ..verifiers import DatabaseSnapshot
|
|
16
|
+
|
|
14
17
|
from ..exceptions import FleetEnvironmentError, FleetAPIError
|
|
15
18
|
|
|
16
19
|
from .base import SyncWrapper, AsyncWrapper
|
|
@@ -20,6 +23,8 @@ from .models import (
|
|
|
20
23
|
Resource as ResourceModel,
|
|
21
24
|
ResourceType,
|
|
22
25
|
HealthResponse,
|
|
26
|
+
ExecuteFunctionRequest,
|
|
27
|
+
ExecuteFunctionResponse,
|
|
23
28
|
)
|
|
24
29
|
|
|
25
30
|
|
|
@@ -31,6 +36,11 @@ RESOURCE_TYPES = {
|
|
|
31
36
|
ResourceType.cdp: AsyncBrowserResource,
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
ValidatorType = Callable[
|
|
40
|
+
[DatabaseSnapshot, DatabaseSnapshot, Optional[str]],
|
|
41
|
+
int,
|
|
42
|
+
]
|
|
43
|
+
|
|
34
44
|
|
|
35
45
|
class InstanceClient:
|
|
36
46
|
def __init__(
|
|
@@ -57,7 +67,8 @@ class AsyncInstanceClient:
|
|
|
57
67
|
):
|
|
58
68
|
self.base_url = url
|
|
59
69
|
self.client = AsyncWrapper(
|
|
60
|
-
url=self.base_url,
|
|
70
|
+
url=self.base_url,
|
|
71
|
+
httpx_client=httpx_client or httpx.AsyncClient(timeout=60.0),
|
|
61
72
|
)
|
|
62
73
|
self._resources: Optional[List[ResourceModel]] = None
|
|
63
74
|
self._resources_state: Dict[str, Dict[str, Resource]] = {
|
|
@@ -106,15 +117,41 @@ class AsyncInstanceClient:
|
|
|
106
117
|
for resource in resources_by_name.values()
|
|
107
118
|
]
|
|
108
119
|
|
|
120
|
+
async def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
121
|
+
function_code = inspect.getsource(validator)
|
|
122
|
+
function_name = validator.__name__
|
|
123
|
+
return await self.verify_raw(function_code, function_name)
|
|
124
|
+
|
|
125
|
+
async def verify_raw(
|
|
126
|
+
self, function_code: str, function_name: str
|
|
127
|
+
) -> ExecuteFunctionResponse:
|
|
128
|
+
response = await self.client.request(
|
|
129
|
+
"POST",
|
|
130
|
+
"/execute_verifier_function",
|
|
131
|
+
json=ExecuteFunctionRequest(
|
|
132
|
+
function_code=function_code,
|
|
133
|
+
function_name=function_name,
|
|
134
|
+
).model_dump(),
|
|
135
|
+
)
|
|
136
|
+
return ExecuteFunctionResponse(**response.json())
|
|
137
|
+
|
|
109
138
|
async def _load_resources(self) -> None:
|
|
110
139
|
if self._resources is None:
|
|
111
140
|
response = await self.client.request("GET", "/resources")
|
|
112
141
|
if response.status_code != 200:
|
|
113
142
|
self._resources = []
|
|
114
143
|
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
|
|
145
|
+
# Handle both old and new response formats
|
|
146
|
+
response_data = response.json()
|
|
147
|
+
if isinstance(response_data, dict) and "resources" in response_data:
|
|
148
|
+
# Old format: {"resources": [...]}
|
|
149
|
+
resources_list = response_data["resources"]
|
|
150
|
+
else:
|
|
151
|
+
# New format: [...]
|
|
152
|
+
resources_list = response_data
|
|
153
|
+
|
|
154
|
+
self._resources = [ResourceModel(**resource) for resource in resources_list]
|
|
118
155
|
for resource in self._resources:
|
|
119
156
|
if resource.type not in self._resources_state:
|
|
120
157
|
self._resources_state[resource.type.value] = {}
|
|
@@ -126,3 +126,16 @@ class Resource(BaseModel):
|
|
|
126
126
|
type: ResourceType
|
|
127
127
|
mode: ResourceMode
|
|
128
128
|
label: Optional[str] = Field(None, title="Label")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ExecuteFunctionRequest(BaseModel):
|
|
132
|
+
function_code: str
|
|
133
|
+
function_name: str
|
|
134
|
+
text_solution: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ExecuteFunctionResponse(BaseModel):
|
|
138
|
+
success: bool
|
|
139
|
+
result: Optional[Any] = None
|
|
140
|
+
error: Optional[str] = None
|
|
141
|
+
message: str
|
fleet/playwright.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
from playwright.async_api import async_playwright, Browser, Page
|
|
4
|
+
from .client import AsyncEnvironment
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Key mapping for computer use actions
|
|
8
|
+
CUA_KEY_TO_PLAYWRIGHT_KEY = {
|
|
9
|
+
"/": "Divide",
|
|
10
|
+
"\\": "Backslash",
|
|
11
|
+
"alt": "Alt",
|
|
12
|
+
"arrowdown": "ArrowDown",
|
|
13
|
+
"arrowleft": "ArrowLeft",
|
|
14
|
+
"arrowright": "ArrowRight",
|
|
15
|
+
"arrowup": "ArrowUp",
|
|
16
|
+
"backspace": "Backspace",
|
|
17
|
+
"capslock": "CapsLock",
|
|
18
|
+
"cmd": "Meta",
|
|
19
|
+
"ctrl": "Control",
|
|
20
|
+
"delete": "Delete",
|
|
21
|
+
"end": "End",
|
|
22
|
+
"enter": "Enter",
|
|
23
|
+
"esc": "Escape",
|
|
24
|
+
"home": "Home",
|
|
25
|
+
"insert": "Insert",
|
|
26
|
+
"option": "Alt",
|
|
27
|
+
"pagedown": "PageDown",
|
|
28
|
+
"pageup": "PageUp",
|
|
29
|
+
"shift": "Shift",
|
|
30
|
+
"space": " ",
|
|
31
|
+
"super": "Meta",
|
|
32
|
+
"tab": "Tab",
|
|
33
|
+
"win": "Meta",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FleetPlaywrightWrapper:
|
|
38
|
+
"""
|
|
39
|
+
A wrapper that adds Playwright browser automation to Fleet environment instances.
|
|
40
|
+
|
|
41
|
+
This class handles:
|
|
42
|
+
- Browser connection via CDP
|
|
43
|
+
- Computer actions (click, scroll, type, etc.)
|
|
44
|
+
- Screenshot capture
|
|
45
|
+
- Integration with OpenAI computer use API
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
instance = await fleet.env.make(env_key="hubspot", version="v1.2.7")
|
|
49
|
+
browser = FleetPlaywrightWrapper(instance)
|
|
50
|
+
await browser.start()
|
|
51
|
+
|
|
52
|
+
# Use browser methods
|
|
53
|
+
screenshot = await browser.screenshot()
|
|
54
|
+
tools = [browser.openai_cua_tool]
|
|
55
|
+
|
|
56
|
+
# Clean up when done
|
|
57
|
+
await browser.close()
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def get_environment(self):
|
|
61
|
+
return "browser"
|
|
62
|
+
|
|
63
|
+
def get_dimensions(self):
|
|
64
|
+
return (1920, 1080)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
env: AsyncEnvironment,
|
|
69
|
+
display_width: int = 1920,
|
|
70
|
+
display_height: int = 1080,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Initialize the Fleet Playwright wrapper.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
env: Fleet environment instance
|
|
77
|
+
display_width: Browser viewport width
|
|
78
|
+
display_height: Browser viewport height
|
|
79
|
+
"""
|
|
80
|
+
self.env = env
|
|
81
|
+
self.display_width = display_width
|
|
82
|
+
self.display_height = display_height
|
|
83
|
+
|
|
84
|
+
self._playwright = None
|
|
85
|
+
self._browser: Browser | None = None
|
|
86
|
+
self._page: Page | None = None
|
|
87
|
+
self._started = False
|
|
88
|
+
|
|
89
|
+
async def start(self):
|
|
90
|
+
"""Start the browser and establish connection."""
|
|
91
|
+
if self._started:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Start Playwright
|
|
95
|
+
self._playwright = await async_playwright().start()
|
|
96
|
+
|
|
97
|
+
# Start browser on the Fleet instance
|
|
98
|
+
print("Starting browser...")
|
|
99
|
+
await self.env.browser().start()
|
|
100
|
+
cdp = await self.env.browser().describe()
|
|
101
|
+
|
|
102
|
+
# Connect to browser
|
|
103
|
+
self._browser = await self._playwright.chromium.connect_over_cdp(
|
|
104
|
+
cdp.cdp_browser_url
|
|
105
|
+
)
|
|
106
|
+
self._page = self._browser.contexts[0].pages[0]
|
|
107
|
+
await self._page.set_viewport_size(
|
|
108
|
+
{"width": self.display_width, "height": self.display_height}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._started = True
|
|
112
|
+
print(f"Track agent: {cdp.cdp_devtools_url}")
|
|
113
|
+
|
|
114
|
+
async def close(self):
|
|
115
|
+
"""Close the browser connection."""
|
|
116
|
+
if self._playwright:
|
|
117
|
+
await self._playwright.stop()
|
|
118
|
+
self._playwright = None
|
|
119
|
+
self._browser = None
|
|
120
|
+
self._page = None
|
|
121
|
+
self._started = False
|
|
122
|
+
|
|
123
|
+
def _ensure_started(self):
|
|
124
|
+
"""Ensure browser is started before operations."""
|
|
125
|
+
if not self._started:
|
|
126
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def openai_cua_tool(self) -> Dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Tool definition for OpenAI computer use API.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tool definition dict for use with OpenAI responses API
|
|
135
|
+
"""
|
|
136
|
+
return {
|
|
137
|
+
"type": "computer_use_preview",
|
|
138
|
+
"display_width": self.display_width,
|
|
139
|
+
"display_height": self.display_height,
|
|
140
|
+
"environment": "browser",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async def screenshot(self) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Take a screenshot and return base64 encoded string.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Base64 encoded PNG screenshot
|
|
149
|
+
"""
|
|
150
|
+
self._ensure_started()
|
|
151
|
+
|
|
152
|
+
png_bytes = await self._page.screenshot(full_page=False)
|
|
153
|
+
return base64.b64encode(png_bytes).decode("utf-8")
|
|
154
|
+
|
|
155
|
+
def get_current_url(self) -> str:
|
|
156
|
+
"""Get the current page URL."""
|
|
157
|
+
self._ensure_started()
|
|
158
|
+
return self._page.url
|
|
159
|
+
|
|
160
|
+
async def execute_computer_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Execute a computer action and return the result for OpenAI API.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
action: Computer action dict from OpenAI response
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Result dict for computer_call_output
|
|
169
|
+
"""
|
|
170
|
+
self._ensure_started()
|
|
171
|
+
|
|
172
|
+
action_type = action["type"]
|
|
173
|
+
action_args = {k: v for k, v in action.items() if k != "type"}
|
|
174
|
+
|
|
175
|
+
print(f"Executing: {action_type}({action_args})")
|
|
176
|
+
|
|
177
|
+
# Execute the action
|
|
178
|
+
if hasattr(self, f"_{action_type}"):
|
|
179
|
+
method = getattr(self, f"_{action_type}")
|
|
180
|
+
await method(**action_args)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(f"Unsupported action type: {action_type}")
|
|
183
|
+
|
|
184
|
+
# Take screenshot after action
|
|
185
|
+
screenshot_base64 = await self.screenshot()
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"type": "input_image",
|
|
189
|
+
"image_url": f"data:image/png;base64,{screenshot_base64}",
|
|
190
|
+
"current_url": self.get_current_url(),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Computer action implementations
|
|
194
|
+
async def _click(self, x: int, y: int, button: str = "left") -> None:
|
|
195
|
+
"""Click at coordinates."""
|
|
196
|
+
self._ensure_started()
|
|
197
|
+
await self._page.mouse.click(x, y, button=button)
|
|
198
|
+
|
|
199
|
+
async def _double_click(self, x: int, y: int) -> None:
|
|
200
|
+
"""Double-click at coordinates."""
|
|
201
|
+
self._ensure_started()
|
|
202
|
+
await self._page.mouse.dblclick(x, y)
|
|
203
|
+
|
|
204
|
+
async def _scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
|
205
|
+
"""Scroll from coordinates."""
|
|
206
|
+
self._ensure_started()
|
|
207
|
+
await self._page.mouse.move(x, y)
|
|
208
|
+
await self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
|
|
209
|
+
|
|
210
|
+
async def _type(self, text: str) -> None:
|
|
211
|
+
"""Type text."""
|
|
212
|
+
self._ensure_started()
|
|
213
|
+
await self._page.keyboard.type(text)
|
|
214
|
+
|
|
215
|
+
async def _keypress(self, keys: List[str]) -> None:
|
|
216
|
+
"""Press key combination."""
|
|
217
|
+
self._ensure_started()
|
|
218
|
+
mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
|
|
219
|
+
for key in mapped_keys:
|
|
220
|
+
await self._page.keyboard.down(key)
|
|
221
|
+
for key in reversed(mapped_keys):
|
|
222
|
+
await self._page.keyboard.up(key)
|
|
223
|
+
|
|
224
|
+
async def _move(self, x: int, y: int) -> None:
|
|
225
|
+
"""Move mouse to coordinates."""
|
|
226
|
+
self._ensure_started()
|
|
227
|
+
await self._page.mouse.move(x, y)
|
|
228
|
+
|
|
229
|
+
async def _drag(self, path: List[Dict[str, int]]) -> None:
|
|
230
|
+
"""Drag mouse along path."""
|
|
231
|
+
self._ensure_started()
|
|
232
|
+
if not path:
|
|
233
|
+
return
|
|
234
|
+
await self._page.mouse.move(path[0]["x"], path[0]["y"])
|
|
235
|
+
await self._page.mouse.down()
|
|
236
|
+
for point in path[1:]:
|
|
237
|
+
await self._page.mouse.move(point["x"], point["y"])
|
|
238
|
+
await self._page.mouse.up()
|
|
239
|
+
|
|
240
|
+
async def _wait(self, ms: int = 1000) -> None:
|
|
241
|
+
"""Wait for specified milliseconds."""
|
|
242
|
+
import asyncio
|
|
243
|
+
|
|
244
|
+
await asyncio.sleep(ms / 1000)
|
|
245
|
+
|
|
246
|
+
# Browser-specific actions
|
|
247
|
+
async def _goto(self, url: str) -> None:
|
|
248
|
+
"""Navigate to URL."""
|
|
249
|
+
self._ensure_started()
|
|
250
|
+
try:
|
|
251
|
+
await self._page.goto(url)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"Error navigating to {url}: {e}")
|
|
254
|
+
|
|
255
|
+
async def _back(self) -> None:
|
|
256
|
+
"""Go back in browser history."""
|
|
257
|
+
self._ensure_started()
|
|
258
|
+
await self._page.go_back()
|
|
259
|
+
|
|
260
|
+
async def _forward(self) -> None:
|
|
261
|
+
"""Go forward in browser history."""
|
|
262
|
+
self._ensure_started()
|
|
263
|
+
await self._page.go_forward()
|
|
264
|
+
|
|
265
|
+
async def _refresh(self) -> None:
|
|
266
|
+
"""Refresh the page."""
|
|
267
|
+
self._ensure_started()
|
|
268
|
+
await self._page.reload()
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
# Public aliases (no leading underscore) expected by the Agent &
|
|
272
|
+
# OpenAI computer-use API. They forward directly to the underscored
|
|
273
|
+
# implementations above so the external interface matches the older
|
|
274
|
+
# BasePlaywrightComputer class.
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
# Mouse / keyboard actions
|
|
278
|
+
click = _click
|
|
279
|
+
double_click = _double_click
|
|
280
|
+
scroll = _scroll
|
|
281
|
+
type = _type # noqa: A003 – shadowing built-in for API compatibility
|
|
282
|
+
keypress = _keypress
|
|
283
|
+
move = _move
|
|
284
|
+
drag = _drag
|
|
285
|
+
wait = _wait
|
|
286
|
+
|
|
287
|
+
# Browser navigation actions
|
|
288
|
+
goto = _goto
|
|
289
|
+
back = _back
|
|
290
|
+
forward = _forward
|
|
291
|
+
refresh = _refresh
|
fleet/resources/base.py
CHANGED
fleet/resources/browser.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
|
-
from ..
|
|
2
|
+
from ..instance.models import (
|
|
3
3
|
Resource as ResourceModel,
|
|
4
4
|
CDPDescribeResponse,
|
|
5
5
|
ChromeStartRequest,
|
|
@@ -10,14 +10,13 @@ from .base import Resource
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from ..
|
|
13
|
+
from ..instance.base import AsyncWrapper
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class AsyncBrowserResource(Resource):
|
|
17
17
|
def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
|
|
18
18
|
super().__init__(resource)
|
|
19
19
|
self.client = client
|
|
20
|
-
self._describe: Optional[CDPDescribeResponse] = None
|
|
21
20
|
|
|
22
21
|
async def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
|
|
23
22
|
response = await self.client.request(
|
|
@@ -29,13 +28,11 @@ class AsyncBrowserResource(Resource):
|
|
|
29
28
|
return await self.describe()
|
|
30
29
|
|
|
31
30
|
async def describe(self) -> CDPDescribeResponse:
|
|
32
|
-
|
|
31
|
+
response = await self.client.request("GET", "/resources/cdp/describe")
|
|
32
|
+
if response.status_code != 200:
|
|
33
|
+
await self.start()
|
|
33
34
|
response = await self.client.request("GET", "/resources/cdp/describe")
|
|
34
|
-
|
|
35
|
-
await self.start()
|
|
36
|
-
response = await self.client.request("GET", "/resources/cdp/describe")
|
|
37
|
-
self._describe = CDPDescribeResponse(**response.json())
|
|
38
|
-
return self._describe
|
|
35
|
+
return CDPDescribeResponse(**response.json())
|
|
39
36
|
|
|
40
37
|
async def cdp_url(self) -> str:
|
|
41
38
|
return (await self.describe()).cdp_browser_url
|
fleet/resources/sqlite.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from typing import Any, List, Optional
|
|
2
|
-
from ..
|
|
3
|
-
from ..
|
|
2
|
+
from ..instance.models import Resource as ResourceModel
|
|
3
|
+
from ..instance.models import DescribeResponse, QueryRequest, QueryResponse
|
|
4
4
|
from .base import Resource
|
|
5
5
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from ..
|
|
9
|
+
from ..instance.base import AsyncWrapper
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class AsyncSQLiteResource(Resource):
|
fleet/verifiers/__init__.py
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
1
|
+
from .db import QueryBuilder, DatabaseSnapshot, SnapshotDiff, IgnoreConfig
|
|
2
|
+
from .code import (
|
|
3
|
+
TASK_SUCCESSFUL_SCORE,
|
|
4
|
+
extract_last_assistant_message,
|
|
5
|
+
execute_validation_function,
|
|
6
|
+
)
|
|
3
7
|
|
|
4
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DatabaseSnapshot",
|
|
10
|
+
"QueryBuilder",
|
|
11
|
+
"SnapshotDiff",
|
|
12
|
+
"IgnoreConfig",
|
|
13
|
+
"TASK_SUCCESSFUL_SCORE",
|
|
14
|
+
"extract_last_assistant_message",
|
|
15
|
+
"execute_validation_function",
|
|
16
|
+
]
|
fleet/verifiers/code.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from .db import DatabaseSnapshot, IgnoreConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TASK_SUCCESSFUL_SCORE = 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_last_assistant_message(transcript: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Extract only the last assistant message from the transcript, filtering out tool calls.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
transcript: The full conversation transcript
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The content of the last assistant message with tool calls filtered out
|
|
22
|
+
"""
|
|
23
|
+
if not transcript:
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
# Split transcript into sections by "Assistant:" markers
|
|
27
|
+
sections = transcript.split("Assistant:")
|
|
28
|
+
if len(sections) < 2:
|
|
29
|
+
# No "Assistant:" markers found, treat entire transcript as assistant message
|
|
30
|
+
last_assistant_section = transcript
|
|
31
|
+
else:
|
|
32
|
+
# Get the last assistant section
|
|
33
|
+
last_assistant_section = sections[-1]
|
|
34
|
+
|
|
35
|
+
# Filter out specific content blocks using regex-like approach
|
|
36
|
+
import re
|
|
37
|
+
|
|
38
|
+
# Remove image blocks: <img src="data:..."/>
|
|
39
|
+
last_assistant_section = re.sub(
|
|
40
|
+
r'<img src="data:[^"]*"[^>]*/?>', "", last_assistant_section
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Remove tool call blocks: .../>
|
|
44
|
+
last_assistant_section = re.sub(
|
|
45
|
+
r'<tool_call[^>]*>.*?"/>', "", last_assistant_section, flags=re.DOTALL
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Remove tool result blocks: <tool_result>...</tool_result>
|
|
49
|
+
last_assistant_section = re.sub(
|
|
50
|
+
r"<tool_result>.*?</tool_result>", "", last_assistant_section, flags=re.DOTALL
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Clean up extra whitespace
|
|
54
|
+
filtered_transcript = last_assistant_section.strip()
|
|
55
|
+
|
|
56
|
+
return filtered_transcript
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def execute_validation_function(
|
|
60
|
+
function_code: str,
|
|
61
|
+
function_name: str,
|
|
62
|
+
before_snapshot_path: str,
|
|
63
|
+
after_snapshot_path: str,
|
|
64
|
+
transcript: str | None = None,
|
|
65
|
+
) -> Dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Execute arbitrary validation function code with database snapshots.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
function_code: The Python code containing the function definition
|
|
71
|
+
function_name: Name of the function to call after executing the code
|
|
72
|
+
before_snapshot_path: Path to the before database snapshot
|
|
73
|
+
after_snapshot_path: Path to the after database snapshot
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict containing success status, result, and any error message
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
# Create database snapshots
|
|
80
|
+
before = DatabaseSnapshot(before_snapshot_path)
|
|
81
|
+
after = DatabaseSnapshot(after_snapshot_path)
|
|
82
|
+
|
|
83
|
+
# Create a namespace with the required imports and constants
|
|
84
|
+
namespace = {
|
|
85
|
+
"DatabaseSnapshot": DatabaseSnapshot,
|
|
86
|
+
"IgnoreConfig": IgnoreConfig,
|
|
87
|
+
"TASK_SUCCESSFUL_SCORE": TASK_SUCCESSFUL_SCORE,
|
|
88
|
+
"extract_last_assistant_message": extract_last_assistant_message,
|
|
89
|
+
"__builtins__": __builtins__,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Execute the provided code in the namespace
|
|
93
|
+
exec(function_code, namespace)
|
|
94
|
+
|
|
95
|
+
# Check if the function exists in the namespace
|
|
96
|
+
if function_name not in namespace:
|
|
97
|
+
return {
|
|
98
|
+
"success": False,
|
|
99
|
+
"error": f"Function '{function_name}' not found in the provided code",
|
|
100
|
+
"result": None,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Get the function from the namespace
|
|
104
|
+
func = namespace[function_name]
|
|
105
|
+
|
|
106
|
+
# Call the function with before/after snapshots
|
|
107
|
+
# Support both sync and async functions
|
|
108
|
+
import inspect
|
|
109
|
+
|
|
110
|
+
# Check the function signature to determine how many arguments it accepts
|
|
111
|
+
sig = inspect.signature(func)
|
|
112
|
+
param_count = len(sig.parameters)
|
|
113
|
+
|
|
114
|
+
if inspect.iscoroutinefunction(func):
|
|
115
|
+
# Handle async function - we can await it since we're now async
|
|
116
|
+
if param_count >= 3:
|
|
117
|
+
result = await func(before, after, transcript)
|
|
118
|
+
else:
|
|
119
|
+
result = await func(before, after)
|
|
120
|
+
else:
|
|
121
|
+
# Handle sync function
|
|
122
|
+
if param_count >= 3:
|
|
123
|
+
result = func(before, after, transcript)
|
|
124
|
+
else:
|
|
125
|
+
result = func(before, after)
|
|
126
|
+
|
|
127
|
+
return {"success": True, "result": result, "error": None}
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
error_msg = f"Error executing function: {str(e)}\n{traceback.format_exc()}"
|
|
131
|
+
logger.error(error_msg)
|
|
132
|
+
return {"success": False, "error": error_msg, "result": None}
|