edsl 0.1.59__py3-none-any.whl → 0.1.61__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.
@@ -5,6 +5,7 @@ from ..data_transfer_models import EDSLResultObjectInput
5
5
 
6
6
  # from edsl.data_transfer_models import VisibilityType
7
7
  from ..caching import Cache
8
+
8
9
  # Import BucketCollection lazily to avoid circular imports
9
10
  from ..key_management import KeyLookup
10
11
  from ..base import Base
@@ -18,23 +19,27 @@ if TYPE_CHECKING:
18
19
 
19
20
  VisibilityType = Literal["private", "public", "unlisted"]
20
21
 
22
+
21
23
  @dataclass
22
24
  class RunEnvironment:
23
25
  """
24
26
  Contains environment-related resources for job execution.
25
-
26
- This dataclass holds references to shared resources and infrastructure components
27
- needed for job execution. These components are typically long-lived and may be
27
+
28
+ This dataclass holds references to shared resources and infrastructure components
29
+ needed for job execution. These components are typically long-lived and may be
28
30
  shared across multiple job runs.
29
-
31
+
30
32
  Attributes:
31
33
  cache (Cache, optional): Cache for storing and retrieving interview results
32
34
  bucket_collection (BucketCollection, optional): Collection of token rate limit buckets
33
35
  key_lookup (KeyLookup, optional): Manager for API keys across models
34
36
  jobs_runner_status (JobsRunnerStatus, optional): Tracker for job execution progress
35
37
  """
38
+
36
39
  cache: Optional[Cache] = None
37
- bucket_collection: Optional[Any] = None # Using Any to avoid circular import of BucketCollection
40
+ bucket_collection: Optional[
41
+ Any
42
+ ] = None # Using Any to avoid circular import of BucketCollection
38
43
  key_lookup: Optional[KeyLookup] = None
39
44
  jobs_runner_status: Optional["JobsRunnerStatus"] = None
40
45
 
@@ -43,11 +48,11 @@ class RunEnvironment:
43
48
  class RunParameters(Base):
44
49
  """
45
50
  Contains execution-specific parameters for job runs.
46
-
51
+
47
52
  This dataclass holds parameters that control the behavior of a specific job run,
48
53
  such as iteration count, error handling preferences, and remote execution options.
49
54
  Unlike RunEnvironment, these parameters are specific to a single job execution.
50
-
55
+
51
56
  Attributes:
52
57
  n (int): Number of iterations to run each interview, default is 1
53
58
  progress_bar (bool): Whether to show a progress bar, default is False
@@ -66,7 +71,9 @@ class RunParameters(Base):
66
71
  disable_remote_inference (bool): Whether to disable remote inference, default is False
67
72
  job_uuid (str, optional): UUID for the job, used for tracking
68
73
  fresh (bool): If True, ignore cache and generate new results, default is False
74
+ new_format (bool): If True, uses remote_inference_create method, if False uses old_remote_inference_create method, default is True
69
75
  """
76
+
70
77
  n: int = 1
71
78
  progress_bar: bool = False
72
79
  stop_on_exception: bool = False
@@ -82,8 +89,13 @@ class RunParameters(Base):
82
89
  disable_remote_cache: bool = False
83
90
  disable_remote_inference: bool = False
84
91
  job_uuid: Optional[str] = None
85
- fresh: bool = False # if True, will not use cache and will save new results to cache
86
- memory_threshold: Optional[int] = None # Threshold in bytes for Results SQLList memory management
92
+ fresh: bool = (
93
+ False # if True, will not use cache and will save new results to cache
94
+ )
95
+ memory_threshold: Optional[
96
+ int
97
+ ] = None # Threshold in bytes for Results SQLList memory management
98
+ new_format: bool = True # if True, uses remote_inference_create, if False uses old_remote_inference_create
87
99
 
88
100
  def to_dict(self, add_edsl_version=False) -> dict:
89
101
  d = asdict(self)
@@ -110,24 +122,25 @@ class RunParameters(Base):
110
122
  class RunConfig:
111
123
  """
112
124
  Combines environment resources and execution parameters for a job run.
113
-
125
+
114
126
  This class brings together the two aspects of job configuration:
115
127
  1. Environment resources (caches, API keys, etc.) via RunEnvironment
116
128
  2. Execution parameters (iterations, error handling, etc.) via RunParameters
117
-
129
+
118
130
  It provides helper methods for modifying environment components after construction.
119
-
131
+
120
132
  Attributes:
121
133
  environment (RunEnvironment): The environment resources for the job
122
134
  parameters (RunParameters): The execution parameters for the job
123
135
  """
136
+
124
137
  environment: RunEnvironment
125
138
  parameters: RunParameters
126
139
 
127
140
  def add_environment(self, environment: RunEnvironment) -> None:
128
141
  """
129
142
  Replace the entire environment configuration.
130
-
143
+
131
144
  Parameters:
132
145
  environment (RunEnvironment): The new environment configuration
133
146
  """
@@ -136,7 +149,7 @@ class RunConfig:
136
149
  def add_bucket_collection(self, bucket_collection: "BucketCollection") -> None:
137
150
  """
138
151
  Set or replace the bucket collection in the environment.
139
-
152
+
140
153
  Parameters:
141
154
  bucket_collection (BucketCollection): The bucket collection to use
142
155
  """
@@ -145,7 +158,7 @@ class RunConfig:
145
158
  def add_cache(self, cache: Cache) -> None:
146
159
  """
147
160
  Set or replace the cache in the environment.
148
-
161
+
149
162
  Parameters:
150
163
  cache (Cache): The cache to use
151
164
  """
@@ -154,7 +167,7 @@ class RunConfig:
154
167
  def add_key_lookup(self, key_lookup: KeyLookup) -> None:
155
168
  """
156
169
  Set or replace the key lookup in the environment.
157
-
170
+
158
171
  Parameters:
159
172
  key_lookup (KeyLookup): The key lookup to use
160
173
  """
@@ -169,10 +182,10 @@ Additional data structures for working with job results and answers.
169
182
  class Answers(UserDict):
170
183
  """
171
184
  A specialized dictionary for holding interview response data.
172
-
185
+
173
186
  This class extends UserDict to provide a flexible container for survey answers,
174
187
  with special handling for response metadata like comments and token usage.
175
-
188
+
176
189
  Key features:
177
190
  - Stores answers by question name
178
191
  - Associates comments with their respective questions
@@ -185,14 +198,14 @@ class Answers(UserDict):
185
198
  ) -> None:
186
199
  """
187
200
  Add a response to the answers dictionary.
188
-
201
+
189
202
  This method processes a response and stores it in the dictionary with appropriate
190
203
  naming conventions for the answer itself, comments, and token usage tracking.
191
-
204
+
192
205
  Parameters:
193
206
  response (EDSLResultObjectInput): The response object containing answer data
194
207
  question (QuestionBase): The question that was answered
195
-
208
+
196
209
  Notes:
197
210
  - The main answer is stored with the question's name as the key
198
211
  - Comments are stored with "_comment" appended to the question name
@@ -201,28 +214,33 @@ class Answers(UserDict):
201
214
  answer = response.answer
202
215
  comment = response.comment
203
216
  generated_tokens = response.generated_tokens
204
-
217
+
205
218
  # Record token usage if available
206
219
  if generated_tokens:
207
220
  self[question.question_name + "_generated_tokens"] = generated_tokens
208
-
221
+
209
222
  # Record the primary answer
210
223
  self[question.question_name] = answer
211
-
224
+
212
225
  # Record comment if present
213
226
  if comment:
214
227
  self[question.question_name + "_comment"] = comment
215
228
 
229
+ if getattr(response, "reasoning_summary", None):
230
+ self[
231
+ question.question_name + "_reasoning_summary"
232
+ ] = response.reasoning_summary
233
+
216
234
  def replace_missing_answers_with_none(self, survey: "Survey") -> None:
217
235
  """
218
236
  Replace missing answers with None for all questions in the survey.
219
-
237
+
220
238
  This method ensures that all questions in the survey have an entry in the
221
239
  answers dictionary, even if they were skipped during the interview.
222
-
240
+
223
241
  Parameters:
224
242
  survey (Survey): The survey containing the questions to check
225
-
243
+
226
244
  Notes:
227
245
  - Answers can be missing if the agent skips a question due to skip logic
228
246
  - This ensures consistent data structure even with partial responses
@@ -234,7 +252,7 @@ class Answers(UserDict):
234
252
  def to_dict(self) -> dict:
235
253
  """
236
254
  Convert the answers to a standard dictionary.
237
-
255
+
238
256
  Returns:
239
257
  dict: A plain dictionary containing all the answers data
240
258
  """
@@ -244,10 +262,10 @@ class Answers(UserDict):
244
262
  def from_dict(cls, d: dict) -> "Answers":
245
263
  """
246
264
  Create an Answers object from a dictionary.
247
-
265
+
248
266
  Parameters:
249
267
  d (dict): The dictionary containing answer data
250
-
268
+
251
269
  Returns:
252
270
  Answers: A new Answers instance with the provided data
253
271
  """
edsl/jobs/jobs.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """
2
2
  The Jobs module is the core orchestration component of the EDSL framework.
3
3
 
4
- It provides functionality to define, configure, and execute computational jobs that
5
- involve multiple agents, scenarios, models, and a survey. Jobs are the primary way
4
+ It provides functionality to define, configure, and execute computational jobs that
5
+ involve multiple agents, scenarios, models, and a survey. Jobs are the primary way
6
6
  that users run large-scale experiments or simulations in EDSL.
7
7
 
8
8
  The Jobs class handles:
@@ -15,6 +15,7 @@ The Jobs class handles:
15
15
  This module is designed to be used by both application developers and researchers
16
16
  who need to run complex simulations with language models.
