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
@@ -0,0 +1,286 @@
1
+ """This module contains the Interview class, which is responsible for conducting an interview asynchronously."""
2
+
3
+ from __future__ import annotations
4
+ import asyncio
5
+ import time
6
+ import traceback
7
+ from typing import Generator, Union
8
+
9
+ from edsl import CONFIG
10
+ from edsl.exceptions import InterviewTimeoutError
11
+
12
+ # from edsl.questions.QuestionBase import QuestionBase
13
+ from edsl.surveys.base import EndOfSurvey
14
+ from edsl.jobs.buckets.ModelBuckets import ModelBuckets
15
+ from edsl.jobs.interviews.InterviewExceptionEntry import InterviewExceptionEntry
16
+ from edsl.jobs.interviews.retry_management import retry_strategy
17
+ from edsl.jobs.tasks.task_status_enum import TaskStatus
18
+ from edsl.jobs.tasks.QuestionTaskCreator import QuestionTaskCreator
19
+
20
+ # from edsl.agents.InvigilatorBase import InvigilatorBase
21
+
22
+ from rich.console import Console
23
+ from rich.traceback import Traceback
24
+
25
+ TIMEOUT = float(CONFIG.get("EDSL_API_TIMEOUT"))
26
+
27
+
28
+ def frame_summary_to_dict(frame):
29
+ """
30
+ Convert a FrameSummary object to a dictionary.
31
+
32
+ :param frame: A traceback FrameSummary object
33
+ :return: A dictionary containing the frame's details
34
+ """
35
+ return {
36
+ "filename": frame.filename,
37
+ "lineno": frame.lineno,
38
+ "name": frame.name,
39
+ "line": frame.line,
40
+ }
41
+
42
+
43
+ class InterviewTaskBuildingMixin:
44
+ def _build_invigilators(
45
+ self, debug: bool
46
+ ) -> Generator["InvigilatorBase", None, None]:
47
+ """Create an invigilator for each question.
48
+
49
+ :param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
50
+
51
+ An invigilator is responsible for answering a particular question in the survey.
52
+ """
53
+ for question in self.survey.questions:
54
+ yield self._get_invigilator(question=question, debug=debug)
55
+
56
+ def _get_invigilator(self, question: "QuestionBase", debug: bool) -> "Invigilator":
57
+ """Return an invigilator for the given question.
58
+
59
+ :param question: the question to be answered
60
+ :param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
61
+ """
62
+ invigilator = self.agent.create_invigilator(
63
+ question=question,
64
+ scenario=self.scenario,
65
+ model=self.model,
66
+ debug=debug,
67
+ survey=self.survey,
68
+ memory_plan=self.survey.memory_plan,
69
+ current_answers=self.answers,
70
+ iteration=self.iteration,
71
+ cache=self.cache,
72
+ sidecar_model=self.sidecar_model,
73
+ )
74
+ """Return an invigilator for the given question."""
75
+ return invigilator
76
+
77
+ def _build_question_tasks(
78
+ self,
79
+ debug: bool,
80
+ model_buckets: ModelBuckets,
81
+ ) -> list[asyncio.Task]:
82
+ """Create a task for each question, with dependencies on the questions that must be answered before this one can be answered.
83
+
84
+ :param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
85
+ :param model_buckets: the model buckets used to track and control usage rates.
86
+ """
87
+ tasks = []
88
+ for question in self.survey.questions:
89
+ tasks_that_must_be_completed_before = list(
90
+ self._get_tasks_that_must_be_completed_before(
91
+ tasks=tasks, question=question
92
+ )
93
+ )
94
+ question_task = self._create_question_task(
95
+ question=question,
96
+ tasks_that_must_be_completed_before=tasks_that_must_be_completed_before,
97
+ model_buckets=model_buckets,
98
+ debug=debug,
99
+ iteration=self.iteration,
100
+ )
101
+ tasks.append(question_task)
102
+ return tuple(tasks) # , invigilators
103
+
104
+ def _get_tasks_that_must_be_completed_before(
105
+ self, *, tasks: list[asyncio.Task], question: "QuestionBase"
106
+ ) -> Generator[asyncio.Task, None, None]:
107
+ """Return the tasks that must be completed before the given question can be answered.
108
+
109
+ :param tasks: a list of tasks that have been created so far.
110
+ :param question: the question for which we are determining dependencies.
111
+
112
+ If a question has no dependencies, this will be an empty list, [].
113
+ """
114
+ parents_of_focal_question = self.dag.get(question.question_name, [])
115
+ for parent_question_name in parents_of_focal_question:
116
+ yield tasks[self.to_index[parent_question_name]]
117
+
118
+ def _create_question_task(
119
+ self,
120
+ *,
121
+ question: "QuestionBase",
122
+ tasks_that_must_be_completed_before: list[asyncio.Task],
123
+ model_buckets: ModelBuckets,
124
+ debug: bool,
125
+ iteration: int = 0,
126
+ ) -> asyncio.Task:
127
+ """Create a task that depends on the passed-in dependencies that are awaited before the task is run.
128
+
129
+ :param question: the question to be answered. This is the question we are creating a task for.
130
+ :param tasks_that_must_be_completed_before: the tasks that must be completed before the focal task is run.
131
+ :param model_buckets: the model buckets used to track and control usage rates.
132
+ :param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
133
+ :param iteration: the iteration number for the interview.
134
+
135
+ The task is created by a `QuestionTaskCreator`, which is responsible for creating the task and managing its dependencies.
136
+ It is passed a reference to the function that will be called to answer the question.
137
+ It is passed a list "tasks_that_must_be_completed_before" that are awaited before the task is run.
138
+ These are added as a dependency to the focal task.
139
+ """
140
+ task_creator = QuestionTaskCreator(
141
+ question=question,
142
+ answer_question_func=self._answer_question_and_record_task,
143
+ token_estimator=self._get_estimated_request_tokens,
144
+ model_buckets=model_buckets,
145
+ iteration=iteration,
146
+ )
147
+ for task in tasks_that_must_be_completed_before:
148
+ task_creator.add_dependency(task)
149
+
150
+ self.task_creators.update(
151
+ {question.question_name: task_creator}
152
+ ) # track this task creator
153
+ return task_creator.generate_task(debug)
154
+
155
+ def _get_estimated_request_tokens(self, question) -> float:
156
+ """Estimate the number of tokens that will be required to run the focal task."""
157
+ invigilator = self._get_invigilator(question=question, debug=False)
158
+ # TODO: There should be a way to get a more accurate estimate.
159
+ combined_text = ""
160
+ for prompt in invigilator.get_prompts().values():
161
+ if hasattr(prompt, "text"):
162
+ combined_text += prompt.text
163
+ elif isinstance(prompt, str):
164
+ combined_text += prompt
165
+ else:
166
+ raise ValueError(f"Prompt is of type {type(prompt)}")
167
+ return len(combined_text) / 4.0
168
+
169
+ async def _answer_question_and_record_task(
170
+ self,
171
+ *,
172
+ question: "QuestionBase",
173
+ debug: bool,
174
+ task=None,
175
+ ) -> "AgentResponseDict":
176
+ """Answer a question and records the task.
177
+
178
+ This in turn calls the the passed-in agent's async_answer_question method, which returns a response dictionary.
179
+ Note that is updates answers dictionary with the response.
180
+ """
181
+ from edsl.data_transfer_models import AgentResponseDict
182
+
183
+ async def _inner():
184
+ try:
185
+ invigilator = self._get_invigilator(question, debug=debug)
186
+
187
+ if self._skip_this_question(question):
188
+ return invigilator.get_failed_task_result()
189
+
190
+ response: AgentResponseDict = await self._attempt_to_answer_question(
191
+ invigilator, task
192
+ )
193
+
194
+ self._add_answer(response=response, question=question)
195
+
196
+ self._cancel_skipped_questions(question)
197
+ return AgentResponseDict(**response)
198
+ except Exception as e:
199
+ raise e
200
+
201
+ skip_rety = getattr(self, "skip_retry", False)
202
+ if not skip_rety:
203
+ _inner = retry_strategy(_inner)
204
+
205
+ return await _inner()
206
+
207
+ def _add_answer(
208
+ self, response: "AgentResponseDict", question: "QuestionBase"
209
+ ) -> None:
210
+ """Add the answer to the answers dictionary.
211
+
212
+ :param response: the response to the question.
213
+ :param question: the question that was answered.
214
+ """
215
+ self.answers.add_answer(response=response, question=question)
216
+
217
+ def _skip_this_question(self, current_question: "QuestionBase") -> bool:
218
+ """Determine if the current question should be skipped.
219
+
220
+ :param current_question: the question to be answered.
221
+ """
222
+ current_question_index = self.to_index[current_question.question_name]
223
+
224
+ answers = self.answers | self.scenario | self.agent["traits"]
225
+ skip = self.survey.rule_collection.skip_question_before_running(
226
+ current_question_index, answers
227
+ )
228
+ return skip
229
+
230
+ def _handle_exception(self, e, question_name: str, task=None):
231
+ exception_entry = InterviewExceptionEntry(e)
232
+ if task:
233
+ task.task_status = TaskStatus.FAILED
234
+ self.exceptions.add(question_name, exception_entry)
235
+
236
+ async def _attempt_to_answer_question(
237
+ self, invigilator: "InvigilatorBase", task: asyncio.Task
238
+ ) -> "AgentResponseDict":
239
+ """Attempt to answer the question, and handle exceptions.
240
+
241
+ :param invigilator: the invigilator that will answer the question.
242
+ :param task: the task that is being run.
243
+
244
+ """
245
+ try:
246
+ return await asyncio.wait_for(
247
+ invigilator.async_answer_question(), timeout=TIMEOUT
248
+ )
249
+ except asyncio.TimeoutError as e:
250
+ self._handle_exception(e, invigilator.question.question_name, task)
251
+ raise InterviewTimeoutError(f"Task timed out after {TIMEOUT} seconds.")
252
+ except Exception as e:
253
+ self._handle_exception(e, invigilator.question.question_name, task)
254
+ raise e
255
+
256
+ def _cancel_skipped_questions(self, current_question: QuestionBase) -> None:
257
+ """Cancel the tasks for questions that are skipped.
258
+
259
+ :param current_question: the question that was just answered.
260
+
261
+ It first determines the next question, given the current question and the current answers.
262
+ If the next question is the end of the survey, it cancels all remaining tasks.
263
+ If the next question is after the current question, it cancels all tasks between the current question and the next question.
264
+ """
265
+ current_question_index: int = self.to_index[current_question.question_name]
266
+
267
+ next_question: Union[
268
+ int, EndOfSurvey
269
+ ] = self.survey.rule_collection.next_question(
270
+ q_now=current_question_index,
271
+ answers=self.answers | self.scenario | self.agent["traits"],
272
+ )
273
+
274
+ next_question_index = next_question.next_q
275
+
276
+ def cancel_between(start, end):
277
+ """Cancel the tasks between the start and end indices."""
278
+ for i in range(start, end):
279
+ self.tasks[i].cancel()
280
+
281
+ if next_question_index == EndOfSurvey:
282
+ cancel_between(current_question_index + 1, len(self.survey.questions))
283
+ return
284
+
285
+ if next_question_index > (current_question_index + 1):
286
+ cancel_between(current_question_index + 1, next_question_index)
@@ -1,26 +1,74 @@
1
+ import traceback
2
+ import datetime
3
+ import time
1
4
  from collections import UserDict
