edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.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 (163) hide show
  1. edsl/TemplateLoader.py +24 -0
  2. edsl/__init__.py +8 -4
  3. edsl/agents/Agent.py +46 -14
  4. edsl/agents/AgentList.py +43 -0
  5. edsl/agents/Invigilator.py +125 -212
  6. edsl/agents/InvigilatorBase.py +140 -32
  7. edsl/agents/PromptConstructionMixin.py +43 -66
  8. edsl/agents/__init__.py +1 -0
  9. edsl/auto/AutoStudy.py +117 -0
  10. edsl/auto/StageBase.py +230 -0
  11. edsl/auto/StageGenerateSurvey.py +178 -0
  12. edsl/auto/StageLabelQuestions.py +125 -0
  13. edsl/auto/StagePersona.py +61 -0
  14. edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
  15. edsl/auto/StagePersonaDimensionValues.py +74 -0
  16. edsl/auto/StagePersonaDimensions.py +69 -0
  17. edsl/auto/StageQuestions.py +73 -0
  18. edsl/auto/SurveyCreatorPipeline.py +21 -0
  19. edsl/auto/utilities.py +224 -0
  20. edsl/config.py +38 -39
  21. edsl/coop/PriceFetcher.py +58 -0
  22. edsl/coop/coop.py +39 -5
  23. edsl/data/Cache.py +35 -1
  24. edsl/data_transfer_models.py +120 -38
  25. edsl/enums.py +2 -0
  26. edsl/exceptions/language_models.py +25 -1
  27. edsl/exceptions/questions.py +62 -5
  28. edsl/exceptions/results.py +4 -0
  29. edsl/inference_services/AnthropicService.py +13 -11
  30. edsl/inference_services/AwsBedrock.py +19 -17
  31. edsl/inference_services/AzureAI.py +37 -20
  32. edsl/inference_services/GoogleService.py +16 -12
  33. edsl/inference_services/GroqService.py +2 -0
  34. edsl/inference_services/InferenceServiceABC.py +24 -0
  35. edsl/inference_services/MistralAIService.py +120 -0
  36. edsl/inference_services/OpenAIService.py +41 -50
  37. edsl/inference_services/TestService.py +71 -0
  38. edsl/inference_services/models_available_cache.py +0 -6
  39. edsl/inference_services/registry.py +4 -0
  40. edsl/jobs/Answers.py +10 -12
  41. edsl/jobs/FailedQuestion.py +78 -0
  42. edsl/jobs/Jobs.py +18 -13
  43. edsl/jobs/buckets/TokenBucket.py +39 -14
  44. edsl/jobs/interviews/Interview.py +297 -77
  45. edsl/jobs/interviews/InterviewExceptionEntry.py +83 -19
  46. edsl/jobs/interviews/interview_exception_tracking.py +0 -70
  47. edsl/jobs/interviews/retry_management.py +3 -1
  48. edsl/jobs/runners/JobsRunnerAsyncio.py +116 -70
  49. edsl/jobs/runners/JobsRunnerStatusMixin.py +1 -1
  50. edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
  51. edsl/jobs/tasks/TaskHistory.py +131 -213
  52. edsl/language_models/LanguageModel.py +239 -129
  53. edsl/language_models/ModelList.py +2 -2
  54. edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
  55. edsl/language_models/fake_openai_call.py +15 -0
  56. edsl/language_models/fake_openai_service.py +61 -0
  57. edsl/language_models/registry.py +15 -2
  58. edsl/language_models/repair.py +0 -19
  59. edsl/language_models/utilities.py +61 -0
  60. edsl/prompts/Prompt.py +52 -2
  61. edsl/questions/AnswerValidatorMixin.py +23 -26
  62. edsl/questions/QuestionBase.py +273 -242
  63. edsl/questions/QuestionBaseGenMixin.py +133 -0
  64. edsl/questions/QuestionBasePromptsMixin.py +266 -0
  65. edsl/questions/QuestionBudget.py +6 -0
  66. edsl/questions/QuestionCheckBox.py +227 -35
  67. edsl/questions/QuestionExtract.py +98 -27
  68. edsl/questions/QuestionFreeText.py +46 -29
  69. edsl/questions/QuestionFunctional.py +7 -0
  70. edsl/questions/QuestionList.py +141 -22
  71. edsl/questions/QuestionMultipleChoice.py +173 -64
  72. edsl/questions/QuestionNumerical.py +87 -46
  73. edsl/questions/QuestionRank.py +182 -24
  74. edsl/questions/RegisterQuestionsMeta.py +31 -12
  75. edsl/questions/ResponseValidatorABC.py +169 -0
  76. edsl/questions/__init__.py +3 -4
  77. edsl/questions/decorators.py +21 -0
  78. edsl/questions/derived/QuestionLikertFive.py +10 -5
  79. edsl/questions/derived/QuestionLinearScale.py +11 -1
  80. edsl/questions/derived/QuestionTopK.py +6 -0
  81. edsl/questions/derived/QuestionYesNo.py +16 -1
  82. edsl/questions/descriptors.py +43 -7
  83. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  84. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  85. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  86. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  87. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  88. edsl/questions/prompt_templates/question_list.jinja +17 -0
  89. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  90. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  91. edsl/questions/question_registry.py +6 -2
  92. edsl/questions/templates/__init__.py +0 -0
  93. edsl/questions/templates/checkbox/__init__.py +0 -0
  94. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  95. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  96. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  97. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  98. edsl/questions/templates/free_text/__init__.py +0 -0
  99. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  100. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  101. edsl/questions/templates/likert_five/__init__.py +0 -0
  102. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  103. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  104. edsl/questions/templates/linear_scale/__init__.py +0 -0
  105. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  106. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  107. edsl/questions/templates/list/__init__.py +0 -0
  108. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  109. edsl/questions/templates/list/question_presentation.jinja +5 -0
  110. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  111. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  112. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  113. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  114. edsl/questions/templates/numerical/__init__.py +0 -0
  115. edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
  116. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  117. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  118. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  119. edsl/questions/templates/top_k/__init__.py +0 -0
  120. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  121. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  122. edsl/questions/templates/yes_no/__init__.py +0 -0
  123. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  124. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  125. edsl/results/Dataset.py +20 -0
  126. edsl/results/DatasetExportMixin.py +41 -47
  127. edsl/results/DatasetTree.py +145 -0
  128. edsl/results/Result.py +32 -5
  129. edsl/results/Results.py +131 -45
  130. edsl/results/ResultsDBMixin.py +3 -3
  131. edsl/results/Selector.py +118 -0
  132. edsl/results/tree_explore.py +115 -0
  133. edsl/scenarios/Scenario.py +10 -4
  134. edsl/scenarios/ScenarioList.py +348 -39
  135. edsl/scenarios/ScenarioListExportMixin.py +9 -0
  136. edsl/study/SnapShot.py +8 -1
  137. edsl/surveys/RuleCollection.py +2 -2
  138. edsl/surveys/Survey.py +634 -315
  139. edsl/surveys/SurveyExportMixin.py +71 -9
  140. edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
  141. edsl/surveys/SurveyQualtricsImport.py +75 -4
  142. edsl/surveys/instructions/ChangeInstruction.py +47 -0
  143. edsl/surveys/instructions/Instruction.py +34 -0
  144. edsl/surveys/instructions/InstructionCollection.py +77 -0
  145. edsl/surveys/instructions/__init__.py +0 -0
  146. edsl/templates/error_reporting/base.html +24 -0
  147. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  148. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  149. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  150. edsl/templates/error_reporting/interview_details.html +111 -0
  151. edsl/templates/error_reporting/interviews.html +10 -0
  152. edsl/templates/error_reporting/overview.html +5 -0
  153. edsl/templates/error_reporting/performance_plot.html +2 -0
  154. edsl/templates/error_reporting/report.css +74 -0
  155. edsl/templates/error_reporting/report.html +118 -0
  156. edsl/templates/error_reporting/report.js +25 -0
  157. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/METADATA +4 -2
  158. edsl-0.1.33.dev2.dist-info/RECORD +289 -0
  159. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
  160. edsl/utilities/gcp_bucket/simple_example.py +0 -9
  161. edsl-0.1.33.dev1.dist-info/RECORD +0 -209
  162. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/LICENSE +0 -0
  163. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/WHEEL +0 -0