17
17
  """
18
+
18
19
  from __future__ import annotations
19
20
  import asyncio
20
21
  from typing import Optional, Union, TypeVar, Callable, cast
@@ -564,6 +565,7 @@ class Jobs(Base):
564
565
  remote_inference_description=self.run_config.parameters.remote_inference_description,
565
566
  remote_inference_results_visibility=self.run_config.parameters.remote_inference_results_visibility,
566
567
  fresh=self.run_config.parameters.fresh,
568
+ new_format=self.run_config.parameters.new_format,
567
569
  )
568
570
  return job_info
569
571
 
@@ -829,6 +831,7 @@ class Jobs(Base):
829
831
  key_lookup (KeyLookup, optional): Object to manage API keys
830
832
  memory_threshold (int, optional): Memory threshold in bytes for the Results object's SQLList,
831
833
  controlling when data is offloaded to SQLite storage
834
+ new_format (bool): If True, uses remote_inference_create method, if False uses old_remote_inference_create method (default: True)
832
835
 
833
836
  Returns:
834
837
  Results: A Results object containing all responses and metadata
@@ -889,6 +892,7 @@ class Jobs(Base):
889
892
  key_lookup (KeyLookup, optional): Object to manage API keys
890
893
  memory_threshold (int, optional): Memory threshold in bytes for the Results object's SQLList,
891
894
  controlling when data is offloaded to SQLite storage
895
+ new_format (bool): If True, uses remote_inference_create method, if False uses old_remote_inference_create method (default: True)
892
896
 
893
897
  Returns:
894
898
  Results: A Results object containing all responses and metadata
@@ -1084,6 +1088,73 @@ class Jobs(Base):
1084
1088
  """Return the code to create this instance."""
1085
1089
  raise JobsImplementationError("Code generation not implemented yet")
1086
1090
 
1091
+ def humanize(
1092
+ self,
1093
+ project_name: str = "Project",
1094
+ scenario_list_method: Optional[
1095
+ Literal["randomize", "loop", "single_scenario"]
1096
+ ] = None,
1097
+ survey_description: Optional[str] = None,
1098
+ survey_alias: Optional[str] = None,
1099
+ survey_visibility: Optional["VisibilityType"] = "unlisted",
1100
+ scenario_list_description: Optional[str] = None,
1101
+ scenario_list_alias: Optional[str] = None,
1102
+ scenario_list_visibility: Optional["VisibilityType"] = "unlisted",
1103
+ ):
1104
+ """
1105
+ Send the survey and scenario list to Coop.
1106
+
1107
+ Then, create a project on Coop so you can share the survey with human respondents.
1108
+ """
1109
+ from edsl.coop import Coop
1110
+ from edsl.coop.exceptions import CoopValueError
1111
+
1112
+ if len(self.agents) > 0 or len(self.models) > 0:
1113
+ raise CoopValueError("We don't support humanize with agents or models yet.")
1114
+
1115
+ if len(self.scenarios) > 0 and scenario_list_method is None:
1116
+ raise CoopValueError(
1117
+ "You must specify both a scenario list and a scenario list method to use scenarios with your survey."
1118
+ )
1119
+ elif len(self.scenarios) == 0 and scenario_list_method is not None:
1120
+ raise CoopValueError(
1121
+ "You must specify both a scenario list and a scenario list method to use scenarios with your survey."
1122
+ )
1123
+ elif scenario_list_method is "loop":
1124
+ questions, long_scenario_list = self.survey.to_long_format(self.scenarios)
1125
+
1126
+ # Replace the questions with new ones from the loop method
1127
+ self.survey = Survey(questions)
1128
+ self.scenarios = long_scenario_list
1129
+
1130
+ if len(self.scenarios) != 1:
1131
+ raise CoopValueError("Something went wrong with the loop method.")
1132
+ elif len(self.scenarios) != 1 and scenario_list_method == "single_scenario":
1133
+ raise CoopValueError(
1134
+ f"The single_scenario method requires exactly one scenario. "
1135
+ f"If you have a scenario list with multiple scenarios, try using the randomize or loop methods."
1136
+ )
1137
+
1138
+ if len(self.scenarios) == 0:
1139
+ scenario_list = None
1140
+ else:
1141
+ scenario_list = self.scenarios
1142
+
1143
+ c = Coop()
1144
+ project_details = c.create_project(
1145
+ self.survey,
1146
+ scenario_list,
1147
+ scenario_list_method,
1148
+ project_name,
1149
+ survey_description,
1150
+ survey_alias,
1151
+ survey_visibility,
1152
+ scenario_list_description,
1153
+ scenario_list_alias,
1154
+ scenario_list_visibility,
1155
+ )
1156
+ return project_details
1157
+
1087
1158
 
1088
1159
  def main():
1089
1160
  """Run the module's doctests."""
@@ -31,6 +31,7 @@ class RemoteJobInfo:
31
31
  creation_data: RemoteInferenceCreationInfo
32
32
  job_uuid: JobUUID
33
33
  logger: JobLogger
34
+ new_format: bool = True
34
35
 
35
36
 
36
37
  class JobsRemoteInferenceHandler:
@@ -85,7 +86,21 @@ class JobsRemoteInferenceHandler:
85
86
  remote_inference_description: Optional[str] = None,
86
87
  remote_inference_results_visibility: Optional["VisibilityType"] = "unlisted",
87
88
  fresh: Optional[bool] = False,
89
+ new_format: Optional[bool] = True,
88
90
  ) -> RemoteJobInfo:
91
+ """
92
+ Create a remote inference job and return job information.
93
+
94
+ Args:
95
+ iterations: Number of times to run each interview
96
+ remote_inference_description: Optional description for the remote job
97
+ remote_inference_results_visibility: Visibility setting for results
98
+ fresh: If True, ignore existing cache entries and generate new results
99
+ new_format: If True, use pull method for result retrieval; if False, use legacy get method
100
+
101
+ Returns:
102
+ RemoteJobInfo: Information about the created job including UUID and logger
103
+ """
89
104
  from ..coop import Coop
