fleet-python 0.2.29__py3-none-any.whl → 0.2.34__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/diff_example.py +30 -20
- examples/dsl_example.py +12 -7
- examples/example.py +4 -4
- examples/exampleResume.py +191 -0
- examples/example_account.py +8 -0
- examples/example_action_log.py +2 -2
- examples/example_client.py +2 -2
- examples/example_mcp_anthropic.py +8 -5
- examples/example_mcp_openai.py +2 -2
- examples/example_sync.py +4 -4
- examples/example_task.py +16 -6
- examples/example_tasks.py +3 -6
- examples/example_verifier.py +16 -3
- examples/gemini_example.py +6 -6
- examples/json_tasks_example.py +2 -2
- examples/nova_act_example.py +2 -2
- examples/openai_example.py +3 -3
- examples/openai_simple_example.py +3 -3
- examples/query_builder_example.py +11 -7
- examples/test_cdp_logging.py +80 -0
- fleet/__init__.py +60 -5
- fleet/_async/__init__.py +258 -1
- fleet/_async/base.py +2 -1
- fleet/_async/client.py +164 -144
- fleet/_async/env/client.py +2 -0
- fleet/_async/global_client.py +43 -0
- fleet/_async/instance/client.py +1 -1
- fleet/_async/models.py +172 -171
- fleet/_async/resources/base.py +1 -1
- fleet/_async/resources/mcp.py +55 -0
- fleet/_async/resources/sqlite.py +141 -130
- fleet/_async/tasks.py +69 -16
- fleet/_async/verifiers/__init__.py +2 -2
- fleet/_async/verifiers/bundler.py +18 -14
- fleet/_async/verifiers/verifier.py +77 -71
- fleet/base.py +2 -1
- fleet/client.py +162 -148
- fleet/config.py +3 -2
- fleet/env/__init__.py +9 -1
- fleet/env/client.py +4 -1
- fleet/global_client.py +43 -0
- fleet/instance/__init__.py +1 -1
- fleet/instance/client.py +1 -1
- fleet/models.py +172 -171
- fleet/resources/base.py +1 -1
- fleet/resources/mcp.py +11 -16
- fleet/resources/sqlite.py +141 -130
- fleet/tasks.py +86 -15
- fleet/types.py +1 -1
- fleet/verifiers/__init__.py +2 -2
- fleet/verifiers/bundler.py +18 -14
- fleet/verifiers/code.py +1 -1
- fleet/verifiers/decorator.py +25 -34
- fleet/verifiers/parse.py +98 -68
- fleet/verifiers/verifier.py +77 -71
- {fleet_python-0.2.29.dist-info → fleet_python-0.2.34.dist-info}/METADATA +9 -9
- fleet_python-0.2.34.dist-info/RECORD +76 -0
- scripts/fix_sync_imports.py +87 -59
- scripts/unasync.py +10 -9
- fleet_python-0.2.29.dist-info/RECORD +0 -70
- {fleet_python-0.2.29.dist-info → fleet_python-0.2.34.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.29.dist-info → fleet_python-0.2.34.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.29.dist-info → fleet_python-0.2.34.dist-info}/top_level.txt +0 -0
fleet/_async/tasks.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
7
|
+
from typing import Any, Dict, Optional, List
|
|
8
8
|
from uuid import UUID
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel, Field, validator
|
|
@@ -15,31 +15,39 @@ from fleet.types import VerifierFunction
|
|
|
15
15
|
|
|
16
16
|
class Task(BaseModel):
|
|
17
17
|
"""A task model representing a single task in the Fleet system."""
|
|
18
|
-
|
|
18
|
+
|
|
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(
|
|
22
|
+
env_variables: Optional[Dict[str, Any]] = Field(
|
|
23
|
+
default_factory=dict, description="Environment variables"
|
|
24
|
+
)
|
|
23
25
|
created_at: Optional[datetime] = Field(None, description="Task creation timestamp")
|
|
24
26
|
version: Optional[str] = Field(None, description="Task version")
|
|
25
27
|
verifier_func: Optional[str] = Field(None, description="Verifier function code")
|
|
26
|
-
verifier: Optional[Any] = Field(
|
|
28
|
+
verifier: Optional[Any] = Field(
|
|
29
|
+
None, description="Verifier function with decorator (async or sync)"
|
|
30
|
+
)
|
|
27
31
|
verifier_id: Optional[str] = Field(None, description="Verifier identifier")
|
|
28
32
|
verifier_sha: Optional[str] = Field(None, description="Verifier SHA256 hash")
|
|
29
|
-
metadata: Optional[Dict[str, Any]] = Field(
|
|
33
|
+
metadata: Optional[Dict[str, Any]] = Field(
|
|
34
|
+
default_factory=dict, description="Additional task metadata"
|
|
35
|
+
)
|
|
30
36
|
|
|
31
|
-
@validator(
|
|
37
|
+
@validator("key")
|
|
32
38
|
def validate_key_format(cls, v):
|
|
33
39
|
"""Validate key follows kebab-case format."""
|
|
34
|
-
if not re.match(r
|
|
35
|
-
raise ValueError(
|
|
40
|
+
if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", v):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Invalid task key format: {v}. Must follow kebab-case format."
|
|
43
|
+
)
|
|
36
44
|
return v
|
|
37
45
|
|
|
38
|
-
@validator(
|
|
46
|
+
@validator("created_at", pre=True, always=True)
|
|
39
47
|
def set_created_at(cls, v):
|
|
40
48
|
"""Set created_at to current time if not provided."""
|
|
41
49
|
return v or datetime.now()
|
|
42
|
-
|
|
50
|
+
|
|
43
51
|
@property
|
|
44
52
|
def env_key(self) -> str:
|
|
45
53
|
"""Get the environment key combining env_id and version."""
|
|
@@ -49,24 +57,25 @@ class Task(BaseModel):
|
|
|
49
57
|
|
|
50
58
|
class Config:
|
|
51
59
|
"""Pydantic model configuration."""
|
|
60
|
+
|
|
52
61
|
json_encoders = {
|
|
53
62
|
datetime: lambda v: v.isoformat(),
|
|
54
63
|
}
|
|
55
64
|
# Allow arbitrary types for the verifier field
|
|
56
|
-
arbitrary_types_allowed = True
|
|
65
|
+
arbitrary_types_allowed = True
|
|
57
66
|
|
|
58
67
|
def verify(self, env, *args, **kwargs) -> float:
|
|
59
68
|
"""Verify the task using the verifier function (sync version).
|
|
60
|
-
|
|
69
|
+
|
|
61
70
|
For sync environments, calls the sync verifier directly.
|
|
62
71
|
For async verifiers, automatically runs them with asyncio.run().
|
|
63
72
|
"""
|
|
64
73
|
if self.verifier:
|
|
65
74
|
import asyncio
|
|
66
75
|
import inspect
|
|
67
|
-
|
|
76
|
+
|
|
68
77
|
result = self.verifier.remote(env, *args, **kwargs)
|
|
69
|
-
|
|
78
|
+
|
|
70
79
|
# If the result is a coroutine, we need to run it
|
|
71
80
|
if inspect.iscoroutine(result):
|
|
72
81
|
# Check if we're already in an event loop
|
|
@@ -84,10 +93,10 @@ class Task(BaseModel):
|
|
|
84
93
|
return result
|
|
85
94
|
else:
|
|
86
95
|
raise ValueError("No verifier function found for this task")
|
|
87
|
-
|
|
96
|
+
|
|
88
97
|
async def verify_async(self, *args, **kwargs) -> float:
|
|
89
98
|
"""Verify the task using the verifier function (async version).
|
|
90
|
-
|
|
99
|
+
|
|
91
100
|
For async environments, awaits the async verifier.
|
|
92
101
|
Works with both sync and async verifiers in async contexts.
|
|
93
102
|
"""
|
|
@@ -95,9 +104,53 @@ class Task(BaseModel):
|
|
|
95
104
|
result = self.verifier.remote(*args, **kwargs)
|
|
96
105
|
# If it's a coroutine, await it
|
|
97
106
|
import inspect
|
|
107
|
+
|
|
98
108
|
if inspect.iscoroutine(result):
|
|
99
109
|
return await result
|
|
100
110
|
else:
|
|
101
111
|
return result
|
|
102
112
|
else:
|
|
103
113
|
raise ValueError("No verifier function found for this task")
|
|
114
|
+
|
|
115
|
+
async def make_env(self, region: Optional[str] = None):
|
|
116
|
+
"""Create an environment instance for this task's environment.
|
|
117
|
+
|
|
118
|
+
Uses the task's env_id (and version if present) to create the env.
|
|
119
|
+
"""
|
|
120
|
+
if not self.env_id:
|
|
121
|
+
raise ValueError("Task has no env_id defined")
|
|
122
|
+
# Deferred import to avoid circular dependencies
|
|
123
|
+
from .client import AsyncFleet
|
|
124
|
+
|
|
125
|
+
return await AsyncFleet().make(env_key=self.env_key, region=region)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def load_tasks(
|
|
129
|
+
env_key: Optional[str] = None,
|
|
130
|
+
keys: Optional[List[str]] = None,
|
|
131
|
+
version: Optional[str] = None,
|
|
132
|
+
team_id: Optional[str] = None
|
|
133
|
+
) -> List[Task]:
|
|
134
|
+
"""Convenience function to load tasks with optional filtering.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
env_key: Optional environment key to filter tasks by
|
|
138
|
+
keys: Optional list of task keys to filter by
|
|
139
|
+
version: Optional version to filter tasks by
|
|
140
|
+
team_id: Optional team_id to filter by (admin only)
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
tasks = await fleet.load_tasks(env_key="fira")
|
|
144
|
+
tasks = await fleet.load_tasks(keys=["task1", "task2"])
|
|
145
|
+
tasks = await fleet.load_tasks(env_key="fira", version="v1.0")
|
|
146
|
+
"""
|
|
147
|
+
# Use the global client by default so users can pre-configure it once
|
|
148
|
+
from .global_client import get_client
|
|
149
|
+
|
|
150
|
+
client = get_client()
|
|
151
|
+
return await client.load_tasks(
|
|
152
|
+
env_key=env_key,
|
|
153
|
+
keys=keys,
|
|
154
|
+
version=version,
|
|
155
|
+
team_id=team_id
|
|
156
|
+
)
|
|
@@ -544,7 +544,9 @@ class FunctionBundler:
|
|
|
544
544
|
# Ensure fleet-python is always included
|
|
545
545
|
if not requirements:
|
|
546
546
|
requirements = ["fleet-python"]
|
|
547
|
-
elif "fleet-python" not in [
|
|
547
|
+
elif "fleet-python" not in [
|
|
548
|
+
r.split("==")[0].split(">=")[0] for r in requirements
|
|
549
|
+
]:
|
|
548
550
|
requirements.append("fleet-python")
|
|
549
551
|
requirements_file.write_text("\n".join(sorted(set(requirements))))
|
|
550
552
|
|
|
@@ -663,37 +665,39 @@ class FunctionBundler:
|
|
|
663
665
|
logger.warning(f"Failed to extract function {function_name}: {e}")
|
|
664
666
|
|
|
665
667
|
return None
|
|
666
|
-
|
|
668
|
+
|
|
667
669
|
def _get_function_source_without_decorator(self, func: Callable) -> str:
|
|
668
670
|
"""Get function source code without the @verifier decorator."""
|
|
669
671
|
source = inspect.getsource(func)
|
|
670
|
-
lines = source.split(
|
|
671
|
-
|
|
672
|
+
lines = source.split("\n")
|
|
673
|
+
|
|
672
674
|
# Find where the function definition starts
|
|
673
675
|
func_start = -1
|
|
674
676
|
for i, line in enumerate(lines):
|
|
675
|
-
if line.strip().startswith(
|
|
677
|
+
if line.strip().startswith("def "):
|
|
676
678
|
func_start = i
|
|
677
679
|
break
|
|
678
|
-
|
|
680
|
+
|
|
679
681
|
if func_start == -1:
|
|
680
682
|
# Couldn't find function definition, return original
|
|
681
683
|
return source
|
|
682
|
-
|
|
684
|
+
|
|
683
685
|
# Return only from the function definition onward
|
|
684
686
|
func_lines = lines[func_start:]
|
|
685
|
-
|
|
687
|
+
|
|
686
688
|
# Remove common indentation
|
|
687
689
|
if func_lines:
|
|
688
690
|
# Find minimum indentation (excluding empty lines)
|
|
689
|
-
min_indent = float(
|
|
691
|
+
min_indent = float("inf")
|
|
690
692
|
for line in func_lines:
|
|
691
693
|
if line.strip():
|
|
692
694
|
indent = len(line) - len(line.lstrip())
|
|
693
695
|
min_indent = min(min_indent, indent)
|
|
694
|
-
|
|
696
|
+
|
|
695
697
|
# Remove the common indentation
|
|
696
|
-
if min_indent < float(
|
|
697
|
-
func_lines = [
|
|
698
|
-
|
|
699
|
-
|
|
698
|
+
if min_indent < float("inf"):
|
|
699
|
+
func_lines = [
|
|
700
|
+
line[min_indent:] if line.strip() else line for line in func_lines
|
|
701
|
+
]
|
|
702
|
+
|
|
703
|
+
return "\n".join(func_lines)
|
|
@@ -19,7 +19,7 @@ from ..client import AsyncEnv
|
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
|
-
F = TypeVar(
|
|
22
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
23
23
|
|
|
24
24
|
# Global cache to track which bundle SHAs have been uploaded to S3
|
|
25
25
|
_uploaded_bundle_shas: Set[str] = set()
|
|
@@ -33,7 +33,7 @@ def _get_bundle_sha(bundle_data: bytes) -> str:
|
|
|
33
33
|
|
|
34
34
|
class AsyncVerifierFunction:
|
|
35
35
|
"""Wrapper for a verified function that supports local execution with env-first pattern."""
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
def __init__(
|
|
38
38
|
self,
|
|
39
39
|
func: F,
|
|
@@ -41,7 +41,7 @@ class AsyncVerifierFunction:
|
|
|
41
41
|
extra_requirements: Optional[List[str]] = None,
|
|
42
42
|
verifier_id: Optional[str] = None,
|
|
43
43
|
sha256: Optional[str] = None,
|
|
44
|
-
raw_code: Optional[str] = None
|
|
44
|
+
raw_code: Optional[str] = None,
|
|
45
45
|
):
|
|
46
46
|
self.func = func
|
|
47
47
|
self.key = key
|
|
@@ -52,10 +52,10 @@ class AsyncVerifierFunction:
|
|
|
52
52
|
self._bundle_data: Optional[bytes] = None # Cached bundle data
|
|
53
53
|
self._raw_code: Optional[str] = raw_code # Store raw code if provided
|
|
54
54
|
self._is_async = asyncio.iscoroutinefunction(func)
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
# Copy function metadata
|
|
57
57
|
functools.update_wrapper(self, func)
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
def _get_or_create_bundle(self) -> tuple[bytes, str]:
|
|
60
60
|
"""Get or create bundle data and return (bundle_data, sha)."""
|
|
61
61
|
if self._bundle_data is None or self._bundle_sha is None:
|
|
@@ -63,68 +63,72 @@ class AsyncVerifierFunction:
|
|
|
63
63
|
if self._raw_code:
|
|
64
64
|
import io
|
|
65
65
|
import zipfile
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
# Create zip bundle directly (matching bundler format)
|
|
68
68
|
zip_buffer = io.BytesIO()
|
|
69
|
-
with zipfile.ZipFile(zip_buffer,
|
|
69
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
70
70
|
# Add requirements.txt
|
|
71
71
|
requirements = self.extra_requirements or []
|
|
72
72
|
if "fleet-python" not in requirements:
|
|
73
73
|
requirements.append("fleet-python")
|
|
74
74
|
req_content = "\n".join(requirements)
|
|
75
75
|
zf.writestr("requirements.txt", req_content)
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
# Add verifier.py with the raw code
|
|
78
78
|
zf.writestr("verifier.py", self._raw_code)
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
self._bundle_data = zip_buffer.getvalue()
|
|
81
81
|
self._bundle_sha = _get_bundle_sha(self._bundle_data)
|
|
82
|
-
logger.debug(
|
|
82
|
+
logger.debug(
|
|
83
|
+
f"Created bundle from raw code for {self.key} with SHA: {self._bundle_sha}"
|
|
84
|
+
)
|
|
83
85
|
else:
|
|
84
86
|
# Try to create bundle from function source
|
|
85
87
|
try:
|
|
86
88
|
self._bundle_data = self._bundler.create_bundle(
|
|
87
|
-
self.func,
|
|
88
|
-
self.extra_requirements,
|
|
89
|
-
self.verifier_id
|
|
89
|
+
self.func, self.extra_requirements, self.verifier_id
|
|
90
90
|
)
|
|
91
91
|
self._bundle_sha = _get_bundle_sha(self._bundle_data)
|
|
92
|
-
logger.debug(
|
|
92
|
+
logger.debug(
|
|
93
|
+
f"Created bundle for {self.key} with SHA: {self._bundle_sha}"
|
|
94
|
+
)
|
|
93
95
|
except OSError as e:
|
|
94
96
|
# Can't create bundle - no source and no raw code
|
|
95
97
|
raise OSError(f"Cannot create bundle for {self.key}: {e}")
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
return self._bundle_data, self._bundle_sha
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
async def _check_bundle_status(self, env: AsyncEnv) -> tuple[str, bool]:
|
|
100
102
|
"""Check if bundle needs to be uploaded and return (sha, needs_upload)."""
|
|
101
103
|
bundle_data, bundle_sha = self._get_or_create_bundle()
|
|
102
|
-
|
|
104
|
+
|
|
103
105
|
# If bundle_data is empty, we're using server-side bundle
|
|
104
106
|
if not bundle_data:
|
|
105
107
|
logger.debug(f"Using server-side bundle {bundle_sha[:8]}...")
|
|
106
108
|
return bundle_sha, False # No upload needed, server has it
|
|
107
|
-
|
|
109
|
+
|
|
108
110
|
# 1. Check local process cache first
|
|
109
111
|
if bundle_sha in _uploaded_bundle_shas:
|
|
110
112
|
logger.debug(f"Bundle {bundle_sha[:8]}... found in local cache")
|
|
111
113
|
return bundle_sha, False # Already uploaded, no upload needed
|
|
112
|
-
|
|
114
|
+
|
|
113
115
|
# 2. Check if bundle exists on server (pseudocode)
|
|
114
116
|
# TODO: Add endpoint to check if bundle SHA exists in S3
|
|
115
117
|
try:
|
|
116
118
|
exists = await env.check_bundle_exists(bundle_sha)
|
|
117
119
|
if exists.success:
|
|
118
|
-
logger.info(
|
|
120
|
+
logger.info(
|
|
121
|
+
f"Bundle {bundle_sha[:8]}... found on server, updating cache"
|
|
122
|
+
)
|
|
119
123
|
_uploaded_bundle_shas.add(bundle_sha)
|
|
120
124
|
return bundle_sha, False # Found on server, no upload needed
|
|
121
125
|
except Exception as e:
|
|
122
126
|
logger.warning(f"Failed to check bundle existence: {e}")
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
# 3. Bundle not found locally or on server - upload needed
|
|
125
129
|
logger.info(f"Bundle {bundle_sha[:8]}... needs to be uploaded")
|
|
126
130
|
return bundle_sha, True # Upload needed
|
|
127
|
-
|
|
131
|
+
|
|
128
132
|
async def __call__(self, env: AsyncEnv, *args, **kwargs) -> float:
|
|
129
133
|
"""Local execution of the verifier function with env as first parameter."""
|
|
130
134
|
try:
|
|
@@ -134,7 +138,7 @@ class AsyncVerifierFunction:
|
|
|
134
138
|
else:
|
|
135
139
|
# For sync functions, call directly
|
|
136
140
|
result = self.func(env, *args, **kwargs)
|
|
137
|
-
|
|
141
|
+
|
|
138
142
|
# Handle different return types
|
|
139
143
|
if isinstance(result, (int, float)):
|
|
140
144
|
# Direct score return
|
|
@@ -144,16 +148,18 @@ class AsyncVerifierFunction:
|
|
|
144
148
|
return result
|
|
145
149
|
else:
|
|
146
150
|
# Try to extract score from object attributes
|
|
147
|
-
if hasattr(result,
|
|
151
|
+
if hasattr(result, "score"):
|
|
148
152
|
return float(result.score)
|
|
149
153
|
else:
|
|
150
|
-
raise ValueError(
|
|
151
|
-
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Verifier function must return a score (number). Got {type(result)}"
|
|
156
|
+
)
|
|
157
|
+
|
|
152
158
|
except Exception as e:
|
|
153
159
|
logger.error(f"Error in verifier {self.key}: {e}")
|
|
154
160
|
# Return error score 0
|
|
155
161
|
return 0.0
|
|
156
|
-
|
|
162
|
+
|
|
157
163
|
async def remote(self, env: AsyncEnv, *args, **kwargs) -> float:
|
|
158
164
|
"""Remote execution of the verifier function with SHA-based bundle caching."""
|
|
159
165
|
# Async verifiers are now supported by the backend
|
|
@@ -163,20 +169,20 @@ class AsyncVerifierFunction:
|
|
|
163
169
|
# "The remote execution environment only supports synchronous functions. "
|
|
164
170
|
# "Please provide a synchronous version of your verifier."
|
|
165
171
|
# )
|
|
166
|
-
|
|
172
|
+
|
|
167
173
|
args_array = list(args)
|
|
168
174
|
args_array.append({"env": env.instance_id})
|
|
169
175
|
args = tuple(args_array)
|
|
170
|
-
|
|
176
|
+
|
|
171
177
|
try:
|
|
172
178
|
# Check if bundle needs to be uploaded
|
|
173
179
|
bundle_sha, needs_upload = await self._check_bundle_status(env)
|
|
174
|
-
|
|
180
|
+
|
|
175
181
|
if needs_upload:
|
|
176
182
|
# Need to upload bundle to S3
|
|
177
183
|
logger.info(f"Uploading bundle {bundle_sha[:8]}... for {self.key}")
|
|
178
184
|
bundle_data, _ = self._get_or_create_bundle()
|
|
179
|
-
|
|
185
|
+
|
|
180
186
|
response = await env.execute_verifier_remote(
|
|
181
187
|
bundle_data=bundle_data,
|
|
182
188
|
bundle_sha=bundle_sha,
|
|
@@ -185,42 +191,46 @@ class AsyncVerifierFunction:
|
|
|
185
191
|
args=args,
|
|
186
192
|
args_array=args_array,
|
|
187
193
|
kwargs=kwargs,
|
|
188
|
-
needs_upload=True
|
|
194
|
+
needs_upload=True,
|
|
189
195
|
)
|
|
190
|
-
|
|
196
|
+
|
|
191
197
|
# Mark as uploaded after successful execution
|
|
192
198
|
_uploaded_bundle_shas.add(bundle_sha)
|
|
193
199
|
logger.debug(f"Registered bundle {bundle_sha[:8]}... as uploaded")
|
|
194
|
-
|
|
200
|
+
|
|
195
201
|
else:
|
|
196
202
|
# Bundle already available - execute without upload
|
|
197
|
-
logger.info(
|
|
203
|
+
logger.info(
|
|
204
|
+
f"Executing cached bundle {bundle_sha[:8]}... for {self.key}"
|
|
205
|
+
)
|
|
198
206
|
bundle_data, _ = self._get_or_create_bundle()
|
|
199
|
-
|
|
207
|
+
|
|
200
208
|
response = await env.execute_verifier_remote(
|
|
201
|
-
bundle_data=bundle_data or b
|
|
209
|
+
bundle_data=bundle_data or b"", # Empty if using server-side bundle
|
|
202
210
|
bundle_sha=bundle_sha,
|
|
203
211
|
key=self.key,
|
|
204
212
|
function_name=self.func.__name__,
|
|
205
213
|
args=args,
|
|
206
214
|
args_array=args_array,
|
|
207
215
|
kwargs=kwargs,
|
|
208
|
-
needs_upload=False # Don't upload, just execute
|
|
216
|
+
needs_upload=False, # Don't upload, just execute
|
|
209
217
|
)
|
|
210
|
-
|
|
218
|
+
|
|
211
219
|
# Handle response
|
|
220
|
+
if response.stdout:
|
|
221
|
+
print(response.stdout)
|
|
212
222
|
if response.success:
|
|
213
223
|
return self._process_result(response.result)
|
|
214
224
|
else:
|
|
215
225
|
self._raise_remote_error(response.error)
|
|
216
|
-
|
|
226
|
+
|
|
217
227
|
except Exception as e:
|
|
218
228
|
logger.error(f"Remote execution failed for {self.key}: {e}")
|
|
219
229
|
# If it's an HTTP error, try to get more details
|
|
220
|
-
if hasattr(e,
|
|
230
|
+
if hasattr(e, "response") and hasattr(e.response, "text"):
|
|
221
231
|
logger.error(f"Server response: {e.response.text}")
|
|
222
232
|
raise
|
|
223
|
-
|
|
233
|
+
|
|
224
234
|
def _process_result(self, result: Any) -> float:
|
|
225
235
|
"""Process remote execution result, handling different return types."""
|
|
226
236
|
# Handle different return types like local execution
|
|
@@ -230,7 +240,7 @@ class AsyncVerifierFunction:
|
|
|
230
240
|
return float(result["score"])
|
|
231
241
|
else:
|
|
232
242
|
# Try to extract score from object attributes
|
|
233
|
-
if hasattr(result,
|
|
243
|
+
if hasattr(result, "score"):
|
|
234
244
|
return float(result.score)
|
|
235
245
|
else:
|
|
236
246
|
# Best effort conversion
|
|
@@ -239,13 +249,13 @@ class AsyncVerifierFunction:
|
|
|
239
249
|
except (ValueError, TypeError):
|
|
240
250
|
logger.warning(f"Could not convert result to float: {result}")
|
|
241
251
|
return 0.0
|
|
242
|
-
|
|
252
|
+
|
|
243
253
|
def _raise_remote_error(self, error_info: Dict[str, Any]):
|
|
244
254
|
"""Reconstruct remote error as local exception."""
|
|
245
255
|
error_type = error_info.get("type", "RuntimeError")
|
|
246
256
|
message = error_info.get("message", "Remote execution failed")
|
|
247
257
|
traceback_str = error_info.get("traceback", "")
|
|
248
|
-
|
|
258
|
+
|
|
249
259
|
# Create a rich error message
|
|
250
260
|
full_message = f"""
|
|
251
261
|
Remote verifier execution failed:
|
|
@@ -254,32 +264,32 @@ Remote verifier execution failed:
|
|
|
254
264
|
Remote traceback:
|
|
255
265
|
{traceback_str}
|
|
256
266
|
""".strip()
|
|
257
|
-
|
|
267
|
+
|
|
258
268
|
# Try to raise the original exception type
|
|
259
269
|
try:
|
|
260
270
|
exception_class = getattr(__builtins__, error_type, RuntimeError)
|
|
261
271
|
raise exception_class(full_message)
|
|
262
272
|
except:
|
|
263
273
|
raise RuntimeError(full_message)
|
|
264
|
-
|
|
274
|
+
|
|
265
275
|
def _get_env_id(self, env: AsyncEnv) -> str:
|
|
266
276
|
"""Generate a unique identifier for the environment."""
|
|
267
277
|
# Use instance base URL or similar unique identifier
|
|
268
|
-
if hasattr(env,
|
|
278
|
+
if hasattr(env, "instance") and hasattr(env.instance, "base_url"):
|
|
269
279
|
return f"{env.instance.base_url}"
|
|
270
280
|
else:
|
|
271
281
|
# Fallback to object id (less ideal but works)
|
|
272
282
|
return str(id(env))
|
|
273
|
-
|
|
283
|
+
|
|
274
284
|
def _is_bundle_not_found_error(self, error: Exception) -> bool:
|
|
275
285
|
"""Check if the error indicates the bundle was not found on the server."""
|
|
276
286
|
# Check for common "bundle not found" error patterns
|
|
277
287
|
error_msg = str(error).lower()
|
|
278
288
|
return (
|
|
279
|
-
"bundle not found" in error_msg
|
|
280
|
-
"verifier not found" in error_msg
|
|
281
|
-
"404" in error_msg
|
|
282
|
-
"not found" in error_msg
|
|
289
|
+
"bundle not found" in error_msg
|
|
290
|
+
or "verifier not found" in error_msg
|
|
291
|
+
or "404" in error_msg
|
|
292
|
+
or "not found" in error_msg
|
|
283
293
|
)
|
|
284
294
|
|
|
285
295
|
|
|
@@ -287,21 +297,21 @@ def verifier(
|
|
|
287
297
|
key: Optional[str] = None,
|
|
288
298
|
extra_requirements: Optional[List[str]] = None,
|
|
289
299
|
sha256: Optional[str] = None,
|
|
290
|
-
raw_code: Optional[str] = None
|
|
300
|
+
raw_code: Optional[str] = None,
|
|
291
301
|
) -> Callable[[F], AsyncVerifierFunction]:
|
|
292
302
|
"""
|
|
293
303
|
Decorator to create a verifier function with env-first pattern.
|
|
294
|
-
|
|
304
|
+
|
|
295
305
|
The decorated function must take 'env' as its first parameter, making it explicit
|
|
296
306
|
that verifiers operate within an environment context. This makes verifiers reusable
|
|
297
307
|
across different environments.
|
|
298
|
-
|
|
308
|
+
|
|
299
309
|
Args:
|
|
300
310
|
key: Optional key for the verifier. Defaults to function name.
|
|
301
311
|
extra_requirements: Additional PyPI packages needed by the verifier.
|
|
302
312
|
sha256: Optional SHA256 hash of existing server-side bundle to use.
|
|
303
313
|
raw_code: Optional raw code to use as bundle (bypasses source extraction).
|
|
304
|
-
|
|
314
|
+
|
|
305
315
|
Example:
|
|
306
316
|
# Synchronous verifier (works locally and remotely)
|
|
307
317
|
@verifier(key="check_user_count")
|
|
@@ -310,7 +320,7 @@ def verifier(
|
|
|
310
320
|
result = db.query("SELECT COUNT(*) FROM users")
|
|
311
321
|
actual_count = result.rows[0][0]
|
|
312
322
|
return 1.0 if actual_count >= expected_count else 0.0
|
|
313
|
-
|
|
323
|
+
|
|
314
324
|
# Async verifier (only works locally)
|
|
315
325
|
@verifier(key="check_user_async")
|
|
316
326
|
async def check_user_async(env, expected_count: int) -> float:
|
|
@@ -318,29 +328,25 @@ def verifier(
|
|
|
318
328
|
result = await db.query("SELECT COUNT(*) FROM users")
|
|
319
329
|
actual_count = result.rows[0][0]
|
|
320
330
|
return 1.0 if actual_count >= expected_count else 0.0
|
|
321
|
-
|
|
331
|
+
|
|
322
332
|
# Usage
|
|
323
|
-
env = await
|
|
324
|
-
|
|
333
|
+
env = await fleet.env.make_async("fira")
|
|
334
|
+
|
|
325
335
|
# Local execution
|
|
326
336
|
result = await check_user_count(env, 5) # sync verifier
|
|
327
337
|
result = await check_user_async(env, 5) # async verifier
|
|
328
|
-
|
|
338
|
+
|
|
329
339
|
# Remote execution
|
|
330
340
|
result = await check_user_count.remote(env, 5) # sync verifier works
|
|
331
341
|
# await check_user_async.remote(env, 5) # raises NotImplementedError
|
|
332
342
|
"""
|
|
343
|
+
|
|
333
344
|
def decorator(func: F) -> AsyncVerifierFunction:
|
|
334
345
|
verifier_key = key or func.__name__
|
|
335
346
|
verifier_uuid = str(uuid.uuid4())
|
|
336
|
-
|
|
347
|
+
|
|
337
348
|
return AsyncVerifierFunction(
|
|
338
|
-
func,
|
|
339
|
-
verifier_key,
|
|
340
|
-
extra_requirements,
|
|
341
|
-
verifier_uuid,
|
|
342
|
-
sha256,
|
|
343
|
-
raw_code
|
|
349
|
+
func, verifier_key, extra_requirements, verifier_uuid, sha256, raw_code
|
|
344
350
|
)
|
|
345
|
-
|
|
346
|
-
return decorator
|
|
351
|
+
|
|
352
|
+
return decorator
|
fleet/base.py
CHANGED
|
@@ -46,6 +46,7 @@ class BaseWrapper:
|
|
|
46
46
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
47
47
|
# Debug log
|
|
48
48
|
import logging
|
|
49
|
+
|
|
49
50
|
logger = logging.getLogger(__name__)
|
|
50
51
|
logger.debug(f"Headers being sent: {headers}")
|
|
51
52
|
return headers
|
|
@@ -89,7 +90,7 @@ class SyncWrapper(BaseWrapper):
|
|
|
89
90
|
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
90
91
|
"""Handle HTTP error responses and convert to appropriate Fleet exceptions."""
|
|
91
92
|
status_code = response.status_code
|
|
92
|
-
|
|
93
|
+
|
|
93
94
|
# Debug log 500 errors
|
|
94
95
|
if status_code == 500:
|
|
95
96
|
logger.error(f"Got 500 error from {response.url}")
|