edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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 (212) hide show
  1. edsl/Base.py +197 -116
  2. edsl/__init__.py +15 -7
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +351 -147
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +101 -50
  7. edsl/agents/InvigilatorBase.py +62 -70
  8. edsl/agents/PromptConstructor.py +143 -225
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  11. edsl/agents/__init__.py +0 -1
  12. edsl/agents/prompt_helpers.py +3 -3
  13. edsl/agents/question_option_processor.py +172 -0
  14. edsl/auto/AutoStudy.py +18 -5
  15. edsl/auto/StageBase.py +53 -40
  16. edsl/auto/StageQuestions.py +2 -1
  17. edsl/auto/utilities.py +0 -6
  18. edsl/config.py +22 -2
  19. edsl/conversation/car_buying.py +2 -1
  20. edsl/coop/CoopFunctionsMixin.py +15 -0
  21. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  22. edsl/coop/PriceFetcher.py +1 -1
  23. edsl/coop/coop.py +125 -47
  24. edsl/coop/utils.py +14 -14
  25. edsl/data/Cache.py +45 -27
  26. edsl/data/CacheEntry.py +12 -15
  27. edsl/data/CacheHandler.py +31 -12
  28. edsl/data/RemoteCacheSync.py +154 -46
  29. edsl/data/__init__.py +4 -3
  30. edsl/data_transfer_models.py +2 -1
  31. edsl/enums.py +27 -0
  32. edsl/exceptions/__init__.py +50 -50
  33. edsl/exceptions/agents.py +12 -0
  34. edsl/exceptions/inference_services.py +5 -0
  35. edsl/exceptions/questions.py +24 -6
  36. edsl/exceptions/scenarios.py +7 -0
  37. edsl/inference_services/AnthropicService.py +38 -19
  38. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  39. edsl/inference_services/AvailableModelFetcher.py +215 -0
  40. edsl/inference_services/AwsBedrock.py +0 -2
  41. edsl/inference_services/AzureAI.py +0 -2
  42. edsl/inference_services/GoogleService.py +7 -12
  43. edsl/inference_services/InferenceServiceABC.py +18 -85
  44. edsl/inference_services/InferenceServicesCollection.py +120 -79
  45. edsl/inference_services/MistralAIService.py +0 -3
  46. edsl/inference_services/OpenAIService.py +47 -35
  47. edsl/inference_services/PerplexityService.py +0 -3
  48. edsl/inference_services/ServiceAvailability.py +135 -0
  49. edsl/inference_services/TestService.py +11 -10
  50. edsl/inference_services/TogetherAIService.py +5 -3
  51. edsl/inference_services/data_structures.py +134 -0
  52. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  53. edsl/jobs/Answers.py +1 -14
  54. edsl/jobs/FetchInvigilator.py +47 -0
  55. edsl/jobs/InterviewTaskManager.py +98 -0
  56. edsl/jobs/InterviewsConstructor.py +50 -0
  57. edsl/jobs/Jobs.py +356 -431
  58. edsl/jobs/JobsChecks.py +35 -10
  59. edsl/jobs/JobsComponentConstructor.py +189 -0
  60. edsl/jobs/JobsPrompts.py +6 -4
  61. edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
  62. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  63. edsl/jobs/RequestTokenEstimator.py +30 -0
  64. edsl/jobs/async_interview_runner.py +138 -0
  65. edsl/jobs/buckets/BucketCollection.py +44 -3
  66. edsl/jobs/buckets/TokenBucket.py +53 -21
  67. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  68. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  69. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  70. edsl/jobs/data_structures.py +120 -0
  71. edsl/jobs/decorators.py +35 -0
  72. edsl/jobs/interviews/Interview.py +143 -408
  73. edsl/jobs/jobs_status_enums.py +9 -0
  74. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  75. edsl/jobs/results_exceptions_handler.py +98 -0
  76. edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
  77. edsl/jobs/runners/JobsRunnerStatus.py +133 -165
  78. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  79. edsl/jobs/tasks/TaskHistory.py +38 -18
  80. edsl/jobs/tasks/task_status_enum.py +0 -2
  81. edsl/language_models/ComputeCost.py +63 -0
  82. edsl/language_models/LanguageModel.py +194 -236
  83. edsl/language_models/ModelList.py +28 -19
  84. edsl/language_models/PriceManager.py +127 -0
  85. edsl/language_models/RawResponseHandler.py +106 -0
  86. edsl/language_models/ServiceDataSources.py +0 -0
  87. edsl/language_models/__init__.py +1 -2
  88. edsl/language_models/key_management/KeyLookup.py +63 -0
  89. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  90. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  91. edsl/language_models/key_management/__init__.py +0 -0
  92. edsl/language_models/key_management/models.py +131 -0
  93. edsl/language_models/model.py +256 -0
  94. edsl/language_models/repair.py +2 -2
  95. edsl/language_models/utilities.py +5 -4
  96. edsl/notebooks/Notebook.py +19 -14
  97. edsl/notebooks/NotebookToLaTeX.py +142 -0
  98. edsl/prompts/Prompt.py +29 -39
  99. edsl/questions/ExceptionExplainer.py +77 -0
  100. edsl/questions/HTMLQuestion.py +103 -0
  101. edsl/questions/QuestionBase.py +68 -214
  102. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  103. edsl/questions/QuestionBudget.py +1 -1
  104. edsl/questions/QuestionCheckBox.py +3 -3
  105. edsl/questions/QuestionExtract.py +5 -7
  106. edsl/questions/QuestionFreeText.py +2 -3
  107. edsl/questions/QuestionList.py +10 -18
  108. edsl/questions/QuestionMatrix.py +265 -0
  109. edsl/questions/QuestionMultipleChoice.py +67 -23
  110. edsl/questions/QuestionNumerical.py +2 -4
  111. edsl/questions/QuestionRank.py +7 -17
  112. edsl/questions/SimpleAskMixin.py +4 -3
  113. edsl/questions/__init__.py +2 -1
  114. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
  115. edsl/questions/data_structures.py +20 -0
  116. edsl/questions/derived/QuestionLinearScale.py +6 -3
  117. edsl/questions/derived/QuestionTopK.py +1 -1
  118. edsl/questions/descriptors.py +17 -3
  119. edsl/questions/loop_processor.py +149 -0
  120. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
  121. edsl/questions/question_registry.py +1 -1
  122. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
  123. edsl/questions/response_validator_factory.py +34 -0
  124. edsl/questions/templates/matrix/__init__.py +1 -0
  125. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  126. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  127. edsl/results/CSSParameterizer.py +1 -1
  128. edsl/results/Dataset.py +170 -7
  129. edsl/results/DatasetExportMixin.py +168 -305
  130. edsl/results/DatasetTree.py +28 -8
  131. edsl/results/MarkdownToDocx.py +122 -0
  132. edsl/results/MarkdownToPDF.py +111 -0
  133. edsl/results/Result.py +298 -206
  134. edsl/results/Results.py +149 -131
  135. edsl/results/ResultsExportMixin.py +2 -0
  136. edsl/results/TableDisplay.py +98 -171
  137. edsl/results/TextEditor.py +50 -0
  138. edsl/results/__init__.py +1 -1
  139. edsl/results/file_exports.py +252 -0
  140. edsl/results/{Selector.py → results_selector.py} +23 -13
  141. edsl/results/smart_objects.py +96 -0
  142. edsl/results/table_data_class.py +12 -0
  143. edsl/results/table_renderers.py +118 -0
  144. edsl/scenarios/ConstructDownloadLink.py +109 -0
  145. edsl/scenarios/DocumentChunker.py +102 -0
  146. edsl/scenarios/DocxScenario.py +16 -0
  147. edsl/scenarios/FileStore.py +150 -239
  148. edsl/scenarios/PdfExtractor.py +40 -0
  149. edsl/scenarios/Scenario.py +90 -193
  150. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  151. edsl/scenarios/ScenarioList.py +415 -244
  152. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  153. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  154. edsl/scenarios/__init__.py +1 -2
  155. edsl/scenarios/directory_scanner.py +96 -0
  156. edsl/scenarios/file_methods.py +85 -0
  157. edsl/scenarios/handlers/__init__.py +13 -0
  158. edsl/scenarios/handlers/csv.py +49 -0
  159. edsl/scenarios/handlers/docx.py +76 -0
  160. edsl/scenarios/handlers/html.py +37 -0
  161. edsl/scenarios/handlers/json.py +111 -0
  162. edsl/scenarios/handlers/latex.py +5 -0
  163. edsl/scenarios/handlers/md.py +51 -0
  164. edsl/scenarios/handlers/pdf.py +68 -0
  165. edsl/scenarios/handlers/png.py +39 -0
  166. edsl/scenarios/handlers/pptx.py +105 -0
  167. edsl/scenarios/handlers/py.py +294 -0
  168. edsl/scenarios/handlers/sql.py +313 -0
  169. edsl/scenarios/handlers/sqlite.py +149 -0
  170. edsl/scenarios/handlers/txt.py +33 -0
  171. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
  172. edsl/scenarios/scenario_selector.py +156 -0
  173. edsl/study/ObjectEntry.py +1 -1
  174. edsl/study/SnapShot.py +1 -1
  175. edsl/study/Study.py +5 -12
  176. edsl/surveys/ConstructDAG.py +92 -0
  177. edsl/surveys/EditSurvey.py +221 -0
  178. edsl/surveys/InstructionHandler.py +100 -0
  179. edsl/surveys/MemoryManagement.py +72 -0
  180. edsl/surveys/Rule.py +5 -4
  181. edsl/surveys/RuleCollection.py +25 -27
  182. edsl/surveys/RuleManager.py +172 -0
  183. edsl/surveys/Simulator.py +75 -0
  184. edsl/surveys/Survey.py +270 -791
  185. edsl/surveys/SurveyCSS.py +20 -8
  186. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  187. edsl/surveys/SurveyToApp.py +141 -0
  188. edsl/surveys/__init__.py +4 -2
  189. edsl/surveys/descriptors.py +6 -2
  190. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  191. edsl/surveys/instructions/Instruction.py +4 -13
  192. edsl/surveys/instructions/InstructionCollection.py +11 -6
  193. edsl/templates/error_reporting/interview_details.html +1 -1
  194. edsl/templates/error_reporting/report.html +1 -1
  195. edsl/tools/plotting.py +1 -1
  196. edsl/utilities/PrettyList.py +56 -0
  197. edsl/utilities/is_notebook.py +18 -0
  198. edsl/utilities/is_valid_variable_name.py +11 -0
  199. edsl/utilities/remove_edsl_version.py +24 -0
  200. edsl/utilities/utilities.py +35 -23
  201. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
  202. edsl-0.1.39.dist-info/RECORD +358 -0
  203. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
  204. edsl/language_models/KeyLookup.py +0 -30
  205. edsl/language_models/registry.py +0 -190
  206. edsl/language_models/unused/ReplicateBase.py +0 -83
  207. edsl/results/ResultsDBMixin.py +0 -238
  208. edsl-0.1.38.dev4.dist-info/RECORD +0 -277
  209. /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
  210. /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
  211. /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
  212. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