@@ -1,13 +1,13 @@
1
1
  """This module contains the Question class, which is the base class for all questions in EDSL."""
2
2
 
3
3
  from __future__ import annotations
4
- import time
5
4
  from abc import ABC, abstractmethod
6
- from typing import Any, Type, Optional, List, Callable
5
+ from typing import Any, Type, Optional, List, Callable, Union, TypedDict
7
6
  import copy
8
7
 
9
8
  from edsl.exceptions import (
10
9
  QuestionResponseValidationError,
10
+ QuestionAnswerValidationError,
11
11
  QuestionSerializationError,
12
12
  )
13
13
  from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
@@ -19,6 +19,8 @@ from edsl.Base import PersistenceMixin, RichPrintingMixin
19
19
  from edsl.BaseDiff import BaseDiff, BaseDiffCollection
20
20
 
21
21
  from edsl.questions.SimpleAskMixin import SimpleAskMixin
22
+ from edsl.questions.QuestionBasePromptsMixin import QuestionBasePromptsMixin
23
+ from edsl.questions.QuestionBaseGenMixin import QuestionBaseGenMixin
22
24
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
23
25
 
24
26
 
@@ -26,80 +28,133 @@ class QuestionBase(
26
28
  PersistenceMixin,
27
29
  RichPrintingMixin,
28
30
  SimpleAskMixin,
31
+ QuestionBasePromptsMixin,
32
+ QuestionBaseGenMixin,
29
33
  ABC,
30
34
  AnswerValidatorMixin,
31
35
  metaclass=RegisterQuestionsMeta,
32
36
  ):