2
5
 
3
6
  from edsl.jobs.interviews.InterviewExceptionEntry import InterviewExceptionEntry
4
7
 
8
+ # #traceback=traceback.format_exc(),
9
+ # #traceback = frame_summary_to_dict(traceback.extract_tb(e.__traceback__))
10
+ # #traceback = [frame_summary_to_dict(f) for f in traceback.extract_tb(e.__traceback__)]
5
11
 
6
- class InterviewExceptionCollection(UserDict):
7
- """A collection of exceptions that occurred during the interview."""
12
+ # class InterviewExceptionEntry:
13
+ # """Class to record an exception that occurred during the interview.
14
+
15
+ # >>> entry = InterviewExceptionEntry.example()
16
+ # >>> entry.to_dict()['exception']
17
+ # "ValueError('An error occurred.')"
18
+ # """
19
+
20
+ # def __init__(self, exception: Exception):
21
+ # self.time = datetime.datetime.now().isoformat()
22
+ # self.exception = exception
23
+
24
+ # def __getitem__(self, key):
25
+ # # Support dict-like access obj['a']
26
+ # return str(getattr(self, key))
27
+
28
+ # @classmethod
29
+ # def example(cls):
30
+ # try:
31
+ # raise ValueError("An error occurred.")
32
+ # except Exception as e:
33
+ # entry = InterviewExceptionEntry(e)
34
+ # return entry
35
+
36
+ # @property
37
+ # def traceback(self):
38
+ # """Return the exception as HTML."""
39
+ # e = self.exception
40
+ # tb_str = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
41
+ # return tb_str
8
42
 
