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,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
4
5
  from abc import ABC, abstractmethod
5
- from typing import Any, Type, Optional, List, Callable, Union, TypedDict
6
+ from typing import Any, Type, Optional, List, Callable
6
7
  import copy
7
8
 
8
9
  from edsl.exceptions import (
9
10
  QuestionResponseValidationError,
10
- QuestionAnswerValidationError,
11
11
  QuestionSerializationError,
12
12
  )
13
13
  from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
@@ -19,8 +19,6 @@ 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
24
22
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
25
23
 
26
24
 
@@ -28,128 +26,80 @@ class QuestionBase(
28
26
  PersistenceMixin,
29
27
  RichPrintingMixin,
30
28
  SimpleAskMixin,
31
- QuestionBasePromptsMixin,
32
- QuestionBaseGenMixin,
33
29
  ABC,
34
30
  AnswerValidatorMixin,
35
31
  metaclass=RegisterQuestionsMeta,
36
32
  ):
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
- """
33
+ """ABC for the Question class. All questions should inherit from this class."""
40
34
 
41
35
  question_name: str = QuestionNameDescriptor()
42
36
  question_text: str = QuestionTextDescriptor()
43
37
 
44
- _answering_instructions = None
45
- _question_presentation = None
46
-
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)
60
-
61
- @property
62
- def validator_parameters(self) -> list[str]:
63
- """Return the parameters required for the response validator.
38
+ def __getitem__(self, key: str) -> Any:
39
+ """Get an attribute of the question."""
40
+ return getattr(self, key)
64
41
 
65
- >>> from edsl import QuestionMultipleChoice as Q
66
- >>> Q.example().validator_parameters
67
- ['question_options', 'use_code']
42
+ def __hash__(self) -> int:
43
+ """Return a hash of the question."""
44
+ from edsl.utilities.utilities import dict_hash
68
45
 
69
- """
70
- return self.response_validator_class.required_params
46
+ return dict_hash(self._to_dict())
71
47
 
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', 'generated_tokens': 'Hello'})
107
- {'answer': 'Hello', 'generated_tokens': 'Hello'}
108
- """
48
+ def _repr_html_(self):
49
+ from edsl.utilities.utilities import data_to_html
109
50
 
110
- return self.response_validator.validate(answer)
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.")
111
57
 
112
- # endregion
58
+ return data_to_html(data)
113
59
 
114
- # region: Serialization methods
115
- @property
116
- def name(self) -> str:
117
- "Helper function so questions and instructions can use the same access method"
118
- return self.question_name
60
+ def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
61
+ """Apply a function to the question parts
119
62
 
120
- def __hash__(self) -> int:
121
- """Return a hash of the question.
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?\""")
122
68
 
123
- >>> from edsl import QuestionFreeText as Q
124
- >>> hash(Q.example())
125
- 1144312636257752766
126
69
  """
127
- from edsl.utilities.utilities import dict_hash
128
-
129
- return dict_hash(self._to_dict())
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)
130
88
 
131
89
  @property
132
90
  def data(self) -> dict:
133
- """Return a dictionary of question attributes **except** for question_type.
134
-
135
- >>> from edsl import QuestionFreeText as Q
136
- >>> Q.example().data
137
- {'question_name': 'how_are_you', 'question_text': 'How are you?'}
138
- """
139
- exclude_list = [
140
- "question_type",
141
- "_include_comment",
142
- "_fake_data_factory",
143
- "_use_code",
144
- "_answering_instructions",
145
- "_question_presentation",
146
- "_model_instructions",
147
- ]
91
+ """Return a dictionary of question attributes **except** for question_type."""
148
92
  candidate_data = {
149
93
  k.replace("_", "", 1): v
150
94
  for k, v in self.__dict__.items()
151
- if k.startswith("_") and k not in exclude_list
95
+ if k.startswith("_")
152
96
  }
97
+ optional_attributes = {
98
+ "set_instructions": "instructions",
99
+ }
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)
153
103
 
154
104
  if "func" in candidate_data:
155
105
  func = candidate_data.pop("func")
