fleet-python 0.2.26__tar.gz → 0.2.27__tar.gz
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.
- {fleet_python-0.2.26 → fleet_python-0.2.27}/PKG-INFO +5 -4
- {fleet_python-0.2.26 → fleet_python-0.2.27}/README.md +4 -3
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_task.py +10 -6
- fleet_python-0.2.27/examples/example_tasks.py +34 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/client.py +199 -2
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/models.py +16 -0
- fleet_python-0.2.27/fleet/_async/tasks.py +101 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/verifiers/__init__.py +1 -1
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/verifiers/verifier.py +61 -19
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/client.py +205 -2
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/instance/client.py +1 -1
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/models.py +16 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/__init__.py +2 -2
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/parse.py +65 -1
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/verifier.py +63 -21
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet_python.egg-info/PKG-INFO +5 -4
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet_python.egg-info/SOURCES.txt +1 -1
- {fleet_python-0.2.26 → fleet_python-0.2.27}/pyproject.toml +1 -1
- {fleet_python-0.2.26 → fleet_python-0.2.27}/scripts/fix_sync_imports.py +44 -7
- fleet_python-0.2.26/fleet/_async/tasks.py +0 -44
- fleet_python-0.2.26/fleet/tasks.py +0 -44
- {fleet_python-0.2.26 → fleet_python-0.2.27}/LICENSE +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/diff_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_client.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_sync.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/openai_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/examples/quickstart.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/base.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/base.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/config.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/env/client.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/types.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/scripts/unasync.py +0 -0
- {fleet_python-0.2.26 → fleet_python-0.2.27}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleet-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.27
|
|
4
4
|
Summary: Python SDK for Fleet environments
|
|
5
5
|
Author-email: Fleet AI <nic@fleet.so>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -66,6 +66,7 @@ export FLEET_API_KEY="sk_your_key_here"
|
|
|
66
66
|
|
|
67
67
|
```python
|
|
68
68
|
import fleet as flt
|
|
69
|
+
import datetime
|
|
69
70
|
|
|
70
71
|
# Create environment by key
|
|
71
72
|
env = flt.env.make("fira")
|
|
@@ -73,11 +74,11 @@ env = flt.env.make("fira")
|
|
|
73
74
|
# Reset environment with seed and options
|
|
74
75
|
env.reset(
|
|
75
76
|
seed=42,
|
|
76
|
-
timestamp=datetime.now()
|
|
77
|
+
timestamp=int(datetime.datetime.now().timestamp())
|
|
77
78
|
)
|
|
78
79
|
|
|
79
|
-
# Access environment state ('
|
|
80
|
-
sql = env.state("sqlite://
|
|
80
|
+
# Access environment state ('current' is the resource id for a sqlite database)
|
|
81
|
+
sql = env.state("sqlite://current")
|
|
81
82
|
sql.exec("UPDATE customers SET status = 'active' WHERE id = 123")
|
|
82
83
|
|
|
83
84
|
# Clean up
|
|
@@ -24,6 +24,7 @@ export FLEET_API_KEY="sk_your_key_here"
|
|
|
24
24
|
|
|
25
25
|
```python
|
|
26
26
|
import fleet as flt
|
|
27
|
+
import datetime
|
|
27
28
|
|
|
28
29
|
# Create environment by key
|
|
29
30
|
env = flt.env.make("fira")
|
|
@@ -31,11 +32,11 @@ env = flt.env.make("fira")
|
|
|
31
32
|
# Reset environment with seed and options
|
|
32
33
|
env.reset(
|
|
33
34
|
seed=42,
|
|
34
|
-
timestamp=datetime.now()
|
|
35
|
+
timestamp=int(datetime.datetime.now().timestamp())
|
|
35
36
|
)
|
|
36
37
|
|
|
37
|
-
# Access environment state ('
|
|
38
|
-
sql = env.state("sqlite://
|
|
38
|
+
# Access environment state ('current' is the resource id for a sqlite database)
|
|
39
|
+
sql = env.state("sqlite://current")
|
|
39
40
|
sql.exec("UPDATE customers SET status = 'active' WHERE id = 123")
|
|
40
41
|
|
|
41
42
|
# Clean up
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
This example shows how to create a simple task with the @verifier decorator
|
|
5
5
|
that can be verified in a Jira environment.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
For async environments, we need separate sync and async verifiers.
|
|
7
|
+
Both sync and async verifiers are now supported for remote execution.
|
|
9
8
|
"""
|
|
10
9
|
|
|
11
10
|
import os
|
|
@@ -97,7 +96,7 @@ async def create_bug_issue_async(
|
|
|
97
96
|
async def main():
|
|
98
97
|
"""Run the task example."""
|
|
99
98
|
print("=== Fleet Task Example with Jira ===\n")
|
|
100
|
-
print("Note:
|
|
99
|
+
print("Note: Both sync and async verifiers are now supported for remote execution.\n")
|
|
101
100
|
|
|
102
101
|
# Create task using the async verifier for local execution
|
|
103
102
|
task = Task(
|
|
@@ -151,12 +150,17 @@ async def main():
|
|
|
151
150
|
print(f" ✗ Remote execution failed: {e}")
|
|
152
151
|
print()
|
|
153
152
|
|
|
154
|
-
#
|
|
155
|
-
print("
|
|
153
|
+
# Test async verifier remote execution
|
|
154
|
+
print("Testing remote execution with async verifier...")
|
|
156
155
|
try:
|
|
157
|
-
await create_bug_issue_async.remote(env)
|
|
156
|
+
result = await create_bug_issue_async.remote(env, project_key="SCRUM", issue_title="Login button not working")
|
|
157
|
+
print(f" ✓ Async remote check result: {result}")
|
|
158
158
|
except NotImplementedError as e:
|
|
159
159
|
print(f" ✓ Expected error: {e}")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f" ✗ Unexpected error: {e}")
|
|
162
|
+
import traceback
|
|
163
|
+
traceback.print_exc()
|
|
160
164
|
print()
|
|
161
165
|
|
|
162
166
|
# Create the issue
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fleet as flt
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
|
|
4
|
+
load_dotenv()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
client = flt.Fleet()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
env = flt.env.make("fira")
|
|
12
|
+
|
|
13
|
+
tasks = client.load_tasks(env_key="fira")
|
|
14
|
+
print(f"Loaded {len(tasks)} tasks")
|
|
15
|
+
|
|
16
|
+
for i, task in enumerate(tasks):
|
|
17
|
+
print(f"\nTask {i + 1}:")
|
|
18
|
+
print(f" Key: {task.key}")
|
|
19
|
+
print(f" Prompt: {task.prompt[:80]}...")
|
|
20
|
+
print(f" Verifier: {task.verifier_func[:80]}...")
|
|
21
|
+
|
|
22
|
+
print(f" Verifier: {task.verifier.key}")
|
|
23
|
+
print(" Running verifier...")
|
|
24
|
+
try:
|
|
25
|
+
score = task.verify(env)
|
|
26
|
+
print(f" ✓ Score: {score}")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f" ✗ Error: {type(e).__name__}: {e}")
|
|
29
|
+
|
|
30
|
+
print("-" * 60)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import base64
|
|
18
18
|
import cloudpickle
|
|
19
19
|
import httpx
|
|
20
|
+
import json
|
|
20
21
|
import logging
|
|
21
22
|
import os
|
|
22
|
-
from typing import List, Optional, Dict
|
|
23
|
+
from typing import List, Optional, Dict, TYPE_CHECKING
|
|
23
24
|
|
|
24
25
|
from .base import EnvironmentBase, AsyncWrapper
|
|
25
26
|
from ..models import (
|
|
@@ -30,8 +31,13 @@ from ..models import (
|
|
|
30
31
|
VerifiersExecuteResponse,
|
|
31
32
|
TaskListResponse,
|
|
32
33
|
AccountResponse,
|
|
34
|
+
TaskRequest,
|
|
33
35
|
)
|
|
34
36
|
from .tasks import Task
|
|
37
|
+
from ..verifiers.parse import extract_function_name, convert_new_to_old_verifier
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from .verifiers import AsyncVerifierFunction
|
|
35
41
|
|
|
36
42
|
from .instance import (
|
|
37
43
|
AsyncInstanceClient,
|
|
@@ -260,18 +266,112 @@ class AsyncFleet:
|
|
|
260
266
|
# Transform TaskResponse objects to Task objects
|
|
261
267
|
tasks = []
|
|
262
268
|
for task_response in task_list_response.tasks:
|
|
269
|
+
# Create verifier function if verifier data is present
|
|
270
|
+
verifier = None
|
|
271
|
+
verifier_func = task_response.verifier_func
|
|
272
|
+
|
|
273
|
+
if task_response.verifier:
|
|
274
|
+
# Use verifier code from the embedded verifier object
|
|
275
|
+
verifier_func = task_response.verifier.code
|
|
276
|
+
|
|
277
|
+
# Create VerifierFunction from the embedded data
|
|
278
|
+
try:
|
|
279
|
+
verifier = await self._create_verifier_from_data(
|
|
280
|
+
verifier_id=task_response.verifier.verifier_id,
|
|
281
|
+
verifier_key=task_response.verifier.key,
|
|
282
|
+
verifier_code=task_response.verifier.code,
|
|
283
|
+
verifier_sha=task_response.verifier.sha256
|
|
284
|
+
)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning(f"Failed to create verifier {task_response.verifier.key}: {e}")
|
|
287
|
+
|
|
263
288
|
task = Task(
|
|
264
289
|
key=task_response.key,
|
|
265
290
|
prompt=task_response.prompt,
|
|
266
291
|
env_id=task_response.environment_id, # Map environment_id -> env_id
|
|
267
292
|
created_at=task_response.created_at,
|
|
268
|
-
|
|
293
|
+
version=task_response.version,
|
|
294
|
+
env_variables=task_response.env_variables or {},
|
|
295
|
+
verifier_func=verifier_func, # Set verifier code
|
|
296
|
+
verifier=verifier, # Use created verifier or None
|
|
269
297
|
metadata={} # Default empty metadata
|
|
270
298
|
)
|
|
271
299
|
tasks.append(task)
|
|
272
300
|
|
|
273
301
|
return tasks
|
|
274
302
|
|
|
303
|
+
async def export_tasks(self, env_key: Optional[str] = None, filename: Optional[str] = None):
|
|
304
|
+
"""Export tasks for the authenticated team, optionally filtered by environment.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
env_key: Optional environment key to filter tasks by
|
|
308
|
+
filename: Optional filename to write tasks to. If not provided, defaults to 'tasks.json' or 'tasks_{env_key}.json'
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
str: Path to the exported file if tasks were written, None if no tasks found
|
|
312
|
+
"""
|
|
313
|
+
tasks = await self.load_tasks(env_key)
|
|
314
|
+
if tasks:
|
|
315
|
+
# Generate filename if not provided
|
|
316
|
+
if filename is None:
|
|
317
|
+
if env_key:
|
|
318
|
+
filename = f"tasks_{env_key}.json"
|
|
319
|
+
else:
|
|
320
|
+
filename = "tasks.json"
|
|
321
|
+
|
|
322
|
+
# Convert tasks to serializable format
|
|
323
|
+
tasks_data = []
|
|
324
|
+
for task in tasks:
|
|
325
|
+
task_dict = task.model_dump()
|
|
326
|
+
# Remove non-serializable verifier object, keep verifier_func (code string)
|
|
327
|
+
if 'verifier' in task_dict:
|
|
328
|
+
task_dict.pop('verifier')
|
|
329
|
+
tasks_data.append(task_dict)
|
|
330
|
+
|
|
331
|
+
# Write to JSON file
|
|
332
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
333
|
+
json.dump(tasks_data, f, indent=2, default=str)
|
|
334
|
+
|
|
335
|
+
logger.info(f"Exported {len(tasks)} tasks to {filename}")
|
|
336
|
+
return filename
|
|
337
|
+
else:
|
|
338
|
+
logger.info("No tasks found to export")
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
async def import_tasks(self, filename: str):
|
|
342
|
+
"""Import tasks from a JSON file.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
filename: Path to the JSON file of Task objects to import
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List[Task] containing imported Task objects
|
|
349
|
+
"""
|
|
350
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
|
351
|
+
tasks_data = json.load(f)
|
|
352
|
+
|
|
353
|
+
# Create tasks from the loaded data
|
|
354
|
+
tasks = []
|
|
355
|
+
for task_data in tasks_data:
|
|
356
|
+
task = Task(**task_data)
|
|
357
|
+
tasks.append(task)
|
|
358
|
+
|
|
359
|
+
for task in tasks:
|
|
360
|
+
payload = TaskRequest(
|
|
361
|
+
key=task.key,
|
|
362
|
+
prompt=task.prompt,
|
|
363
|
+
environment_id=task.env_id,
|
|
364
|
+
verifier_func=task.verifier_func,
|
|
365
|
+
version=task.version or None,
|
|
366
|
+
env_variables=task.env_variables or {},
|
|
367
|
+
)
|
|
368
|
+
try:
|
|
369
|
+
response = await self.client.request("POST", "/v1/tasks", json=payload.model_dump())
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Failed to import task {task.key}: {e}")
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
|
|
275
375
|
async def account(self) -> AccountResponse:
|
|
276
376
|
"""Get account information including instance limits and usage.
|
|
277
377
|
|
|
@@ -280,6 +380,103 @@ class AsyncFleet:
|
|
|
280
380
|
"""
|
|
281
381
|
response = await self.client.request("GET", "/v1/account")
|
|
282
382
|
return AccountResponse(**response.json())
|
|
383
|
+
|
|
384
|
+
async def _create_verifier_from_data(
|
|
385
|
+
self,
|
|
386
|
+
verifier_id: str,
|
|
387
|
+
verifier_key: str,
|
|
388
|
+
verifier_code: str,
|
|
389
|
+
verifier_sha: str
|
|
390
|
+
) -> "AsyncVerifierFunction":
|
|
391
|
+
"""Create an AsyncVerifierFunction from verifier data.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
verifier_id: The verifier ID
|
|
395
|
+
verifier_key: The verifier key
|
|
396
|
+
verifier_code: The verifier code
|
|
397
|
+
verifier_sha: The verifier SHA256
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
AsyncVerifierFunction created from the verifier code
|
|
401
|
+
"""
|
|
402
|
+
from .verifiers.verifier import AsyncVerifierFunction
|
|
403
|
+
|
|
404
|
+
# Check if this is a new format verifier (has before/after parameters)
|
|
405
|
+
if 'before: DatabaseSnapshot' in verifier_code and 'after: DatabaseSnapshot' in verifier_code:
|
|
406
|
+
# Convert new format to old format
|
|
407
|
+
verifier_code = convert_new_to_old_verifier(verifier_code)
|
|
408
|
+
# Update function name since wrapper adds _wrapper suffix
|
|
409
|
+
original_name = extract_function_name(verifier_code.split('\n')[0])
|
|
410
|
+
if original_name and original_name.endswith('_wrapper'):
|
|
411
|
+
function_name = original_name
|
|
412
|
+
else:
|
|
413
|
+
function_name = extract_function_name(verifier_code)
|
|
414
|
+
else:
|
|
415
|
+
# Extract function name from code
|
|
416
|
+
function_name = extract_function_name(verifier_code)
|
|
417
|
+
|
|
418
|
+
if not function_name:
|
|
419
|
+
raise ValueError(f"Could not extract function name from verifier {verifier_id}")
|
|
420
|
+
|
|
421
|
+
# Create a function object from the code
|
|
422
|
+
# Import necessary classes for the namespace
|
|
423
|
+
from ..verifiers.db import IgnoreConfig, DatabaseSnapshot
|
|
424
|
+
|
|
425
|
+
# Create a namespace for the function
|
|
426
|
+
namespace = {
|
|
427
|
+
'__builtins__': __builtins__,
|
|
428
|
+
'Environment': object, # Placeholder, will be provided at runtime
|
|
429
|
+
'IgnoreConfig': IgnoreConfig,
|
|
430
|
+
'DatabaseSnapshot': DatabaseSnapshot,
|
|
431
|
+
'TASK_FAILED_SCORE': 0,
|
|
432
|
+
'TASK_SUCCESSFUL_SCORE': 1,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Execute the code to define the function
|
|
436
|
+
exec(verifier_code, namespace)
|
|
437
|
+
|
|
438
|
+
# Get the function object
|
|
439
|
+
if function_name not in namespace:
|
|
440
|
+
raise ValueError(f"Function {function_name} not found in verifier code")
|
|
441
|
+
|
|
442
|
+
func = namespace[function_name]
|
|
443
|
+
|
|
444
|
+
# Create and return AsyncVerifierFunction with SHA if available
|
|
445
|
+
# Since the function was created via exec, we need to provide the raw code
|
|
446
|
+
verifier_func = AsyncVerifierFunction(
|
|
447
|
+
func=func,
|
|
448
|
+
key=verifier_key,
|
|
449
|
+
extra_requirements=[],
|
|
450
|
+
verifier_id=verifier_id,
|
|
451
|
+
sha256=verifier_sha, # Pass SHA directly to constructor
|
|
452
|
+
raw_code=verifier_code # Provide raw code since func won't have extractable source
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Store the original verifier code for reference
|
|
456
|
+
verifier_func._verifier_code = verifier_code
|
|
457
|
+
|
|
458
|
+
return verifier_func
|
|
459
|
+
|
|
460
|
+
async def _load_verifier(self, verifier_id: str) -> "AsyncVerifierFunction":
|
|
461
|
+
"""Load a verifier by ID and create an AsyncVerifierFunction.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
verifier_id: The verifier ID to fetch
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
AsyncVerifierFunction created from the verifier code
|
|
468
|
+
"""
|
|
469
|
+
# Fetch verifier from API
|
|
470
|
+
response = await self.client.request("GET", f"/v1/verifier/{verifier_id}")
|
|
471
|
+
verifier_data = response.json()
|
|
472
|
+
|
|
473
|
+
# Use the common method to create verifier
|
|
474
|
+
return await self._create_verifier_from_data(
|
|
475
|
+
verifier_id=verifier_id,
|
|
476
|
+
verifier_key=verifier_data["key"],
|
|
477
|
+
verifier_code=verifier_data["code"],
|
|
478
|
+
verifier_sha=verifier_data.get("sha256", "")
|
|
479
|
+
)
|
|
283
480
|
|
|
284
481
|
|
|
285
482
|
# Shared
|
|
@@ -152,6 +152,18 @@ class TaskRequest(BaseModel):
|
|
|
152
152
|
prompt: str = Field(..., title='Prompt')
|
|
153
153
|
environment_id: str = Field(..., title='Environment Id')
|
|
154
154
|
verifier_id: Optional[str] = Field(None, title='Verifier Id')
|
|
155
|
+
version: Optional[str] = Field(None, title='Version')
|
|
156
|
+
env_variables: Optional[Dict[str, Any]] = Field(None, title='Env Variables')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class VerifierData(BaseModel):
|
|
160
|
+
verifier_id: str = Field(..., title='Verifier Id')
|
|
161
|
+
key: str = Field(..., title='Key')
|
|
162
|
+
version: int = Field(..., title='Version')
|
|
163
|
+
sha256: str = Field(..., title='Sha256')
|
|
164
|
+
code: str = Field(..., title='Code')
|
|
165
|
+
comment: Optional[str] = Field(None, title='Comment')
|
|
166
|
+
created_at: str = Field(..., title='Created At')
|
|
155
167
|
|
|
156
168
|
|
|
157
169
|
class TaskResponse(BaseModel):
|
|
@@ -161,6 +173,10 @@ class TaskResponse(BaseModel):
|
|
|
161
173
|
environment_id: str = Field(..., title='Environment Id')
|
|
162
174
|
created_at: str = Field(..., title='Created At')
|
|
163
175
|
verifier_id: Optional[str] = Field(None, title='Verifier Id')
|
|
176
|
+
verifier_func: Optional[str] = Field(None, title='Verifier Func')
|
|
177
|
+
version: Optional[str] = Field(None, title='Version')
|
|
178
|
+
env_variables: Optional[Dict[str, Any]] = Field(None, title='Env Variables')
|
|
179
|
+
verifier: Optional[VerifierData] = Field(None, title='Verifier')
|
|
164
180
|
|
|
165
181
|
|
|
166
182
|
class ValidationError(BaseModel):
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Fleet SDK Task Model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, validator
|
|
11
|
+
|
|
12
|
+
# Import the shared VerifierFunction type that works for both async and sync
|
|
13
|
+
from fleet.types import VerifierFunction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Task(BaseModel):
|
|
17
|
+
"""A task model representing a single task in the Fleet system."""
|
|
18
|
+
|
|
19
|
+
key: str = Field(..., description="Unique task key identifier")
|
|
20
|
+
prompt: str = Field(..., description="Task prompt or instruction")
|
|
21
|
+
env_id: str = Field(..., description="Environment identifier")
|
|
22
|
+
env_variables: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Environment variables")
|
|
23
|
+
created_at: Optional[datetime] = Field(None, description="Task creation timestamp")
|
|
24
|
+
version: Optional[str] = Field(None, description="Task version")
|
|
25
|
+
verifier_func: Optional[str] = Field(None, description="Verifier function code")
|
|
26
|
+
verifier: Optional[Any] = Field(None, description="Verifier function with decorator (async or sync)")
|
|
27
|
+
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional task metadata")
|
|
28
|
+
|
|
29
|
+
@validator('key')
|
|
30
|
+
def validate_key_format(cls, v):
|
|
31
|
+
"""Validate key follows kebab-case format."""
|
|
32
|
+
if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', v):
|
|
33
|
+
raise ValueError(f'Invalid task key format: {v}. Must follow kebab-case format.')
|
|
34
|
+
return v
|
|
35
|
+
|
|
36
|
+
@validator('created_at', pre=True, always=True)
|
|
37
|
+
def set_created_at(cls, v):
|
|
38
|
+
"""Set created_at to current time if not provided."""
|
|
39
|
+
return v or datetime.now()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def env_key(self) -> str:
|
|
43
|
+
"""Get the environment key combining env_id and version."""
|
|
44
|
+
if self.version:
|
|
45
|
+
return f"{self.env_id}:{self.version}"
|
|
46
|
+
return self.env_id
|
|
47
|
+
|
|
48
|
+
class Config:
|
|
49
|
+
"""Pydantic model configuration."""
|
|
50
|
+
json_encoders = {
|
|
51
|
+
datetime: lambda v: v.isoformat(),
|
|
52
|
+
}
|
|
53
|
+
# Allow arbitrary types for the verifier field
|
|
54
|
+
arbitrary_types_allowed = True
|
|
55
|
+
|
|
56
|
+
def verify(self, env, *args, **kwargs) -> float:
|
|
57
|
+
"""Verify the task using the verifier function (sync version).
|
|
58
|
+
|
|
59
|
+
For sync environments, calls the sync verifier directly.
|
|
60
|
+
For async verifiers, automatically runs them with asyncio.run().
|
|
61
|
+
"""
|
|
62
|
+
if self.verifier:
|
|
63
|
+
import asyncio
|
|
64
|
+
import inspect
|
|
65
|
+
|
|
66
|
+
result = self.verifier.remote(env, *args, **kwargs)
|
|
67
|
+
|
|
68
|
+
# If the result is a coroutine, we need to run it
|
|
69
|
+
if inspect.iscoroutine(result):
|
|
70
|
+
# Check if we're already in an event loop
|
|
71
|
+
try:
|
|
72
|
+
loop = asyncio.get_running_loop()
|
|
73
|
+
# We're in an async context, can't use asyncio.run()
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"Cannot run async verifier in sync mode while event loop is running. "
|
|
76
|
+
"Use await task.verify_async() instead."
|
|
77
|
+
)
|
|
78
|
+
except RuntimeError:
|
|
79
|
+
# No event loop running, safe to use asyncio.run()
|
|
80
|
+
return asyncio.run(result)
|
|
81
|
+
else:
|
|
82
|
+
return result
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError("No verifier function found for this task")
|
|
85
|
+
|
|
86
|
+
async def verify_async(self, *args, **kwargs) -> float:
|
|
87
|
+
"""Verify the task using the verifier function (async version).
|
|
88
|
+
|
|
89
|
+
For async environments, awaits the async verifier.
|
|
90
|
+
Works with both sync and async verifiers in async contexts.
|
|
91
|
+
"""
|
|
92
|
+
if self.verifier:
|
|
93
|
+
result = self.verifier.remote(*args, **kwargs)
|
|
94
|
+
# If it's a coroutine, await it
|
|
95
|
+
import inspect
|
|
96
|
+
if inspect.iscoroutine(result):
|
|
97
|
+
return await result
|
|
98
|
+
else:
|
|
99
|
+
return result
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError("No verifier function found for this task")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Fleet verifiers module - database snapshot validation utilities and verifier decorator."""
|
|
2
2
|
|
|
3
3
|
from fleet.verifiers.db import DatabaseSnapshot, IgnoreConfig, SnapshotDiff
|
|
4
|
-
from fleet.verifiers.code import TASK_SUCCESSFUL_SCORE
|
|
4
|
+
from fleet.verifiers.code import TASK_SUCCESSFUL_SCORE, TASK_FAILED_SCORE
|
|
5
5
|
from .verifier import (
|
|
6
6
|
verifier,
|
|
7
7
|
AsyncVerifierFunction,
|
|
@@ -39,15 +39,18 @@ class AsyncVerifierFunction:
|
|
|
39
39
|
func: F,
|
|
40
40
|
key: str,
|
|
41
41
|
extra_requirements: Optional[List[str]] = None,
|
|
42
|
-
verifier_id: Optional[str] = None
|
|
42
|
+
verifier_id: Optional[str] = None,
|
|
43
|
+
sha256: Optional[str] = None,
|
|
44
|
+
raw_code: Optional[str] = None
|
|
43
45
|
):
|
|
44
46
|
self.func = func
|
|
45
47
|
self.key = key
|
|
46
48
|
self.verifier_id = verifier_id or str(uuid.uuid4())
|
|
47
49
|
self.extra_requirements = extra_requirements or []
|
|
48
50
|
self._bundler = FunctionBundler()
|
|
49
|
-
self._bundle_sha: Optional[str] =
|
|
51
|
+
self._bundle_sha: Optional[str] = sha256 # Use provided SHA if available
|
|
50
52
|
self._bundle_data: Optional[bytes] = None # Cached bundle data
|
|
53
|
+
self._raw_code: Optional[str] = raw_code # Store raw code if provided
|
|
51
54
|
self._is_async = asyncio.iscoroutinefunction(func)
|
|
52
55
|
|
|
53
56
|
# Copy function metadata
|
|
@@ -56,14 +59,40 @@ class AsyncVerifierFunction:
|
|
|
56
59
|
def _get_or_create_bundle(self) -> tuple[bytes, str]:
|
|
57
60
|
"""Get or create bundle data and return (bundle_data, sha)."""
|
|
58
61
|
if self._bundle_data is None or self._bundle_sha is None:
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
# If we have raw code, create a bundle from it
|
|
63
|
+
if self._raw_code:
|
|
64
|
+
import io
|
|
65
|
+
import zipfile
|
|
66
|
+
|
|
67
|
+
# Create zip bundle directly (matching bundler format)
|
|
68
|
+
zip_buffer = io.BytesIO()
|
|
69
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
70
|
+
# Add requirements.txt
|
|
71
|
+
requirements = self.extra_requirements or []
|
|
72
|
+
if "fleet-python" not in requirements:
|
|
73
|
+
requirements.append("fleet-python")
|
|
74
|
+
req_content = "\n".join(requirements)
|
|
75
|
+
zf.writestr("requirements.txt", req_content)
|
|
76
|
+
|
|
77
|
+
# Add verifier.py with the raw code
|
|
78
|
+
zf.writestr("verifier.py", self._raw_code)
|
|
79
|
+
|
|
80
|
+
self._bundle_data = zip_buffer.getvalue()
|
|
81
|
+
self._bundle_sha = _get_bundle_sha(self._bundle_data)
|
|
82
|
+
logger.debug(f"Created bundle from raw code for {self.key} with SHA: {self._bundle_sha}")
|
|
83
|
+
else:
|
|
84
|
+
# Try to create bundle from function source
|
|
85
|
+
try:
|
|
86
|
+
self._bundle_data = self._bundler.create_bundle(
|
|
87
|
+
self.func,
|
|
88
|
+
self.extra_requirements,
|
|
89
|
+
self.verifier_id
|
|
90
|
+
)
|
|
91
|
+
self._bundle_sha = _get_bundle_sha(self._bundle_data)
|
|
92
|
+
logger.debug(f"Created bundle for {self.key} with SHA: {self._bundle_sha}")
|
|
93
|
+
except OSError as e:
|
|
94
|
+
# Can't create bundle - no source and no raw code
|
|
95
|
+
raise OSError(f"Cannot create bundle for {self.key}: {e}")
|
|
67
96
|
|
|
68
97
|
return self._bundle_data, self._bundle_sha
|
|
69
98
|
|
|
@@ -71,6 +100,11 @@ class AsyncVerifierFunction:
|
|
|
71
100
|
"""Check if bundle needs to be uploaded and return (sha, needs_upload)."""
|
|
72
101
|
bundle_data, bundle_sha = self._get_or_create_bundle()
|
|
73
102
|
|
|
103
|
+
# If bundle_data is empty, we're using server-side bundle
|
|
104
|
+
if not bundle_data:
|
|
105
|
+
logger.debug(f"Using server-side bundle {bundle_sha[:8]}...")
|
|
106
|
+
return bundle_sha, False # No upload needed, server has it
|
|
107
|
+
|
|
74
108
|
# 1. Check local process cache first
|
|
75
109
|
if bundle_sha in _uploaded_bundle_shas:
|
|
76
110
|
logger.debug(f"Bundle {bundle_sha[:8]}... found in local cache")
|
|
@@ -122,12 +156,13 @@ class AsyncVerifierFunction:
|
|
|
122
156
|
|
|
123
157
|
async def remote(self, env: AsyncEnv, *args, **kwargs) -> float:
|
|
124
158
|
"""Remote execution of the verifier function with SHA-based bundle caching."""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
# Async verifiers are now supported by the backend
|
|
160
|
+
# if self._is_async:
|
|
161
|
+
# raise NotImplementedError(
|
|
162
|
+
# f"Async verifier '{self.key}' cannot be executed remotely. "
|
|
163
|
+
# "The remote execution environment only supports synchronous functions. "
|
|
164
|
+
# "Please provide a synchronous version of your verifier."
|
|
165
|
+
# )
|
|
131
166
|
|
|
132
167
|
args_array = list(args)
|
|
133
168
|
args_array.append({"env": env.instance_id})
|
|
@@ -163,11 +198,12 @@ class AsyncVerifierFunction:
|
|
|
163
198
|
bundle_data, _ = self._get_or_create_bundle()
|
|
164
199
|
|
|
165
200
|
response = await env.execute_verifier_remote(
|
|
166
|
-
bundle_data=bundle_data, #
|
|
201
|
+
bundle_data=bundle_data or b'', # Empty if using server-side bundle
|
|
167
202
|
bundle_sha=bundle_sha,
|
|
168
203
|
key=self.key,
|
|
169
204
|
function_name=self.func.__name__,
|
|
170
205
|
args=args,
|
|
206
|
+
args_array=args_array,
|
|
171
207
|
kwargs=kwargs,
|
|
172
208
|
needs_upload=False # Don't upload, just execute
|
|
173
209
|
)
|
|
@@ -249,7 +285,9 @@ Remote traceback:
|
|
|
249
285
|
|
|
250
286
|
def verifier(
|
|
251
287
|
key: Optional[str] = None,
|
|
252
|
-
extra_requirements: Optional[List[str]] = None
|
|
288
|
+
extra_requirements: Optional[List[str]] = None,
|
|
289
|
+
sha256: Optional[str] = None,
|
|
290
|
+
raw_code: Optional[str] = None
|
|
253
291
|
) -> Callable[[F], AsyncVerifierFunction]:
|
|
254
292
|
"""
|
|
255
293
|
Decorator to create a verifier function with env-first pattern.
|
|
@@ -261,6 +299,8 @@ def verifier(
|
|
|
261
299
|
Args:
|
|
262
300
|
key: Optional key for the verifier. Defaults to function name.
|
|
263
301
|
extra_requirements: Additional PyPI packages needed by the verifier.
|
|
302
|
+
sha256: Optional SHA256 hash of existing server-side bundle to use.
|
|
303
|
+
raw_code: Optional raw code to use as bundle (bypasses source extraction).
|
|
264
304
|
|
|
265
305
|
Example:
|
|
266
306
|
# Synchronous verifier (works locally and remotely)
|
|
@@ -298,7 +338,9 @@ def verifier(
|
|
|
298
338
|
func,
|
|
299
339
|
verifier_key,
|
|
300
340
|
extra_requirements,
|
|
301
|
-
verifier_uuid
|
|
341
|
+
verifier_uuid,
|
|
342
|
+
sha256,
|
|
343
|
+
raw_code
|
|
302
344
|
)
|
|
303
345
|
|
|
304
346
|
return decorator
|