edsl 0.1.33__py3-none-any.whl → 0.1.33.dev1__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 (180) hide show
  1. edsl/Base.py +3 -9
  2. edsl/__init__.py +3 -8
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +8 -40
  5. edsl/agents/AgentList.py +0 -43
  6. edsl/agents/Invigilator.py +219 -135
  7. edsl/agents/InvigilatorBase.py +59 -148
  8. edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
  9. edsl/agents/__init__.py +0 -1
  10. edsl/config.py +56 -47
  11. edsl/coop/coop.py +7 -50
  12. edsl/data/Cache.py +1 -35
  13. edsl/data_transfer_models.py +38 -73
  14. edsl/enums.py +0 -4
  15. edsl/exceptions/language_models.py +1 -25
  16. edsl/exceptions/questions.py +5 -62
  17. edsl/exceptions/results.py +0 -4
  18. edsl/inference_services/AnthropicService.py +11 -13
  19. edsl/inference_services/AwsBedrock.py +17 -19
  20. edsl/inference_services/AzureAI.py +20 -37
  21. edsl/inference_services/GoogleService.py +12 -16
  22. edsl/inference_services/GroqService.py +0 -2
  23. edsl/inference_services/InferenceServiceABC.py +3 -58
  24. edsl/inference_services/OpenAIService.py +54 -48
  25. edsl/inference_services/models_available_cache.py +6 -0
  26. edsl/inference_services/registry.py +0 -6
  27. edsl/jobs/Answers.py +12 -10
  28. edsl/jobs/Jobs.py +21 -36
  29. edsl/jobs/buckets/BucketCollection.py +15 -24
  30. edsl/jobs/buckets/TokenBucket.py +14 -93
  31. edsl/jobs/interviews/Interview.py +78 -366
  32. edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
  33. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
  34. edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
  35. edsl/jobs/interviews/retry_management.py +37 -0
  36. edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
  37. edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
  38. edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
  39. edsl/jobs/tasks/TaskHistory.py +213 -148
  40. edsl/language_models/LanguageModel.py +156 -261
  41. edsl/language_models/ModelList.py +2 -2
  42. edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
  43. edsl/language_models/registry.py +6 -23
  44. edsl/language_models/repair.py +19 -0
  45. edsl/prompts/Prompt.py +2 -52
  46. edsl/questions/AnswerValidatorMixin.py +26 -23
  47. edsl/questions/QuestionBase.py +249 -329
  48. edsl/questions/QuestionBudget.py +41 -99
  49. edsl/questions/QuestionCheckBox.py +35 -227
  50. edsl/questions/QuestionExtract.py +27 -98
  51. edsl/questions/QuestionFreeText.py +29 -52
  52. edsl/questions/QuestionFunctional.py +0 -7
  53. edsl/questions/QuestionList.py +22 -141
  54. edsl/questions/QuestionMultipleChoice.py +65 -159
  55. edsl/questions/QuestionNumerical.py +46 -88
  56. edsl/questions/QuestionRank.py +24 -182
  57. edsl/questions/RegisterQuestionsMeta.py +12 -31
  58. edsl/questions/__init__.py +4 -3
  59. edsl/questions/derived/QuestionLikertFive.py +5 -10
  60. edsl/questions/derived/QuestionLinearScale.py +2 -15
  61. edsl/questions/derived/QuestionTopK.py +1 -10
  62. edsl/questions/derived/QuestionYesNo.py +3 -24
  63. edsl/questions/descriptors.py +7 -43
  64. edsl/questions/question_registry.py +2 -6
  65. edsl/results/Dataset.py +0 -20
  66. edsl/results/DatasetExportMixin.py +48 -46
  67. edsl/results/Result.py +5 -32
  68. edsl/results/Results.py +46 -135
  69. edsl/results/ResultsDBMixin.py +3 -3
  70. edsl/scenarios/FileStore.py +10 -71
  71. edsl/scenarios/Scenario.py +25 -96
  72. edsl/scenarios/ScenarioImageMixin.py +2 -2
  73. edsl/scenarios/ScenarioList.py +39 -361
  74. edsl/scenarios/ScenarioListExportMixin.py +0 -9
  75. edsl/scenarios/ScenarioListPdfMixin.py +4 -150
  76. edsl/study/SnapShot.py +1 -8
  77. edsl/study/Study.py +0 -32
  78. edsl/surveys/Rule.py +1 -10
  79. edsl/surveys/RuleCollection.py +5 -21
  80. edsl/surveys/Survey.py +310 -636
  81. edsl/surveys/SurveyExportMixin.py +9 -71
  82. edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
  83. edsl/surveys/SurveyQualtricsImport.py +4 -75
  84. edsl/utilities/gcp_bucket/simple_example.py +9 -0
  85. edsl/utilities/utilities.py +1 -9
  86. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
  87. edsl-0.1.33.dev1.dist-info/RECORD +209 -0
  88. edsl/TemplateLoader.py +0 -24
  89. edsl/auto/AutoStudy.py +0 -117
  90. edsl/auto/StageBase.py +0 -230
  91. edsl/auto/StageGenerateSurvey.py +0 -178
  92. edsl/auto/StageLabelQuestions.py +0 -125
  93. edsl/auto/StagePersona.py +0 -61
  94. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  95. edsl/auto/StagePersonaDimensionValues.py +0 -74
  96. edsl/auto/StagePersonaDimensions.py +0 -69
  97. edsl/auto/StageQuestions.py +0 -73
  98. edsl/auto/SurveyCreatorPipeline.py +0 -21
  99. edsl/auto/utilities.py +0 -224
  100. edsl/coop/PriceFetcher.py +0 -58
  101. edsl/inference_services/MistralAIService.py +0 -120
  102. edsl/inference_services/TestService.py +0 -80
  103. edsl/inference_services/TogetherAIService.py +0 -170
  104. edsl/jobs/FailedQuestion.py +0 -78
  105. edsl/jobs/runners/JobsRunnerStatus.py +0 -331
  106. edsl/language_models/fake_openai_call.py +0 -15
  107. edsl/language_models/fake_openai_service.py +0 -61
  108. edsl/language_models/utilities.py +0 -61
  109. edsl/questions/QuestionBaseGenMixin.py +0 -133
  110. edsl/questions/QuestionBasePromptsMixin.py +0 -266
  111. edsl/questions/Quick.py +0 -41
  112. edsl/questions/ResponseValidatorABC.py +0 -170
  113. edsl/questions/decorators.py +0 -21
  114. edsl/questions/prompt_templates/question_budget.jinja +0 -13
  115. edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
  116. edsl/questions/prompt_templates/question_extract.jinja +0 -11
  117. edsl/questions/prompt_templates/question_free_text.jinja +0 -3
  118. edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
  119. edsl/questions/prompt_templates/question_list.jinja +0 -17
  120. edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
  121. edsl/questions/prompt_templates/question_numerical.jinja +0 -37
  122. edsl/questions/templates/__init__.py +0 -0
  123. edsl/questions/templates/budget/__init__.py +0 -0
  124. edsl/questions/templates/budget/answering_instructions.jinja +0 -7
  125. edsl/questions/templates/budget/question_presentation.jinja +0 -7
  126. edsl/questions/templates/checkbox/__init__.py +0 -0
  127. edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
  128. edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
  129. edsl/questions/templates/extract/__init__.py +0 -0
  130. edsl/questions/templates/extract/answering_instructions.jinja +0 -7
  131. edsl/questions/templates/extract/question_presentation.jinja +0 -1
  132. edsl/questions/templates/free_text/__init__.py +0 -0
  133. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  134. edsl/questions/templates/free_text/question_presentation.jinja +0 -1
  135. edsl/questions/templates/likert_five/__init__.py +0 -0
  136. edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
  137. edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
  138. edsl/questions/templates/linear_scale/__init__.py +0 -0
  139. edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
  140. edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
  141. edsl/questions/templates/list/__init__.py +0 -0
  142. edsl/questions/templates/list/answering_instructions.jinja +0 -4
  143. edsl/questions/templates/list/question_presentation.jinja +0 -5
  144. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  145. edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
  146. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  147. edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
  148. edsl/questions/templates/numerical/__init__.py +0 -0
  149. edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
  150. edsl/questions/templates/numerical/question_presentation.jinja +0 -7
  151. edsl/questions/templates/rank/__init__.py +0 -0
  152. edsl/questions/templates/rank/answering_instructions.jinja +0 -11
  153. edsl/questions/templates/rank/question_presentation.jinja +0 -15
  154. edsl/questions/templates/top_k/__init__.py +0 -0
  155. edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
  156. edsl/questions/templates/top_k/question_presentation.jinja +0 -22
  157. edsl/questions/templates/yes_no/__init__.py +0 -0
  158. edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
  159. edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
  160. edsl/results/DatasetTree.py +0 -145
  161. edsl/results/Selector.py +0 -118
  162. edsl/results/tree_explore.py +0 -115
  163. edsl/surveys/instructions/ChangeInstruction.py +0 -47
  164. edsl/surveys/instructions/Instruction.py +0 -34
  165. edsl/surveys/instructions/InstructionCollection.py +0 -77
  166. edsl/surveys/instructions/__init__.py +0 -0
  167. edsl/templates/error_reporting/base.html +0 -24
  168. edsl/templates/error_reporting/exceptions_by_model.html +0 -35
  169. edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
  170. edsl/templates/error_reporting/exceptions_by_type.html +0 -17
  171. edsl/templates/error_reporting/interview_details.html +0 -116
  172. edsl/templates/error_reporting/interviews.html +0 -10
  173. edsl/templates/error_reporting/overview.html +0 -5
  174. edsl/templates/error_reporting/performance_plot.html +0 -2
  175. edsl/templates/error_reporting/report.css +0 -74
  176. edsl/templates/error_reporting/report.html +0 -118
  177. edsl/templates/error_reporting/report.js +0 -25
  178. edsl-0.1.33.dist-info/RECORD +0 -295
  179. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
  180. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/WHEEL +0 -0
