edsl 0.1.56__py3-none-any.whl → 0.1.58__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.
edsl/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.56"
1
+ __version__ = "0.1.58"
edsl/coop/coop.py CHANGED
@@ -37,17 +37,59 @@ from .ep_key_handling import ExpectedParrotKeyHandler
37
37
  from ..inference_services.data_structures import ServiceToModelsMapping
38
38
 
39
39
 
40
+ class JobRunExpense(TypedDict):
41
+ service: str
42
+ model: str
43
+ token_type: Literal["input", "output"]
44
+ price_per_million_tokens: float
45
+ tokens_count: int
46
+ cost_credits: float
47
+ cost_usd: float
48
+
49
+
50
+ class JobRunExceptionCounter(TypedDict):
51
+ exception_type: str
52
+ inference_service: str
53
+ model: str
54
+ question_name: str
55
+ exception_count: int
56
+
57
+
58
+ class JobRunInterviewDetails(TypedDict):
59
+ total_interviews: int
60
+ completed_interviews: int
61
+ interviews_with_exceptions: int
62
+ exception_summary: List[JobRunExceptionCounter]
63
+
64
+
65
+ class LatestJobRunDetails(TypedDict):
66
+
67
+ # For running, completed, and partially failed jobs
68
+ interview_details: Optional[JobRunInterviewDetails] = None
69
+
70
+ # For failed jobs only
71
+ failure_reason: Optional[Literal["error", "insufficient funds"]] = None
72
+ failure_description: Optional[str] = None
73
+
74
+ # For partially failed jobs only
75
+ error_report_uuid: Optional[UUID] = None
76
+
77
+ # For completed and partially failed jobs
78
+ cost_credits: Optional[float] = None
79
+ cost_usd: Optional[float] = None
80
+ expenses: Optional[list[JobRunExpense]] = None
81
+
82
+
40
83
  class RemoteInferenceResponse(TypedDict):
41
84
  job_uuid: str
42
85
  results_uuid: str
43
- results_url: str
44
- latest_error_report_uuid: str
45
- latest_error_report_url: str
46
- status: str
47
- reason: str
48
- credits_consumed: float
49
- version: str
50
86
  job_json_string: Optional[str]
87
+ status: RemoteJobStatus
88
+ latest_job_run_details: LatestJobRunDetails
89
+ description: Optional[str]
90
+ version: str
91
+ visibility: VisibilityType
92
+ results_url: str
51
93
 
52
94
 
53
95
  class RemoteInferenceCreationInfo(TypedDict):
@@ -168,7 +210,9 @@ class Coop(CoopFunctionsMixin):
168
210
  and "json_string" in payload
169
211
  and payload.get("json_string") is not None
170
212
  ):