33
- """ABC for the Question class. All questions should inherit from this class."""
37
+ """ABC for the Question class. All questions inherit from this class.
38
+ Some of the constraints on child questions are defined in the RegisterQuestionsMeta metaclass.
39
+ """
34
40
 
35
41
  question_name: str = QuestionNameDescriptor()
36
42
  question_text: str = QuestionTextDescriptor()
37
43
 
38
- def __getitem__(self, key: str) -> Any:
39
- """Get an attribute of the question."""
40
- return getattr(self, key)
44
+ _answering_instructions = None
45
+ _question_presentation = None
41
46
 
42
- def __hash__(self) -> int:
43
- """Return a hash of the question."""
44
- from edsl.utilities.utilities import dict_hash
47
+ # region: Validation and simulation methods
48
+ @property
49
+ def response_validator(self) -> "ResponseValidatorBase":
50
+ """Return the response validator."""
51
+ params = (
52
+ {
53
+ "response_model": self.response_model,
54
+ }
55
+ | {k: getattr(self, k) for k in self.validator_parameters}
56
+ | {"exception_to_throw": getattr(self, "exception_to_throw", None)}
57
+ | {"override_answer": getattr(self, "override_answer", None)}
58
+ )
59
+ return self.response_validator_class(**params)
45
60
 
46
- return dict_hash(self._to_dict())
61
+ @property
62
+ def validator_parameters(self) -> list[str]:
63
+ """Return the parameters required for the response validator.
47
64
 
48
- def _repr_html_(self):
49
- from edsl.utilities.utilities import data_to_html
65
+ >>> from edsl import QuestionMultipleChoice as Q
66
+ >>> Q.example().validator_parameters
67
+ ['question_options', 'use_code']
50
68
 
51
- data = self.to_dict()
52
- try:
53
- _ = data.pop("edsl_version")
54
- _ = data.pop("edsl_class_name")
55
- except KeyError:
56
- print("Serialized question lacks edsl version, but is should have it.")
69
+ """
70
+ return self.response_validator_class.required_params
57
71
 