@@ -1,27 +1,42 @@
1
1
  from __future__ import annotations
2
2
  import time
3
- import math
4
3
  import asyncio
5
- import functools
6
- import threading
7
- from typing import Coroutine, List, AsyncGenerator, Optional, Union, Generator
4
+ import time
8
5
  from contextlib import contextmanager
9
- from collections import UserList
10
6
 
11
- from edsl.results.Results import Results
12
- from rich.live import Live
13
- from rich.console import Console
7
+ from typing import Coroutine, List, AsyncGenerator, Optional, Union
14
8
 
15
9
  from edsl import shared_globals
16
10
  from edsl.jobs.interviews.Interview import Interview
17
- from edsl.jobs.runners.JobsRunnerStatus import JobsRunnerStatus
18
-
11
+ from edsl.jobs.runners.JobsRunnerStatusMixin import JobsRunnerStatusMixin
19
12
  from edsl.jobs.tasks.TaskHistory import TaskHistory
20
13
  from edsl.jobs.buckets.BucketCollection import BucketCollection
21
14
  from edsl.utilities.decorators import jupyter_nb_handler
22
- from edsl.data.Cache import Cache
23
- from edsl.results.Result import Result
24
- from edsl.results.Results import Results
15
+
16
+ import time
17
+ import functools
18
+
19
+
20
+ def cache_with_timeout(timeout):
21
+ def decorator(func):
22
+ cached_result = {}
23
+ last_computation_time = [0] # Using list to store mutable value
24
+
25
+ @functools.wraps(func)
26
+ def wrapper(*args, **kwargs):
27
+ current_time = time.time()
28
+ if (current_time - last_computation_time[0]) >= timeout:
29
+ cached_result["value"] = func(*args, **kwargs)
30
+ last_computation_time[0] = current_time
31
+ return cached_result["value"]
32
+
33
+ return wrapper
34
+
35
+ return decorator
36
+
37
+
38
+ # from queue import Queue
39
+ from collections import UserList
25
40
 