9
- def __init__(self):
10
- super().__init__()
11
- self.fixed = set()
12
43
 
13
- def unfixed_exceptions(self) -> list:
14
- """Return a list of unfixed exceptions."""
15
- return {k: v for k, v in self.data.items() if k not in self.fixed}
44
+ # @property
45
+ # def html(self):
46
+ # from rich.console import Console
47
+ # from rich.table import Table
48
+ # from rich.traceback import Traceback
16
49
 
17
- def num_unfixed(self) -> list:
18
- """Return a list of unfixed questions."""
19
- return len([k for k in self.data.keys() if k not in self.fixed])
50
+ # from io import StringIO
51
+ # html_output = StringIO()
20
52
 
21
- def record_fixed_question(self, question_name: str) -> None:
22
- """Record that a question has been fixed."""
23
- self.fixed.add(question_name)
53
+ # console = Console(file=html_output, record=True)
54
+ # tb = Traceback(show_locals=True)
55
+ # console.print(tb)
56
+
57
+ # tb = Traceback.from_exception(type(self.exception), self.exception, self.exception.__traceback__, show_locals=True)
58
+ # console.print(tb)
59
+ # return html_output.getvalue()
60
+
61
+ # def to_dict(self) -> dict:
62
+ # """Return the exception as a dictionary."""
63
+ # return {
64
+ # 'exception': repr(self.exception),
65
+ # 'time': self.time,
66
+ # 'traceback': self.traceback
67
+ # }
68
+
69
+
70
+ class InterviewExceptionCollection(UserDict):
71
+ """A collection of exceptions that occurred during the interview."""
24
72
 