58
- return data_to_html(data)
72
+ @property
73
+ def fake_data_factory(self):
74
+ """Return the fake data factory."""
75
+ if not hasattr(self, "_fake_data_factory"):
76
+ from polyfactory.factories.pydantic_factory import ModelFactory
77
+
78
+ class FakeData(ModelFactory[self.response_model]):
79
+ ...
80
+
81
+ self._fake_data_factory = FakeData
82
+ return self._fake_data_factory
83
+
84
+ def _simulate_answer(self, human_readable: bool = False) -> dict:
85
+ """Simulate a valid answer for debugging purposes (what the validator expects).
86
+ >>> from edsl import QuestionFreeText as Q
87
+ >>> Q.example()._simulate_answer()
88
+ {'answer': '...', 'generated_tokens': ...}
89
+ """
90
+ simulated_answer = self.fake_data_factory.build().dict()
91
+ if human_readable and hasattr(self, "question_options") and self.use_code:
92
+ simulated_answer["answer"] = [
93
+ self.question_options[index] for index in simulated_answer["answer"]
94
+ ]
95
+ return simulated_answer
96
+
97
+ class ValidatedAnswer(TypedDict):
98
+ answer: Any
99
+ comment: Optional[str]
100
+ generated_tokens: Optional[str]
101
+
102
+ def _validate_answer(self, answer: dict) -> ValidatedAnswer:
103
+ """Validate the answer.
104
+ >>> from edsl.exceptions import QuestionAnswerValidationError
105
+ >>> from edsl import QuestionFreeText as Q
106
+ >>> Q.example()._validate_answer({'answer': 'Hello'})
107
+ {'answer': 'Hello', 'generated_tokens': None}
108
+ >>> Q.example()._validate_answer({'shmanswer': 1})
109
+ Traceback (most recent call last):
110
+ ...
111
+ edsl.exceptions.questions.QuestionAnswerValidationError:...
112
+ ...
113
+ """
59
114
 