171
- timeout = max(40, (len(payload.get("json_string", "")) // (1024 * 1024)))
213
+ timeout = max(
214
+ 60, 2 * (len(payload.get("json_string", "")) // (1024 * 1024))
215
+ )
172
216
  try:
173
217
  if method in ["GET", "DELETE"]:
174
218
  response = requests.request(
@@ -244,7 +288,6 @@ class Coop(CoopFunctionsMixin):
244
288
  # print(
245
289
  # "Please upgrade your EDSL version to access our latest features. Open your terminal and run `pip install --upgrade edsl`"
246
290
  # )
247
-
248
291
  if response.status_code >= 400:
249
292
  try:
250
293
  message = str(response.json().get("detail"))
@@ -1063,16 +1106,36 @@ class Coop(CoopFunctionsMixin):
1063
1106
 
1064
1107
  Returns:
1065
1108
  RemoteInferenceResponse: Information about the job including:
1066
- - job_uuid: The unique identifier for the job
1067
- - results_uuid: The UUID of the results (if job is completed)
1068
- - results_url: URL to access the results (if available)
1069
- - latest_error_report_uuid: UUID of error report (if job failed)
1070
- - latest_error_report_url: URL to access error details (if available)
1071
- - status: Current status ("queued", "running", "completed", "failed")
1072
- - reason: Reason for failure (if applicable)
1073
- - credits_consumed: Credits used for the job execution
1074
- - version: EDSL version used for the job
1075
- - job_json_string: The json string for the job (if include_json_string is True)
1109
+ job_uuid: The unique identifier for the job
1110
+ results_uuid: The UUID of the results
1111
+ results_url: URL to access the results
1112
+ status: Current status ("queued", "running", "completed", "failed")
1113
+ version: EDSL version used for the job
1114
+ job_json_string: The json string for the job (if include_json_string is True)
1115
+ latest_job_run_details: Metadata about the job status
1116
+ interview_details: Metadata about the job interview status (for jobs that have reached running status)
1117
+ total_interviews: The total number of interviews in the job
1118
+ completed_interviews: The number of completed interviews
1119
+ interviews_with_exceptions: The number of completed interviews that have exceptions
1120
+ exception_counters: A list of exception counts for the job
1121
+ exception_type: The type of exception
1122
+ inference_service: The inference service
1123
+ model: The model
1124
+ question_name: The name of the question
1125
+ exception_count: The number of exceptions
1126
+ failure_reason: The reason the job failed (failed jobs only)
1127
+ failure_description: The description of the failure (failed jobs only)
1128
+ error_report_uuid: The UUID of the error report (partially failed jobs only)
1129
+ cost_credits: The cost of the job run in credits
1130
+ cost_usd: The cost of the job run in USD
1131
+ expenses: The expenses incurred by the job run
1132
+ service: The service
1133
+ model: The model
1134
+ token_type: The type of token (input or output)
1135
+ price_per_million_tokens: The price per million tokens
1136
+ tokens_count: The number of tokens consumed
1137
+ cost_credits: The cost of the service/model/token type combination in credits
1138
+ cost_usd: The cost of the service/model/token type combination in USD
1076
1139
 
1077
1140
  Raises:
1078
1141
  ValueError: If neither job_uuid nor results_uuid is provided
@@ -1098,6 +1161,8 @@ class Coop(CoopFunctionsMixin):
1098
1161
  params = {"job_uuid": job_uuid}
1099
1162
  else:
1100
1163
  params = {"results_uuid": results_uuid}
1164
+ if include_json_string:
1165
+ params["include_json_string"] = include_json_string
1101
1166
 
1102
1167
  response = self._send_server_request(
1103
1168
  uri="api/v0/remote-inference",
@@ -1108,35 +1173,32 @@ class Coop(CoopFunctionsMixin):
1108
1173
  data = response.json()
1109
1174
 
1110
1175
  results_uuid = data.get("results_uuid")
1111
- latest_error_report_uuid = data.get("latest_error_report_uuid")
1112
1176
 
1113
1177
  if results_uuid is None:
1114
1178
  results_url = None
1115
1179
  else:
1116
1180
  results_url = f"{self.url}/content/{results_uuid}"
1117
1181
 
1118
- if latest_error_report_uuid is None:
1119
- latest_error_report_url = None
1120
- else:
1121
- latest_error_report_url = (
1122
- f"{self.url}/home/remote-inference/error/{latest_error_report_uuid}"
1123
- )
1182
+ latest_job_run_details = data.get("latest_job_run_details", {})
1183
+ if data.get("status") == "partial_failed":
1184
+ latest_error_report_uuid = latest_job_run_details.get("error_report_uuid")
1185
+ if latest_error_report_uuid is None:
1186
+ latest_job_run_details["error_report_url"] = None
1187
+ else:
1188
+ latest_error_report_url = (
1189
+ f"{self.url}/home/remote-inference/error/{latest_error_report_uuid}"
1190
+ )
1191
+ latest_job_run_details["error_report_url"] = latest_error_report_url
1124
1192
 
1125
1193
  return RemoteInferenceResponse(
1126
1194
  **{
1127
1195
  "job_uuid": data.get("job_uuid"),
1128
1196
  "results_uuid": results_uuid,
1129
1197
  "results_url": results_url,
1130
- "latest_error_report_uuid": latest_error_report_uuid,
1131
- "latest_error_report_url": latest_error_report_url,
1132
- "latest_failure_description": data.get("latest_failure_details"),
1133
1198
  "status": data.get("status"),
1134
- "reason": data.get("latest_failure_reason"),
1135
- "credits_consumed": data.get("price"),
1136
1199
  "version": data.get("version"),
1137
- "job_json_string": (
1138
- data.get("job_json_string") if include_json_string else None
1139
- ),
1200
+ "job_json_string": data.get("job_json_string"),
1201
+ "latest_job_run_details": latest_job_run_details,
1140
1202
  }
1141
1203
  )
1142
1204
 
@@ -1256,13 +1318,13 @@ class Coop(CoopFunctionsMixin):
1256
1318
  self, input: Union["Jobs", "Survey"], iterations: int = 1
1257
1319
  ) -> int:
1258
1320
  """
1259
- Get the cost of a remote inference job.
1321
+ Get the estimated cost in credits of a remote inference job.
1260
1322
 
1261
1323
  :param input: The EDSL job to send to the server.
1262
1324
 
1263
1325
  >>> job = Jobs.example()
1264
1326
  >>> coop.remote_inference_cost(input=job)
1265
- {'credits': 0.77, 'usd': 0.0076950000000000005}
1327
+ {'credits_hold': 0.77, 'usd': 0.0076950000000000005}
1266
1328
  """
1267
1329
  from ..jobs import Jobs
1268
1330
  from ..surveys import Survey
@@ -1290,7 +1352,7 @@ class Coop(CoopFunctionsMixin):
1290
1352
  self._resolve_server_response(response)
1291
1353
  response_json = response.json()
1292
1354
  return {
1293
- "credits": response_json.get("cost_in_credits"),
1355
+ "credits_hold": response_json.get("cost_in_credits"),
1294
1356
  "usd": response_json.get("cost_in_usd"),
1295
1357
  }
1296
1358
 
@@ -1556,6 +1618,11 @@ class Coop(CoopFunctionsMixin):
1556
1618
  f"[#38bdf8][link={url}][underline]Log in and automatically store key[/underline][/link][/#38bdf8]"
1557
1619
  )
1558
1620
 
1621
+ print("Logging in will activate the following features:")
1622
+ print(" - Remote inference: Runs jobs remotely on the Expected Parrot server.")
1623
+ print(" - Remote logging: Sends error messages to the Expected Parrot server.")
1624
+ print("\n")
1625
+
1559
1626
  def _get_api_key(self, edsl_auth_token: str):
1560
1627
  """
1561
1628
  Given an EDSL auth token, find the corresponding user's API key.
@@ -1600,6 +1667,76 @@ class Coop(CoopFunctionsMixin):
1600
1667
  # Add API key to environment
1601
1668
  load_dotenv()
1602
1669
 
1670
+ def transfer_credits(
1671
+ self,
1672
+ credits_transferred: int,
1673
+ recipient_username: str,
1674
+ transfer_note: str = None,
1675
+ ) -> dict:
1676
+ """
1677
+ Transfer credits to another user.
1678
+
1679
+ This method transfers a specified number of credits from the authenticated user's
1680
+ account to another user's account on the Expected Parrot platform.
1681
+
1682
+ Parameters:
1683
+ credits_transferred (int): The number of credits to transfer to the recipient
1684
+ recipient_username (str): The username of the recipient
1685
+ transfer_note (str, optional): A personal note to include with the transfer
1686
+
1687
+ Returns:
1688
+ dict: Information about the transfer transaction, including:
1689
+ - success: Whether the transaction was successful
1690
+ - transaction_id: A unique identifier for the transaction
1691
+ - remaining_credits: The number of credits remaining in the sender's account
1692
+
1693
+ Raises:
1694
+ CoopServerResponseError: If there's an error communicating with the server
1695
+ or if the transfer criteria aren't met (e.g., insufficient credits)
1696
+
1697
+ Example:
1698
+ >>> result = coop.transfer_credits(
1699
+ ... credits_transferred=100,
1700
+ ... recipient_username="friend_username",
1701
+ ... transfer_note="Thanks for your help!"
1702
+ ... )
1703
+ >>> print(f"Transfer successful! You have {result['remaining_credits']} credits left.")
1704
+ """
1705
+ response = self._send_server_request(
1706
+ uri="api/users/gift",
1707
+ method="POST",
1708
+ payload={
1709
+ "credits_gifted": credits_transferred,
1710
+ "recipient_username": recipient_username,
1711
+ "gift_note": transfer_note,
1712
+ },
1713
+ )
1714
+ self._resolve_server_response(response)
1715
+ return response.json()
1716
+
1717
+ def get_balance(self) -> dict:
1718
+ """
1719
+ Get the current credit balance for the authenticated user.
1720
+
1721
+ This method retrieves the user's current credit balance information from
1722
+ the Expected Parrot platform.
1723
+
1724
+ Returns:
1725
+ dict: Information about the user's credit balance, including:
1726
+ - credits: The current number of credits in the user's account
1727
+ - usage_history: Recent credit usage if available
1728
+
1729
+ Raises:
1730
+ CoopServerResponseError: If there's an error communicating with the server
1731
+
1732
+ Example:
1733
+ >>> balance = coop.get_balance()
1734
+ >>> print(f"You have {balance['credits']} credits available.")
1735
+ """
1736
+ response = self._send_server_request(uri="api/users/get_balance", method="GET")
1737
+ self._resolve_server_response(response)
1738
+ return response.json()
1739
+
1603
1740
 
1604
1741
  def main():
1605
1742
  """
edsl/coop/utils.py CHANGED
@@ -1,3 +1,4 @@
1
+ import math
1
2
  from typing import Literal, Optional, Type, Union
2
3
 
3
4
  from ..agents import Agent, AgentList
@@ -197,3 +198,65 @@ class ObjectRegistry:
197
198
  if (class_name := o["edsl_class"].__name__) not in subclass_registry
198
199
  and class_name not in exclude_classes
199
200
  }
201
+
202
+
203
+ class CostConverter:
204
+ CENTS_PER_CREDIT = 1
205
+
206
+ @staticmethod
207
+ def _credits_to_minicredits(credits: float) -> float:
208
+ """
209
+ Converts credits to minicredits.
210
+
211
+ Current conversion: minicredits = credits * 100
212
+ """
213
+
214
+ return credits * 100
215
+
216
+ @staticmethod
217
+ def _minicredits_to_credits(minicredits: float) -> float:
218
+ """
219
+ Converts minicredits to credits.
220
+
221
+ Current conversion: credits = minicredits / 100
222
+ """
223
+
224
+ return minicredits / 100
225
+
226
+ def _usd_to_minicredits(self, usd: float) -> float:
227
+ """Converts USD to minicredits."""
228
+
229
+ cents = usd * 100
230
+ credits_per_cent = 1 / int(self.CENTS_PER_CREDIT)
231
+
232
+ credits = cents * credits_per_cent
233
+
234
+ minicredits = self._credits_to_minicredits(credits)
235
+
236
+ return minicredits
237
+
238
+ def _minicredits_to_usd(self, minicredits: float) -> float:
239
+ """Converts minicredits to USD."""
240
+
241
+ credits = self._minicredits_to_credits(minicredits)
242
+
243
+ cents_per_credit = int(self.CENTS_PER_CREDIT)
244
+
245
+ cents = credits * cents_per_credit
246
+ usd = cents / 100
247
+
248
+ return usd
249
+
250
+ def usd_to_credits(self, usd: float) -> float:
251
+ """Converts USD to credits."""
252
+
253
+ minicredits = math.ceil(self._usd_to_minicredits(usd))
254
+ credits = self._minicredits_to_credits(minicredits)
255
+ return round(credits, 2)
256
+
257
+ def credits_to_usd(self, credits: float) -> float:
258
+ """Converts credits to USD."""
259
+
260
+ minicredits = math.ceil(self._credits_to_minicredits(credits))
261
+ usd = self._minicredits_to_usd(minicredits)
262
+ return usd
@@ -23,6 +23,11 @@ class InterviewExceptionEntry:
23
23
  self.traceback_format = traceback_format
24
24
  self.answers = answers
25
25
 
26
+ @property
27
+ def exception_type(self) -> str:
28
+ """Return the type of the exception."""
29
+ return type(self.exception).__name__
30
+
26
31
  @property
27
32
  def question_type(self) -> str:
28
33
  """Return the type of the question that failed."""
@@ -239,6 +244,27 @@ class InterviewExceptionCollection(UserDict):
239
244
  """Return the number of unfixed exceptions."""
240
245
  return sum(len(v) for v in self.unfixed_exceptions().values())
241
246
 
247
+ def list(self) -> list[dict]:
248
+ """
249
+ Return a list of exception dicts with the following metadata:
250
+ - exception_type: the type of the exception
251
+ - inference_service: the inference service used
252
+ - model: the model used
253
+ - question_name: the name of the question that failed
254
+ """
255
+ exception_list = []
256
+ for question_name, exceptions in self.data.items():
257
+ for exception in exceptions:
258
+ exception_list.append(
259
+ {
260
+ "exception_type": exception.exception_type,
261
+ "inference_service": exception.invigilator.model._inference_service_,
262
+ "model": exception.invigilator.model.model,
263
+ "question_name": question_name,
264
+ }
265
+ )
266
+ return exception_list
267
+
242
268
  def num_unfixed(self) -> int:
243
269
  """Return a list of unfixed questions."""
244
270
  return len([k for k in self.data.keys() if k not in self.fixed])