25
73
  def add(self, question_name: str, entry: InterviewExceptionEntry) -> None:
26
74
  """Add an exception entry to the collection."""
@@ -32,6 +80,12 @@ class InterviewExceptionCollection(UserDict):
32
80
  def to_dict(self, include_traceback=True) -> dict:
33
81
  """Return the collection of exceptions as a dictionary."""
34
82
  newdata = {k: [e.to_dict() for e in v] for k, v in self.data.items()}
83
+ # if not include_traceback:
84
+ # for question in newdata:
85
+ # for exception in newdata[question]:
86
+ # exception[
87
+ # "traceback"
88
+ # ] = "Traceback removed. Set include_traceback=True to include."
35
89
  return newdata
36
90
 
37
91
  def _repr_html_(self) -> str:
@@ -0,0 +1,37 @@
1
+ from edsl import CONFIG
2
+
3
+ from tenacity import (
4
+ retry,
5
+ wait_exponential,
6
+ stop_after_attempt,
7
+ retry_if_exception_type,
8
+ before_sleep,
9
+ )
10
+
11
+ EDSL_BACKOFF_START_SEC = float(CONFIG.get("EDSL_BACKOFF_START_SEC"))
12
+ EDSL_MAX_BACKOFF_SEC = float(CONFIG.get("EDSL_MAX_BACKOFF_SEC"))
13
+ EDSL_MAX_ATTEMPTS = int(CONFIG.get("EDSL_MAX_ATTEMPTS"))
14
+
15
+
16
+ def print_retry(retry_state, print_to_terminal=True):
17
+ "Prints details on tenacity retries."
18
+ attempt_number = retry_state.attempt_number
19
+ exception = retry_state.outcome.exception()
20
+ wait_time = retry_state.next_action.sleep
21
+ if print_to_terminal:
22
+ print(
23
+ f"Attempt {attempt_number} failed with exception:" f"{exception}",
24
+ f"now waiting {wait_time:.2f} seconds before retrying."
25
+ f"Parameters: start={EDSL_BACKOFF_START_SEC}, max={EDSL_MAX_BACKOFF_SEC}, max_attempts={EDSL_MAX_ATTEMPTS}."
26
+ "\n\n",
27
+ )
28
+
29
+
30
+ retry_strategy = retry(
31
+ wait=wait_exponential(
32
+ multiplier=EDSL_BACKOFF_START_SEC, max=EDSL_MAX_BACKOFF_SEC
33
+ ), # Exponential back-off starting at 1s, doubling, maxing out at 60s
34
+ stop=stop_after_attempt(EDSL_MAX_ATTEMPTS), # Stop after 5 attempts
35
+ # retry=retry_if_exception_type(Exception), # Customize this as per your specific retry-able exception
36
+ before_sleep=print_retry, # Use custom print function for retries
37
+ )