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 +1 -1
- edsl/coop/coop.py +174 -37
- edsl/coop/utils.py +63 -0
- edsl/interviews/exception_tracking.py +26 -0
- edsl/jobs/html_table_job_logger.py +377 -48
- edsl/jobs/jobs_pricing_estimation.py +19 -96
- edsl/jobs/jobs_remote_inference_logger.py +27 -0
- edsl/jobs/jobs_runner_status.py +52 -21
- edsl/jobs/remote_inference.py +187 -30
- edsl/language_models/language_model.py +1 -1
- edsl/language_models/price_manager.py +91 -57
- {edsl-0.1.56.dist-info → edsl-0.1.58.dist-info}/METADATA +2 -2
- {edsl-0.1.56.dist-info → edsl-0.1.58.dist-info}/RECORD +16 -17
- edsl/language_models/compute_cost.py +0 -78
- {edsl-0.1.56.dist-info → edsl-0.1.58.dist-info}/LICENSE +0 -0
- {edsl-0.1.56.dist-info → edsl-0.1.58.dist-info}/WHEEL +0 -0
- {edsl-0.1.56.dist-info → edsl-0.1.58.dist-info}/entry_points.txt +0 -0
edsl/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
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(
|
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
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
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
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
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
|
-
|
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
|
-
{'
|
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
|
-
"
|
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])
|