futurehouse-client 0.3.20.dev215__py3-none-any.whl → 0.3.20.dev266__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/clients/rest_client.py +590 -35
- futurehouse_client/models/app.py +79 -0
- futurehouse_client/models/rest.py +48 -1
- futurehouse_client/version.py +2 -2
- {futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/METADATA +1 -1
- {futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/RECORD +9 -9
- {futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/WHEEL +0 -0
- {futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/licenses/LICENSE +0 -0
- {futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ import time
|
|
14
14
|
import uuid
|
15
15
|
from collections.abc import Collection
|
16
16
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
17
|
+
from http import HTTPStatus
|
17
18
|
from pathlib import Path
|
18
19
|
from types import ModuleType
|
19
20
|
from typing import Any, ClassVar, cast
|
@@ -37,6 +38,7 @@ from httpx import (
|
|
37
38
|
from ldp.agent import AgentConfig
|
38
39
|
from requests.exceptions import RequestException, Timeout
|
39
40
|
from tenacity import (
|
41
|
+
before_sleep_log,
|
40
42
|
retry,
|
41
43
|
retry_if_exception_type,
|
42
44
|
stop_after_attempt,
|
@@ -53,9 +55,14 @@ from futurehouse_client.models.app import (
|
|
53
55
|
TaskRequest,
|
54
56
|
TaskResponse,
|
55
57
|
TaskResponseVerbose,
|
58
|
+
TrajectoryQueryParams,
|
56
59
|
)
|
57
60
|
from futurehouse_client.models.rest import (
|
58
61
|
ExecutionStatus,
|
62
|
+
UserAgentRequest,
|
63
|
+
UserAgentRequestPostPayload,
|
64
|
+
UserAgentRequestStatus,
|
65
|
+
UserAgentResponsePayload,
|
59
66
|
WorldModel,
|
60
67
|
WorldModelResponse,
|
61
68
|
)
|
@@ -104,6 +111,22 @@ class JobCreationError(RestClientError):
|
|
104
111
|
"""Raised when there's an error creating a job."""
|
105
112
|
|
106
113
|
|
114
|
+
class UserAgentRequestError(RestClientError):
|
115
|
+
"""Base exception for User Agent Request operations."""
|
116
|
+
|
117
|
+
|
118
|
+
class UserAgentRequestFetchError(UserAgentRequestError):
|
119
|
+
"""Raised when there's an error fetching a user agent request."""
|
120
|
+
|
121
|
+
|
122
|
+
class UserAgentRequestCreationError(UserAgentRequestError):
|
123
|
+
"""Raised when there's an error creating a user agent request."""
|
124
|
+
|
125
|
+
|
126
|
+
class UserAgentRequestResponseError(UserAgentRequestError):
|
127
|
+
"""Raised when there's an error responding to a user agent request."""
|
128
|
+
|
129
|
+
|
107
130
|
class WorldModelFetchError(RestClientError):
|
108
131
|
"""Raised when there's an error fetching a world model."""
|
109
132
|
|
@@ -295,7 +318,7 @@ class RestClient:
|
|
295
318
|
response.raise_for_status()
|
296
319
|
return response.json()
|
297
320
|
except Exception as e:
|
298
|
-
raise JobFetchError(f"Error checking job: {e!
|
321
|
+
raise JobFetchError(f"Error checking job: {e!r}.") from e
|
299
322
|
|
300
323
|
def _fetch_my_orgs(self) -> list[str]:
|
301
324
|
response = self.client.get(f"/v0.1/organizations?filter={True}")
|
@@ -325,7 +348,7 @@ class RestClient:
|
|
325
348
|
response.raise_for_status()
|
326
349
|
return response.json()
|
327
350
|
except Exception as e:
|
328
|
-
raise RestClientError(f"Error checking assembly status: {e}") from e
|
351
|
+
raise RestClientError(f"Error checking assembly status: {e!r}.") from e
|
329
352
|
|
330
353
|
def _wait_for_all_assemblies_completion(
|
331
354
|
self,
|
@@ -375,7 +398,7 @@ class RestClient:
|
|
375
398
|
elif status == ExecutionStatus.FAIL.value:
|
376
399
|
error_msg = status_data.get("error", "Unknown assembly error")
|
377
400
|
raise RestClientError(
|
378
|
-
f"Assembly failed for {file_name}: {error_msg}"
|
401
|
+
f"Assembly failed for {file_name}: {error_msg}."
|
379
402
|
)
|
380
403
|
elif status == ExecutionStatus.IN_PROGRESS.value:
|
381
404
|
logger.debug(f"Assembly in progress for {file_name}...")
|
@@ -384,7 +407,7 @@ class RestClient:
|
|
384
407
|
raise # Re-raise assembly errors
|
385
408
|
except Exception as e:
|
386
409
|
logger.warning(
|
387
|
-
f"Error checking assembly status for {file_name}: {e}"
|
410
|
+
f"Error checking assembly status for {file_name}: {e!r}."
|
388
411
|
)
|
389
412
|
|
390
413
|
# Don't sleep if all files are complete
|
@@ -394,7 +417,7 @@ class RestClient:
|
|
394
417
|
if len(completed_files) < len(file_names):
|
395
418
|
remaining_files = set(file_names) - completed_files
|
396
419
|
logger.warning(
|
397
|
-
f"Assembly timeout for files: {remaining_files} after {timeout} seconds"
|
420
|
+
f"Assembly timeout for files: {remaining_files} after {timeout} seconds."
|
398
421
|
)
|
399
422
|
return False
|
400
423
|
|
@@ -413,7 +436,7 @@ class RestClient:
|
|
413
436
|
|
414
437
|
"""
|
415
438
|
if not path.is_dir():
|
416
|
-
raise JobFetchError(f"Path {path} is not a directory")
|
439
|
+
raise JobFetchError(f"Path {path} is not a directory.")
|
417
440
|
|
418
441
|
@staticmethod
|
419
442
|
def _validate_template_path(template_path: str | os.PathLike) -> None:
|
@@ -450,12 +473,13 @@ class RestClient:
|
|
450
473
|
|
451
474
|
"""
|
452
475
|
if not files:
|
453
|
-
raise TaskFetchError(f"No files found in {path}")
|
476
|
+
raise TaskFetchError(f"No files found in {path}.")
|
454
477
|
|
455
478
|
@retry(
|
456
479
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
457
480
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
458
481
|
retry=retry_if_connection_error,
|
482
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
459
483
|
)
|
460
484
|
def get_task(
|
461
485
|
self, task_id: str | None = None, history: bool = False, verbose: bool = False
|
@@ -496,6 +520,7 @@ class RestClient:
|
|
496
520
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
497
521
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
498
522
|
retry=retry_if_connection_error,
|
523
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
499
524
|
)
|
500
525
|
async def aget_task(
|
501
526
|
self, task_id: str | None = None, history: bool = False, verbose: bool = False
|
@@ -519,7 +544,7 @@ class RestClient:
|
|
519
544
|
) as response:
|
520
545
|
if response.status_code in {401, 403}:
|
521
546
|
raise PermissionError(
|
522
|
-
f"Error getting task: Permission denied for task {task_id}"
|
547
|
+
f"Error getting task: Permission denied for task {task_id}."
|
523
548
|
)
|
524
549
|
response.raise_for_status()
|
525
550
|
json_data = "".join([chunk async for chunk in response.aiter_text()])
|
@@ -536,6 +561,53 @@ class RestClient:
|
|
536
561
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
537
562
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
538
563
|
retry=retry_if_connection_error,
|
564
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
565
|
+
)
|
566
|
+
def cancel_task(self, task_id: str | None = None) -> bool:
|
567
|
+
"""Cancel a specific task/trajectory."""
|
568
|
+
task_id = task_id or self.trajectory_id
|
569
|
+
url = f"/v0.1/trajectories/{task_id}/cancel"
|
570
|
+
full_url = f"{self.base_url}{url}"
|
571
|
+
|
572
|
+
with external_trace(
|
573
|
+
url=full_url,
|
574
|
+
method="POST",
|
575
|
+
library="httpx",
|
576
|
+
custom_params={
|
577
|
+
"operation": "cancel_job",
|
578
|
+
"job_id": task_id,
|
579
|
+
},
|
580
|
+
):
|
581
|
+
get_task_response = self.get_task(task_id)
|
582
|
+
# cancel if task is in progress
|
583
|
+
if get_task_response.status == ExecutionStatus.IN_PROGRESS.value:
|
584
|
+
response = self.client.post(url)
|
585
|
+
try:
|
586
|
+
response.raise_for_status()
|
587
|
+
|
588
|
+
except HTTPStatusError as e:
|
589
|
+
if e.response.status_code in {
|
590
|
+
HTTPStatus.UNAUTHORIZED,
|
591
|
+
HTTPStatus.FORBIDDEN,
|
592
|
+
}:
|
593
|
+
raise PermissionError(
|
594
|
+
f"Error canceling task: Permission denied for task {task_id}"
|
595
|
+
) from e
|
596
|
+
if e.response.status_code == HTTPStatus.NOT_FOUND:
|
597
|
+
raise TaskFetchError(
|
598
|
+
f"Error canceling task: Trajectory not found for task {task_id}"
|
599
|
+
) from e
|
600
|
+
raise
|
601
|
+
|
602
|
+
get_task_response = self.get_task(task_id)
|
603
|
+
return get_task_response.status == ExecutionStatus.CANCELLED.value
|
604
|
+
return False
|
605
|
+
|
606
|
+
@retry(
|
607
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
608
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
609
|
+
retry=retry_if_connection_error,
|
610
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
539
611
|
)
|
540
612
|
def create_task(
|
541
613
|
self,
|
@@ -560,7 +632,7 @@ class RestClient:
|
|
560
632
|
)
|
561
633
|
if response.status_code in {401, 403}:
|
562
634
|
raise PermissionError(
|
563
|
-
f"Error creating task: Permission denied for task {task_data.name}"
|
635
|
+
f"Error creating task: Permission denied for task {task_data.name}."
|
564
636
|
)
|
565
637
|
response.raise_for_status()
|
566
638
|
trajectory_id = response.json()["trajectory_id"]
|
@@ -571,6 +643,7 @@ class RestClient:
|
|
571
643
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
572
644
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
573
645
|
retry=retry_if_connection_error,
|
646
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
574
647
|
)
|
575
648
|
async def acreate_task(
|
576
649
|
self,
|
@@ -595,7 +668,7 @@ class RestClient:
|
|
595
668
|
)
|
596
669
|
if response.status_code in {401, 403}:
|
597
670
|
raise PermissionError(
|
598
|
-
f"Error creating task: Permission denied for task {task_data.name}"
|
671
|
+
f"Error creating task: Permission denied for task {task_data.name}."
|
599
672
|
)
|
600
673
|
response.raise_for_status()
|
601
674
|
trajectory_id = response.json()["trajectory_id"]
|
@@ -747,6 +820,7 @@ class RestClient:
|
|
747
820
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
748
821
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
749
822
|
retry=retry_if_connection_error,
|
823
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
750
824
|
)
|
751
825
|
def get_build_status(self, build_id: UUID | None = None) -> dict[str, Any]:
|
752
826
|
"""Get the status of a build."""
|
@@ -755,7 +829,7 @@ class RestClient:
|
|
755
829
|
response = self.client.get(f"/v0.1/builds/{build_id}")
|
756
830
|
response.raise_for_status()
|
757
831
|
except Exception as e:
|
758
|
-
raise JobFetchError(f"Error getting build status: {e!
|
832
|
+
raise JobFetchError(f"Error getting build status: {e!r}.") from e
|
759
833
|
return response.json()
|
760
834
|
|
761
835
|
# TODO: Refactor later so we don't have to ignore PLR0915
|
@@ -763,6 +837,7 @@ class RestClient:
|
|
763
837
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
764
838
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
765
839
|
retry=retry_if_connection_error,
|
840
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
766
841
|
)
|
767
842
|
def create_job(self, config: JobDeploymentConfig) -> dict[str, Any]: # noqa: PLR0915
|
768
843
|
"""Creates a futurehouse job deployment from the environment and environment files.
|
@@ -920,6 +995,13 @@ class RestClient:
|
|
920
995
|
if config.task_queues_config
|
921
996
|
else None
|
922
997
|
),
|
998
|
+
"user_input_config": (
|
999
|
+
json.dumps([
|
1000
|
+
entity.model_dump() for entity in config.user_input_config
|
1001
|
+
])
|
1002
|
+
if config.user_input_config
|
1003
|
+
else None
|
1004
|
+
),
|
923
1005
|
}
|
924
1006
|
response = self.multipart_client.post(
|
925
1007
|
"/v0.1/builds",
|
@@ -940,10 +1022,10 @@ class RestClient:
|
|
940
1022
|
error_detail = response.json()
|
941
1023
|
error_message = error_detail.get("detail", str(e))
|
942
1024
|
raise JobCreationError(
|
943
|
-
f"Server validation error: {error_message}"
|
1025
|
+
f"Server validation error: {error_message}."
|
944
1026
|
) from e
|
945
1027
|
except Exception as e:
|
946
|
-
raise JobCreationError(f"Error generating docker image: {e!
|
1028
|
+
raise JobCreationError(f"Error generating docker image: {e!r}.") from e
|
947
1029
|
return build_context
|
948
1030
|
|
949
1031
|
# TODO: we should have have an async upload_file, check_assembly_status,
|
@@ -952,6 +1034,7 @@ class RestClient:
|
|
952
1034
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
953
1035
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
954
1036
|
retry=retry_if_connection_error,
|
1037
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
955
1038
|
)
|
956
1039
|
def upload_file(
|
957
1040
|
self,
|
@@ -999,7 +1082,7 @@ class RestClient:
|
|
999
1082
|
)
|
1000
1083
|
if not success:
|
1001
1084
|
raise RestClientError(
|
1002
|
-
f"Assembly failed or timed out for one or more files: {uploaded_files}"
|
1085
|
+
f"Assembly failed or timed out for one or more files: {uploaded_files}."
|
1003
1086
|
)
|
1004
1087
|
|
1005
1088
|
logger.info(f"Successfully uploaded {file_path} to {upload_id}")
|
@@ -1044,7 +1127,9 @@ class RestClient:
|
|
1044
1127
|
)
|
1045
1128
|
uploaded_files.append(file_name)
|
1046
1129
|
except Exception as e:
|
1047
|
-
raise FileUploadError(
|
1130
|
+
raise FileUploadError(
|
1131
|
+
f"Error uploading directory {dir_path}: {e!r}."
|
1132
|
+
) from e
|
1048
1133
|
|
1049
1134
|
return uploaded_files
|
1050
1135
|
|
@@ -1095,7 +1180,7 @@ class RestClient:
|
|
1095
1180
|
logger.info(f"Successfully uploaded {file_name}")
|
1096
1181
|
except Exception as e:
|
1097
1182
|
logger.exception(f"Error uploading file {file_path}")
|
1098
|
-
raise FileUploadError(f"Error uploading file {file_path}: {e}") from e
|
1183
|
+
raise FileUploadError(f"Error uploading file {file_path}: {e!r}.") from e
|
1099
1184
|
return status_url
|
1100
1185
|
|
1101
1186
|
def _upload_chunks_parallel(
|
@@ -1156,9 +1241,9 @@ class RestClient:
|
|
1156
1241
|
f"Uploaded chunk {chunk_index + 1}/{total_chunks} of {file_name}"
|
1157
1242
|
)
|
1158
1243
|
except Exception as e:
|
1159
|
-
logger.error(f"Error uploading chunk {chunk_index}: {e}")
|
1244
|
+
logger.error(f"Error uploading chunk {chunk_index}: {e!r}.")
|
1160
1245
|
raise FileUploadError(
|
1161
|
-
f"Error uploading chunk {chunk_index} of {file_name}: {e}"
|
1246
|
+
f"Error uploading chunk {chunk_index} of {file_name}: {e!r}."
|
1162
1247
|
) from e
|
1163
1248
|
|
1164
1249
|
# Upload the final chunk with retry logic
|
@@ -1224,11 +1309,11 @@ class RestClient:
|
|
1224
1309
|
except Exception as e:
|
1225
1310
|
if retries >= max_retries - 1:
|
1226
1311
|
raise FileUploadError(
|
1227
|
-
f"Error uploading final chunk of {file_name}: {e}"
|
1312
|
+
f"Error uploading final chunk of {file_name}: {e!r}."
|
1228
1313
|
) from e
|
1229
1314
|
retries += 1
|
1230
1315
|
logger.warning(
|
1231
|
-
f"Error uploading final chunk of {file_name}, retrying in {retry_delay}s... (attempt {retries}/{max_retries}): {e}"
|
1316
|
+
f"Error uploading final chunk of {file_name}, retrying in {retry_delay}s... (attempt {retries}/{max_retries}): {e!r}."
|
1232
1317
|
)
|
1233
1318
|
time.sleep(retry_delay)
|
1234
1319
|
|
@@ -1296,6 +1381,7 @@ class RestClient:
|
|
1296
1381
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1297
1382
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1298
1383
|
retry=retry_if_connection_error,
|
1384
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1299
1385
|
)
|
1300
1386
|
def list_files(
|
1301
1387
|
self,
|
@@ -1318,7 +1404,7 @@ class RestClient:
|
|
1318
1404
|
"""
|
1319
1405
|
if not bool(trajectory_id) ^ bool(upload_id):
|
1320
1406
|
raise RestClientError(
|
1321
|
-
"Must at least specify one of trajectory_id or upload_id, but not both"
|
1407
|
+
"Must at least specify one of trajectory_id or upload_id, but not both."
|
1322
1408
|
)
|
1323
1409
|
try:
|
1324
1410
|
url = f"/v0.1/crows/{job_name}/list-files"
|
@@ -1332,18 +1418,19 @@ class RestClient:
|
|
1332
1418
|
f"Error listing files for job {job_name}, trajectory {trajectory_id}, upload_id {upload_id}: {e.response.text}"
|
1333
1419
|
)
|
1334
1420
|
raise RestClientError(
|
1335
|
-
f"Error listing files: {e.response.status_code} - {e.response.text}"
|
1421
|
+
f"Error listing files: {e.response.status_code} - {e.response.text}."
|
1336
1422
|
) from e
|
1337
1423
|
except Exception as e:
|
1338
1424
|
logger.exception(
|
1339
1425
|
f"Error listing files for job {job_name}, trajectory {trajectory_id}, upload_id {upload_id}"
|
1340
1426
|
)
|
1341
|
-
raise RestClientError(f"Error listing files: {e!
|
1427
|
+
raise RestClientError(f"Error listing files: {e!r}.") from e
|
1342
1428
|
|
1343
1429
|
@retry(
|
1344
1430
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1345
1431
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1346
1432
|
retry=retry_if_connection_error,
|
1433
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1347
1434
|
)
|
1348
1435
|
def download_file(
|
1349
1436
|
self,
|
@@ -1394,7 +1481,7 @@ class RestClient:
|
|
1394
1481
|
if destination_path.exists():
|
1395
1482
|
destination_path.unlink()
|
1396
1483
|
raise RestClientError(
|
1397
|
-
f"Error downloading file: {e.response.status_code} - {e.response.text}"
|
1484
|
+
f"Error downloading file: {e.response.status_code} - {e.response.text}."
|
1398
1485
|
) from e
|
1399
1486
|
except RemoteProtocolError as e:
|
1400
1487
|
logger.error(
|
@@ -1413,12 +1500,13 @@ class RestClient:
|
|
1413
1500
|
)
|
1414
1501
|
if destination_path.exists():
|
1415
1502
|
destination_path.unlink() # Clean up partial file
|
1416
|
-
raise RestClientError(f"Error downloading file: {e!
|
1503
|
+
raise RestClientError(f"Error downloading file: {e!r}.") from e
|
1417
1504
|
|
1418
1505
|
@retry(
|
1419
1506
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1420
1507
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1421
1508
|
retry=retry_if_connection_error,
|
1509
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1422
1510
|
)
|
1423
1511
|
def get_world_model(
|
1424
1512
|
self, world_model_id: UUID | None = None, name: str | None = None
|
@@ -1450,15 +1538,16 @@ class RestClient:
|
|
1450
1538
|
"World model not found with the specified identifier."
|
1451
1539
|
) from e
|
1452
1540
|
raise WorldModelFetchError(
|
1453
|
-
f"Error fetching world model: {e.response.status_code} - {e.response.text}"
|
1541
|
+
f"Error fetching world model: {e.response.status_code} - {e.response.text}."
|
1454
1542
|
) from e
|
1455
1543
|
except Exception as e:
|
1456
|
-
raise WorldModelFetchError(f"An unexpected error occurred: {e}") from e
|
1544
|
+
raise WorldModelFetchError(f"An unexpected error occurred: {e!r}.") from e
|
1457
1545
|
|
1458
1546
|
@retry(
|
1459
1547
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1460
1548
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1461
1549
|
retry=retry_if_connection_error,
|
1550
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1462
1551
|
)
|
1463
1552
|
async def aget_world_model(
|
1464
1553
|
self, world_model_id: UUID | None = None, name: str | None = None
|
@@ -1490,15 +1579,16 @@ class RestClient:
|
|
1490
1579
|
"World model not found with the specified identifier."
|
1491
1580
|
) from e
|
1492
1581
|
raise WorldModelFetchError(
|
1493
|
-
f"Error fetching world model: {e.response.status_code} - {e.response.text}"
|
1582
|
+
f"Error fetching world model: {e.response.status_code} - {e.response.text}."
|
1494
1583
|
) from e
|
1495
1584
|
except Exception as e:
|
1496
|
-
raise WorldModelFetchError(f"An unexpected error occurred: {e}") from e
|
1585
|
+
raise WorldModelFetchError(f"An unexpected error occurred: {e!r}.") from e
|
1497
1586
|
|
1498
1587
|
@retry(
|
1499
1588
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1500
1589
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1501
1590
|
retry=retry_if_connection_error,
|
1591
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1502
1592
|
)
|
1503
1593
|
def create_world_model(self, payload: WorldModel) -> UUID:
|
1504
1594
|
"""Create a new, immutable world model snapshot.
|
@@ -1522,20 +1612,21 @@ class RestClient:
|
|
1522
1612
|
except HTTPStatusError as e:
|
1523
1613
|
if e.response.status_code == codes.BAD_REQUEST:
|
1524
1614
|
raise WorldModelCreationError(
|
1525
|
-
f"Invalid payload for world model creation: {e.response.text}"
|
1615
|
+
f"Invalid payload for world model creation: {e.response.text}."
|
1526
1616
|
) from e
|
1527
1617
|
raise WorldModelCreationError(
|
1528
|
-
f"Error creating world model: {e.response.status_code} - {e.response.text}"
|
1618
|
+
f"Error creating world model: {e.response.status_code} - {e.response.text}."
|
1529
1619
|
) from e
|
1530
1620
|
except Exception as e:
|
1531
1621
|
raise WorldModelCreationError(
|
1532
|
-
f"An unexpected error occurred during world model creation: {e}"
|
1622
|
+
f"An unexpected error occurred during world model creation: {e!r}."
|
1533
1623
|
) from e
|
1534
1624
|
|
1535
1625
|
@retry(
|
1536
1626
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1537
1627
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1538
1628
|
retry=retry_if_connection_error,
|
1629
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1539
1630
|
)
|
1540
1631
|
async def acreate_world_model(self, payload: WorldModel) -> UUID:
|
1541
1632
|
"""Asynchronously create a new, immutable world model snapshot.
|
@@ -1558,14 +1649,14 @@ class RestClient:
|
|
1558
1649
|
except HTTPStatusError as e:
|
1559
1650
|
if e.response.status_code == codes.BAD_REQUEST:
|
1560
1651
|
raise WorldModelCreationError(
|
1561
|
-
f"Invalid payload for world model creation: {e.response.text}"
|
1652
|
+
f"Invalid payload for world model creation: {e.response.text}."
|
1562
1653
|
) from e
|
1563
1654
|
raise WorldModelCreationError(
|
1564
|
-
f"Error creating world model: {e.response.status_code} - {e.response.text}"
|
1655
|
+
f"Error creating world model: {e.response.status_code} - {e.response.text}."
|
1565
1656
|
) from e
|
1566
1657
|
except Exception as e:
|
1567
1658
|
raise WorldModelCreationError(
|
1568
|
-
f"An unexpected error occurred during world model creation: {e}"
|
1659
|
+
f"An unexpected error occurred during world model creation: {e!r}."
|
1569
1660
|
) from e
|
1570
1661
|
|
1571
1662
|
@retry(
|
@@ -1771,6 +1862,470 @@ class RestClient:
|
|
1771
1862
|
except Exception as e:
|
1772
1863
|
raise ProjectError(f"Error adding trajectory to project: {e}") from e
|
1773
1864
|
|
1865
|
+
@retry(
|
1866
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1867
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1868
|
+
retry=retry_if_connection_error,
|
1869
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1870
|
+
)
|
1871
|
+
def get_tasks(
|
1872
|
+
self,
|
1873
|
+
query_params: TrajectoryQueryParams | None = None,
|
1874
|
+
*,
|
1875
|
+
project_id: UUID | None = None,
|
1876
|
+
name: str | None = None,
|
1877
|
+
user: str | None = None,
|
1878
|
+
limit: int = 50,
|
1879
|
+
offset: int = 0,
|
1880
|
+
sort_by: str = "created_at",
|
1881
|
+
sort_order: str = "desc",
|
1882
|
+
) -> list[dict[str, Any]]:
|
1883
|
+
"""Fetches trajectories with applied filtering.
|
1884
|
+
|
1885
|
+
Args:
|
1886
|
+
query_params: Optional TrajectoryQueryParams model with all parameters
|
1887
|
+
project_id: Optional project ID to filter trajectories by
|
1888
|
+
name: Optional name filter for trajectories
|
1889
|
+
user: Optional user email filter for trajectories
|
1890
|
+
limit: Maximum number of trajectories to return (default: 50, max: 200)
|
1891
|
+
offset: Number of trajectories to skip for pagination (default: 0)
|
1892
|
+
sort_by: Field to sort by, either "created_at" or "name" (default: "created_at")
|
1893
|
+
sort_order: Sort order, either "asc" or "desc" (default: "desc")
|
1894
|
+
|
1895
|
+
Returns:
|
1896
|
+
List of trajectory dictionaries
|
1897
|
+
|
1898
|
+
Raises:
|
1899
|
+
TaskFetchError: If there's an error fetching trajectories
|
1900
|
+
"""
|
1901
|
+
try:
|
1902
|
+
if query_params is not None:
|
1903
|
+
params = query_params.to_query_params()
|
1904
|
+
else:
|
1905
|
+
params_model = TrajectoryQueryParams(
|
1906
|
+
project_id=project_id,
|
1907
|
+
name=name,
|
1908
|
+
user=user,
|
1909
|
+
limit=limit,
|
1910
|
+
offset=offset,
|
1911
|
+
sort_by=sort_by,
|
1912
|
+
sort_order=sort_order,
|
1913
|
+
)
|
1914
|
+
params = params_model.to_query_params()
|
1915
|
+
|
1916
|
+
response = self.client.get("/v0.1/trajectories", params=params)
|
1917
|
+
response.raise_for_status()
|
1918
|
+
return response.json()
|
1919
|
+
except HTTPStatusError as e:
|
1920
|
+
if e.response.status_code in {401, 403}:
|
1921
|
+
raise PermissionError(
|
1922
|
+
"Error getting trajectories: Permission denied"
|
1923
|
+
) from e
|
1924
|
+
raise TaskFetchError(
|
1925
|
+
f"Error getting trajectories: {e.response.status_code} - {e.response.text}"
|
1926
|
+
) from e
|
1927
|
+
except Exception as e:
|
1928
|
+
raise TaskFetchError(f"Error getting trajectories: {e!r}") from e
|
1929
|
+
|
1930
|
+
@retry(
|
1931
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1932
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1933
|
+
retry=retry_if_connection_error,
|
1934
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
1935
|
+
)
|
1936
|
+
async def aget_tasks(
|
1937
|
+
self,
|
1938
|
+
query_params: TrajectoryQueryParams | None = None,
|
1939
|
+
*,
|
1940
|
+
project_id: UUID | None = None,
|
1941
|
+
name: str | None = None,
|
1942
|
+
user: str | None = None,
|
1943
|
+
limit: int = 50,
|
1944
|
+
offset: int = 0,
|
1945
|
+
sort_by: str = "created_at",
|
1946
|
+
sort_order: str = "desc",
|
1947
|
+
) -> list[dict[str, Any]]:
|
1948
|
+
"""Asynchronously fetch trajectories with applied filtering.
|
1949
|
+
|
1950
|
+
Args:
|
1951
|
+
query_params: Optional TrajectoryQueryParams model with all parameters
|
1952
|
+
project_id: Optional project ID to filter trajectories by
|
1953
|
+
name: Optional name filter for trajectories
|
1954
|
+
user: Optional user email filter for trajectories
|
1955
|
+
limit: Maximum number of trajectories to return (default: 50, max: 200)
|
1956
|
+
offset: Number of trajectories to skip for pagination (default: 0)
|
1957
|
+
sort_by: Field to sort by, either "created_at" or "name" (default: "created_at")
|
1958
|
+
sort_order: Sort order, either "asc" or "desc" (default: "desc")
|
1959
|
+
|
1960
|
+
Returns:
|
1961
|
+
List of trajectory dictionaries
|
1962
|
+
|
1963
|
+
Raises:
|
1964
|
+
TaskFetchError: If there's an error fetching trajectories
|
1965
|
+
"""
|
1966
|
+
try:
|
1967
|
+
if query_params is not None:
|
1968
|
+
params = query_params.to_query_params()
|
1969
|
+
else:
|
1970
|
+
params_model = TrajectoryQueryParams(
|
1971
|
+
project_id=project_id,
|
1972
|
+
name=name,
|
1973
|
+
user=user,
|
1974
|
+
limit=limit,
|
1975
|
+
offset=offset,
|
1976
|
+
sort_by=sort_by,
|
1977
|
+
sort_order=sort_order,
|
1978
|
+
)
|
1979
|
+
params = params_model.to_query_params()
|
1980
|
+
|
1981
|
+
response = await self.async_client.get("/v0.1/trajectories", params=params)
|
1982
|
+
response.raise_for_status()
|
1983
|
+
return response.json()
|
1984
|
+
except HTTPStatusError as e:
|
1985
|
+
if e.response.status_code in {401, 403}:
|
1986
|
+
raise PermissionError(
|
1987
|
+
"Error getting trajectories: Permission denied"
|
1988
|
+
) from e
|
1989
|
+
raise TaskFetchError(
|
1990
|
+
f"Error getting trajectories: {e.response.status_code} - {e.response.text}"
|
1991
|
+
) from e
|
1992
|
+
except Exception as e:
|
1993
|
+
raise TaskFetchError(f"Error getting trajectories: {e!r}") from e
|
1994
|
+
|
1995
|
+
@retry(
|
1996
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1997
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1998
|
+
retry=retry_if_connection_error,
|
1999
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2000
|
+
)
|
2001
|
+
def list_user_agent_requests(
|
2002
|
+
self,
|
2003
|
+
user_id: str | None = None,
|
2004
|
+
trajectory_id: UUID | None = None,
|
2005
|
+
request_status: UserAgentRequestStatus | None = None,
|
2006
|
+
limit: int = 50,
|
2007
|
+
offset: int = 0,
|
2008
|
+
) -> list[UserAgentRequest]:
|
2009
|
+
"""List user agent requests with optional filters.
|
2010
|
+
|
2011
|
+
Args:
|
2012
|
+
user_id: Filter requests by user ID. Defaults to the authenticated user's ID if not provided.
|
2013
|
+
trajectory_id: Filter requests by trajectory ID.
|
2014
|
+
request_status: Filter requests by status (e.g., PENDING).
|
2015
|
+
limit: Maximum number of requests to return.
|
2016
|
+
offset: Offset for pagination.
|
2017
|
+
|
2018
|
+
Returns:
|
2019
|
+
A list of user agent requests.
|
2020
|
+
|
2021
|
+
Raises:
|
2022
|
+
UserAgentRequestFetchError: If the API call fails.
|
2023
|
+
"""
|
2024
|
+
params = {
|
2025
|
+
"user_id": user_id,
|
2026
|
+
"trajectory_id": str(trajectory_id) if trajectory_id else None,
|
2027
|
+
"request_status": request_status.value if request_status else None,
|
2028
|
+
"limit": limit,
|
2029
|
+
"offset": offset,
|
2030
|
+
}
|
2031
|
+
# Filter out None values
|
2032
|
+
params = {k: v for k, v in params.items() if v is not None}
|
2033
|
+
try:
|
2034
|
+
response = self.client.get("/v0.1/user-agent-requests", params=params)
|
2035
|
+
response.raise_for_status()
|
2036
|
+
return [UserAgentRequest.model_validate(item) for item in response.json()]
|
2037
|
+
except HTTPStatusError as e:
|
2038
|
+
raise UserAgentRequestFetchError(
|
2039
|
+
f"Error listing user agent requests: {e.response.status_code} - {e.response.text}"
|
2040
|
+
) from e
|
2041
|
+
except Exception as e:
|
2042
|
+
raise UserAgentRequestFetchError(
|
2043
|
+
f"An unexpected error occurred: {e!r}"
|
2044
|
+
) from e
|
2045
|
+
|
2046
|
+
@retry(
|
2047
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2048
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2049
|
+
retry=retry_if_connection_error,
|
2050
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2051
|
+
)
|
2052
|
+
async def alist_user_agent_requests(
|
2053
|
+
self,
|
2054
|
+
user_id: str | None = None,
|
2055
|
+
trajectory_id: UUID | None = None,
|
2056
|
+
request_status: UserAgentRequestStatus | None = None,
|
2057
|
+
limit: int = 50,
|
2058
|
+
offset: int = 0,
|
2059
|
+
) -> list[UserAgentRequest]:
|
2060
|
+
"""Asynchronously list user agent requests with optional filters.
|
2061
|
+
|
2062
|
+
Args:
|
2063
|
+
user_id: Filter requests by user ID. Defaults to the authenticated user's ID if not provided.
|
2064
|
+
trajectory_id: Filter requests by trajectory ID.
|
2065
|
+
request_status: Filter requests by status (e.g., PENDING).
|
2066
|
+
limit: Maximum number of requests to return.
|
2067
|
+
offset: Offset for pagination.
|
2068
|
+
|
2069
|
+
Returns:
|
2070
|
+
A list of user agent requests.
|
2071
|
+
|
2072
|
+
Raises:
|
2073
|
+
UserAgentRequestFetchError: If the API call fails.
|
2074
|
+
"""
|
2075
|
+
params = {
|
2076
|
+
"user_id": user_id,
|
2077
|
+
"trajectory_id": str(trajectory_id) if trajectory_id else None,
|
2078
|
+
"request_status": request_status.value if request_status else None,
|
2079
|
+
"limit": limit,
|
2080
|
+
"offset": offset,
|
2081
|
+
}
|
2082
|
+
# Filter out None values
|
2083
|
+
params = {k: v for k, v in params.items() if v is not None}
|
2084
|
+
try:
|
2085
|
+
response = await self.async_client.get(
|
2086
|
+
"/v0.1/user-agent-requests", params=params
|
2087
|
+
)
|
2088
|
+
response.raise_for_status()
|
2089
|
+
return [UserAgentRequest.model_validate(item) for item in response.json()]
|
2090
|
+
except HTTPStatusError as e:
|
2091
|
+
raise UserAgentRequestFetchError(
|
2092
|
+
f"Error listing user agent requests: {e.response.status_code} - {e.response.text}"
|
2093
|
+
) from e
|
2094
|
+
except Exception as e:
|
2095
|
+
raise UserAgentRequestFetchError(
|
2096
|
+
f"An unexpected error occurred: {e!r}"
|
2097
|
+
) from e
|
2098
|
+
|
2099
|
+
@retry(
|
2100
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2101
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2102
|
+
retry=retry_if_connection_error,
|
2103
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2104
|
+
)
|
2105
|
+
def get_user_agent_request(self, request_id: UUID) -> UserAgentRequest:
|
2106
|
+
"""Retrieve a single user agent request by its unique ID.
|
2107
|
+
|
2108
|
+
Args:
|
2109
|
+
request_id: The unique ID of the request.
|
2110
|
+
|
2111
|
+
Returns:
|
2112
|
+
The user agent request.
|
2113
|
+
|
2114
|
+
Raises:
|
2115
|
+
UserAgentRequestFetchError: If the API call fails or the request is not found.
|
2116
|
+
"""
|
2117
|
+
try:
|
2118
|
+
response = self.client.get(f"/v0.1/user-agent-requests/{request_id}")
|
2119
|
+
response.raise_for_status()
|
2120
|
+
return UserAgentRequest.model_validate(response.json())
|
2121
|
+
except HTTPStatusError as e:
|
2122
|
+
if e.response.status_code == codes.NOT_FOUND:
|
2123
|
+
raise UserAgentRequestFetchError(
|
2124
|
+
f"User agent request with ID {request_id} not found."
|
2125
|
+
) from e
|
2126
|
+
raise UserAgentRequestFetchError(
|
2127
|
+
f"Error fetching user agent request: {e.response.status_code} - {e.response.text}"
|
2128
|
+
) from e
|
2129
|
+
except Exception as e:
|
2130
|
+
raise UserAgentRequestFetchError(
|
2131
|
+
f"An unexpected error occurred: {e!r}"
|
2132
|
+
) from e
|
2133
|
+
|
2134
|
+
@retry(
|
2135
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2136
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2137
|
+
retry=retry_if_connection_error,
|
2138
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2139
|
+
)
|
2140
|
+
async def aget_user_agent_request(self, request_id: UUID) -> UserAgentRequest:
|
2141
|
+
"""Asynchronously retrieve a single user agent request by its unique ID.
|
2142
|
+
|
2143
|
+
Args:
|
2144
|
+
request_id: The unique ID of the request.
|
2145
|
+
|
2146
|
+
Returns:
|
2147
|
+
The user agent request.
|
2148
|
+
|
2149
|
+
Raises:
|
2150
|
+
UserAgentRequestFetchError: If the API call fails or the request is not found.
|
2151
|
+
"""
|
2152
|
+
try:
|
2153
|
+
response = await self.async_client.get(
|
2154
|
+
f"/v0.1/user-agent-requests/{request_id}"
|
2155
|
+
)
|
2156
|
+
response.raise_for_status()
|
2157
|
+
return UserAgentRequest.model_validate(response.json())
|
2158
|
+
except HTTPStatusError as e:
|
2159
|
+
if e.response.status_code == codes.NOT_FOUND:
|
2160
|
+
raise UserAgentRequestFetchError(
|
2161
|
+
f"User agent request with ID {request_id} not found."
|
2162
|
+
) from e
|
2163
|
+
raise UserAgentRequestFetchError(
|
2164
|
+
f"Error fetching user agent request: {e.response.status_code} - {e.response.text}"
|
2165
|
+
) from e
|
2166
|
+
except Exception as e:
|
2167
|
+
raise UserAgentRequestFetchError(
|
2168
|
+
f"An unexpected error occurred: {e!r}"
|
2169
|
+
) from e
|
2170
|
+
|
2171
|
+
@retry(
|
2172
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2173
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2174
|
+
retry=retry_if_connection_error,
|
2175
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2176
|
+
)
|
2177
|
+
def create_user_agent_request(self, payload: UserAgentRequestPostPayload) -> UUID:
|
2178
|
+
"""Creates a new request from an agent to a user.
|
2179
|
+
|
2180
|
+
Args:
|
2181
|
+
payload: An instance of UserAgentRequestPostPayload with the request data.
|
2182
|
+
|
2183
|
+
Returns:
|
2184
|
+
The UUID of the newly created user agent request.
|
2185
|
+
|
2186
|
+
Raises:
|
2187
|
+
UserAgentRequestCreationError: If the API call fails.
|
2188
|
+
"""
|
2189
|
+
try:
|
2190
|
+
response = self.client.post(
|
2191
|
+
"/v0.1/user-agent-requests", json=payload.model_dump(mode="json")
|
2192
|
+
)
|
2193
|
+
response.raise_for_status()
|
2194
|
+
return UUID(response.json())
|
2195
|
+
except HTTPStatusError as e:
|
2196
|
+
if e.response.status_code == codes.UNPROCESSABLE_ENTITY:
|
2197
|
+
raise UserAgentRequestCreationError(
|
2198
|
+
f"Invalid payload for user agent request creation: {e.response.text}."
|
2199
|
+
) from e
|
2200
|
+
raise UserAgentRequestCreationError(
|
2201
|
+
f"Error creating user agent request: {e.response.status_code} - {e.response.text}"
|
2202
|
+
) from e
|
2203
|
+
except Exception as e:
|
2204
|
+
raise UserAgentRequestCreationError(
|
2205
|
+
f"An unexpected error occurred during user agent request creation: {e!r}."
|
2206
|
+
) from e
|
2207
|
+
|
2208
|
+
@retry(
|
2209
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2210
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2211
|
+
retry=retry_if_connection_error,
|
2212
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2213
|
+
)
|
2214
|
+
async def acreate_user_agent_request(
|
2215
|
+
self, payload: UserAgentRequestPostPayload
|
2216
|
+
) -> UUID:
|
2217
|
+
"""Asynchronously creates a new request from an agent to a user.
|
2218
|
+
|
2219
|
+
Args:
|
2220
|
+
payload: An instance of UserAgentRequestPostPayload with the request data.
|
2221
|
+
|
2222
|
+
Returns:
|
2223
|
+
The UUID of the newly created user agent request.
|
2224
|
+
|
2225
|
+
Raises:
|
2226
|
+
UserAgentRequestCreationError: If the API call fails.
|
2227
|
+
"""
|
2228
|
+
try:
|
2229
|
+
response = await self.async_client.post(
|
2230
|
+
"/v0.1/user-agent-requests", json=payload.model_dump(mode="json")
|
2231
|
+
)
|
2232
|
+
response.raise_for_status()
|
2233
|
+
return UUID(response.json())
|
2234
|
+
except HTTPStatusError as e:
|
2235
|
+
if e.response.status_code == codes.UNPROCESSABLE_ENTITY:
|
2236
|
+
raise UserAgentRequestCreationError(
|
2237
|
+
f"Invalid payload for user agent request creation: {e.response.text}."
|
2238
|
+
) from e
|
2239
|
+
raise UserAgentRequestCreationError(
|
2240
|
+
f"Error creating user agent request: {e.response.status_code} - {e.response.text}"
|
2241
|
+
) from e
|
2242
|
+
except Exception as e:
|
2243
|
+
raise UserAgentRequestCreationError(
|
2244
|
+
f"An unexpected error occurred during user agent request creation: {e!r}."
|
2245
|
+
) from e
|
2246
|
+
|
2247
|
+
@retry(
|
2248
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2249
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2250
|
+
retry=retry_if_connection_error,
|
2251
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2252
|
+
)
|
2253
|
+
def respond_to_user_agent_request(
|
2254
|
+
self, request_id: UUID, payload: UserAgentResponsePayload
|
2255
|
+
) -> None:
|
2256
|
+
"""Submit a user's response to a pending agent request.
|
2257
|
+
|
2258
|
+
Args:
|
2259
|
+
request_id: The unique ID of the request to respond to.
|
2260
|
+
payload: An instance of UserAgentResponsePayload with the response data.
|
2261
|
+
|
2262
|
+
Raises:
|
2263
|
+
UserAgentRequestResponseError: If the API call fails.
|
2264
|
+
"""
|
2265
|
+
try:
|
2266
|
+
response = self.client.post(
|
2267
|
+
f"/v0.1/user-agent-requests/{request_id}/response",
|
2268
|
+
json=payload.model_dump(mode="json"),
|
2269
|
+
)
|
2270
|
+
response.raise_for_status()
|
2271
|
+
except HTTPStatusError as e:
|
2272
|
+
if e.response.status_code == codes.NOT_FOUND:
|
2273
|
+
raise UserAgentRequestResponseError(
|
2274
|
+
f"User agent request with ID {request_id} not found."
|
2275
|
+
) from e
|
2276
|
+
if e.response.status_code == codes.UNPROCESSABLE_ENTITY:
|
2277
|
+
raise UserAgentRequestResponseError(
|
2278
|
+
f"Invalid response payload: {e.response.text}."
|
2279
|
+
) from e
|
2280
|
+
raise UserAgentRequestResponseError(
|
2281
|
+
f"Error responding to user agent request: {e.response.status_code} - {e.response.text}"
|
2282
|
+
) from e
|
2283
|
+
except Exception as e:
|
2284
|
+
raise UserAgentRequestResponseError(
|
2285
|
+
f"An unexpected error occurred while responding to the request: {e!r}."
|
2286
|
+
) from e
|
2287
|
+
|
2288
|
+
@retry(
|
2289
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
2290
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
2291
|
+
retry=retry_if_connection_error,
|
2292
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
2293
|
+
)
|
2294
|
+
async def arespond_to_user_agent_request(
|
2295
|
+
self, request_id: UUID, payload: UserAgentResponsePayload
|
2296
|
+
) -> None:
|
2297
|
+
"""Asynchronously submit a user's response to a pending agent request.
|
2298
|
+
|
2299
|
+
Args:
|
2300
|
+
request_id: The unique ID of the request to respond to.
|
2301
|
+
payload: An instance of UserAgentResponsePayload with the response data.
|
2302
|
+
|
2303
|
+
Raises:
|
2304
|
+
UserAgentRequestResponseError: If the API call fails.
|
2305
|
+
"""
|
2306
|
+
try:
|
2307
|
+
response = await self.async_client.post(
|
2308
|
+
f"/v0.1/user-agent-requests/{request_id}/response",
|
2309
|
+
json=payload.model_dump(mode="json"),
|
2310
|
+
)
|
2311
|
+
response.raise_for_status()
|
2312
|
+
except HTTPStatusError as e:
|
2313
|
+
if e.response.status_code == codes.NOT_FOUND:
|
2314
|
+
raise UserAgentRequestResponseError(
|
2315
|
+
f"User agent request with ID {request_id} not found."
|
2316
|
+
) from e
|
2317
|
+
if e.response.status_code == codes.UNPROCESSABLE_ENTITY:
|
2318
|
+
raise UserAgentRequestResponseError(
|
2319
|
+
f"Invalid response payload: {e.response.text}."
|
2320
|
+
) from e
|
2321
|
+
raise UserAgentRequestResponseError(
|
2322
|
+
f"Error responding to user agent request: {e.response.status_code} - {e.response.text}"
|
2323
|
+
) from e
|
2324
|
+
except Exception as e:
|
2325
|
+
raise UserAgentRequestResponseError(
|
2326
|
+
f"An unexpected error occurred while responding to the request: {e!r}."
|
2327
|
+
) from e
|
2328
|
+
|
1774
2329
|
|
1775
2330
|
def get_installed_packages() -> dict[str, str]:
|
1776
2331
|
"""Returns a dictionary of installed packages and their versions."""
|
futurehouse_client/models/app.py
CHANGED
@@ -278,6 +278,20 @@ class FramePath(BaseModel):
|
|
278
278
|
)
|
279
279
|
|
280
280
|
|
281
|
+
class NamedEntity(BaseModel):
|
282
|
+
name: str = Field(
|
283
|
+
description=(
|
284
|
+
"Name of an entity for a user to provide a value to during query submission. "
|
285
|
+
"This will be used as a key to prompt users for a value. "
|
286
|
+
"Example: 'pdb' would result in <pdb>user input here</pdb> in the task string."
|
287
|
+
)
|
288
|
+
)
|
289
|
+
description: str | None = Field(
|
290
|
+
default=None,
|
291
|
+
description="Helper text to provide the user context to what the name or format needs to be.",
|
292
|
+
)
|
293
|
+
|
294
|
+
|
281
295
|
class DockerContainerConfiguration(BaseModel):
|
282
296
|
cpu: str = Field(description="CPU allotment for the container")
|
283
297
|
memory: str = Field(description="Memory allotment for the container")
|
@@ -458,6 +472,15 @@ class JobDeploymentConfig(BaseModel):
|
|
458
472
|
description="The configuration for the task queue(s) that will be created for this deployment.",
|
459
473
|
)
|
460
474
|
|
475
|
+
user_input_config: list[NamedEntity] | None = Field(
|
476
|
+
default=None,
|
477
|
+
description=(
|
478
|
+
"List of NamedEntity objects that represent user input fields "
|
479
|
+
"to be included in the task string. "
|
480
|
+
"These will be used to prompt users for values during query submission."
|
481
|
+
),
|
482
|
+
)
|
483
|
+
|
461
484
|
@field_validator("markdown_template_path")
|
462
485
|
@classmethod
|
463
486
|
def validate_markdown_path(
|
@@ -646,6 +669,62 @@ class RuntimeConfig(BaseModel):
|
|
646
669
|
return value
|
647
670
|
|
648
671
|
|
672
|
+
class TrajectoryQueryParams(BaseModel):
|
673
|
+
"""Params for trajectories with filtering."""
|
674
|
+
|
675
|
+
model_config = ConfigDict(extra="forbid")
|
676
|
+
|
677
|
+
project_id: UUID | None = Field(
|
678
|
+
default=None, description="Optional project ID to filter trajectories by"
|
679
|
+
)
|
680
|
+
name: str | None = Field(
|
681
|
+
default=None, description="Optional name filter for trajectories"
|
682
|
+
)
|
683
|
+
user: str | None = Field(
|
684
|
+
default=None, description="Optional user email filter for trajectories"
|
685
|
+
)
|
686
|
+
limit: int = Field(
|
687
|
+
default=50,
|
688
|
+
ge=1,
|
689
|
+
le=200,
|
690
|
+
description="Maximum number of trajectories to return (max: 200)",
|
691
|
+
)
|
692
|
+
offset: int = Field(
|
693
|
+
default=0, ge=0, description="Number of trajectories to skip for pagination"
|
694
|
+
)
|
695
|
+
sort_by: str = Field(default="created_at", description="Field to sort by")
|
696
|
+
sort_order: str = Field(default="desc", description="Sort order")
|
697
|
+
|
698
|
+
@field_validator("sort_by")
|
699
|
+
@classmethod
|
700
|
+
def validate_sort_by(cls, v: str) -> str:
|
701
|
+
if v not in {"created_at", "name"}:
|
702
|
+
raise ValueError("sort_by must be either 'created_at' or 'name'")
|
703
|
+
return v
|
704
|
+
|
705
|
+
@field_validator("sort_order")
|
706
|
+
@classmethod
|
707
|
+
def validate_sort_order(cls, v: str) -> str:
|
708
|
+
if v not in {"asc", "desc"}:
|
709
|
+
raise ValueError("sort_order must be either 'asc' or 'desc'")
|
710
|
+
return v
|
711
|
+
|
712
|
+
def to_query_params(self) -> dict[str, str | int]:
|
713
|
+
params: dict[str, str | int] = {
|
714
|
+
"limit": self.limit,
|
715
|
+
"offset": self.offset,
|
716
|
+
"sort_by": self.sort_by,
|
717
|
+
"sort_order": self.sort_order,
|
718
|
+
}
|
719
|
+
if self.project_id is not None:
|
720
|
+
params["project_id"] = str(self.project_id)
|
721
|
+
if self.name is not None:
|
722
|
+
params["name"] = self.name
|
723
|
+
if self.user is not None:
|
724
|
+
params["user"] = self.user
|
725
|
+
return params
|
726
|
+
|
727
|
+
|
649
728
|
class TaskRequest(BaseModel):
|
650
729
|
model_config = ConfigDict(extra="forbid")
|
651
730
|
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
|
2
2
|
from enum import StrEnum, auto
|
3
3
|
from uuid import UUID
|
4
4
|
|
5
|
-
from pydantic import BaseModel, JsonValue
|
5
|
+
from pydantic import BaseModel, ConfigDict, JsonValue
|
6
6
|
|
7
7
|
|
8
8
|
class FinalEnvironmentRequest(BaseModel):
|
@@ -70,3 +70,50 @@ class WorldModelResponse(BaseModel):
|
|
70
70
|
model_metadata: JsonValue | None
|
71
71
|
enabled: bool
|
72
72
|
created_at: datetime
|
73
|
+
|
74
|
+
|
75
|
+
class UserAgentRequestStatus(StrEnum):
|
76
|
+
"""Enum for the status of a user agent request."""
|
77
|
+
|
78
|
+
PENDING = auto()
|
79
|
+
RESPONDED = auto()
|
80
|
+
EXPIRED = auto()
|
81
|
+
CANCELLED = auto()
|
82
|
+
|
83
|
+
|
84
|
+
class UserAgentRequest(BaseModel):
|
85
|
+
"""Sister model for UserAgentRequestsDB."""
|
86
|
+
|
87
|
+
model_config = ConfigDict(from_attributes=True)
|
88
|
+
|
89
|
+
id: UUID
|
90
|
+
user_id: str
|
91
|
+
trajectory_id: UUID
|
92
|
+
response_trajectory_id: UUID | None = None
|
93
|
+
request: JsonValue
|
94
|
+
response: JsonValue | None = None
|
95
|
+
request_world_model_edit_id: UUID | None = None
|
96
|
+
response_world_model_edit_id: UUID | None = None
|
97
|
+
expires_at: datetime | None = None
|
98
|
+
user_response_task: JsonValue | None = None
|
99
|
+
status: UserAgentRequestStatus
|
100
|
+
created_at: datetime | None = None
|
101
|
+
modified_at: datetime | None = None
|
102
|
+
|
103
|
+
|
104
|
+
class UserAgentRequestPostPayload(BaseModel):
|
105
|
+
"""Payload to create a new user agent request."""
|
106
|
+
|
107
|
+
trajectory_id: UUID
|
108
|
+
request: JsonValue
|
109
|
+
request_world_model_edit_id: UUID | None = None
|
110
|
+
status: UserAgentRequestStatus = UserAgentRequestStatus.PENDING
|
111
|
+
expires_in_seconds: int | None = None
|
112
|
+
user_response_task: JsonValue | None = None
|
113
|
+
|
114
|
+
|
115
|
+
class UserAgentResponsePayload(BaseModel):
|
116
|
+
"""Payload for a user to submit a response to a request."""
|
117
|
+
|
118
|
+
response: JsonValue
|
119
|
+
response_world_model_edit_id: UUID | None = None
|
futurehouse_client/version.py
CHANGED
@@ -17,5 +17,5 @@ __version__: str
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
18
18
|
version_tuple: VERSION_TUPLE
|
19
19
|
|
20
|
-
__version__ = version = '0.3.20.
|
21
|
-
__version_tuple__ = version_tuple = (0, 3, 20, '
|
20
|
+
__version__ = version = '0.3.20.dev266'
|
21
|
+
__version_tuple__ = version_tuple = (0, 3, 20, 'dev266')
|
{futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: futurehouse-client
|
3
|
-
Version: 0.3.20.
|
3
|
+
Version: 0.3.20.dev266
|
4
4
|
Summary: A client for interacting with endpoints of the FutureHouse service.
|
5
5
|
Author-email: FutureHouse technical staff <hello@futurehouse.org>
|
6
6
|
License: Apache License
|
{futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/RECORD
RENAMED
@@ -1,20 +1,20 @@
|
|
1
1
|
futurehouse_client/__init__.py,sha256=BztM_ntbgmIEjzvnBWcvPhvLjM8xGDFCK0Upf3-nIn8,488
|
2
2
|
futurehouse_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
futurehouse_client/version.py,sha256=
|
3
|
+
futurehouse_client/version.py,sha256=BuvmuyFVBsO4OXG69bB2-CE0nYFKK5whAaQhu308ukU,530
|
4
4
|
futurehouse_client/clients/__init__.py,sha256=-HXNj-XJ3LRO5XM6MZ709iPs29YpApss0Q2YYg1qMZw,280
|
5
5
|
futurehouse_client/clients/job_client.py,sha256=D51_qTxya6g5Wfg_ZfJdP031TV_YDJeXkGMiYAJ1qRc,11962
|
6
|
-
futurehouse_client/clients/rest_client.py,sha256=
|
6
|
+
futurehouse_client/clients/rest_client.py,sha256=L8U3Ap-91_A90PkC00n-WwhVkK9oQjgxR59JAykyhjw,91929
|
7
7
|
futurehouse_client/models/__init__.py,sha256=kQ4R7VEuRxO0IQEW_sk9CndBL7zzl8rUKI24ddyYLM0,647
|
8
|
-
futurehouse_client/models/app.py,sha256=
|
8
|
+
futurehouse_client/models/app.py,sha256=TGoAeENNPc5mSBkMHjh-Z8VIlnaUNcoWUJLxUhRIkEE,31868
|
9
9
|
futurehouse_client/models/client.py,sha256=n4HD0KStKLm6Ek9nL9ylP-bkK10yzAaD1uIDF83Qp_A,1828
|
10
|
-
futurehouse_client/models/rest.py,sha256=
|
10
|
+
futurehouse_client/models/rest.py,sha256=LDHs6dINdbiwHlLBBP1RFd43PgP7mCAAnSAr7-l_2Mw,2924
|
11
11
|
futurehouse_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
12
|
futurehouse_client/utils/auth.py,sha256=tgWELjKfg8eWme_qdcRmc8TjQN9DVZuHHaVXZNHLchk,2960
|
13
13
|
futurehouse_client/utils/general.py,sha256=A_rtTiYW30ELGEZlWCIArO7q1nEmqi8hUlmBRYkMQ_c,767
|
14
14
|
futurehouse_client/utils/module_utils.py,sha256=aFyd-X-pDARXz9GWpn8SSViUVYdSbuy9vSkrzcVIaGI,4955
|
15
15
|
futurehouse_client/utils/monitoring.py,sha256=UjRlufe67kI3VxRHOd5fLtJmlCbVA2Wqwpd4uZhXkQM,8728
|
16
|
-
futurehouse_client-0.3.20.
|
17
|
-
futurehouse_client-0.3.20.
|
18
|
-
futurehouse_client-0.3.20.
|
19
|
-
futurehouse_client-0.3.20.
|
20
|
-
futurehouse_client-0.3.20.
|
16
|
+
futurehouse_client-0.3.20.dev266.dist-info/licenses/LICENSE,sha256=oQ9ZHjUi-_6GfP3gs14FlPb0OlGwE1QCCKFGnJ4LD2I,11341
|
17
|
+
futurehouse_client-0.3.20.dev266.dist-info/METADATA,sha256=qJo9JB9L8Myv_ErPaEdr4e96gJx3JNPIYbbg8MZ7Qoc,26805
|
18
|
+
futurehouse_client-0.3.20.dev266.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
19
|
+
futurehouse_client-0.3.20.dev266.dist-info/top_level.txt,sha256=TRuLUCt_qBnggdFHCX4O_BoCu1j2X43lKfIZC-ElwWY,19
|
20
|
+
futurehouse_client-0.3.20.dev266.dist-info/RECORD,,
|
{futurehouse_client-0.3.20.dev215.dist-info → futurehouse_client-0.3.20.dev266.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|