@@ -159,22 +109,147 @@ class QuestionBase(
159
109
 
160
110
  return candidate_data
161
111
 
162
- def _to_dict(self):
163
- """Convert the question to a dictionary that includes the question type (used in deserialization).
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.
164
125
 
165
- >>> from edsl import QuestionFreeText as Q; Q.example()._to_dict()
166
- {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
167
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.
207
+
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
+ \""")
214
+ """
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)."""
168
246
  candidate_data = self.data.copy()
169
247
  candidate_data["question_type"] = self.question_type
170
248
  return candidate_data
171
249
 
172
250
  @add_edsl_version
173
251
  def to_dict(self) -> dict[str, Any]:
174
- """Convert the question to a dictionary that includes the question type (used in deserialization).
175
- >>> from edsl import QuestionFreeText as Q; Q.example().to_dict()
176
- {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text', 'edsl_version': '...'}
177
- """
252
+ """Convert the question to a dictionary that includes the question type (used in deserialization)."""
178
253
  return self._to_dict()
179
254
 
180
255
  @classmethod
@@ -214,90 +289,39 @@ class QuestionBase(
214
289
 
215
290
  return question_class(**local_data)
216
291
 
217
- # endregion
218
-
219
- # region: Running methods
220
- @classmethod
221
- def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
222
- """Get a test model for the question."""
223
- from edsl.language_models import LanguageModel
224
-
225
- return LanguageModel.example(canned_response=canned_response, test_model=True)
292
+ def copy(self) -> Type[QuestionBase]:
293
+ """Return a deep copy of the question."""
294
+ return copy.deepcopy(self)
226
295
 
227
- @classmethod
228
- def run_example(
229
- cls,
230
- show_answer: bool = True,
231
- model: Optional["LanguageModel"] = None,
232
- cache=False,
233
- **kwargs,
234
- ):
235
- """Run an example of the question.
236
- >>> from edsl.language_models import LanguageModel
237
- >>> from edsl import QuestionFreeText as Q
238
- >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
239
- >>> m.execute_model_call("", "")
240
- {'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
241
- >>> Q.run_example(show_answer = True, model = m)
242
- ┏━━━━━━━━━━━━━━━━┓
243
- ┃ answer ┃
244
- ┃ .how_are_you ┃
245
- ┡━━━━━━━━━━━━━━━━┩
246
- │ Yo, what's up? │
247
- └────────────────┘
248
- """
249
- if model is None:
250
- from edsl import Model
296
+ ############################
297
+ # Dunder methods
298
+ ############################
299
+ def print(self):
300
+ from rich import print_json
301
+ import json
251
302
 
252
- model = Model()
253
- results = cls.example(**kwargs).by(model).run(cache=cache)
254
- if show_answer:
255
- results.select("answer.*").print()
256
- else:
257
- return results
303
+ print_json(json.dumps(self.to_dict()))
258
304
 
259
305
  def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
260
306
  """Call the question.
261
307
 
262
-
263
- >>> from edsl import QuestionFreeText as Q
264
- >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
265
- >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
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?")
266
312
  >>> q(model = m)
267
313
  "Yo, what's up?"
268
314
 
269
315
  """
270
316
  survey = self.to_survey()
271
- results = survey(model=model, agent=agent, **kwargs, cache=False)
317
+ results = survey(model=model, agent=agent, **kwargs)
272
318
  if just_answer:
273
319
  return results.select(f"answer.{self.question_name}").first()
274
320
  else:
275
321
  return results
276
322
 
277
- def run(self, *args, **kwargs) -> "Results":
278
- """Turn a single question into a survey and runs it."""
279
- from edsl.surveys.Survey import Survey
280
-
281
- s = self.to_survey()
282
- return s.run(*args, **kwargs)
283
-
284
- async def run_async(
285
- self,
286
- just_answer: bool = True,
287
- model: Optional["Model"] = None,
288
- agent: Optional["Agent"] = None,
289
- **kwargs,
290
- ) -> Union[Any, "Results"]:
291
- """Call the question asynchronously.
292
-
293
- >>> import asyncio
294
- >>> from edsl import QuestionFreeText as Q
295
- >>> m = Q._get_test_model(canned_response = "Blue")
296
- >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
297
- >>> async def test_run_async(): result = await q.run_async(model=m); print(result)
298
- >>> asyncio.run(test_run_async())
299
- Blue
300
- """
323
+ async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
324
+ """Call the question."""
301
325
  survey = self.to_survey()
302
326
  results = await survey.run_async(model=model, agent=agent, **kwargs)
303
327
  if just_answer:
@@ -305,37 +329,9 @@ class QuestionBase(
305
329
  else:
306
330
  return results
307
331
 
308
- # endregion
309
-
310
- # region: Magic methods
311
- def _repr_html_(self):
312
- from edsl.utilities.utilities import data_to_html
313
-
314
- data = self.to_dict()
315
- try:
316
- _ = data.pop("edsl_version")
317
- _ = data.pop("edsl_class_name")
318
- except KeyError:
319
- print("Serialized question lacks edsl version, but is should have it.")
320
-
321
- return data_to_html(data)
322
-
323
- def __getitem__(self, key: str) -> Any:
324
- """Get an attribute of the question so it can be treated like a dictionary.
325
-
326
- >>> from edsl.questions import QuestionFreeText as Q
327
- >>> Q.example()['question_text']
328
- 'How are you?'
329
- """
330
- return getattr(self, key)
331
-
332
332
  def __repr__(self) -> str:
333
- """Return a string representation of the question. Should be able to be used to reconstruct the question.
334
-
335
- >>> from edsl import QuestionFreeText as Q
336
- >>> repr(Q.example())
337
- 'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
338
- """
333
+ """Return a string representation of the question. Should be able to be used to reconstruct the question."""
334
+ class_name = self.__class__.__name__
339
335
  items = [
340
336
  f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
341
337
  for k, v in self.data.items()
@@ -344,31 +340,14 @@ class QuestionBase(
344
340
  question_type = self.to_dict().get("question_type", "None")
345
341
  return f"Question('{question_type}', {', '.join(items)})"
346
342
 
347
- def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
348
- """Check if two questions are equal. Equality is defined as having the .to_dict().
349
-
350
- >>> from edsl import QuestionFreeText as Q
351
- >>> q1 = Q.example()
352
- >>> q2 = Q.example()
353
- >>> q1 == q2
354
- True
355
- >>> q1.question_text = "How are you John?"
356
- >>> q1 == q2
357
- False
358
-
359
- """
343
+ def __eq__(self, other: Type[QuestionBase]) -> bool:
344
+ """Check if two questions are equal. Equality is defined as having the .to_dict()."""
360
345
  if not isinstance(other, QuestionBase):
361
346
  return False
362
347
  return self.to_dict() == other.to_dict()
363
348
 
364
349
  def __sub__(self, other) -> BaseDiff:
365
- """Return the difference between two objects.
366
- >>> from edsl import QuestionFreeText as Q
367
- >>> q1 = Q.example()
368
- >>> q2 = q1.copy()
369
- >>> q2.question_text = "How are you John?"
370
- >>> diff = q1 - q2
371
- """
350
+ """Return the difference between two objects."""
372
351
 
373
352
  return BaseDiff(other, self)
374
353
 
@@ -385,51 +364,57 @@ class QuestionBase(
385
364
  ):
386
365
  return other_question_or_diff.apply(self)
387
366
 
388
- # from edsl.questions import compose_questions
389
- # return compose_questions(self, other_question_or_diff)
367
+ from edsl.questions import compose_questions
390
368
 
391
- # def _validate_response(self, response):
392
- # """Validate the response from the LLM. Behavior depends on the question type."""
393
- # if "answer" not in response:
394
- # raise QuestionResponseValidationError(
395
- # "Response from LLM does not have an answer"
396
- # )
397
- # return response
369
+ return compose_questions(self, other_question_or_diff)
398
370
 
399
- def _translate_answer_code_to_answer(
400
- self, answer, scenario: Optional["Scenario"] = None
401
- ):
402
- """There is over-ridden by child classes that ask for codes."""
403
- return answer
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
404
375
 
405
- # endregion
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
406
383
 
407
- # region: Forward methods
408
- def add_question(self, other: QuestionBase) -> "Survey":
409
- """Add a question to this question by turning them into a survey with two questions.
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
410
388
 
411
- >>> from edsl.questions import QuestionFreeText as Q
412
- >>> from edsl.questions import QuestionMultipleChoice as QMC
413
- >>> s = Q.example().add_question(QMC.example())
414
- >>> len(s.questions)
415
- 2
416
- """
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
393
+
394
+ ############################
395
+ # Forward methods
396
+ ############################
397
+ def add_question(self, other: QuestionBase) -> "Survey":
398
+ """Add a question to this question by turning them into a survey with two questions."""
417
399
  from edsl.surveys.Survey import Survey
418
400
 
419
401
  s = Survey([self, other])
420
402
  return s
421
403
 
422
404
  def to_survey(self) -> "Survey":
423
- """Turn a single question into a survey.
424
- >>> from edsl import QuestionFreeText as Q
425
- >>> Q.example().to_survey().questions[0].question_name
426
- 'how_are_you'
427
- """
405
+ """Turn a single question into a survey."""
428
406
  from edsl.surveys.Survey import Survey
429
407
 
430
408
  s = Survey([self])
431
409
  return s
432
410
 
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
+
433
418
  def by(self, *args) -> "Jobs":
434
419
  """Turn a single question into a survey and then a Job."""
435
420
  from edsl.surveys.Survey import Survey
@@ -437,15 +422,6 @@ class QuestionBase(
437
422
  s = Survey([self])
438
423
  return s.by(*args)
439
424
 
440
- # endregion
441
-
442
- # region: Display methods
443
- def print(self):
444
- from rich import print_json
445
- import json
446
-
447
- print_json(json.dumps(self.to_dict()))
448
-
449
425
  def human_readable(self) -> str:
450
426
  """Print the question in a human readable format.
451
427
 
@@ -466,7 +442,6 @@ class QuestionBase(
466
442
  self,
467
443
  scenario: Optional[dict] = None,
468
444
  agent: Optional[dict] = {},
469
- answers: Optional[dict] = None,
470
445
  include_question_name: bool = False,
471
446
  height: Optional[int] = None,
472
447
  width: Optional[int] = None,
@@ -478,17 +453,6 @@ class QuestionBase(
478
453
  if scenario is None:
479
454
  scenario = {}
480
455
 
481
- prior_answers_dict = {}
482
-
483
- if isinstance(answers, dict):
484
- for key, value in answers.items():
485
- if not key.endswith("_comment") and not key.endswith(
486
- "_generated_tokens"
487
- ):
488
- prior_answers_dict[key] = {"answer": value}
489
-
490
- # breakpoint()
491
-
492
456
  base_template = """
493
457
  <div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
494
458
  {% if include_question_name %}
@@ -508,40 +472,13 @@ class QuestionBase(
508
472
 
509
473
  base_template = Template(base_template)
510
474
 
511
- context = {
512
- "scenario": scenario,
513
- "agent": agent,
514
- } | prior_answers_dict
515
-
516
- # Render the question text
517
- try:
518
- question_text = Template(self.question_text).render(context)
519
- except Exception as e:
520
- print(
521
- f"Error rendering question: question_text = {self.question_text}, error = {e}"
522
- )
523
- question_text = self.question_text
524
-
525
- try:
526
- question_content = Template(question_content).render(context)
527
- except Exception as e:
528
- print(
529
- f"Error rendering question: question_content = {question_content}, error = {e}"
530
- )
531
- question_content = question_content
532
-
533
- try:
534
- params = {
535
- "question_name": self.question_name,
536
- "question_text": question_text,
537
- "question_type": self.question_type,
538
- "question_content": question_content,
539
- "include_question_name": include_question_name,
540
- }
541
- except Exception as e:
542
- raise ValueError(
543
- f"Error rendering question: params = {params}, error = {e}"
544
- )
475
+ params = {
476
+ "question_name": self.question_name,
477
+ "question_text": Template(self.question_text).render(scenario, agent=agent),
478
+ "question_type": self.question_type,
479
+ "question_content": Template(question_content).render(scenario),
480
+ "include_question_name": include_question_name,
481
+ }
545
482
  rendered_html = base_template.render(**params)
546
483
 
547
484
  if iframe:
@@ -560,21 +497,6 @@ class QuestionBase(
560
497
 
561
498
  return rendered_html
562
499
 
563
- @classmethod
564
- def example_model(cls):
565
- from edsl import Model
566
-
567
- q = cls.example()
568
- m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
569
-
570
- return m
571
-
572
- @classmethod
573
- def example_results(cls):
574
- m = cls.example_model()
575
- q = cls.example()
576
- return q.by(m).run(cache=False)
577
-
578
500
  def rich_print(self):
579
501
  """Print the question in a rich format."""
580
502
  from rich.table import Table
@@ -598,8 +520,6 @@ class QuestionBase(
598
520
  )
599
521
  return table
600
522
 
601
- # endregion
602
-
603
523
 
604
524
  if __name__ == "__main__":
605
525
  import doctest