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.
@@ -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 .clients.rest_client import TaskResponse, TaskResponseVerbose
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 ExecutionStatus
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."""
@@ -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')