26
41
 
27
42
  class StatusTracker(UserList):
@@ -33,87 +48,99 @@ class StatusTracker(UserList):
33
48
  return print(f"Completed: {len(self.data)} of {self.total_tasks}", end="\r")
34
49
 
35
50
 
36
- class JobsRunnerAsyncio:
51
+ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
37
52
  """A class for running a collection of interviews asynchronously.
38
53
 
39
54
  It gets instaniated from a Jobs object.
40
55
  The Jobs object is a collection of interviews that are to be run.
41
56
  """
42
57
 
43
- def __init__(self, jobs: "Jobs"):
58
+ def __init__(self, jobs: Jobs):
44
59
  self.jobs = jobs
60
+ # this creates the interviews, which can take a while
45
61
  self.interviews: List["Interview"] = jobs.interviews()
46
62
  self.bucket_collection: "BucketCollection" = jobs.bucket_collection
47
63
  self.total_interviews: List["Interview"] = []
48
64
 
49
- # self.jobs_runner_status = JobsRunnerStatus(self, n=1)
50
-
51
65
  async def run_async_generator(
52
66
  self,
53
67
  cache: "Cache",
54
68
  n: int = 1,
69
+ debug: bool = False,
55
70
  stop_on_exception: bool = False,
56
- sidecar_model: Optional["LanguageModel"] = None,
71
+ sidecar_model: "LanguageModel" = None,
57
72
  total_interviews: Optional[List["Interview"]] = None,
58
- raise_validation_errors: bool = False,
59
73
  ) -> AsyncGenerator["Result", None]:
60
74
  """Creates the tasks, runs them asynchronously, and returns the results as a Results object.
61
75
 
62
76
  Completed tasks are yielded as they are completed.
63
77
 
64
78
  :param n: how many times to run each interview
79
+ :param debug:
65
80
  :param stop_on_exception: Whether to stop the interview if an exception is raised
66
81
  :param sidecar_model: a language model to use in addition to the interview's model
67
82
  :param total_interviews: A list of interviews to run can be provided instead.
68
- :param raise_validation_errors: Whether to raise validation errors
69
83
  """
70
84
  tasks = []
71
- if total_interviews: # was already passed in total interviews
85
+ if total_interviews:
72
86
  self.total_interviews = total_interviews
73
87
  else:
74
- self.total_interviews = list(
75
- self._populate_total_interviews(n=n)
88
+ self._populate_total_interviews(
89
+ n=n
76
90
  ) # Populate self.total_interviews before creating tasks
77
91
 
78
92
  for interview in self.total_interviews:
79
93
  interviewing_task = self._build_interview_task(
80
94
  interview=interview,
95
+ debug=debug,
81
96
  stop_on_exception=stop_on_exception,
82
97
  sidecar_model=sidecar_model,
83
- raise_validation_errors=raise_validation_errors,
84
98
  )
85
99
  tasks.append(asyncio.create_task(interviewing_task))
86
100
 
87
101
  for task in asyncio.as_completed(tasks):
88
102
  result = await task
89
- self.jobs_runner_status.add_completed_interview(result)
90
103
  yield result
91
104
 
92
- def _populate_total_interviews(
93
- self, n: int = 1
94
- ) -> Generator["Interview", None, None]:
105
+ def _populate_total_interviews(self, n: int = 1) -> None:
95
106
  """Populates self.total_interviews with n copies of each interview.
96
107
 
97
108
  :param n: how many times to run each interview.
98
109
  """
110
+ # TODO: Why not return a list of interviews instead of modifying the object?
111
+
112
+ self.total_interviews = []
99
113
  for interview in self.interviews:
100
114
  for iteration in range(n):
101
115
  if iteration > 0:
102
- yield interview.duplicate(iteration=iteration, cache=self.cache)
116
+ new_interview = interview.duplicate(
117
+ iteration=iteration, cache=self.cache
118
+ )
119
+ self.total_interviews.append(new_interview)
103
120
  else:
104
- interview.cache = self.cache
105
- yield interview
121
+ interview.cache = (
122
+ self.cache
123
+ ) # set the cache for the first interview
124
+ self.total_interviews.append(interview)
125
+
126
+ async def run_async(self, cache=None, n=1) -> Results:
127
+ from edsl.results.Results import Results
106
128
 
107
- async def run_async(self, cache: Optional["Cache"] = None, n: int = 1) -> Results:
108
- """Used for some other modules that have a non-standard way of running interviews."""
109
- self.jobs_runner_status = JobsRunnerStatus(self, n=n)
110
- self.cache = Cache() if cache is None else cache
129
+ # breakpoint()
130
+ # tracker = StatusTracker(total_tasks=len(self.interviews))
131
+
132
+ if cache is None:
133
+ self.cache = Cache()
134
+ else:
135
+ self.cache = cache
111
136
  data = []
112
137
  async for result in self.run_async_generator(cache=self.cache, n=n):
113
138
  data.append(result)
114
139
  return Results(survey=self.jobs.survey, data=data)
115
140
 
116
141
  def simple_run(self):
142
+ from edsl.results.Results import Results
143
+
117
144
  data = asyncio.run(self.run_async())
118
145
  return Results(survey=self.jobs.survey, data=data)
119
146
 
@@ -121,13 +148,14 @@ class JobsRunnerAsyncio:
121
148
  self,
122
149
  *,
123
150
  interview: Interview,
151
+ debug: bool,
124
152
  stop_on_exception: bool = False,
125
- sidecar_model: Optional["LanguageModel"] = None,
126
- raise_validation_errors: bool = False,
127
- ) -> "Result":
153
+ sidecar_model: Optional[LanguageModel] = None,
154
+ ) -> Result:
128
155
  """Conducts an interview and returns the result.
129
156
 
130
157
  :param interview: the interview to conduct
158
+ :param debug: prints debug messages
131
159
  :param stop_on_exception: stops the interview if an exception is raised
132
160
  :param sidecar_model: a language model to use in addition to the interview's model
133
161
  """