@@ -1,21 +1,23 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import asyncio
3
- from typing import Coroutine, Dict, Any, Optional
3
+ from typing import Coroutine, Dict, Any, Optional, TYPE_CHECKING
4
4
 
5
- from edsl.prompts.Prompt import Prompt
6
5
  from edsl.utilities.decorators import jupyter_nb_handler
7
6
  from edsl.data_transfer_models import AgentResponseDict
8
7
 
9
- from edsl.data.Cache import Cache
10
-
11
- from edsl.questions.QuestionBase import QuestionBase
12
- from edsl.scenarios.Scenario import Scenario
13
- from edsl.surveys.MemoryPlan import MemoryPlan
14
- from edsl.language_models.LanguageModel import LanguageModel
8
+ if TYPE_CHECKING:
9
+ from edsl.prompts.Prompt import Prompt
10
+ from edsl.data.Cache import Cache
11
+ from edsl.questions.QuestionBase import QuestionBase
12
+ from edsl.scenarios.Scenario import Scenario
13
+ from edsl.surveys.MemoryPlan import MemoryPlan
14
+ from edsl.language_models.LanguageModel import LanguageModel
15
+ from edsl.surveys.Survey import Survey
16
+ from edsl.agents.Agent import Agent
17
+ from edsl.language_models.key_management.KeyLookup import KeyLookup
15
18
 
