edsl 0.1.38__py3-none-any.whl → 0.1.38.dev2__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.
Files changed (53) hide show
  1. edsl/Base.py +31 -60
  2. edsl/__version__.py +1 -1
  3. edsl/agents/Agent.py +9 -18
  4. edsl/agents/AgentList.py +8 -59
  5. edsl/agents/Invigilator.py +7 -18
  6. edsl/agents/InvigilatorBase.py +19 -0
  7. edsl/agents/PromptConstructor.py +4 -5
  8. edsl/config.py +0 -8
  9. edsl/coop/coop.py +7 -74
  10. edsl/data/Cache.py +2 -27
  11. edsl/data/CacheEntry.py +3 -8
  12. edsl/data/RemoteCacheSync.py +19 -0
  13. edsl/enums.py +0 -2
  14. edsl/inference_services/GoogleService.py +15 -7
  15. edsl/inference_services/registry.py +0 -2
  16. edsl/jobs/Jobs.py +548 -88
  17. edsl/jobs/interviews/Interview.py +11 -11
  18. edsl/jobs/runners/JobsRunnerAsyncio.py +35 -140
  19. edsl/jobs/runners/JobsRunnerStatus.py +2 -0
  20. edsl/jobs/tasks/TaskHistory.py +16 -15
  21. edsl/language_models/LanguageModel.py +84 -44
  22. edsl/language_models/ModelList.py +1 -47
  23. edsl/language_models/registry.py +4 -57
  24. edsl/prompts/Prompt.py +3 -8
  25. edsl/questions/QuestionBase.py +16 -20
  26. edsl/questions/QuestionExtract.py +4 -3
  27. edsl/questions/question_registry.py +6 -36
  28. edsl/results/Dataset.py +15 -146
  29. edsl/results/DatasetExportMixin.py +217 -231
  30. edsl/results/DatasetTree.py +4 -134
  31. edsl/results/Result.py +9 -18
  32. edsl/results/Results.py +51 -145
  33. edsl/scenarios/FileStore.py +13 -187
  34. edsl/scenarios/Scenario.py +4 -61
  35. edsl/scenarios/ScenarioList.py +62 -237
  36. edsl/surveys/Survey.py +2 -16
  37. edsl/surveys/SurveyFlowVisualizationMixin.py +9 -67
  38. edsl/surveys/instructions/Instruction.py +0 -12
  39. edsl/templates/error_reporting/interview_details.html +3 -3
  40. edsl/templates/error_reporting/interviews.html +9 -18
  41. edsl/utilities/utilities.py +0 -15
  42. {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/METADATA +1 -2
  43. {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/RECORD +45 -53
  44. edsl/inference_services/PerplexityService.py +0 -163
  45. edsl/jobs/JobsChecks.py +0 -147
  46. edsl/jobs/JobsPrompts.py +0 -268
  47. edsl/jobs/JobsRemoteInferenceHandler.py +0 -239
  48. edsl/results/CSSParameterizer.py +0 -108
  49. edsl/results/TableDisplay.py +0 -198
  50. edsl/results/table_display.css +0 -78
  51. edsl/scenarios/ScenarioJoin.py +0 -127
  52. {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/LICENSE +0 -0
  53. {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/WHEEL +0 -0
@@ -110,9 +110,9 @@ class Interview:
110
110
  self.debug = debug
111
111
  self.iteration = iteration
112
112
  self.cache = cache
113
- self.answers: dict[
114
- str, str
115
- ] = Answers() # will get filled in as interview progresses
113
+ self.answers: dict[str, str] = (
114
+ Answers()
115
+ ) # will get filled in as interview progresses
116
116
  self.sidecar_model = sidecar_model
117
117
 
118
118
  # Trackers
@@ -143,9 +143,9 @@ class Interview:
143
143
  The keys are the question names; the values are the lists of status log changes for each task.
144
144
  """
145
145
  for task_creator in self.task_creators.values():
146
- self._task_status_log_dict[
147
- task_creator.question.question_name
148
- ] = task_creator.status_log
146
+ self._task_status_log_dict[task_creator.question.question_name] = (
147
+ task_creator.status_log
148
+ )
149
149
  return self._task_status_log_dict
150
150
 
151
151
  @property
@@ -486,11 +486,11 @@ class Interview:
486
486
  """
487
487
  current_question_index: int = self.to_index[current_question.question_name]
488
488
 
489
- next_question: Union[
490
- int, EndOfSurvey
491
- ] = self.survey.rule_collection.next_question(
492
- q_now=current_question_index,
493
- answers=self.answers | self.scenario | self.agent["traits"],
489
+ next_question: Union[int, EndOfSurvey] = (
490
+ self.survey.rule_collection.next_question(
491
+ q_now=current_question_index,
492
+ answers=self.answers | self.scenario | self.agent["traits"],
493
+ )
494
494
  )
495
495
 
496
496
  next_question_index = next_question.next_q
@@ -4,7 +4,6 @@ import asyncio
4
4
  import threading
5
5
  import warnings
6
6
  from typing import Coroutine, List, AsyncGenerator, Optional, Union, Generator, Type
7
- from uuid import UUID
8
7
  from collections import UserList
9
8
 
10
9
  from edsl.results.Results import Results
@@ -37,8 +36,6 @@ class JobsRunnerAsyncio:
37
36
  The Jobs object is a collection of interviews that are to be run.
38
37
  """
39
38
 
40
- MAX_CONCURRENT_DEFAULT = 500
41
-
42
39
  def __init__(self, jobs: "Jobs"):
43
40
  self.jobs = jobs
44
41
  self.interviews: List["Interview"] = jobs.interviews()
@@ -46,53 +43,6 @@ class JobsRunnerAsyncio:
46
43
  self.total_interviews: List["Interview"] = []
47
44
  self._initialized = threading.Event()
48
45
 
49
- from edsl.config import CONFIG
50
-
51
- self.MAX_CONCURRENT = int(CONFIG.get("EDSL_MAX_CONCURRENT_TASKS"))
52
- # print(f"MAX_CONCURRENT: {self.MAX_CONCURRENT}")
53
-
54
- # async def run_async_generator(
55
- # self,
56
- # cache: Cache,
57
- # n: int = 1,
58
- # stop_on_exception: bool = False,
59
- # sidecar_model: Optional[LanguageModel] = None,
60
- # total_interviews: Optional[List["Interview"]] = None,
61
- # raise_validation_errors: bool = False,
62
- # ) -> AsyncGenerator["Result", None]:
63
- # """Creates the tasks, runs them asynchronously, and returns the results as a Results object.
64
-
65
- # Completed tasks are yielded as they are completed.
66
-
67
- # :param n: how many times to run each interview
68
- # :param stop_on_exception: Whether to stop the interview if an exception is raised
69
- # :param sidecar_model: a language model to use in addition to the interview's model
70
- # :param total_interviews: A list of interviews to run can be provided instead.
71
- # :param raise_validation_errors: Whether to raise validation errors
72
- # """
73
- # tasks = []
74
- # if total_interviews: # was already passed in total interviews
75
- # self.total_interviews = total_interviews
76
- # else:
77
- # self.total_interviews = list(
78
- # self._populate_total_interviews(n=n)
79
- # ) # Populate self.total_interviews before creating tasks
80
- # self._initialized.set() # Signal that we're ready
81
-
82
- # for interview in self.total_interviews:
83
- # interviewing_task = self._build_interview_task(
84
- # interview=interview,
85
- # stop_on_exception=stop_on_exception,
86
- # sidecar_model=sidecar_model,
87
- # raise_validation_errors=raise_validation_errors,
88
- # )
89
- # tasks.append(asyncio.create_task(interviewing_task))
90
-
91
- # for task in asyncio.as_completed(tasks):
92
- # result = await task
93
- # self.jobs_runner_status.add_completed_interview(result)
94
- # yield result
95
-
96
46
  async def run_async_generator(
97
47
  self,
98
48
  cache: Cache,
@@ -102,10 +52,9 @@ class JobsRunnerAsyncio:
102
52
  total_interviews: Optional[List["Interview"]] = None,
103
53
  raise_validation_errors: bool = False,
104
54
  ) -> AsyncGenerator["Result", None]:
105
- """Creates and processes tasks asynchronously, yielding results as they complete.
55
+ """Creates the tasks, runs them asynchronously, and returns the results as a Results object.
106
56
 
107
- Tasks are created and processed in a streaming fashion rather than building the full list upfront.
108
- Results are yielded as soon as they are available.
57
+ Completed tasks are yielded as they are completed.
109
58
 
110
59
  :param n: how many times to run each interview
111
60
  :param stop_on_exception: Whether to stop the interview if an exception is raised
@@ -113,70 +62,29 @@ class JobsRunnerAsyncio:
113
62
  :param total_interviews: A list of interviews to run can be provided instead.
114
63
  :param raise_validation_errors: Whether to raise validation errors
115
64
  """
116
- # Initialize interviews iterator
117
- if total_interviews:
118
- interviews_iter = iter(total_interviews)
65
+ tasks = []
66
+ if total_interviews: # was already passed in total interviews
119
67
  self.total_interviews = total_interviews
120
68
  else:
121
- interviews_iter = self._populate_total_interviews(n=n)
122
- self.total_interviews = list(interviews_iter)
123
- interviews_iter = iter(self.total_interviews) # Create fresh iterator
69
+ self.total_interviews = list(
70
+ self._populate_total_interviews(n=n)
71
+ ) # Populate self.total_interviews before creating tasks
124
72
 
125
73
  self._initialized.set() # Signal that we're ready
126
74
 
127
- # Keep track of active tasks
128
- active_tasks = set()
75
+ for interview in self.total_interviews:
76
+ interviewing_task = self._build_interview_task(
77
+ interview=interview,
78
+ stop_on_exception=stop_on_exception,
79
+ sidecar_model=sidecar_model,
80
+ raise_validation_errors=raise_validation_errors,
81
+ )
82
+ tasks.append(asyncio.create_task(interviewing_task))
129
83
 
130
- try:
131
- while True:
132
- # Add new tasks if we're below max_concurrent and there are more interviews
133
- while len(active_tasks) < self.MAX_CONCURRENT:
134
- try:
135
- interview = next(interviews_iter)
136
- task = asyncio.create_task(
137
- self._build_interview_task(
138
- interview=interview,
139
- stop_on_exception=stop_on_exception,
140
- sidecar_model=sidecar_model,
141
- raise_validation_errors=raise_validation_errors,
142
- )
143
- )
144
- active_tasks.add(task)
145
- # Add callback to remove task from set when done
146
- task.add_done_callback(active_tasks.discard)
147
- except StopIteration:
148
- break
149
-
150
- if not active_tasks:
151
- break
152
-
153
- # Wait for next completed task
154
- done, _ = await asyncio.wait(
155
- active_tasks, return_when=asyncio.FIRST_COMPLETED
156
- )
157
-
158
- # Process completed tasks
159
- for task in done:
160
- try:
161
- result = await task
162
- self.jobs_runner_status.add_completed_interview(result)
163
- yield result
164
- except Exception as e:
165
- if stop_on_exception:
166
- # Cancel remaining tasks
167
- for t in active_tasks:
168
- if not t.done():
169
- t.cancel()
170
- raise
171
- else:
172
- # Log error and continue
173
- # logger.error(f"Task failed with error: {e}")
174
- continue
175
- finally:
176
- # Ensure we cancel any remaining tasks if we exit early
177
- for task in active_tasks:
178
- if not task.done():
179
- task.cancel()
84
+ for task in asyncio.as_completed(tasks):
85
+ result = await task
86
+ self.jobs_runner_status.add_completed_interview(result)
87
+ yield result
180
88
 
181
89
  def _populate_total_interviews(
182
90
  self, n: int = 1
@@ -260,20 +168,20 @@ class JobsRunnerAsyncio:
260
168
 
261
169
  prompt_dictionary = {}
262
170
  for answer_key_name in answer_key_names:
263
- prompt_dictionary[
264
- answer_key_name + "_user_prompt"
265
- ] = question_name_to_prompts[answer_key_name]["user_prompt"]
266
- prompt_dictionary[
267
- answer_key_name + "_system_prompt"
268
- ] = question_name_to_prompts[answer_key_name]["system_prompt"]
171
+ prompt_dictionary[answer_key_name + "_user_prompt"] = (
172
+ question_name_to_prompts[answer_key_name]["user_prompt"]
173
+ )
174
+ prompt_dictionary[answer_key_name + "_system_prompt"] = (
175
+ question_name_to_prompts[answer_key_name]["system_prompt"]
176
+ )
269
177
 
270
178
  raw_model_results_dictionary = {}
271
179
  cache_used_dictionary = {}
272
180
  for result in valid_results:
273
181
  question_name = result.question_name
274
- raw_model_results_dictionary[
275
- question_name + "_raw_model_response"
276
- ] = result.raw_model_response
182
+ raw_model_results_dictionary[question_name + "_raw_model_response"] = (
183
+ result.raw_model_response
184
+ )
277
185
  raw_model_results_dictionary[question_name + "_cost"] = result.cost
278
186
  one_use_buys = (
279
187
  "NA"
@@ -337,25 +245,11 @@ class JobsRunnerAsyncio:
337
245
  if len(results.task_history.indices) > 5:
338
246
  msg += f"Exceptions were raised in the following interviews: {results.task_history.indices}.\n"
339
247
 
340
- import sys
341
-
342
- print(msg, file=sys.stderr)
343
- from edsl.config import CONFIG
344
-
345
- if CONFIG.get("EDSL_OPEN_EXCEPTION_REPORT_URL") == "True":
346
- open_in_browser = True
347
- elif CONFIG.get("EDSL_OPEN_EXCEPTION_REPORT_URL") == "False":
348
- open_in_browser = False
349
- else:
350
- raise Exception(
351
- "EDSL_OPEN_EXCEPTION_REPORT_URL", "must be either True or False"
352
- )
353
-
354
- # print("open_in_browser", open_in_browser)
355
-
248
+ print(msg)
249
+ # this is where exceptions are opening up
356
250
  filepath = results.task_history.html(
357
251
  cta="Open report to see details.",
358
- open_in_browser=open_in_browser,
252
+ open_in_browser=True,
359
253
  return_link=True,
360
254
  )
361
255
 
@@ -385,7 +279,6 @@ class JobsRunnerAsyncio:
385
279
  progress_bar: bool = False,
386
280
  sidecar_model: Optional[LanguageModel] = None,
387
281
  jobs_runner_status: Optional[Type[JobsRunnerStatusBase]] = None,
388
- job_uuid: Optional[UUID] = None,
389
282
  print_exceptions: bool = True,
390
283
  raise_validation_errors: bool = False,
391
284
  ) -> "Coroutine":
@@ -404,11 +297,13 @@ class JobsRunnerAsyncio:
404
297
 
405
298
  if jobs_runner_status is not None:
406
299
  self.jobs_runner_status = jobs_runner_status(
407
- self, n=n, endpoint_url=endpoint_url, job_uuid=job_uuid
300
+ self, n=n, endpoint_url=endpoint_url
408
301
  )
409
302
  else:
410
303
  self.jobs_runner_status = JobsRunnerStatus(
411
- self, n=n, endpoint_url=endpoint_url, job_uuid=job_uuid
304
+ self,
305
+ n=n,
306
+ endpoint_url=endpoint_url,
412
307
  )
413
308
 
414
309
  stop_event = threading.Event()
@@ -239,6 +239,7 @@ class JobsRunnerStatusBase(ABC):
239
239
  return stat_definitions[stat_name]()
240
240
 
241
241
  def update_progress(self, stop_event):
242
+
242
243
  while not stop_event.is_set():
243
244
  self.send_status_update()
244
245
  time.sleep(self.refresh_rate)
@@ -247,6 +248,7 @@ class JobsRunnerStatusBase(ABC):
247
248
 
248
249
 
249
250
  class JobsRunnerStatus(JobsRunnerStatusBase):
251
+
250
252
  @property
251
253
  def create_url(self) -> str:
252
254
  return f"{self.base_url}/api/v0/local-job"
@@ -8,12 +8,7 @@ from edsl.jobs.tasks.task_status_enum import TaskStatus
8
8
 
9
9
 
10
10
  class TaskHistory:
11
- def __init__(
12
- self,
13
- interviews: List["Interview"],
14
- include_traceback: bool = False,
15
- max_interviews: int = 10,
16
- ):
11
+ def __init__(self, interviews: List["Interview"], include_traceback: bool = False):
17
12
  """
18
13
  The structure of a TaskHistory exception
19
14
 
@@ -27,7 +22,6 @@ class TaskHistory:
27
22
  self.include_traceback = include_traceback
28
23
 
29
24
  self._interviews = {index: i for index, i in enumerate(self.total_interviews)}
30
- self.max_interviews = max_interviews
31
25
 
32
26
  @classmethod
33
27
  def example(cls):
@@ -81,6 +75,13 @@ class TaskHistory:
81
75
 
82
76
  def to_dict(self, add_edsl_version=True):
83
77
  """Return the TaskHistory as a dictionary."""
78
+ # return {
79
+ # "exceptions": [
80
+ # e.to_dict(include_traceback=self.include_traceback)
81
+ # for e in self.exceptions
82
+ # ],
83
+ # "indices": self.indices,
84
+ # }
84
85
  d = {
85
86
  "interviews": [
86
87
  i.to_dict(add_edsl_version=add_edsl_version)
@@ -123,11 +124,10 @@ class TaskHistory:
123
124
 
124
125
  def _repr_html_(self):
125
126
  """Return an HTML representation of the TaskHistory."""
126
- d = self.to_dict(add_edsl_version=False)
127
- data = [[k, v] for k, v in d.items()]
128
- from tabulate import tabulate
127
+ from edsl.utilities.utilities import data_to_html
129
128
 
130
- return tabulate(data, headers=["keys", "values"], tablefmt="html")
129
+ newdata = self.to_dict()["exceptions"]
130
+ return data_to_html(newdata, replace_new_lines=True)
131
131
 
132
132
  def show_exceptions(self, tracebacks=False):
133
133
  """Print the exceptions."""
@@ -257,6 +257,8 @@ class TaskHistory:
257
257
  for question_name, exceptions in interview.exceptions.items():
258
258
  for exception in exceptions:
259
259
  exception_type = exception.exception.__class__.__name__
260
+ # exception_type = exception["exception"]
261
+ # breakpoint()
260
262
  if exception_type in exceptions_by_type:
261
263
  exceptions_by_type[exception_type] += 1
262
264
  else:
@@ -343,9 +345,9 @@ class TaskHistory:
343
345
 
344
346
  env = Environment(loader=TemplateLoader("edsl", "templates/error_reporting"))
345
347
 
346
- # Get current memory usage at this point
347
-
348
+ # Load and render a template
348
349
  template = env.get_template("base.html")
350
+ # rendered_template = template.render(your_data=your_data)
349
351
 
350
352
  # Render the template with data
351
353
  output = template.render(
@@ -359,7 +361,6 @@ class TaskHistory:
359
361
  exceptions_by_model=self.exceptions_by_model,
360
362
  exceptions_by_service=self.exceptions_by_service,
361
363
  models_used=models_used,
362
- max_interviews=self.max_interviews,
363
364
  )
364
365
  return output
365
366
 
@@ -369,7 +370,7 @@ class TaskHistory:
369
370
  return_link=False,
370
371
  css=None,
371
372
  cta="Open Report in New Tab",
372
- open_in_browser=False,
373
+ open_in_browser=True,
373
374
  ):
374
375
  """Return an HTML report."""
375
376