90
105
 
91
106
  logger = self._create_logger()
@@ -101,14 +116,24 @@ class JobsRemoteInferenceHandler:
101
116
  logger.add_info(
102
117
  "remote_cache_url", f"{self.expected_parrot_url}/home/remote-cache"
103
118
  )
104
- remote_job_creation_data = coop.remote_inference_create(
105
- self.jobs,
106
- description=remote_inference_description,
107
- status="queued",
108
- iterations=iterations,
109
- initial_results_visibility=remote_inference_results_visibility,
110
- fresh=fresh,
111
- )
119
+ if new_format:
120
+ remote_job_creation_data = coop.remote_inference_create(
121
+ self.jobs,
122
+ description=remote_inference_description,
123
+ status="queued",
124
+ iterations=iterations,
125
+ initial_results_visibility=remote_inference_results_visibility,
126
+ fresh=fresh,
127
+ )
128
+ else:
129
+ remote_job_creation_data = coop.old_remote_inference_create(
130
+ self.jobs,
131
+ description=remote_inference_description,
132
+ status="queued",
133
+ iterations=iterations,
134
+ initial_results_visibility=remote_inference_results_visibility,
135
+ fresh=fresh,
136
+ )
112
137
  logger.update(
113
138
  "Your survey is running at the Expected Parrot server...",
114
139
  status=JobsStatus.RUNNING,
@@ -141,6 +166,7 @@ class JobsRemoteInferenceHandler:
141
166
  creation_data=remote_job_creation_data,
142
167
  job_uuid=job_uuid,
143
168
  logger=logger,
169
+ new_format=new_format,
144
170
  )
145
171
 
146
172
  @staticmethod
@@ -164,7 +190,7 @@ class JobsRemoteInferenceHandler:
164
190
  return coop.remote_inference_get
165
191
 
166
192
  def _construct_object_fetcher(
167
- self, testing_simulated_response: Optional[Any] = None
193
+ self, new_format: bool = True, testing_simulated_response: Optional[Any] = None
168
194
  ) -> Callable:
169
195
  "Constructs a function to fetch the results object from Coop."
170
196
  if testing_simulated_response is not None:
@@ -173,7 +199,10 @@ class JobsRemoteInferenceHandler:
173
199
  from ..coop import Coop
174
200
 
175
201
  coop = Coop()
176
- return coop.get
202
+ if new_format:
203
+ return coop.pull
204
+ else:
205
+ return coop.get
177
206
 
178
207
  def _handle_cancelled_job(self, job_info: RemoteJobInfo) -> None:
179
208
  "Handles a cancelled job by logging the cancellation and updating the job status."
@@ -395,7 +424,6 @@ class JobsRemoteInferenceHandler:
395
424
 
396
425
  converter = CostConverter()
397
426
  for model_key, model_cost_dict in expenses_by_model.items():
398
-
399
427
  # Handle full cost (without cache)
400
428
  input_cost = model_cost_dict["input_cost_usd"]
401
429
  output_cost = model_cost_dict["output_cost_usd"]
@@ -417,9 +445,9 @@ class JobsRemoteInferenceHandler:
417
445
  model_cost_dict["input_cost_credits_with_cache"] = converter.usd_to_credits(
418
446
  input_cost_with_cache
419
447
  )
420
- model_cost_dict["output_cost_credits_with_cache"] = (
421
- converter.usd_to_credits(output_cost_with_cache)
422
- )
448
+ model_cost_dict[
449
+ "output_cost_credits_with_cache"
450
+ ] = converter.usd_to_credits(output_cost_with_cache)
423
451
  return list(expenses_by_model.values())
424
452
 
