fleet-python 0.2.106__tar.gz → 0.2.108__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.
- {fleet_python-0.2.106/fleet_python.egg-info → fleet_python-0.2.108}/PKG-INFO +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/__init__.py +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/client.py +6 -11
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/instance/client.py +14 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/models.py +0 -9
- fleet_python-0.2.108/fleet/_async/resources/filesystem.py +397 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/tasks.py +6 -27
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/agent.py +159 -377
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/base.py +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/client.py +6 -11
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/client.py +14 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/models.py +74 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/models.py +0 -9
- fleet_python-0.2.108/fleet/resources/filesystem.py +397 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/tasks.py +2 -19
- {fleet_python-0.2.106 → fleet_python-0.2.108/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/SOURCES.txt +2 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/pyproject.toml +1 -1
- {fleet_python-0.2.106 → fleet_python-0.2.108}/LICENSE +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/README.md +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/diff_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_account.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_client.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_sync.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_task.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/export_tasks_filtered.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/openai_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/quickstart.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/api.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/cli.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/config.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/env/client.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/global_client.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/api.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/types.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/scripts/unasync.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/setup.cfg +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/__init__.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_expect_exactly.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_verifier_from_string.py +0 -0
|
@@ -171,6 +171,7 @@ from .instance.client import ValidatorType
|
|
|
171
171
|
from .resources.base import Resource
|
|
172
172
|
from .resources.sqlite import AsyncSQLiteResource
|
|
173
173
|
from .resources.browser import AsyncBrowserResource
|
|
174
|
+
from .resources.filesystem import AsyncFilesystemResource
|
|
174
175
|
from .resources.mcp import AsyncMCPResource
|
|
175
176
|
from .resources.api import AsyncAPIResource
|
|
176
177
|
|
|
@@ -386,6 +387,10 @@ class AsyncEnv(EnvironmentBase):
|
|
|
386
387
|
def browser(self, name: str = "cdp") -> AsyncBrowserResource:
|
|
387
388
|
return self.instance.browser(name)
|
|
388
389
|
|
|
390
|
+
def fs(self) -> AsyncFilesystemResource:
|
|
391
|
+
"""Get a filesystem diff resource for inspecting file changes."""
|
|
392
|
+
return self.instance.fs()
|
|
393
|
+
|
|
389
394
|
def api(self, name: str = "api") -> AsyncAPIResource:
|
|
390
395
|
"""Get an API resource for making HTTP requests to the app's API.
|
|
391
396
|
|
|
@@ -1270,8 +1275,6 @@ class AsyncFleet:
|
|
|
1270
1275
|
prompt: Optional[str] = None,
|
|
1271
1276
|
verifier_code: Optional[str] = None,
|
|
1272
1277
|
metadata: Optional[Dict[str, Any]] = None,
|
|
1273
|
-
writer_metadata: Optional[Dict[str, Any]] = None,
|
|
1274
|
-
qa_metadata: Optional[Dict[str, Any]] = None,
|
|
1275
1278
|
) -> TaskResponse:
|
|
1276
1279
|
"""Update an existing task.
|
|
1277
1280
|
|
|
@@ -1280,19 +1283,11 @@ class AsyncFleet:
|
|
|
1280
1283
|
prompt: New prompt text for the task (optional)
|
|
1281
1284
|
verifier_code: Python code for task verification (optional)
|
|
1282
1285
|
metadata: Additional metadata for the task (optional)
|
|
1283
|
-
writer_metadata: Metadata filled by task writer (optional)
|
|
1284
|
-
qa_metadata: Metadata filled by QA reviewer (optional)
|
|
1285
1286
|
|
|
1286
1287
|
Returns:
|
|
1287
1288
|
TaskResponse containing the updated task details
|
|
1288
1289
|
"""
|
|
1289
|
-
payload = TaskUpdateRequest(
|
|
1290
|
-
prompt=prompt,
|
|
1291
|
-
verifier_code=verifier_code,
|
|
1292
|
-
metadata=metadata,
|
|
1293
|
-
writer_metadata=writer_metadata,
|
|
1294
|
-
qa_metadata=qa_metadata,
|
|
1295
|
-
)
|
|
1290
|
+
payload = TaskUpdateRequest(prompt=prompt, verifier_code=verifier_code, metadata=metadata)
|
|
1296
1291
|
response = await self.client.request(
|
|
1297
1292
|
"PUT", f"/v1/tasks/{task_key}", json=payload.model_dump(exclude_none=True)
|
|
1298
1293
|
)
|
|
@@ -10,6 +10,7 @@ from urllib.parse import urlparse
|
|
|
10
10
|
from ..resources.sqlite import AsyncSQLiteResource
|
|
11
11
|
from ..resources.browser import AsyncBrowserResource
|
|
12
12
|
from ..resources.api import AsyncAPIResource
|
|
13
|
+
from ..resources.filesystem import AsyncFilesystemResource
|
|
13
14
|
from ..resources.base import Resource
|
|
14
15
|
|
|
15
16
|
from fleet.verifiers import DatabaseSnapshot
|
|
@@ -105,6 +106,19 @@ class AsyncInstanceClient:
|
|
|
105
106
|
self._resources_state[ResourceType.cdp.value][name], self.client
|
|
106
107
|
)
|
|
107
108
|
|
|
109
|
+
def fs(self) -> AsyncFilesystemResource:
|
|
110
|
+
"""Returns a filesystem diff resource for inspecting file changes.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
An AsyncFilesystemResource for querying filesystem diffs
|
|
114
|
+
"""
|
|
115
|
+
resource_model = ResourceModel(
|
|
116
|
+
name="fs",
|
|
117
|
+
type=ResourceType.db, # Reuse existing type; fs is not a registered resource type
|
|
118
|
+
mode=ResourceMode.ro,
|
|
119
|
+
)
|
|
120
|
+
return AsyncFilesystemResource(resource_model, self.client)
|
|
121
|
+
|
|
108
122
|
def api(self, name: str, base_url: str) -> AsyncAPIResource:
|
|
109
123
|
"""
|
|
110
124
|
Returns an API resource for making HTTP requests.
|
|
@@ -157,9 +157,6 @@ class TaskRequest(BaseModel):
|
|
|
157
157
|
version: Optional[str] = Field(None, title="Version")
|
|
158
158
|
env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
|
|
159
159
|
metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
|
|
160
|
-
writer_metadata: Optional[Dict[str, Any]] = Field(
|
|
161
|
-
None, title="Writer Metadata", description="Metadata filled by task writer"
|
|
162
|
-
)
|
|
163
160
|
output_json_schema: Optional[Dict[str, Any]] = Field(None, title="Output Json Schema")
|
|
164
161
|
|
|
165
162
|
|
|
@@ -167,12 +164,6 @@ class TaskUpdateRequest(BaseModel):
|
|
|
167
164
|
prompt: Optional[str] = Field(None, title="Prompt")
|
|
168
165
|
verifier_code: Optional[str] = Field(None, title="Verifier Code")
|
|
169
166
|
metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
|
|
170
|
-
writer_metadata: Optional[Dict[str, Any]] = Field(
|
|
171
|
-
None, title="Writer Metadata", description="Metadata filled by task writer"
|
|
172
|
-
)
|
|
173
|
-
qa_metadata: Optional[Dict[str, Any]] = Field(
|
|
174
|
-
None, title="QA Metadata", description="Metadata filled by QA reviewer"
|
|
175
|
-
)
|
|
176
167
|
|
|
177
168
|
|
|
178
169
|
class VerifierData(BaseModel):
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from ...instance.models import (
|
|
3
|
+
Resource as ResourceModel,
|
|
4
|
+
FsDiffRequest,
|
|
5
|
+
FsDiffResponse,
|
|
6
|
+
FsFileDiffEntry,
|
|
7
|
+
FileStateRequest,
|
|
8
|
+
FileStateResponse,
|
|
9
|
+
FileStateTextRequest,
|
|
10
|
+
DocTextRequest,
|
|
11
|
+
DocMetadataRequest,
|
|
12
|
+
DocMetadataResponse,
|
|
13
|
+
DocStructuredRequest,
|
|
14
|
+
DocStructuredResponse,
|
|
15
|
+
)
|
|
16
|
+
from .base import Resource
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..instance.base import AsyncWrapper
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Document extensions that need /fs/doc/text for readable content extraction
|
|
25
|
+
_DOC_EXTENSIONS = {
|
|
26
|
+
".docx", ".doc", ".pptx", ".ppt", ".xlsx", ".xls",
|
|
27
|
+
".odt", ".ods", ".odp", ".rtf", ".pdf", ".epub",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_doc_file(path: str) -> bool:
|
|
32
|
+
lower = path.lower()
|
|
33
|
+
return any(lower.endswith(ext) for ext in _DOC_EXTENSIONS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncFilesystemDiff:
|
|
37
|
+
"""Wraps a filesystem diff response with assertion helpers."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, response: FsDiffResponse, resource: "AsyncFilesystemResource"):
|
|
40
|
+
self.response = response
|
|
41
|
+
self.files = response.files
|
|
42
|
+
self.total_files = response.total_files
|
|
43
|
+
self.total_size = response.total_size
|
|
44
|
+
self._resource = resource
|
|
45
|
+
|
|
46
|
+
def _files_by_path(self) -> Dict[str, FsFileDiffEntry]:
|
|
47
|
+
return {f.path: f for f in self.files}
|
|
48
|
+
|
|
49
|
+
def expect_no_changes(self) -> "AsyncFilesystemDiff":
|
|
50
|
+
"""Assert that no filesystem changes occurred."""
|
|
51
|
+
if self.files:
|
|
52
|
+
paths = [f.path for f in self.files]
|
|
53
|
+
raise AssertionError(
|
|
54
|
+
f"Expected no filesystem changes, but found {len(self.files)} changed file(s):\n"
|
|
55
|
+
+ "\n".join(f" - {p}" for p in paths)
|
|
56
|
+
)
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
async def expect_only(self, allowed_changes: List[Dict[str, Any]]) -> "AsyncFilesystemDiff":
|
|
60
|
+
"""Assert that only the specified filesystem changes occurred.
|
|
61
|
+
|
|
62
|
+
Each spec in allowed_changes is a dict with:
|
|
63
|
+
- "path" (required): the file path to expect
|
|
64
|
+
- "content" (optional): expected file content (exact match)
|
|
65
|
+
- "content_contains" (optional): substring that must appear in content
|
|
66
|
+
- "doc_text" (optional): expected extracted text from doc files (exact match)
|
|
67
|
+
- "doc_text_contains" (optional): substring that must appear in doc text
|
|
68
|
+
- "file_type" (optional): expected file_type value
|
|
69
|
+
- "size" (optional): expected file size
|
|
70
|
+
|
|
71
|
+
Use ... (Ellipsis) as a value to accept any value for that field.
|
|
72
|
+
|
|
73
|
+
For document files (docx, pptx, xlsx, etc.), use doc_text / doc_text_contains
|
|
74
|
+
instead of content / content_contains. These call /fs/doc/text to extract
|
|
75
|
+
readable text from binary document formats.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
AssertionError: if unexpected files changed or specs don't match
|
|
79
|
+
"""
|
|
80
|
+
if not allowed_changes:
|
|
81
|
+
return self.expect_no_changes()
|
|
82
|
+
|
|
83
|
+
files_by_path = self._files_by_path()
|
|
84
|
+
allowed_paths = set()
|
|
85
|
+
errors: List[str] = []
|
|
86
|
+
|
|
87
|
+
for spec in allowed_changes:
|
|
88
|
+
path = spec.get("path")
|
|
89
|
+
if path is None:
|
|
90
|
+
raise ValueError("Each allowed change spec must include a 'path' key")
|
|
91
|
+
allowed_paths.add(path)
|
|
92
|
+
|
|
93
|
+
if path not in files_by_path:
|
|
94
|
+
errors.append(f"Expected change at '{path}' but file was not in diff")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
entry = files_by_path[path]
|
|
98
|
+
await self._validate_entry(entry, spec, errors)
|
|
99
|
+
|
|
100
|
+
# Check for unexpected changes
|
|
101
|
+
unexpected = set(files_by_path.keys()) - allowed_paths
|
|
102
|
+
if unexpected:
|
|
103
|
+
errors.append(
|
|
104
|
+
f"Unexpected filesystem changes ({len(unexpected)} file(s)):\n"
|
|
105
|
+
+ "\n".join(f" - {p}" for p in sorted(unexpected))
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if errors:
|
|
109
|
+
raise AssertionError(
|
|
110
|
+
f"Filesystem expect_only failed with {len(errors)} error(s):\n"
|
|
111
|
+
+ "\n".join(f" {i+1}. {e}" for i, e in enumerate(errors))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
async def expect_exactly(self, expected_changes: List[Dict[str, Any]]) -> "AsyncFilesystemDiff":
|
|
117
|
+
"""Assert that EXACTLY the specified filesystem changes occurred.
|
|
118
|
+
|
|
119
|
+
Like expect_only, but also fails if an expected path is missing from the diff.
|
|
120
|
+
See expect_only for the full spec format including doc_text / doc_text_contains.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
AssertionError: if changes don't match exactly
|
|
124
|
+
"""
|
|
125
|
+
if not expected_changes and self.files:
|
|
126
|
+
paths = [f.path for f in self.files]
|
|
127
|
+
raise AssertionError(
|
|
128
|
+
f"Expected no filesystem changes, but found {len(self.files)} changed file(s):\n"
|
|
129
|
+
+ "\n".join(f" - {p}" for p in paths)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
files_by_path = self._files_by_path()
|
|
133
|
+
expected_paths = set()
|
|
134
|
+
errors: List[str] = []
|
|
135
|
+
|
|
136
|
+
for spec in expected_changes:
|
|
137
|
+
path = spec.get("path")
|
|
138
|
+
if path is None:
|
|
139
|
+
raise ValueError("Each expected change spec must include a 'path' key")
|
|
140
|
+
expected_paths.add(path)
|
|
141
|
+
|
|
142
|
+
if path not in files_by_path:
|
|
143
|
+
errors.append(f"Expected change at '{path}' but file was not in diff")
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
entry = files_by_path[path]
|
|
147
|
+
await self._validate_entry(entry, spec, errors)
|
|
148
|
+
|
|
149
|
+
# Check for unexpected changes
|
|
150
|
+
unexpected = set(files_by_path.keys()) - expected_paths
|
|
151
|
+
if unexpected:
|
|
152
|
+
errors.append(
|
|
153
|
+
f"Unexpected filesystem changes ({len(unexpected)} file(s)):\n"
|
|
154
|
+
+ "\n".join(f" - {p}" for p in sorted(unexpected))
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Check for missing expected changes
|
|
158
|
+
missing = expected_paths - set(files_by_path.keys())
|
|
159
|
+
if missing:
|
|
160
|
+
errors.append(
|
|
161
|
+
f"Missing expected filesystem changes ({len(missing)} file(s)):\n"
|
|
162
|
+
+ "\n".join(f" - {p}" for p in sorted(missing))
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if errors:
|
|
166
|
+
raise AssertionError(
|
|
167
|
+
f"Filesystem expect_exactly failed with {len(errors)} error(s):\n"
|
|
168
|
+
+ "\n".join(f" {i+1}. {e}" for i, e in enumerate(errors))
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
async def _validate_entry(
|
|
174
|
+
self, entry: FsFileDiffEntry, spec: Dict[str, Any], errors: List[str]
|
|
175
|
+
) -> None:
|
|
176
|
+
path = spec["path"]
|
|
177
|
+
|
|
178
|
+
# Plain content checks (for text files)
|
|
179
|
+
if "content" in spec and spec["content"] is not ...:
|
|
180
|
+
if entry.content is None:
|
|
181
|
+
errors.append(f"'{path}': content not available (was content excluded from diff?)")
|
|
182
|
+
elif entry.content != spec["content"]:
|
|
183
|
+
errors.append(
|
|
184
|
+
f"'{path}': content mismatch\n"
|
|
185
|
+
f" expected: {repr(spec['content'][:200])}\n"
|
|
186
|
+
f" actual: {repr(entry.content[:200])}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if "content_contains" in spec and spec["content_contains"] is not ...:
|
|
190
|
+
if entry.content is None:
|
|
191
|
+
errors.append(f"'{path}': content not available for content_contains check")
|
|
192
|
+
elif spec["content_contains"] not in entry.content:
|
|
193
|
+
errors.append(
|
|
194
|
+
f"'{path}': content does not contain expected substring: "
|
|
195
|
+
f"{repr(spec['content_contains'][:200])}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Document text checks (for docx, pptx, xlsx, etc. via /fs/doc/text)
|
|
199
|
+
if "doc_text" in spec or "doc_text_contains" in spec:
|
|
200
|
+
try:
|
|
201
|
+
doc_text = await self._resource.doc_text(path)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
errors.append(f"'{path}': failed to extract doc text: {e}")
|
|
204
|
+
doc_text = None
|
|
205
|
+
|
|
206
|
+
if doc_text is not None:
|
|
207
|
+
if "doc_text" in spec and spec["doc_text"] is not ...:
|
|
208
|
+
if doc_text != spec["doc_text"]:
|
|
209
|
+
errors.append(
|
|
210
|
+
f"'{path}': doc_text mismatch\n"
|
|
211
|
+
f" expected: {repr(spec['doc_text'][:200])}\n"
|
|
212
|
+
f" actual: {repr(doc_text[:200])}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if "doc_text_contains" in spec and spec["doc_text_contains"] is not ...:
|
|
216
|
+
if spec["doc_text_contains"] not in doc_text:
|
|
217
|
+
errors.append(
|
|
218
|
+
f"'{path}': doc text does not contain expected substring: "
|
|
219
|
+
f"{repr(spec['doc_text_contains'][:200])}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if "file_type" in spec and spec["file_type"] is not ...:
|
|
223
|
+
if entry.file_type != spec["file_type"]:
|
|
224
|
+
errors.append(
|
|
225
|
+
f"'{path}': file_type mismatch (expected {spec['file_type']!r}, got {entry.file_type!r})"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if "size" in spec and spec["size"] is not ...:
|
|
229
|
+
if entry.size != spec["size"]:
|
|
230
|
+
errors.append(
|
|
231
|
+
f"'{path}': size mismatch (expected {spec['size']}, got {entry.size})"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class AsyncFilesystemResource(Resource):
|
|
236
|
+
"""Filesystem resource that operates via the /diff/fs and /fs/* endpoints."""
|
|
237
|
+
|
|
238
|
+
def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
|
|
239
|
+
super().__init__(resource)
|
|
240
|
+
self.client = client
|
|
241
|
+
|
|
242
|
+
# ── Diff endpoints ────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
async def diff(
|
|
245
|
+
self,
|
|
246
|
+
include_content: bool = True,
|
|
247
|
+
max_content_size: int = 102400,
|
|
248
|
+
exclude_patterns: Optional[List[str]] = None,
|
|
249
|
+
) -> AsyncFilesystemDiff:
|
|
250
|
+
"""Get filesystem diff from the environment.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
include_content: Whether to include file contents (default True)
|
|
254
|
+
max_content_size: Max file size to include content for (default 100KB)
|
|
255
|
+
exclude_patterns: Optional list of glob patterns to exclude
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
AsyncFilesystemDiff with assertion helpers
|
|
259
|
+
"""
|
|
260
|
+
request = FsDiffRequest(
|
|
261
|
+
include_content=include_content,
|
|
262
|
+
max_content_size=max_content_size,
|
|
263
|
+
exclude_patterns=exclude_patterns,
|
|
264
|
+
)
|
|
265
|
+
response = await self.client.request(
|
|
266
|
+
"POST",
|
|
267
|
+
"/diff/fs",
|
|
268
|
+
json=request.model_dump(exclude_none=True),
|
|
269
|
+
)
|
|
270
|
+
result = response.json()
|
|
271
|
+
fs_response = FsDiffResponse(**result)
|
|
272
|
+
if not fs_response.success:
|
|
273
|
+
raise RuntimeError(
|
|
274
|
+
f"Filesystem diff failed: {fs_response.error or fs_response.message}"
|
|
275
|
+
)
|
|
276
|
+
return AsyncFilesystemDiff(fs_response, self)
|
|
277
|
+
|
|
278
|
+
async def diff_simple(
|
|
279
|
+
self,
|
|
280
|
+
include_content: bool = True,
|
|
281
|
+
max_content_size: int = 102400,
|
|
282
|
+
) -> AsyncFilesystemDiff:
|
|
283
|
+
"""Get filesystem diff using the simple GET endpoint.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
include_content: Whether to include file contents (default True)
|
|
287
|
+
max_content_size: Max file size to include content for (default 100KB)
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
AsyncFilesystemDiff with assertion helpers
|
|
291
|
+
"""
|
|
292
|
+
params = {
|
|
293
|
+
"include_content": include_content,
|
|
294
|
+
"max_content_size": max_content_size,
|
|
295
|
+
}
|
|
296
|
+
response = await self.client.request("GET", "/diff/fs", params=params)
|
|
297
|
+
result = response.json()
|
|
298
|
+
fs_response = FsDiffResponse(**result)
|
|
299
|
+
if not fs_response.success:
|
|
300
|
+
raise RuntimeError(
|
|
301
|
+
f"Filesystem diff failed: {fs_response.error or fs_response.message}"
|
|
302
|
+
)
|
|
303
|
+
return AsyncFilesystemDiff(fs_response, self)
|
|
304
|
+
|
|
305
|
+
# ── Single file endpoints ─────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
async def file(
|
|
308
|
+
self,
|
|
309
|
+
path: str,
|
|
310
|
+
include_content: bool = True,
|
|
311
|
+
max_content_size: int = 102400,
|
|
312
|
+
) -> FileStateResponse:
|
|
313
|
+
"""Get current state of a single file.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
path: Absolute path to the file
|
|
317
|
+
include_content: Whether to include file content (default True)
|
|
318
|
+
max_content_size: Max file size to include content for (default 100KB)
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
FileStateResponse with file metadata and optional content
|
|
322
|
+
"""
|
|
323
|
+
request = FileStateRequest(
|
|
324
|
+
path=path,
|
|
325
|
+
include_content=include_content,
|
|
326
|
+
max_content_size=max_content_size,
|
|
327
|
+
)
|
|
328
|
+
response = await self.client.request(
|
|
329
|
+
"POST", "/fs/file", json=request.model_dump()
|
|
330
|
+
)
|
|
331
|
+
return FileStateResponse(**response.json())
|
|
332
|
+
|
|
333
|
+
async def file_text(self, path: str, max_content_size: int = 102400) -> str:
|
|
334
|
+
"""Get file content as plain text.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
path: Absolute path to the file
|
|
338
|
+
max_content_size: Max file size (default 100KB)
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
File content as string
|
|
342
|
+
"""
|
|
343
|
+
request = FileStateTextRequest(
|
|
344
|
+
path=path, max_content_size=max_content_size
|
|
345
|
+
)
|
|
346
|
+
response = await self.client.request(
|
|
347
|
+
"POST", "/fs/file/text", json=request.model_dump()
|
|
348
|
+
)
|
|
349
|
+
return response.text
|
|
350
|
+
|
|
351
|
+
# ── Document extraction endpoints ─────────────────────────────────
|
|
352
|
+
|
|
353
|
+
async def doc_text(self, path: str, max_size: int = 10485760) -> str:
|
|
354
|
+
"""Extract plain text from a document file (docx, pptx, xlsx, pdf, etc.).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
path: Absolute path to the document
|
|
358
|
+
max_size: Max document size (default 10MB)
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Extracted text content as string
|
|
362
|
+
"""
|
|
363
|
+
request = DocTextRequest(path=path, max_size=max_size)
|
|
364
|
+
response = await self.client.request(
|
|
365
|
+
"POST", "/fs/doc/text", json=request.model_dump()
|
|
366
|
+
)
|
|
367
|
+
return response.text
|
|
368
|
+
|
|
369
|
+
async def doc_metadata(self, path: str) -> DocMetadataResponse:
|
|
370
|
+
"""Extract metadata from a document file.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
path: Absolute path to the document
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
DocMetadataResponse with file_type and metadata dict
|
|
377
|
+
"""
|
|
378
|
+
request = DocMetadataRequest(path=path)
|
|
379
|
+
response = await self.client.request(
|
|
380
|
+
"POST", "/fs/doc/metadata", json=request.model_dump()
|
|
381
|
+
)
|
|
382
|
+
return DocMetadataResponse(**response.json())
|
|
383
|
+
|
|
384
|
+
async def doc_structured(self, path: str) -> DocStructuredResponse:
|
|
385
|
+
"""Extract structured content from a document file.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
path: Absolute path to the document
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
DocStructuredResponse with file_type and structured data dict
|
|
392
|
+
"""
|
|
393
|
+
request = DocStructuredRequest(path=path)
|
|
394
|
+
response = await self.client.request(
|
|
395
|
+
"POST", "/fs/doc/structured", json=request.model_dump()
|
|
396
|
+
)
|
|
397
|
+
return DocStructuredResponse(**response.json())
|
|
@@ -346,9 +346,8 @@ def verifier_from_string(
|
|
|
346
346
|
return False
|
|
347
347
|
return target in numbers
|
|
348
348
|
|
|
349
|
-
# Create a
|
|
350
|
-
|
|
351
|
-
exec_globals.update({
|
|
349
|
+
# Create a local namespace for executing the code
|
|
350
|
+
local_namespace = {
|
|
352
351
|
"TASK_SUCCESSFUL_SCORE": TASK_SUCCESSFUL_SCORE,
|
|
353
352
|
"TASK_FAILED_SCORE": TASK_FAILED_SCORE,
|
|
354
353
|
"IgnoreConfig": IgnoreConfig,
|
|
@@ -359,17 +358,10 @@ def verifier_from_string(
|
|
|
359
358
|
"json": json,
|
|
360
359
|
"re": re,
|
|
361
360
|
"string": string,
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
# Create a local namespace for executing the code
|
|
365
|
-
local_namespace = {}
|
|
361
|
+
}
|
|
366
362
|
|
|
367
363
|
# Execute the cleaned verifier code in the namespace
|
|
368
|
-
exec(cleaned_code,
|
|
369
|
-
|
|
370
|
-
# Merge local_namespace into exec_globals so helper functions are accessible
|
|
371
|
-
# from the main verifier function when it's called
|
|
372
|
-
exec_globals.update(local_namespace)
|
|
364
|
+
exec(cleaned_code, globals(), local_namespace)
|
|
373
365
|
|
|
374
366
|
# Find the function that was defined (not imported)
|
|
375
367
|
# Functions defined via exec have co_filename == '<string>'
|
|
@@ -456,12 +448,7 @@ async def load_tasks(
|
|
|
456
448
|
|
|
457
449
|
|
|
458
450
|
async def update_task(
|
|
459
|
-
task_key: str,
|
|
460
|
-
prompt: Optional[str] = None,
|
|
461
|
-
verifier_code: Optional[str] = None,
|
|
462
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
463
|
-
writer_metadata: Optional[Dict[str, Any]] = None,
|
|
464
|
-
qa_metadata: Optional[Dict[str, Any]] = None,
|
|
451
|
+
task_key: str, prompt: Optional[str] = None, verifier_code: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None
|
|
465
452
|
):
|
|
466
453
|
"""Convenience function to update an existing task.
|
|
467
454
|
|
|
@@ -470,8 +457,6 @@ async def update_task(
|
|
|
470
457
|
prompt: New prompt text for the task (optional)
|
|
471
458
|
verifier_code: Python code for task verification (optional)
|
|
472
459
|
metadata: Additional metadata for the task (optional)
|
|
473
|
-
writer_metadata: Metadata filled by task writer (optional)
|
|
474
|
-
qa_metadata: Metadata filled by QA reviewer (optional)
|
|
475
460
|
|
|
476
461
|
Returns:
|
|
477
462
|
TaskResponse containing the updated task details
|
|
@@ -480,18 +465,12 @@ async def update_task(
|
|
|
480
465
|
response = await fleet.update_task("my-task", prompt="New prompt text")
|
|
481
466
|
response = await fleet.update_task("my-task", verifier_code="def verify(env): return True")
|
|
482
467
|
response = await fleet.update_task("my-task", metadata={"seed": 42, "story": "Updated story"})
|
|
483
|
-
response = await fleet.update_task("my-task", writer_metadata={"author": "john"})
|
|
484
468
|
"""
|
|
485
469
|
from .global_client import get_client
|
|
486
470
|
|
|
487
471
|
client = get_client()
|
|
488
472
|
return await client.update_task(
|
|
489
|
-
task_key=task_key,
|
|
490
|
-
prompt=prompt,
|
|
491
|
-
verifier_code=verifier_code,
|
|
492
|
-
metadata=metadata,
|
|
493
|
-
writer_metadata=writer_metadata,
|
|
494
|
-
qa_metadata=qa_metadata,
|
|
473
|
+
task_key=task_key, prompt=prompt, verifier_code=verifier_code, metadata=metadata
|
|
495
474
|
)
|
|
496
475
|
|
|
497
476
|
|