16
19
  from edsl.data_transfer_models import EDSLResultObjectInput
17
20
  from edsl.agents.PromptConstructor import PromptConstructor
18
-
19
21
  from edsl.agents.prompt_helpers import PromptPlan
20
22
 
21
23
 
@@ -29,25 +31,23 @@ class InvigilatorBase(ABC):
29
31
  'Failed to get response'
30
32
 
31
33
  This returns an empty prompt because there is no memory the agent needs to have at q0.
32
-
33
-
34
34
  """
35
35
 
36
36
  def __init__(
37
37
  self,
38
38
  agent: "Agent",
39
- question: QuestionBase,
40
- scenario: Scenario,
41
- model: LanguageModel,
42
- memory_plan: MemoryPlan,
39
+ question: "QuestionBase",
40
+ scenario: "Scenario",
41
+ model: "LanguageModel",
42
+ memory_plan: "MemoryPlan",
43
43
  current_answers: dict,
44
44
  survey: Optional["Survey"],
45
- cache: Optional[Cache] = None,
45
+ cache: Optional["Cache"] = None,
46
46
  iteration: Optional[int] = 1,
47
47
  additional_prompt_data: Optional[dict] = None,
48
- sidecar_model: Optional[LanguageModel] = None,
49
48
  raise_validation_errors: Optional[bool] = True,
50
49
  prompt_plan: Optional["PromptPlan"] = None,
50
+ key_lookup: Optional["KeyLookup"] = None,
51
51
  ):
52
52
  """Initialize a new Invigilator."""
53
53
  self.agent = agent
@@ -59,24 +59,24 @@ class InvigilatorBase(ABC):
59
59
  self.iteration = iteration
60
60
  self.additional_prompt_data = additional_prompt_data
61
61
  self.cache = cache
62
- self.sidecar_model = sidecar_model
63
62
  self.survey = survey
64
63
  self.raise_validation_errors = raise_validation_errors
64
+ self.key_lookup = key_lookup
65
+
65
66
  if prompt_plan is None:
66
67
  self.prompt_plan = PromptPlan()
67
68
  else:
68
69
  self.prompt_plan = prompt_plan
69
70
 
70
- self.raw_model_response = (
71
- None # placeholder for the raw response from the model
72
- )
71
+ # placeholder to store the raw model response
72
+ self.raw_model_response = None
73
73
 
74
74
  @property
75
75
  def prompt_constructor(self) -> PromptConstructor:
76
76
  """Return the prompt constructor."""
77
77
  return PromptConstructor(self, prompt_plan=self.prompt_plan)
78
78
 
79
- def to_dict(self):
79
+ def to_dict(self, include_cache=False) -> Dict[str, Any]:
80
80
  attributes = [
81
81
  "agent",
82
82
  "question",
@@ -86,10 +86,10 @@ class InvigilatorBase(ABC):
86
86
  "current_answers",
87
87
  "iteration",
88
88
  "additional_prompt_data",
89
- "cache",
90
- "sidecar_model",
91
89
  "survey",
92
90
  ]
91
+ if include_cache:
92
+ attributes.append("cache")
93
93
 
94
94
  def serialize_attribute(attr):
95
95
  value = getattr(self, attr)
@@ -104,43 +104,37 @@ class InvigilatorBase(ABC):
104
104
  return {attr: serialize_attribute(attr) for attr in attributes}
105
105
 
106
106
  @classmethod
107
- def from_dict(cls, data):
107
+ def from_dict(cls, data) -> "InvigilatorBase":
108
108
  from edsl.agents.Agent import Agent
109
109
  from edsl.questions import QuestionBase
110
110
  from edsl.scenarios.Scenario import Scenario
111
111
  from edsl.surveys.MemoryPlan import MemoryPlan
112
112
  from edsl.language_models.LanguageModel import LanguageModel
113
113
  from edsl.surveys.Survey import Survey
114
+ from edsl.data.Cache import Cache
115
+
116
+ attributes_to_classes = {
117
+ "agent": Agent,
118
+ "question": QuestionBase,
119
+ "scenario": Scenario,
120
+ "model": LanguageModel,
121
+ "memory_plan": MemoryPlan,
122
+ "survey": Survey,
123
+ "cache": Cache,
124
+ }
125
+ d = {}
126
+ for attr, cls_ in attributes_to_classes.items():
127
+ if attr in data and data[attr] is not None:
128
+ if attr not in data:
129
+ d[attr] = {}
130
+ else:
131
+ d[attr] = cls_.from_dict(data[attr])
114
132
 
115
- agent = Agent.from_dict(data["agent"])
116
- question = QuestionBase.from_dict(data["question"])
117
- scenario = Scenario.from_dict(data["scenario"])
118
- model = LanguageModel.from_dict(data["model"])
119
- memory_plan = MemoryPlan.from_dict(data["memory_plan"])
120
- survey = Survey.from_dict(data["survey"])
121
- current_answers = data["current_answers"]
122
- iteration = data["iteration"]
123
- additional_prompt_data = data["additional_prompt_data"]
124
- cache = Cache.from_dict(data["cache"])
125
-
126
- if data["sidecar_model"] is None:
127
- sidecar_model = None
128
- else:
129
- sidecar_model = LanguageModel.from_dict(data["sidecar_model"])
133
+ d["current_answers"] = data["current_answers"]
134
+ d["iteration"] = data["iteration"]
135
+ d["additional_prompt_data"] = data["additional_prompt_data"]
130
136
 
131
- return cls(
132
- agent=agent,
133
- question=question,
134
- scenario=scenario,
135
- model=model,
136
- memory_plan=memory_plan,
137
- current_answers=current_answers,
138
- survey=survey,
139
- iteration=iteration,
140
- additional_prompt_data=additional_prompt_data,
141
- cache=cache,
142
- sidecar_model=sidecar_model,
143
- )
137
+ d = cls(**d)
144
138
 
145
139
  def __repr__(self) -> str:
146
140
  """Return a string representation of the Invigilator.