425
453
  def _fetch_results_and_log(
@@ -525,7 +553,10 @@ class JobsRemoteInferenceHandler:
525
553
  remote_job_data_fetcher = self._construct_remote_job_fetcher(
526
554
  testing_simulated_response
527
555
  )
528
- object_fetcher = self._construct_object_fetcher(testing_simulated_response)
556
+ object_fetcher = self._construct_object_fetcher(
557
+ new_format=job_info.new_format,
558
+ testing_simulated_response=testing_simulated_response,
559
+ )
529
560
 
530
561
  job_in_queue = True
531
562
  while job_in_queue:
@@ -540,6 +571,7 @@ class JobsRemoteInferenceHandler:
540
571
  iterations: int = 1,
541
572
  remote_inference_description: Optional[str] = None,
542
573
  remote_inference_results_visibility: Optional[VisibilityType] = "unlisted",
574
+ new_format: Optional[bool] = True,
543
575
  ) -> Union["Results", None]:
544
576
  """
545
577
  Creates and polls a remote inference job asynchronously.
@@ -548,6 +580,7 @@ class JobsRemoteInferenceHandler:
548
580
  :param iterations: Number of times to run each interview
549
581
  :param remote_inference_description: Optional description for the remote job
550
582
  :param remote_inference_results_visibility: Visibility setting for results
583
+ :param new_format: If True, use pull method for result retrieval; if False, use legacy get method
551
584
  :return: Results object if successful, None if job fails or is cancelled
552
585
  """
553
586
  import asyncio
@@ -562,6 +595,7 @@ class JobsRemoteInferenceHandler:
562
595
  iterations=iterations,
563
596
  remote_inference_description=remote_inference_description,
564
597
  remote_inference_results_visibility=remote_inference_results_visibility,
598
+ new_format=new_format,
565
599
  ),
566
600
  )
567
601
  if job_info is None:
@@ -363,13 +363,35 @@ class KeyLookupBuilder:
363
363
  >>> builder._add_api_key("OPENAI_API_KEY", "sk-1234", "env")
364
364
  >>> 'sk-1234' == builder.key_data["openai"][-1].value
365
365
  True
366
+ >>> 'sk-1234' == builder.key_data["openai_v2"][-1].value
367
+ True
366
368
  """
367
369
  service = api_keyname_to_service[key]
368
370
  new_entry = APIKeyEntry(service=service, name=key, value=value, source=source)
369
- if service not in self.key_data:
370
- self.key_data[service] = [new_entry]
371
+
372
+ # Special case for OPENAI_API_KEY - add to both openai and openai_v2
373
+ if key == "OPENAI_API_KEY":
374
+ # Add to openai service
375
+ openai_service = "openai"
376
+ openai_entry = APIKeyEntry(service=openai_service, name=key, value=value, source=source)
377
+ if openai_service not in self.key_data:
378
+ self.key_data[openai_service] = [openai_entry]
379
+ else:
380
+ self.key_data[openai_service].append(openai_entry)
381
+
382
+ # Add to openai_v2 service
383
+ openai_v2_service = "openai_v2"
384
+ openai_v2_entry = APIKeyEntry(service=openai_v2_service, name=key, value=value, source=source)
385
+ if openai_v2_service not in self.key_data:
386
+ self.key_data[openai_v2_service] = [openai_v2_entry]
387
+ else:
388
+ self.key_data[openai_v2_service].append(openai_v2_entry)
371
389
  else:
372
- self.key_data[service].append(new_entry)
390
+ # Normal case for all other API keys
391
+ if service not in self.key_data:
392
+ self.key_data[service] = [new_entry]
393
+ else:
394
+ self.key_data[service].append(new_entry)
373
395
 
374
396
  def update_from_dict(self, d: dict) -> None:
375
397
  """
@@ -174,7 +174,8 @@ class LanguageModel(
174
174
  """
175
175
  key_sequence = cls.key_sequence
176
176
  usage_sequence = cls.usage_sequence if hasattr(cls, "usage_sequence") else None
177
- return RawResponseHandler(key_sequence, usage_sequence)
177
+ reasoning_sequence = cls.reasoning_sequence if hasattr(cls, "reasoning_sequence") else None
178
+ return RawResponseHandler(key_sequence, usage_sequence, reasoning_sequence)
178
179
 
179
180
  def __init__(
180
181
  self,