@@ -136,37 +164,24 @@ class JobsRunnerAsyncio:
136
164
 
137
165
  # get the results of the interview
138
166
  answer, valid_results = await interview.async_conduct_interview(
167
+ debug=debug,
139
168
  model_buckets=model_buckets,
140
169
  stop_on_exception=stop_on_exception,
141
170
  sidecar_model=sidecar_model,
142
- raise_validation_errors=raise_validation_errors,
143
171
  )
144
172
 
145
- question_results = {}
146
- for result in valid_results:
147
- question_results[result.question_name] = result
148
-
149
- answer_key_names = list(question_results.keys())
150
-
151
- generated_tokens_dict = {
152
- k + "_generated_tokens": question_results[k].generated_tokens
153
- for k in answer_key_names
154
- }
155
- comments_dict = {
156
- k + "_comment": question_results[k].comment for k in answer_key_names
157
- }
158
-
159
173
  # we should have a valid result for each question
160
- answer_dict = {k: answer[k] for k in answer_key_names}
174
+ answer_key_names = {k for k in set(answer.keys()) if not k.endswith("_comment")}
175
+
161
176
  assert len(valid_results) == len(answer_key_names)
162
177
 
163
178
  # TODO: move this down into Interview
164
179
  question_name_to_prompts = dict({})
165
180
  for result in valid_results:
166
- question_name = result.question_name
181
+ question_name = result["question_name"]
167
182
  question_name_to_prompts[question_name] = {
168
- "user_prompt": result.prompts["user_prompt"],
169
- "system_prompt": result.prompts["system_prompt"],
183
+ "user_prompt": result["prompts"]["user_prompt"],
184
+ "system_prompt": result["prompts"]["system_prompt"],
170
185
  }
171
186
 
172
187
  prompt_dictionary = {}
@@ -180,31 +195,22 @@ class JobsRunnerAsyncio:
180
195
 
181
196
  raw_model_results_dictionary = {}
182
197
  for result in valid_results:
183
- question_name = result.question_name
198
+ question_name = result["question_name"]
184
199
  raw_model_results_dictionary[
185
200
  question_name + "_raw_model_response"
186
- ] = result.raw_model_response
187
- raw_model_results_dictionary[question_name + "_cost"] = result.cost
188
- one_use_buys = (
189
- "NA"
190
- if isinstance(result.cost, str)
191
- or result.cost == 0
192
- or result.cost is None
193
- else 1.0 / result.cost
194
- )
195
- raw_model_results_dictionary[question_name + "_one_usd_buys"] = one_use_buys
201
+ ] = result["raw_model_response"]
202
+
203
+ from edsl.results.Result import Result
196
204
 
