fleet-python 0.2.25__py3-none-any.whl → 0.2.27__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/example_task.py CHANGED
@@ -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
- Note: The remote execution environment only supports synchronous verifiers.
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: Remote execution only supports synchronous verifiers.\n")
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
- # Demonstrate that async verifiers can't run remotely
155
- print("Attempting remote execution with async verifier...")
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()
fleet/_async/client.py CHANGED
@@ -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
- verifier=None, # Keep blank for now as requested
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
fleet/_async/models.py CHANGED
@@ -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):
fleet/_async/tasks.py CHANGED
@@ -19,7 +19,10 @@ class Task(BaseModel):
19
19
  key: str = Field(..., description="Unique task key identifier")
20
20
  prompt: str = Field(..., description="Task prompt or instruction")
21
21
  env_id: str = Field(..., description="Environment identifier")
22
+ env_variables: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Environment variables")
22
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")
23
26
  verifier: Optional[Any] = Field(None, description="Verifier function with decorator (async or sync)")
24
27
  metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional task metadata")
25
28
 
@@ -34,6 +37,13 @@ class Task(BaseModel):
34
37
  def set_created_at(cls, v):
35
38
  """Set created_at to current time if not provided."""
36
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
37
47
 
38
48
  class Config:
39
49
  """Pydantic model configuration."""
@@ -41,4 +51,51 @@ class Task(BaseModel):
41
51
  datetime: lambda v: v.isoformat(),
42
52
  }
43
53
  # Allow arbitrary types for the verifier field
44
- arbitrary_types_allowed = True
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] = None # Cached bundle SHA
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
- # Create bundle and cache it
60
- self._bundle_data = self._bundler.create_bundle(
61
- self.func,
62
- self.extra_requirements,
63
- self.verifier_id
64
- )
65
- self._bundle_sha = _get_bundle_sha(self._bundle_data)
66
- logger.debug(f"Created bundle for {self.key} with SHA: {self._bundle_sha}")
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
- if self._is_async:
126
- raise NotImplementedError(
127
- f"Async verifier '{self.key}' cannot be executed remotely. "
128
- "The remote execution environment only supports synchronous functions. "
129
- "Please provide a synchronous version of your verifier."
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, # Still need bundle_data for local caching
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