@@ -149,9 +143,9 @@ class InvigilatorBase(ABC):
149
143
  'InvigilatorExample(...)'
150
144
 
151
145
  """
152
- return f"{self.__class__.__name__}(agent={repr(self.agent)}, question={repr(self.question)}, scneario={repr(self.scenario)}, model={repr(self.model)}, memory_plan={repr(self.memory_plan)}, current_answers={repr(self.current_answers)}, iteration{repr(self.iteration)}, additional_prompt_data={repr(self.additional_prompt_data)}, cache={repr(self.cache)}, sidecarmodel={repr(self.sidecar_model)})"
146
+ return f"{self.__class__.__name__}(agent={repr(self.agent)}, question={repr(self.question)}, scneario={repr(self.scenario)}, model={repr(self.model)}, memory_plan={repr(self.memory_plan)}, current_answers={repr(self.current_answers)}, iteration{repr(self.iteration)}, additional_prompt_data={repr(self.additional_prompt_data)}, cache={repr(self.cache)})"
153
147
 
154
- def get_failed_task_result(self, failure_reason) -> EDSLResultObjectInput:
148
+ def get_failed_task_result(self, failure_reason: str) -> EDSLResultObjectInput:
155
149
  """Return an AgentResponseDict used in case the question-asking fails.