197
205
  result = Result(
198
206
  agent=interview.agent,
199
207
  scenario=interview.scenario,
200
208
  model=interview.model,
201
209
  iteration=interview.iteration,
202
- answer=answer_dict,
210
+ answer=answer,
203
211
  prompt=prompt_dictionary,
204
212
  raw_model_response=raw_model_results_dictionary,
205
213
  survey=interview.survey,
206
- generated_tokens=generated_tokens_dict,
207
- comments_dict=comments_dict,
208
214
  )
209
215
  result.interview_hash = hash(interview)
210
216
 
@@ -214,109 +220,132 @@ class JobsRunnerAsyncio:
214
220
  def elapsed_time(self):
215
221
  return time.monotonic() - self.start_time
216
222
 
217
- def process_results(
218
- self, raw_results: Results, cache: Cache, print_exceptions: bool
219
- ):
220
- interview_lookup = {
221
- hash(interview): index
222
- for index, interview in enumerate(self.total_interviews)
223
- }
224
- interview_hashes = list(interview_lookup.keys())
225
-
226
- results = Results(
227
- survey=self.jobs.survey,
228
- data=sorted(
229
- raw_results, key=lambda x: interview_hashes.index(x.interview_hash)
230
- ),
231
- )
232
- results.cache = cache
233
- results.task_history = TaskHistory(
234
- self.total_interviews, include_traceback=False
235
- )
236
- results.has_unfixed_exceptions = results.task_history.has_unfixed_exceptions
237
- results.bucket_collection = self.bucket_collection
238
-
239
- if results.has_unfixed_exceptions and print_exceptions:
240
- from edsl.scenarios.FileStore import HTMLFileStore
241
- from edsl.config import CONFIG
242
- from edsl.coop.coop import Coop
243
-
244
- msg = f"Exceptions were raised in {len(results.task_history.indices)} out of {len(self.total_interviews)} interviews.\n"
245
-
246
- if len(results.task_history.indices) > 5:
247
- msg += f"Exceptions were raised in the following interviews: {results.task_history.indices}.\n"
248
-
249
- print(msg)
250
- # this is where exceptions are opening up
251
- filepath = results.task_history.html(
252
- cta="Open report to see details.",
253
- open_in_browser=True,
254
- return_link=True,
255
- )
256
-
257
- try:
258
- coop = Coop()
259
- user_edsl_settings = coop.edsl_settings
260
- remote_logging = user_edsl_settings["remote_logging"]
261
- except Exception as e:
262
- print(e)
263
- remote_logging = False
264
- if remote_logging:
265
- filestore = HTMLFileStore(filepath)
266
- coop_details = filestore.push(description="Error report")
267
- print(coop_details)
268
-
269
- print("Also see: https://docs.expectedparrot.com/en/latest/exceptions.html")
270
-
271
- return results
272
-
273
223
  @jupyter_nb_handler
274
224
  async def run(
275
225
  self,
276
226
  cache: Union[Cache, False, None],
277
227
  n: int = 1,
228
+ debug: bool = False,
278
229
  stop_on_exception: bool = False,
279
230
  progress_bar: bool = False,
280
231
  sidecar_model: Optional[LanguageModel] = None,
281
232
  print_exceptions: bool = True,
282
- raise_validation_errors: bool = False,
283
233
  ) -> "Coroutine":
284
234
  """Runs a collection of interviews, handling both async and sync contexts."""
235
+ from rich.console import Console
285
236
 
237
+ console = Console()
286
238
  self.results = []
287
239
  self.start_time = time.monotonic()
288
240
  self.completed = False
289
241
  self.cache = cache
290
242
  self.sidecar_model = sidecar_model
291
243
 
292
- self.jobs_runner_status = JobsRunnerStatus(self, n=n)
244
+ from edsl.results.Results import Results
245
+ from rich.live import Live
246
+ from rich.console import Console
247
+
248
+ @cache_with_timeout(1)
249
+ def generate_table():
250
+ return self.status_table(self.results, self.elapsed_time)
293
251
 
294
- async def process_results(cache):
252
+ async def process_results(cache, progress_bar_context=None):
295
253
  """Processes results from interviews."""
