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.
@@ -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!s}") from 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!s}") from 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!s}") from 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(f"Error uploading directory {dir_path}: {e}") from e
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!s}") from 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!s}") from 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."""
@@ -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
@@ -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.dev215'
21
- __version_tuple__ = version_tuple = (0, 3, 20, 'dev215')
20
+ __version__ = version = '0.3.20.dev266'
21
+ __version_tuple__ = version_tuple = (0, 3, 20, 'dev266')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futurehouse-client
3
- Version: 0.3.20.dev215
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
@@ -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=pUKvLSR6DkrlymjAn81oaYqONXdmBS18_lBjSKnvJlc,530
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=v5INQ9Wsb3A3U2zdLfRUP2H1hXpsZHtiZdVEav3Sv6o,69215
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=SvkXOu5Qx28_jCS84XKKepnpGyfrQUEVnxS-JGqmsaE,29128
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=AwPMcB1ruoqaI8NIhX2ZzN8UuX6XsaQ7uzeSE8EpwKk,1573
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.dev215.dist-info/licenses/LICENSE,sha256=oQ9ZHjUi-_6GfP3gs14FlPb0OlGwE1QCCKFGnJ4LD2I,11341
17
- futurehouse_client-0.3.20.dev215.dist-info/METADATA,sha256=A7E-dfE-qS76wT9wco1IFgjynobKJnRmiXQoNZ5_ESg,26805
18
- futurehouse_client-0.3.20.dev215.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- futurehouse_client-0.3.20.dev215.dist-info/top_level.txt,sha256=TRuLUCt_qBnggdFHCX4O_BoCu1j2X43lKfIZC-ElwWY,19
20
- futurehouse_client-0.3.20.dev215.dist-info/RECORD,,
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,,