60
- def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
61
- """Apply a function to the question parts
115
+ return self.response_validator.validate(answer)
62
116
 
63
- >>> from edsl.questions import QuestionFreeText
64
- >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
65
- >>> shouting = lambda x: x.upper()
66
- >>> q.apply_function(shouting)
67
- Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
117
+ # endregion
118
+
119
+ # region: Serialization methods
120
+ @property
121
+ def name(self) -> str:
122
+ "Helper function so questions and instructions can use the same access method"
123
+ return self.question_name
68
124
 
125
+ def __hash__(self) -> int:
126
+ """Return a hash of the question.
127
+
128
+ >>> from edsl import QuestionFreeText as Q
129
+ >>> hash(Q.example())
130
+ 1144312636257752766
69
131
  """
70
- if exclude_components is None:
71
- exclude_components = ["question_name", "question_type"]
72
-
73
- d = copy.deepcopy(self._to_dict())
74
- for key, value in d.items():
75
- if key in exclude_components:
76
- continue
77
- if isinstance(value, dict):
78
- for k, v in value.items():
79
- value[k] = func(v)
80
- d[key] = value
81
- continue
82
- if isinstance(value, list):
83
- value = [func(v) for v in value]
84
- d[key] = value
85
- continue
86
- d[key] = func(value)
87
- return QuestionBase.from_dict(d)
132
+ from edsl.utilities.utilities import dict_hash
133
+
134
+ return dict_hash(self._to_dict())
88
135
 
89
136
  @property
90
137
  def data(self) -> dict:
91
- """Return a dictionary of question attributes **except** for question_type."""
138
+ """Return a dictionary of question attributes **except** for question_type.
139
+
140
+ >>> from edsl import QuestionFreeText as Q
141
+ >>> Q.example().data
142
+ {'question_name': 'how_are_you', 'question_text': 'How are you?'}
143
+ """
144
+ exclude_list = [
145
+ "question_type",
146
+ "_include_comment",
147
+ "_fake_data_factory",
148
+ "_use_code",
149
+ "_answering_instructions",
150
+ "_question_presentation",
151
+ "_model_instructions",
152
+ ]
92
153
  candidate_data = {
93
154
  k.replace("_", "", 1): v
94
155
  for k, v in self.__dict__.items()
95
- if k.startswith("_")
96
- }
97
- optional_attributes = {
98
- "set_instructions": "instructions",
156
+ if k.startswith("_") and k not in exclude_list
99
157
  }
100
- for boolean_flag, attribute in optional_attributes.items():
101
- if hasattr(self, boolean_flag) and not getattr(self, boolean_flag):
102
- candidate_data.pop(attribute, None)
103
158
 
104
159
  if "func" in candidate_data:
105
160
  func = candidate_data.pop("func")
@@ -109,147 +164,22 @@ class QuestionBase(
109
164
 
110
165
  return candidate_data
111
166
 
112
- @classmethod
113
- def applicable_prompts(
114
- cls, model: Optional[str] = None
115
- ) -> list[type["PromptBase"]]:
116
- """Get the prompts that are applicable to the question type.
117
-
118
- :param model: The language model to use.
119
-
120
- >>> from edsl.questions import QuestionFreeText
121
- >>> QuestionFreeText.applicable_prompts()
122
- [<class 'edsl.prompts.library.question_freetext.FreeText'>]
123
-
124
- :param model: The language model to use. If None, assumes does not matter.
125
-
126
- """
127
- from edsl.prompts.registry import get_classes as prompt_lookup
128
-
129
- applicable_prompts = prompt_lookup(
130
- component_type="question_instructions",
131
- question_type=cls.question_type,
132
- model=model,
133
- )
134
- return applicable_prompts
135
-
136
- @property
137
- def model_instructions(self) -> dict:
138
- """Get the model-specific instructions for the question."""
139
- if not hasattr(self, "_model_instructions"):
140
- self._model_instructions = {}
141
- return self._model_instructions
142
-
143
- def _all_text(self) -> str:
144
- """Return the question text."""
145
- txt = ""
146
- for key, value in self.data.items():
147
- if isinstance(value, str):
148
- txt += value
149
- elif isinstance(value, list):
150
- txt += "".join(str(value))
151
- return txt
152
-
153
- @property
154
- def parameters(self) -> set[str]:
155
- """Return the parameters of the question."""
156
- from jinja2 import Environment, meta
157
-
158
- env = Environment()
159
- # Parse the template
160
- txt = self._all_text()
161
- # txt = self.question_text
162
- # if hasattr(self, "question_options"):
163
- # txt += " ".join(self.question_options)
164
- parsed_content = env.parse(txt)
165
- # Extract undeclared variables
166
- variables = meta.find_undeclared_variables(parsed_content)
167
- # Return as a list
168
- return set(variables)
169
-
170
- @model_instructions.setter
171
- def model_instructions(self, data: dict):
172
- """Set the model-specific instructions for the question."""
173
- self._model_instructions = data
174
-
175
- def add_model_instructions(
176
- self, *, instructions: str, model: Optional[str] = None
177
- ) -> None:
178
- """Add model-specific instructions for the question that override the default instructions.
179
-
180
- :param instructions: The instructions to add. This is typically a jinja2 template.
181
- :param model: The language model for this instruction.
182
-
183
- >>> from edsl.questions import QuestionFreeText
184
- >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
185
- >>> q.add_model_instructions(instructions = "{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}", model = "gpt3")
186
- >>> q.get_instructions(model = "gpt3")
187
- Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
188
- """
189
- from edsl import Model
190
-
191
- if not hasattr(self, "_model_instructions"):
192
- self._model_instructions = {}
193
- if model is None:
194
- # if not model is passed, all the models are mapped to this instruction, including 'None'
195
- self._model_instructions = {
196
- model_name: instructions
197
- for model_name in Model.available(name_only=True)
198
- }
199
- self._model_instructions.update({model: instructions})
200
- else:
201
- self._model_instructions.update({model: instructions})
202
-
203
- def get_instructions(self, model: Optional[str] = None) -> type["PromptBase"]:
204
- """Get the mathcing question-answering instructions for the question.
205
-
206
- :param model: The language model to use.
167
+ def _to_dict(self):
168
+ """Convert the question to a dictionary that includes the question type (used in deserialization).
207
169
 
208
- >>> from edsl import QuestionFreeText
209
- >>> QuestionFreeText.example().get_instructions()
210
- Prompt(text=\"""You are being asked the following question: {{question_text}}
211
- Return a valid JSON formatted like this:
212
- {"answer": "<put free text answer here>"}
213
- \""")
170
+ >>> from edsl import QuestionFreeText as Q; Q.example()._to_dict()
171
+ {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
214
172
  """
