futurehouse-client 0.3.19.dev133__py3-none-any.whl → 0.3.20.dev63__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.
- futurehouse_client/__init__.py +12 -1
- futurehouse_client/clients/rest_client.py +180 -5
- futurehouse_client/models/app.py +10 -0
- futurehouse_client/models/rest.py +36 -0
- futurehouse_client/version.py +21 -0
- futurehouse_client-0.3.20.dev63.dist-info/METADATA +518 -0
- futurehouse_client-0.3.20.dev63.dist-info/RECORD +20 -0
- futurehouse_client-0.3.20.dev63.dist-info/licenses/LICENSE +201 -0
- futurehouse_client-0.3.19.dev133.dist-info/METADATA +0 -316
- futurehouse_client-0.3.19.dev133.dist-info/RECORD +0 -18
- {futurehouse_client-0.3.19.dev133.dist-info → futurehouse_client-0.3.20.dev63.dist-info}/WHEEL +0 -0
- {futurehouse_client-0.3.19.dev133.dist-info → futurehouse_client-0.3.20.dev63.dist-info}/top_level.txt +0 -0
futurehouse_client/__init__.py
CHANGED
@@ -1,11 +1,22 @@
|
|
1
1
|
from .clients.job_client import JobClient, JobNames
|
2
2
|
from .clients.rest_client import RestClient as FutureHouseClient
|
3
|
-
from .
|
3
|
+
from .models.app import (
|
4
|
+
FinchTaskResponse,
|
5
|
+
PhoenixTaskResponse,
|
6
|
+
PQATaskResponse,
|
7
|
+
TaskRequest,
|
8
|
+
TaskResponse,
|
9
|
+
TaskResponseVerbose,
|
10
|
+
)
|
4
11
|
|
5
12
|
__all__ = [
|
13
|
+
"FinchTaskResponse",
|
6
14
|
"FutureHouseClient",
|
7
15
|
"JobClient",
|
8
16
|
"JobNames",
|
17
|
+
"PQATaskResponse",
|
18
|
+
"PhoenixTaskResponse",
|
19
|
+
"TaskRequest",
|
9
20
|
"TaskResponse",
|
10
21
|
"TaskResponseVerbose",
|
11
22
|
]
|
@@ -54,7 +54,11 @@ from futurehouse_client.models.app import (
|
|
54
54
|
TaskResponse,
|
55
55
|
TaskResponseVerbose,
|
56
56
|
)
|
57
|
-
from futurehouse_client.models.rest import
|
57
|
+
from futurehouse_client.models.rest import (
|
58
|
+
ExecutionStatus,
|
59
|
+
WorldModel,
|
60
|
+
WorldModelResponse,
|
61
|
+
)
|
58
62
|
from futurehouse_client.utils.auth import RefreshingJWT
|
59
63
|
from futurehouse_client.utils.general import gather_with_concurrency
|
60
64
|
from futurehouse_client.utils.module_utils import (
|
@@ -100,6 +104,14 @@ class JobCreationError(RestClientError):
|
|
100
104
|
"""Raised when there's an error creating a job."""
|
101
105
|
|
102
106
|
|
107
|
+
class WorldModelFetchError(RestClientError):
|
108
|
+
"""Raised when there's an error fetching a world model."""
|
109
|
+
|
110
|
+
|
111
|
+
class WorldModelCreationError(RestClientError):
|
112
|
+
"""Raised when there's an error creating a world model."""
|
113
|
+
|
114
|
+
|
103
115
|
class InvalidTaskDescriptionError(Exception):
|
104
116
|
"""Raised when the task description is invalid or empty."""
|
105
117
|
|
@@ -157,7 +169,7 @@ class RestClient:
|
|
157
169
|
self.base_url = service_uri or stage.value
|
158
170
|
self.stage = stage
|
159
171
|
self.auth_type = auth_type
|
160
|
-
self.api_key = api_key
|
172
|
+
self.api_key = api_key or os.environ.get("FUTUREHOUSE_API_KEY")
|
161
173
|
self._clients: dict[str, Client | AsyncClient] = {}
|
162
174
|
self.headers = headers or {}
|
163
175
|
self.jwt = jwt
|
@@ -791,10 +803,9 @@ class RestClient:
|
|
791
803
|
pickled_env = cloudpickle.dumps(config.functional_environment)
|
792
804
|
encoded_pickle = base64.b64encode(pickled_env).decode("utf-8")
|
793
805
|
files = []
|
806
|
+
ignore_parts = set(FILE_UPLOAD_IGNORE_PARTS) | set(config.ignore_dirs or [])
|
794
807
|
for file_path in Path(config.path).rglob("*") if config.path else []:
|
795
|
-
if any(
|
796
|
-
ignore in file_path.parts for ignore in FILE_UPLOAD_IGNORE_PARTS
|
797
|
-
):
|
808
|
+
if any(ignore in file_path.parts for ignore in ignore_parts):
|
798
809
|
continue
|
799
810
|
|
800
811
|
if file_path.is_file():
|
@@ -1384,6 +1395,170 @@ class RestClient:
|
|
1384
1395
|
destination_path.unlink() # Clean up partial file
|
1385
1396
|
raise RestClientError(f"Error downloading file: {e!s}") from e
|
1386
1397
|
|
1398
|
+
@retry(
|
1399
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1400
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1401
|
+
retry=retry_if_connection_error,
|
1402
|
+
)
|
1403
|
+
def get_world_model(
|
1404
|
+
self, world_model_id: UUID | None = None, name: str | None = None
|
1405
|
+
) -> WorldModelResponse:
|
1406
|
+
"""Get a world model snapshot by its ID or name.
|
1407
|
+
|
1408
|
+
Args:
|
1409
|
+
world_model_id: The unique ID of the world model snapshot.
|
1410
|
+
name: The name of the world model to get the latest version of.
|
1411
|
+
|
1412
|
+
Returns:
|
1413
|
+
The requested world model snapshot.
|
1414
|
+
|
1415
|
+
Raises:
|
1416
|
+
ValueError: If neither or both `world_model_id` and `name` are provided.
|
1417
|
+
WorldModelFetchError: If the API call fails or the model is not found.
|
1418
|
+
"""
|
1419
|
+
if not (world_model_id or name) or (world_model_id and name):
|
1420
|
+
raise ValueError("Provide either 'world_model_id' or 'name', but not both.")
|
1421
|
+
|
1422
|
+
params = {
|
1423
|
+
"id": str(world_model_id) if world_model_id else None,
|
1424
|
+
"name": name,
|
1425
|
+
}
|
1426
|
+
# Filter out None values before making the request
|
1427
|
+
params = {k: v for k, v in params.items() if v is not None}
|
1428
|
+
|
1429
|
+
try:
|
1430
|
+
response = self.client.get("/v0.1/world-model", params=params)
|
1431
|
+
response.raise_for_status()
|
1432
|
+
return WorldModelResponse.model_validate(response.json())
|
1433
|
+
except HTTPStatusError as e:
|
1434
|
+
if e.response.status_code == codes.NOT_FOUND:
|
1435
|
+
raise WorldModelFetchError(
|
1436
|
+
"World model not found with the specified identifier."
|
1437
|
+
) from e
|
1438
|
+
raise WorldModelFetchError(
|
1439
|
+
f"Error fetching world model: {e.response.status_code} - {e.response.text}"
|
1440
|
+
) from e
|
1441
|
+
except Exception as e:
|
1442
|
+
raise WorldModelFetchError(f"An unexpected error occurred: {e}") from e
|
1443
|
+
|
1444
|
+
@retry(
|
1445
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1446
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1447
|
+
retry=retry_if_connection_error,
|
1448
|
+
)
|
1449
|
+
async def aget_world_model(
|
1450
|
+
self, world_model_id: UUID | None = None, name: str | None = None
|
1451
|
+
) -> WorldModelResponse:
|
1452
|
+
"""Asynchronously get a world model snapshot by its ID or name.
|
1453
|
+
|
1454
|
+
Args:
|
1455
|
+
world_model_id: The unique ID of the world model snapshot.
|
1456
|
+
name: The name of the world model to get the latest version of.
|
1457
|
+
|
1458
|
+
Returns:
|
1459
|
+
The requested world model snapshot.
|
1460
|
+
|
1461
|
+
Raises:
|
1462
|
+
ValueError: If neither or both `world_model_id` and `name` are provided.
|
1463
|
+
WorldModelFetchError: If the API call fails or the model is not found.
|
1464
|
+
"""
|
1465
|
+
if not (world_model_id or name) or (world_model_id and name):
|
1466
|
+
raise ValueError("Provide either 'world_model_id' or 'name', but not both.")
|
1467
|
+
|
1468
|
+
params = {
|
1469
|
+
"id": str(world_model_id) if world_model_id else None,
|
1470
|
+
"name": name,
|
1471
|
+
}
|
1472
|
+
params = {k: v for k, v in params.items() if v is not None}
|
1473
|
+
|
1474
|
+
try:
|
1475
|
+
response = await self.async_client.get("/v0.1/world-model", params=params)
|
1476
|
+
response.raise_for_status()
|
1477
|
+
return WorldModelResponse.model_validate(response.json())
|
1478
|
+
except HTTPStatusError as e:
|
1479
|
+
if e.response.status_code == codes.NOT_FOUND:
|
1480
|
+
raise WorldModelFetchError(
|
1481
|
+
"World model not found with the specified identifier."
|
1482
|
+
) from e
|
1483
|
+
raise WorldModelFetchError(
|
1484
|
+
f"Error fetching world model: {e.response.status_code} - {e.response.text}"
|
1485
|
+
) from e
|
1486
|
+
except Exception as e:
|
1487
|
+
raise WorldModelFetchError(f"An unexpected error occurred: {e}") from e
|
1488
|
+
|
1489
|
+
@retry(
|
1490
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1491
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1492
|
+
retry=retry_if_connection_error,
|
1493
|
+
)
|
1494
|
+
def create_world_model(self, payload: WorldModel) -> UUID:
|
1495
|
+
"""Create a new, immutable world model snapshot.
|
1496
|
+
|
1497
|
+
Args:
|
1498
|
+
payload: An instance of WorldModel with the snapshot's data.
|
1499
|
+
|
1500
|
+
Returns:
|
1501
|
+
The UUID of the newly created world model.
|
1502
|
+
|
1503
|
+
Raises:
|
1504
|
+
WorldModelCreationError: If the API call fails.
|
1505
|
+
"""
|
1506
|
+
try:
|
1507
|
+
response = self.client.post(
|
1508
|
+
"/v0.1/world-models", json=payload.model_dump(mode="json")
|
1509
|
+
)
|
1510
|
+
response.raise_for_status()
|
1511
|
+
# The server returns a raw UUID string in the body
|
1512
|
+
return UUID(response.json())
|
1513
|
+
except HTTPStatusError as e:
|
1514
|
+
if e.response.status_code == codes.BAD_REQUEST:
|
1515
|
+
raise WorldModelCreationError(
|
1516
|
+
f"Invalid payload for world model creation: {e.response.text}"
|
1517
|
+
) from e
|
1518
|
+
raise WorldModelCreationError(
|
1519
|
+
f"Error creating world model: {e.response.status_code} - {e.response.text}"
|
1520
|
+
) from e
|
1521
|
+
except Exception as e:
|
1522
|
+
raise WorldModelCreationError(
|
1523
|
+
f"An unexpected error occurred during world model creation: {e}"
|
1524
|
+
) from e
|
1525
|
+
|
1526
|
+
@retry(
|
1527
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1528
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1529
|
+
retry=retry_if_connection_error,
|
1530
|
+
)
|
1531
|
+
async def acreate_world_model(self, payload: WorldModel) -> UUID:
|
1532
|
+
"""Asynchronously create a new, immutable world model snapshot.
|
1533
|
+
|
1534
|
+
Args:
|
1535
|
+
payload: An instance of WorldModel with the snapshot's data.
|
1536
|
+
|
1537
|
+
Returns:
|
1538
|
+
The UUID of the newly created world model.
|
1539
|
+
|
1540
|
+
Raises:
|
1541
|
+
WorldModelCreationError: If the API call fails.
|
1542
|
+
"""
|
1543
|
+
try:
|
1544
|
+
response = await self.async_client.post(
|
1545
|
+
"/v0.1/world-models", json=payload.model_dump(mode="json")
|
1546
|
+
)
|
1547
|
+
response.raise_for_status()
|
1548
|
+
return UUID(response.json())
|
1549
|
+
except HTTPStatusError as e:
|
1550
|
+
if e.response.status_code == codes.BAD_REQUEST:
|
1551
|
+
raise WorldModelCreationError(
|
1552
|
+
f"Invalid payload for world model creation: {e.response.text}"
|
1553
|
+
) from e
|
1554
|
+
raise WorldModelCreationError(
|
1555
|
+
f"Error creating world model: {e.response.status_code} - {e.response.text}"
|
1556
|
+
) from e
|
1557
|
+
except Exception as e:
|
1558
|
+
raise WorldModelCreationError(
|
1559
|
+
f"An unexpected error occurred during world model creation: {e}"
|
1560
|
+
) from e
|
1561
|
+
|
1387
1562
|
|
1388
1563
|
def get_installed_packages() -> dict[str, str]:
|
1389
1564
|
"""Returns a dictionary of installed packages and their versions."""
|
futurehouse_client/models/app.py
CHANGED
@@ -365,6 +365,12 @@ class JobDeploymentConfig(BaseModel):
|
|
365
365
|
"Can be None if we are deploying a functional environment (through the functional_environment parameter).",
|
366
366
|
)
|
367
367
|
|
368
|
+
ignore_dirs: list[str] | None = Field(
|
369
|
+
default=None,
|
370
|
+
description="A list of directories to ignore when deploying the job. "
|
371
|
+
"This is a list of directories relative to the path parameter.",
|
372
|
+
)
|
373
|
+
|
368
374
|
name: str | None = Field(
|
369
375
|
default=None,
|
370
376
|
description="The name of the crow job. If None, the crow job will be "
|
@@ -604,6 +610,10 @@ class RuntimeConfig(BaseModel):
|
|
604
610
|
default=None,
|
605
611
|
description="Optional job identifier for a continued job",
|
606
612
|
)
|
613
|
+
world_model_id: UUID | None = Field(
|
614
|
+
default=None,
|
615
|
+
description="Optional world model identifier for the task",
|
616
|
+
)
|
607
617
|
|
608
618
|
@field_validator("agent")
|
609
619
|
@classmethod
|
@@ -1,4 +1,6 @@
|
|
1
|
+
from datetime import datetime
|
1
2
|
from enum import StrEnum, auto
|
3
|
+
from uuid import UUID
|
2
4
|
|
3
5
|
from pydantic import BaseModel, JsonValue
|
4
6
|
|
@@ -34,3 +36,37 @@ class ExecutionStatus(StrEnum):
|
|
34
36
|
@classmethod
|
35
37
|
def terminal_states(cls) -> set["ExecutionStatus"]:
|
36
38
|
return {cls.SUCCESS, cls.FAIL, cls.CANCELLED}
|
39
|
+
|
40
|
+
|
41
|
+
class WorldModel(BaseModel):
|
42
|
+
"""
|
43
|
+
Payload for creating a new world model snapshot.
|
44
|
+
|
45
|
+
This model is sent to the API.
|
46
|
+
"""
|
47
|
+
|
48
|
+
content: str
|
49
|
+
prior: UUID | None = None
|
50
|
+
name: str | None = None
|
51
|
+
description: str | None = None
|
52
|
+
trajectory_id: UUID | None = None
|
53
|
+
model_metadata: JsonValue | None = None
|
54
|
+
|
55
|
+
|
56
|
+
class WorldModelResponse(BaseModel):
|
57
|
+
"""
|
58
|
+
Response model for a world model snapshot.
|
59
|
+
|
60
|
+
This model is received from the API.
|
61
|
+
"""
|
62
|
+
|
63
|
+
id: UUID
|
64
|
+
prior: UUID | None
|
65
|
+
name: str
|
66
|
+
description: str | None
|
67
|
+
content: str
|
68
|
+
trajectory_id: UUID | None
|
69
|
+
email: str | None
|
70
|
+
model_metadata: JsonValue | None
|
71
|
+
enabled: bool
|
72
|
+
created_at: datetime
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# file generated by setuptools-scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
6
|
+
TYPE_CHECKING = False
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
12
|
+
else:
|
13
|
+
VERSION_TUPLE = object
|
14
|
+
|
15
|
+
version: str
|
16
|
+
__version__: str
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
18
|
+
version_tuple: VERSION_TUPLE
|
19
|
+
|
20
|
+
__version__ = version = '0.3.20.dev63'
|
21
|
+
__version_tuple__ = version_tuple = (0, 3, 20, 'dev63')
|