296
254
  async for result in self.run_async_generator(
297
255
  n=n,
256
+ debug=debug,
298
257
  stop_on_exception=stop_on_exception,
299
258
  cache=cache,
300
259
  sidecar_model=sidecar_model,
301
- raise_validation_errors=raise_validation_errors,
302
260
  ):
303
261
  self.results.append(result)
304
- self.completed = True
305
-
306
- def run_progress_bar():
307
- """Runs the progress bar in a separate thread."""
308
- self.jobs_runner_status.update_progress()
262
+ if progress_bar_context:
263
+ progress_bar_context.update(generate_table())
264
+ self.completed = True
265
+
266
+ async def update_progress_bar(progress_bar_context):
267
+ """Updates the progress bar at fixed intervals."""
268
+ if progress_bar_context is None:
269
+ return
270
+
271
+ while True:
272
+ progress_bar_context.update(generate_table())
273
+ await asyncio.sleep(0.1) # Update interval
274
+ if self.completed:
275
+ break
276
+
277
+ @contextmanager
278
+ def conditional_context(condition, context_manager):
279
+ if condition:
280
+ with context_manager as cm:
281
+ yield cm
282
+ else:
283
+ yield
284
+
285
+ with conditional_context(
286
+ progress_bar, Live(generate_table(), console=console, refresh_per_second=1)
287
+ ) as progress_bar_context:
288
+ with cache as c:
289
+ progress_task = asyncio.create_task(
290
+ update_progress_bar(progress_bar_context)
291
+ )
292
+
293
+ try:
294
+ await asyncio.gather(
295
+ progress_task,
296
+ process_results(
297
+ cache=c, progress_bar_context=progress_bar_context
298
+ ),
299
+ )
300
+ except asyncio.CancelledError:
301
+ pass
302
+ finally:
303
+ progress_task.cancel() # Cancel the progress_task when process_results is done
304
+ await progress_task
305
+
306
+ await asyncio.sleep(1) # short delay to show the final status
307
+
308
+ if progress_bar_context:
309
+ progress_bar_context.update(generate_table())
310
+
311
+ # puts results in the same order as the total interviews
312
+ interview_hashes = [hash(interview) for interview in self.total_interviews]
313
+ self.results = sorted(
314
+ self.results, key=lambda x: interview_hashes.index(x.interview_hash)
315
+ )
309
316
 
310
- if progress_bar:
311
- progress_thread = threading.Thread(target=run_progress_bar)
312
- progress_thread.start()
317
+ results = Results(survey=self.jobs.survey, data=self.results)
318
+ task_history = TaskHistory(self.total_interviews, include_traceback=False)
319
+ results.task_history = task_history
320
+
321
+ results.has_exceptions = task_history.has_exceptions
322
+
323
+ if results.has_exceptions:
324
+ # put the failed interviews in the results object as a list
325
+ failed_interviews = [
326
+ interview.duplicate(
327
+ iteration=interview.iteration, cache=interview.cache
328
+ )
329
+ for interview in self.total_interviews
330
+ if interview.has_exceptions
331
+ ]
332
+ from edsl.jobs.Jobs import Jobs
333
+
334
+ results.failed_jobs = Jobs.from_interviews(
335
+ [interview for interview in failed_interviews]
336
+ )
337
+ if print_exceptions:
338
+ msg = f"Exceptions were raised in {len(results.task_history.indices)} out of {len(self.total_interviews)} interviews.\n"
313
339
 
314
- with cache as c:
315
- await process_results(cache=c)
340
+ if len(results.task_history.indices) > 5:
341
+ msg += f"Exceptions were raised in the following interviews: {results.task_history.indices}.\n"
316
342
 
317
- if progress_bar:
318
- progress_thread.join()
343
+ shared_globals["edsl_runner_exceptions"] = task_history
344
+ print(msg)
345
+ # this is where exceptions are opening up
346
+ task_history.html(cta="Open report to see details.")
347
+ print(
348
+ "Also see: https://docs.expectedparrot.com/en/latest/exceptions.html"
349
+ )
319
350
 
320
- return self.process_results(
321
- raw_results=self.results, cache=cache, print_exceptions=print_exceptions
322
- )
351
+ return results