215
- from edsl.prompts.Prompt import Prompt
216
-
217
- if model in self.model_instructions:
218
- return Prompt(text=self.model_instructions[model])
219
- else:
220
- return self.applicable_prompts(model)[0]()
221
-
222
- def option_permutations(self) -> list[QuestionBase]:
223
- """Return a list of questions with all possible permutations of the options."""
224
-
225
- if not hasattr(self, "question_options"):
226
- return [self]
227
-
228
- import copy
229
- import itertools
230
-
231
- questions = []
232
- for index, permutation in enumerate(
233
- itertools.permutations(self.question_options)
234
- ):
235
- question = copy.deepcopy(self)
236
- question.question_options = list(permutation)
237
- question.question_name = f"{self.question_name}_{index}"
238
- questions.append(question)
239
- return questions
240
-
241
- ############################
242
- # Serialization methods
243
- ############################
244
- def _to_dict(self):
245
- """Convert the question to a dictionary that includes the question type (used in deserialization)."""
246
173
  candidate_data = self.data.copy()
247
174
  candidate_data["question_type"] = self.question_type
248
175
  return candidate_data
249
176
 
250
177
  @add_edsl_version
251
178
  def to_dict(self) -> dict[str, Any]:
252
- """Convert the question to a dictionary that includes the question type (used in deserialization)."""
179
+ """Convert the question to a dictionary that includes the question type (used in deserialization).
180
+ >>> from edsl import QuestionFreeText as Q; Q.example().to_dict()
181
+ {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text', 'edsl_version': '...'}
182
+ """
253
183
  return self._to_dict()
254
184
 
255
185
  @classmethod