156
150
 
157
151
  Possible reasons include:
@@ -172,8 +166,9 @@ class InvigilatorBase(ABC):
172
166
  }
173
167
  return EDSLResultObjectInput(**data)
174
168
 
175
- def get_prompts(self) -> Dict[str, Prompt]:
169
+ def get_prompts(self) -> Dict[str, "Prompt"]:
176
170
  """Return the prompt used."""
171
+ from edsl.prompts.Prompt import Prompt
177
172
 
178
173
  return {
179
174
  "user_prompt": Prompt("NA"),
@@ -205,24 +200,25 @@ class InvigilatorBase(ABC):
205
200
  >>> InvigilatorBase.example()
206
201
  InvigilatorExample(...)
207
202
 
203
+ >>> InvigilatorBase.example().answer_question()
204
+ {'message': [{'text': 'SPAM!'}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
205
+
206
+ >>> InvigilatorBase.example(throw_an_exception=True).answer_question()
207
+ Traceback (most recent call last):
208
+ ...
209
+ Exception: This is a test error
208
210
  """
209
211
  from edsl.agents.Agent import Agent
210
- from edsl.questions import QuestionMultipleChoice
211
212
  from edsl.scenarios.Scenario import Scenario
212
- from edsl.language_models import LanguageModel
213
213
  from edsl.surveys.MemoryPlan import MemoryPlan
214
-
215
- from edsl.enums import InferenceServiceType
216
-
217
- from edsl import Model
214
+ from edsl.language_models.model import Model
215
+ from edsl.surveys.Survey import Survey
218
216
 
219
217
  model = Model("test", canned_response="SPAM!")
220
218
 
221
219
  if throw_an_exception:
222
- model.throw_an_exception = True
220
+ model.throw_exception = True
223
221
  agent = Agent.example()
224
- # question = QuestionMultipleChoice.example()
225
- from edsl.surveys import Survey
226
222
 
227
223
  if not survey:
228
224
  survey = Survey.example()
@@ -232,14 +228,10 @@ class InvigilatorBase(ABC):
232
228
 
233
229
  question = question or survey.questions[0]
234
230
  scenario = scenario or Scenario.example()
235
- # memory_plan = None #memory_plan = MemoryPlan()
236
- from edsl import Survey
237
-
238
231
  memory_plan = MemoryPlan(survey=survey)
239
232
  current_answers = None
240
- from edsl.agents.PromptConstructor import PromptConstructor
241
233
 
242
- class InvigilatorExample(InvigilatorBase):
234
+ class InvigilatorExample(cls):
243
235
  """An example invigilator."""
244
236
 
245
237
  async def async_answer_question(self):