@@ -289,39 +219,90 @@ class QuestionBase(
289
219
 
290
220
  return question_class(**local_data)
291
221
 
292
- def copy(self) -> Type[QuestionBase]:
293
- """Return a deep copy of the question."""
294
- return copy.deepcopy(self)
222
+ # endregion
295
223
 
296
- ############################
297
- # Dunder methods
298
- ############################
299
- def print(self):
300
- from rich import print_json
301
- import json
224
+ # region: Running methods
225
+ @classmethod
226
+ def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
227
+ """Get a test model for the question."""
228
+ from edsl.language_models import LanguageModel
302
229
 
303
- print_json(json.dumps(self.to_dict()))
230
+ return LanguageModel.example(canned_response=canned_response, test_model=True)
231
+
232
+ @classmethod
233
+ def run_example(
234
+ cls,
235
+ show_answer: bool = True,
236
+ model: Optional["LanguageModel"] = None,
237
+ cache=False,
238
+ **kwargs,
239
+ ):
240
+ """Run an example of the question.
241
+ >>> from edsl.language_models import LanguageModel
242
+ >>> from edsl import QuestionFreeText as Q
243
+ >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
244
+ >>> m.execute_model_call("", "")
245
+ {'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
246
+ >>> Q.run_example(show_answer = True, model = m)
247
+ ┏━━━━━━━━━━━━━━━━┓
248
+ ┃ answer ┃
249
+ ┃ .how_are_you ┃
250
+ ┡━━━━━━━━━━━━━━━━┩
251
+ │ Yo, what's up? │
252
+ └────────────────┘
253
+ """
254
+ if model is None:
255
+ from edsl import Model
256
+
257
+ model = Model()
258
+ results = cls.example(**kwargs).by(model).run(cache=cache)
259
+ if show_answer:
260
+ results.select("answer.*").print()
261
+ else:
262
+ return results
304
263
 
305
264
  def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
306
265
  """Call the question.
307
266
 
308
- >>> from edsl.language_models import LanguageModel
309
- >>> m = LanguageModel.example(canned_response = "Yo, what's up?", test_model = True)
310
- >>> from edsl import QuestionFreeText
311
- >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
267
+
268
+ >>> from edsl import QuestionFreeText as Q
269
+ >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
270
+ >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
312
271
  >>> q(model = m)
313
272
  "Yo, what's up?"
314
273
 
315
274
  """
316
275
  survey = self.to_survey()
317
- results = survey(model=model, agent=agent, **kwargs)
276
+ results = survey(model=model, agent=agent, **kwargs, cache=False)
318
277
  if just_answer:
319
278
  return results.select(f"answer.{self.question_name}").first()
320
279
  else:
321
280
  return results
322
281
 
323
- async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
324
- """Call the question."""
282
+ def run(self, *args, **kwargs) -> "Results":
283
+ """Turn a single question into a survey and runs it."""
284
+ from edsl.surveys.Survey import Survey
285
+
286
+ s = self.to_survey()
287
+ return s.run(*args, **kwargs)
288
+
289
+ async def run_async(
290
+ self,
291
+ just_answer: bool = True,
292
+ model: Optional["Model"] = None,
293
+ agent: Optional["Agent"] = None,
294
+ **kwargs,
295
+ ) -> Union[Any, "Results"]:
296
+ """Call the question asynchronously.
297
+
298
+ >>> import asyncio
299
+ >>> from edsl import QuestionFreeText as Q
300
+ >>> m = Q._get_test_model(canned_response = "Blue")
301
+ >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
302
+ >>> async def test_run_async(): result = await q.run_async(model=m); print(result)
303
+ >>> asyncio.run(test_run_async())
304
+ Blue
305
+ """
325
306
  survey = self.to_survey()
326
307
  results = await survey.run_async(model=model, agent=agent, **kwargs)
327
308
  if just_answer:
@@ -329,9 +310,37 @@ class QuestionBase(
329
310
  else:
330
311
  return results
331
312
 
313
+ # endregion
314
+
315
+ # region: Magic methods
316
+ def _repr_html_(self):
317
+ from edsl.utilities.utilities import data_to_html
318
+
319
+ data = self.to_dict()
320
+ try:
321
+ _ = data.pop("edsl_version")
322
+ _ = data.pop("edsl_class_name")
323
+ except KeyError:
324
+ print("Serialized question lacks edsl version, but is should have it.")
325
+
326
+ return data_to_html(data)
327
+
328
+ def __getitem__(self, key: str) -> Any:
329
+ """Get an attribute of the question so it can be treated like a dictionary.
330
+
331
+ >>> from edsl.questions import QuestionFreeText as Q
332
+ >>> Q.example()['question_text']
333
+ 'How are you?'
334
+ """
335
+ return getattr(self, key)
336
+
332
337
  def __repr__(self) -> str:
333
- """Return a string representation of the question. Should be able to be used to reconstruct the question."""
334
- class_name = self.__class__.__name__
338
+ """Return a string representation of the question. Should be able to be used to reconstruct the question.
339
+
340
+ >>> from edsl import QuestionFreeText as Q
341
+ >>> repr(Q.example())
342
+ 'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
343
+ """
335
344
  items = [
336
345
  f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
337
346
  for k, v in self.data.items()
@@ -340,14 +349,31 @@ class QuestionBase(
340
349
  question_type = self.to_dict().get("question_type", "None")
341
350
  return f"Question('{question_type}', {', '.join(items)})"
342
351
 
343
- def __eq__(self, other: Type[QuestionBase]) -> bool:
344
- """Check if two questions are equal. Equality is defined as having the .to_dict()."""
352
+ def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
353
+ """Check if two questions are equal. Equality is defined as having the .to_dict().
354
+
355
+ >>> from edsl import QuestionFreeText as Q
356
+ >>> q1 = Q.example()
357
+ >>> q2 = Q.example()
358
+ >>> q1 == q2
359
+ True
360
+ >>> q1.question_text = "How are you John?"
361
+ >>> q1 == q2
362
+ False
363
+
364
+ """
345
365
  if not isinstance(other, QuestionBase):
346
366
  return False
347
367
  return self.to_dict() == other.to_dict()
348
368
 
349
369
  def __sub__(self, other) -> BaseDiff:
350
- """Return the difference between two objects."""
370
+ """Return the difference between two objects.
371
+ >>> from edsl import QuestionFreeText as Q
372
+ >>> q1 = Q.example()
373
+ >>> q2 = q1.copy()
374
+ >>> q2.question_text = "How are you John?"
375
+ >>> diff = q1 - q2
376
+ """
351
377
 
352
378
  return BaseDiff(other, self)
353
379
 
@@ -364,57 +390,51 @@ class QuestionBase(
364
390
  ):
365
391
  return other_question_or_diff.apply(self)
366
392
 
367
- from edsl.questions import compose_questions
368
-
369
- return compose_questions(self, other_question_or_diff)
393
+ # from edsl.questions import compose_questions
394
+ # return compose_questions(self, other_question_or_diff)
370
395
 
371
- @abstractmethod
372
- def _validate_answer(self, answer: dict[str, str]):
373
- """Validate the answer from the LLM. Behavior depends on the question type."""
374
- pass
396
+ # def _validate_response(self, response):
397
+ # """Validate the response from the LLM. Behavior depends on the question type."""
398
+ # if "answer" not in response:
399
+ # raise QuestionResponseValidationError(
400
+ # "Response from LLM does not have an answer"
401
+ # )
402
+ # return response
375
403
 
376
- def _validate_response(self, response):
377
- """Validate the response from the LLM. Behavior depends on the question type."""
378
- if "answer" not in response:
379
- raise QuestionResponseValidationError(
380
- "Response from LLM does not have an answer"
381
- )
382
- return response
383
-
384
- @abstractmethod
385
- def _translate_answer_code_to_answer(self): # pragma: no cover
386
- """Translate the answer code to the actual answer. Behavior depends on the question type."""
387
- pass
404
+ def _translate_answer_code_to_answer(
405
+ self, answer, scenario: Optional["Scenario"] = None
406
+ ):
407
+ """There is over-ridden by child classes that ask for codes."""
408
+ return answer
388
409
 
389
- @abstractmethod
390
- def _simulate_answer(self, human_readable=True) -> dict: # pragma: no cover
391
- """Simulate a valid answer for debugging purposes (what the validator expects)."""
392
- pass
410
+ # endregion
393
411
 
394
- ############################
395
- # Forward methods
396
- ############################
412
+ # region: Forward methods
397
413
  def add_question(self, other: QuestionBase) -> "Survey":
398
- """Add a question to this question by turning them into a survey with two questions."""
414
+ """Add a question to this question by turning them into a survey with two questions.
415
+
416
+ >>> from edsl.questions import QuestionFreeText as Q
417
+ >>> from edsl.questions import QuestionMultipleChoice as QMC
418
+ >>> s = Q.example().add_question(QMC.example())
419
+ >>> len(s.questions)
420
+ 2
421
+ """
399
422
  from edsl.surveys.Survey import Survey
400
423
 
401
424
  s = Survey([self, other])
402
425
  return s
403
426
 
404
427
  def to_survey(self) -> "Survey":
405
- """Turn a single question into a survey."""
428
+ """Turn a single question into a survey.
429
+ >>> from edsl import QuestionFreeText as Q
430
+ >>> Q.example().to_survey().questions[0].question_name
431
+ 'how_are_you'
432
+ """
406
433
  from edsl.surveys.Survey import Survey
407
434
 
408
435
  s = Survey([self])
409
436
  return s
410
437
 
411
- def run(self, *args, **kwargs) -> "Results":
412
- """Turn a single question into a survey and run it."""
413
- from edsl.surveys.Survey import Survey
414
-
415
- s = self.to_survey()
416
- return s.run(*args, **kwargs)
417
-
418
438
  def by(self, *args) -> "Jobs":
419
439
  """Turn a single question into a survey and then a Job."""
420
440
  from edsl.surveys.Survey import Survey
@@ -422,6 +442,15 @@ class QuestionBase(
422
442
  s = Survey([self])
423
443
  return s.by(*args)
424
444
 
445
+ # endregion
446
+
447
+ # region: Display methods
448
+ def print(self):
449
+ from rich import print_json
450
+ import json
451
+
452
+ print_json(json.dumps(self.to_dict()))
453
+
425
454
  def human_readable(self) -> str:
426
455
  """Print the question in a human readable format.
427
456
 
@@ -520,6 +549,8 @@ class QuestionBase(
520
549
  )
521
550
  return table
522
551
 
552
+ # endregion
553
+
523
554
 
524
555
  if __name__ == "__main__":
525
556
  import doctest