edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.dev3__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 +9 -3
  2. edsl/TemplateLoader.py +24 -0
  3. edsl/__init__.py +8 -3
  4. edsl/__version__.py +1 -1
  5. edsl/agents/Agent.py +40 -8
  6. edsl/agents/AgentList.py +43 -0
  7. edsl/agents/Invigilator.py +135 -219
  8. edsl/agents/InvigilatorBase.py +148 -59
  9. edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +138 -89
  10. edsl/agents/__init__.py +1 -0
  11. edsl/auto/AutoStudy.py +117 -0
  12. edsl/auto/StageBase.py +230 -0
  13. edsl/auto/StageGenerateSurvey.py +178 -0
  14. edsl/auto/StageLabelQuestions.py +125 -0
  15. edsl/auto/StagePersona.py +61 -0
  16. edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
  17. edsl/auto/StagePersonaDimensionValues.py +74 -0
  18. edsl/auto/StagePersonaDimensions.py +69 -0
  19. edsl/auto/StageQuestions.py +73 -0
  20. edsl/auto/SurveyCreatorPipeline.py +21 -0
  21. edsl/auto/utilities.py +224 -0
  22. edsl/config.py +47 -56
  23. edsl/coop/PriceFetcher.py +58 -0
  24. edsl/coop/coop.py +50 -7
  25. edsl/data/Cache.py +35 -1
  26. edsl/data_transfer_models.py +74 -38
  27. edsl/enums.py +4 -0
  28. edsl/exceptions/language_models.py +25 -1
  29. edsl/exceptions/questions.py +62 -5
  30. edsl/exceptions/results.py +4 -0
  31. edsl/inference_services/AnthropicService.py +13 -11
  32. edsl/inference_services/AwsBedrock.py +19 -17
  33. edsl/inference_services/AzureAI.py +37 -20
  34. edsl/inference_services/GoogleService.py +16 -12
  35. edsl/inference_services/GroqService.py +2 -0
  36. edsl/inference_services/InferenceServiceABC.py +58 -3
  37. edsl/inference_services/MistralAIService.py +120 -0
  38. edsl/inference_services/OpenAIService.py +48 -54
  39. edsl/inference_services/TestService.py +80 -0
  40. edsl/inference_services/TogetherAIService.py +170 -0
  41. edsl/inference_services/models_available_cache.py +0 -6
  42. edsl/inference_services/registry.py +6 -0
  43. edsl/jobs/Answers.py +10 -12
  44. edsl/jobs/FailedQuestion.py +78 -0
  45. edsl/jobs/Jobs.py +36 -21
  46. edsl/jobs/buckets/BucketCollection.py +24 -15
  47. edsl/jobs/buckets/TokenBucket.py +93 -14
  48. edsl/jobs/interviews/Interview.py +366 -78
  49. edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +14 -68
  50. edsl/jobs/interviews/InterviewExceptionEntry.py +85 -19
  51. edsl/jobs/runners/JobsRunnerAsyncio.py +154 -183
  52. edsl/jobs/runners/JobsRunnerStatus.py +331 -0
  53. edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
  54. edsl/jobs/tasks/TaskHistory.py +148 -213
  55. edsl/language_models/LanguageModel.py +261 -156
  56. edsl/language_models/ModelList.py +2 -2
  57. edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
  58. edsl/language_models/fake_openai_call.py +15 -0
  59. edsl/language_models/fake_openai_service.py +61 -0
  60. edsl/language_models/registry.py +23 -6
  61. edsl/language_models/repair.py +0 -19
  62. edsl/language_models/utilities.py +61 -0
  63. edsl/prompts/Prompt.py +52 -2
  64. edsl/questions/AnswerValidatorMixin.py +23 -26
  65. edsl/questions/QuestionBase.py +328 -249
  66. edsl/questions/QuestionBaseGenMixin.py +133 -0
  67. edsl/questions/QuestionBasePromptsMixin.py +266 -0
  68. edsl/questions/QuestionBudget.py +99 -41
  69. edsl/questions/QuestionCheckBox.py +227 -35
  70. edsl/questions/QuestionExtract.py +98 -27
  71. edsl/questions/QuestionFreeText.py +52 -29
  72. edsl/questions/QuestionFunctional.py +7 -0
  73. edsl/questions/QuestionList.py +141 -22
  74. edsl/questions/QuestionMultipleChoice.py +162 -68
  75. edsl/questions/QuestionNumerical.py +88 -46
  76. edsl/questions/QuestionRank.py +182 -24
  77. edsl/questions/Quick.py +41 -0
  78. edsl/questions/RegisterQuestionsMeta.py +31 -12
  79. edsl/questions/ResponseValidatorABC.py +170 -0
  80. edsl/questions/__init__.py +3 -4
  81. edsl/questions/decorators.py +21 -0
  82. edsl/questions/derived/QuestionLikertFive.py +10 -5
  83. edsl/questions/derived/QuestionLinearScale.py +15 -2
  84. edsl/questions/derived/QuestionTopK.py +10 -1
  85. edsl/questions/derived/QuestionYesNo.py +24 -3
  86. edsl/questions/descriptors.py +43 -7
  87. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  88. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  89. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  90. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  91. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  92. edsl/questions/prompt_templates/question_list.jinja +17 -0
  93. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  94. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  95. edsl/questions/question_registry.py +6 -2
  96. edsl/questions/templates/__init__.py +0 -0
  97. edsl/questions/templates/budget/__init__.py +0 -0
  98. edsl/questions/templates/budget/answering_instructions.jinja +7 -0
  99. edsl/questions/templates/budget/question_presentation.jinja +7 -0
  100. edsl/questions/templates/checkbox/__init__.py +0 -0
  101. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  102. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  103. edsl/questions/templates/extract/__init__.py +0 -0
  104. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  105. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  106. edsl/questions/templates/free_text/__init__.py +0 -0
  107. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  108. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  109. edsl/questions/templates/likert_five/__init__.py +0 -0
  110. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  111. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  112. edsl/questions/templates/linear_scale/__init__.py +0 -0
  113. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  114. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  115. edsl/questions/templates/list/__init__.py +0 -0
  116. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  117. edsl/questions/templates/list/question_presentation.jinja +5 -0
  118. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  119. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  120. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  121. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  122. edsl/questions/templates/numerical/__init__.py +0 -0
  123. edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
  124. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  125. edsl/questions/templates/rank/__init__.py +0 -0
  126. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  127. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  128. edsl/questions/templates/top_k/__init__.py +0 -0
  129. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  130. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  131. edsl/questions/templates/yes_no/__init__.py +0 -0
  132. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  133. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  134. edsl/results/Dataset.py +20 -0
  135. edsl/results/DatasetExportMixin.py +46 -48
  136. edsl/results/DatasetTree.py +145 -0
  137. edsl/results/Result.py +32 -5
  138. edsl/results/Results.py +135 -46
  139. edsl/results/ResultsDBMixin.py +3 -3
  140. edsl/results/Selector.py +118 -0
  141. edsl/results/tree_explore.py +115 -0
  142. edsl/scenarios/FileStore.py +71 -10
  143. edsl/scenarios/Scenario.py +96 -25
  144. edsl/scenarios/ScenarioImageMixin.py +2 -2
  145. edsl/scenarios/ScenarioList.py +361 -39
  146. edsl/scenarios/ScenarioListExportMixin.py +9 -0
  147. edsl/scenarios/ScenarioListPdfMixin.py +150 -4
  148. edsl/study/SnapShot.py +8 -1
  149. edsl/study/Study.py +32 -0
  150. edsl/surveys/Rule.py +10 -1
  151. edsl/surveys/RuleCollection.py +21 -5
  152. edsl/surveys/Survey.py +637 -311
  153. edsl/surveys/SurveyExportMixin.py +71 -9
  154. edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
  155. edsl/surveys/SurveyQualtricsImport.py +75 -4
  156. edsl/surveys/instructions/ChangeInstruction.py +47 -0
  157. edsl/surveys/instructions/Instruction.py +34 -0
  158. edsl/surveys/instructions/InstructionCollection.py +77 -0
  159. edsl/surveys/instructions/__init__.py +0 -0
  160. edsl/templates/error_reporting/base.html +24 -0
  161. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  162. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  163. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  164. edsl/templates/error_reporting/interview_details.html +116 -0
  165. edsl/templates/error_reporting/interviews.html +10 -0
  166. edsl/templates/error_reporting/overview.html +5 -0
  167. edsl/templates/error_reporting/performance_plot.html +2 -0
  168. edsl/templates/error_reporting/report.css +74 -0
  169. edsl/templates/error_reporting/report.html +118 -0
  170. edsl/templates/error_reporting/report.js +25 -0
  171. edsl/utilities/utilities.py +9 -1
  172. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev3.dist-info}/METADATA +5 -2
  173. edsl-0.1.33.dev3.dist-info/RECORD +295 -0
  174. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
  175. edsl/jobs/interviews/retry_management.py +0 -37
  176. edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
  177. edsl/utilities/gcp_bucket/simple_example.py +0 -9
  178. edsl-0.1.33.dev1.dist-info/RECORD +0 -209
  179. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev3.dist-info}/LICENSE +0 -0
  180. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev3.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,127 @@ 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
59
77
 
60
- def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
61
- """Apply a function to the question parts
78
+ class FakeData(ModelFactory[self.response_model]): ...
62
79
 
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?\""")
80
+ self._fake_data_factory = FakeData
81
+ return self._fake_data_factory
82
+
83
+ def _simulate_answer(self, human_readable: bool = False) -> dict:
84
+ """Simulate a valid answer for debugging purposes (what the validator expects).
85
+ >>> from edsl import QuestionFreeText as Q
86
+ >>> Q.example()._simulate_answer()
87
+ {'answer': '...', 'generated_tokens': ...}
88
+ """
89
+ simulated_answer = self.fake_data_factory.build().dict()
90
+ if human_readable and hasattr(self, "question_options") and self.use_code:
91
+ simulated_answer["answer"] = [
92
+ self.question_options[index] for index in simulated_answer["answer"]
93
+ ]
94
+ return simulated_answer
95
+
96
+ class ValidatedAnswer(TypedDict):
97
+ answer: Any
98
+ comment: Optional[str]
99
+ generated_tokens: Optional[str]
100
+
101
+ def _validate_answer(self, answer: dict) -> ValidatedAnswer:
102
+ """Validate the answer.
103
+ >>> from edsl.exceptions import QuestionAnswerValidationError
104
+ >>> from edsl import QuestionFreeText as Q
105
+ >>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
106
+ {'answer': 'Hello', 'generated_tokens': 'Hello'}
107
+ """
108
+
109
+ return self.response_validator.validate(answer)
110
+
111
+ # endregion
112
+
113
+ # region: Serialization methods
114
+ @property
115
+ def name(self) -> str:
116
+ "Helper function so questions and instructions can use the same access method"
117
+ return self.question_name
118
+
119
+ def __hash__(self) -> int:
120
+ """Return a hash of the question.
68
121
 
122
+ >>> from edsl import QuestionFreeText as Q
123
+ >>> hash(Q.example())
124
+ 1144312636257752766
69
125
  """
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)
126
+ from edsl.utilities.utilities import dict_hash
127
+
128
+ return dict_hash(self._to_dict())
88
129
 
89
130
  @property
90
131
  def data(self) -> dict:
91
- """Return a dictionary of question attributes **except** for question_type."""
132
+ """Return a dictionary of question attributes **except** for question_type.
133
+
134
+ >>> from edsl import QuestionFreeText as Q
135
+ >>> Q.example().data
136
+ {'question_name': 'how_are_you', 'question_text': 'How are you?'}
137
+ """
138
+ exclude_list = [
139
+ "question_type",
140
+ "_include_comment",
141
+ "_fake_data_factory",
142
+ "_use_code",
143
+ "_answering_instructions",
144
+ "_question_presentation",
145
+ "_model_instructions",
146
+ ]
92
147
  candidate_data = {
93
148
  k.replace("_", "", 1): v
94
149
  for k, v in self.__dict__.items()
95
- if k.startswith("_")
96
- }
97
- optional_attributes = {
98
- "set_instructions": "instructions",
150
+ if k.startswith("_") and k not in exclude_list
99
151
  }
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
152
 
104
153
  if "func" in candidate_data:
105
154
  func = candidate_data.pop("func")
@@ -109,147 +158,22 @@ class QuestionBase(
109
158
 
110
159
  return candidate_data
111
160
 
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.
161
+ def _to_dict(self):
162
+ """Convert the question to a dictionary that includes the question type (used in deserialization).
207
163
 
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
- \""")
164
+ >>> from edsl import QuestionFreeText as Q; Q.example()._to_dict()
165
+ {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
214
166
  """
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
167
  candidate_data = self.data.copy()
247
168
  candidate_data["question_type"] = self.question_type
248
169
  return candidate_data
249
170
 
250
171
  @add_edsl_version
251
172
  def to_dict(self) -> dict[str, Any]:
252
- """Convert the question to a dictionary that includes the question type (used in deserialization)."""
173
+ """Convert the question to a dictionary that includes the question type (used in deserialization).
174
+ >>> from edsl import QuestionFreeText as Q; Q.example().to_dict()
175
+ {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text', 'edsl_version': '...'}
176
+ """
253
177
  return self._to_dict()
254
178
 
255
179
  @classmethod
@@ -289,39 +213,90 @@ class QuestionBase(
289
213
 
290
214
  return question_class(**local_data)
291
215
 
292
- def copy(self) -> Type[QuestionBase]:
293
- """Return a deep copy of the question."""
294
- return copy.deepcopy(self)
216
+ # endregion
295
217
 
296
- ############################
297
- # Dunder methods
298
- ############################
299
- def print(self):
300
- from rich import print_json
301
- import json
218
+ # region: Running methods
219
+ @classmethod
220
+ def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
221
+ """Get a test model for the question."""
222
+ from edsl.language_models import LanguageModel
302
223
 
303
- print_json(json.dumps(self.to_dict()))
224
+ return LanguageModel.example(canned_response=canned_response, test_model=True)
225
+
226
+ @classmethod
227
+ def run_example(
228
+ cls,
229
+ show_answer: bool = True,
230
+ model: Optional["LanguageModel"] = None,
231
+ cache=False,
232
+ **kwargs,
233
+ ):
234
+ """Run an example of the question.
235
+ >>> from edsl.language_models import LanguageModel
236
+ >>> from edsl import QuestionFreeText as Q
237
+ >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
238
+ >>> m.execute_model_call("", "")
239
+ {'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
240
+ >>> Q.run_example(show_answer = True, model = m)
241
+ ┏━━━━━━━━━━━━━━━━┓
242
+ ┃ answer ┃
243
+ ┃ .how_are_you ┃
244
+ ┡━━━━━━━━━━━━━━━━┩
245
+ │ Yo, what's up? │
246
+ └────────────────┘
247
+ """
248
+ if model is None:
249
+ from edsl import Model
250
+
251
+ model = Model()
252
+ results = cls.example(**kwargs).by(model).run(cache=cache)
253
+ if show_answer:
254
+ results.select("answer.*").print()
255
+ else:
256
+ return results
304
257
 
305
258
  def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
306
259
  """Call the question.
307
260
 
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?")
261
+
262
+ >>> from edsl import QuestionFreeText as Q
263
+ >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
264
+ >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
312
265
  >>> q(model = m)
313
266
  "Yo, what's up?"
314
267
 
315
268
  """
316
269
  survey = self.to_survey()
317
- results = survey(model=model, agent=agent, **kwargs)
270
+ results = survey(model=model, agent=agent, **kwargs, cache=False)
318
271
  if just_answer:
319
272
  return results.select(f"answer.{self.question_name}").first()
320
273
  else:
321
274
  return results
322
275
 
323
- async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
324
- """Call the question."""
276
+ def run(self, *args, **kwargs) -> "Results":
277
+ """Turn a single question into a survey and runs it."""
278
+ from edsl.surveys.Survey import Survey
279
+
280
+ s = self.to_survey()
281
+ return s.run(*args, **kwargs)
282
+
283
+ async def run_async(
284
+ self,
285
+ just_answer: bool = True,
286
+ model: Optional["Model"] = None,
287
+ agent: Optional["Agent"] = None,
288
+ **kwargs,
289
+ ) -> Union[Any, "Results"]:
290
+ """Call the question asynchronously.
291
+
292
+ >>> import asyncio
293
+ >>> from edsl import QuestionFreeText as Q
294
+ >>> m = Q._get_test_model(canned_response = "Blue")
295
+ >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
296
+ >>> async def test_run_async(): result = await q.run_async(model=m); print(result)
297
+ >>> asyncio.run(test_run_async())
298
+ Blue
299
+ """
325
300
  survey = self.to_survey()
326
301
  results = await survey.run_async(model=model, agent=agent, **kwargs)
327
302
  if just_answer:
@@ -329,9 +304,37 @@ class QuestionBase(
329
304
  else:
330
305
  return results
331
306
 
307
+ # endregion
308
+
309
+ # region: Magic methods
310
+ def _repr_html_(self):
311
+ from edsl.utilities.utilities import data_to_html
312
+
313
+ data = self.to_dict()
314
+ try:
315
+ _ = data.pop("edsl_version")
316
+ _ = data.pop("edsl_class_name")
317
+ except KeyError:
318
+ print("Serialized question lacks edsl version, but is should have it.")
319
+
320
+ return data_to_html(data)
321
+
322
+ def __getitem__(self, key: str) -> Any:
323
+ """Get an attribute of the question so it can be treated like a dictionary.
324
+
325
+ >>> from edsl.questions import QuestionFreeText as Q
326
+ >>> Q.example()['question_text']
327
+ 'How are you?'
328
+ """
329
+ return getattr(self, key)
330
+
332
331
  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__
332
+ """Return a string representation of the question. Should be able to be used to reconstruct the question.
333
+
334
+ >>> from edsl import QuestionFreeText as Q
335
+ >>> repr(Q.example())
336
+ 'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
337
+ """
335
338
  items = [
336
339
  f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
337
340
  for k, v in self.data.items()
@@ -340,14 +343,31 @@ class QuestionBase(
340
343
  question_type = self.to_dict().get("question_type", "None")
341
344
  return f"Question('{question_type}', {', '.join(items)})"
342
345
 
343
- def __eq__(self, other: Type[QuestionBase]) -> bool:
344
- """Check if two questions are equal. Equality is defined as having the .to_dict()."""
346
+ def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
347
+ """Check if two questions are equal. Equality is defined as having the .to_dict().
348
+
349
+ >>> from edsl import QuestionFreeText as Q
350
+ >>> q1 = Q.example()
351
+ >>> q2 = Q.example()
352
+ >>> q1 == q2
353
+ True
354
+ >>> q1.question_text = "How are you John?"
355
+ >>> q1 == q2
356
+ False
357
+
358
+ """
345
359
  if not isinstance(other, QuestionBase):
346
360
  return False
347
361
  return self.to_dict() == other.to_dict()
348
362
 
349
363
  def __sub__(self, other) -> BaseDiff:
350
- """Return the difference between two objects."""
364
+ """Return the difference between two objects.
365
+ >>> from edsl import QuestionFreeText as Q
366
+ >>> q1 = Q.example()
367
+ >>> q2 = q1.copy()
368
+ >>> q2.question_text = "How are you John?"
369
+ >>> diff = q1 - q2
370
+ """
351
371
 
352
372
  return BaseDiff(other, self)
353
373
 
@@ -364,57 +384,51 @@ class QuestionBase(
364
384
  ):
365
385
  return other_question_or_diff.apply(self)
366
386
 
367
- from edsl.questions import compose_questions
368
-
369
- return compose_questions(self, other_question_or_diff)
370
-
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
387
+ # from edsl.questions import compose_questions
388
+ # return compose_questions(self, other_question_or_diff)
375
389
 
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
390
+ # def _validate_response(self, response):
391
+ # """Validate the response from the LLM. Behavior depends on the question type."""
392
+ # if "answer" not in response:
393
+ # raise QuestionResponseValidationError(
394
+ # "Response from LLM does not have an answer"
395
+ # )
396
+ # return response
383
397
 
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
398
+ def _translate_answer_code_to_answer(
399
+ self, answer, scenario: Optional["Scenario"] = None
400
+ ):
401
+ """There is over-ridden by child classes that ask for codes."""
402
+ return answer
388
403
 
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
404
+ # endregion
393
405
 
394
- ############################
395
- # Forward methods
396
- ############################
406
+ # region: Forward methods
397
407
  def add_question(self, other: QuestionBase) -> "Survey":
398
- """Add a question to this question by turning them into a survey with two questions."""
408
+ """Add a question to this question by turning them into a survey with two questions.
409
+
410
+ >>> from edsl.questions import QuestionFreeText as Q
411
+ >>> from edsl.questions import QuestionMultipleChoice as QMC
412
+ >>> s = Q.example().add_question(QMC.example())
413
+ >>> len(s.questions)
414
+ 2
415
+ """
399
416
  from edsl.surveys.Survey import Survey
400
417
 
401
418
  s = Survey([self, other])
402
419
  return s
403
420
 
404
421
  def to_survey(self) -> "Survey":
405
- """Turn a single question into a survey."""
422
+ """Turn a single question into a survey.
423
+ >>> from edsl import QuestionFreeText as Q
424
+ >>> Q.example().to_survey().questions[0].question_name
425
+ 'how_are_you'
426
+ """
406
427
  from edsl.surveys.Survey import Survey
407
428
 
408
429
  s = Survey([self])
409
430
  return s
410
431
 
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
432
  def by(self, *args) -> "Jobs":
419
433
  """Turn a single question into a survey and then a Job."""
420
434
  from edsl.surveys.Survey import Survey
@@ -422,6 +436,15 @@ class QuestionBase(
422
436
  s = Survey([self])
423
437
  return s.by(*args)
424
438
 
439
+ # endregion
440
+
441
+ # region: Display methods
442
+ def print(self):
443
+ from rich import print_json
444
+ import json
445
+
446
+ print_json(json.dumps(self.to_dict()))
447
+
425
448
  def human_readable(self) -> str:
426
449
  """Print the question in a human readable format.
427
450
 
@@ -442,6 +465,7 @@ class QuestionBase(
442
465
  self,
443
466
  scenario: Optional[dict] = None,
444
467
  agent: Optional[dict] = {},
468
+ answers: Optional[dict] = None,
445
469
  include_question_name: bool = False,
446
470
  height: Optional[int] = None,
447
471
  width: Optional[int] = None,
@@ -453,6 +477,17 @@ class QuestionBase(
453
477
  if scenario is None:
454
478
  scenario = {}
455
479
 
480
+ prior_answers_dict = {}
481
+
482
+ if isinstance(answers, dict):
483
+ for key, value in answers.items():
484
+ if not key.endswith("_comment") and not key.endswith(
485
+ "_generated_tokens"
486
+ ):
487
+ prior_answers_dict[key] = {"answer": value}
488
+
489
+ # breakpoint()
490
+
456
491
  base_template = """
457
492
  <div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
458
493
  {% if include_question_name %}
@@ -472,13 +507,40 @@ class QuestionBase(
472
507
 
473
508
  base_template = Template(base_template)
474
509
 
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
- }
510
+ context = {
511
+ "scenario": scenario,
512
+ "agent": agent,
513
+ } | prior_answers_dict
514
+
515
+ # Render the question text
516
+ try:
517
+ question_text = Template(self.question_text).render(context)
518
+ except Exception as e:
519
+ print(
520
+ f"Error rendering question: question_text = {self.question_text}, error = {e}"
521
+ )
522
+ question_text = self.question_text
523
+
524
+ try:
525
+ question_content = Template(question_content).render(context)
526
+ except Exception as e:
527
+ print(
528
+ f"Error rendering question: question_content = {question_content}, error = {e}"
529
+ )
530
+ question_content = question_content
531
+
532
+ try:
533
+ params = {
534
+ "question_name": self.question_name,
535
+ "question_text": question_text,
536
+ "question_type": self.question_type,
537
+ "question_content": question_content,
538
+ "include_question_name": include_question_name,
539
+ }
540
+ except Exception as e:
541
+ raise ValueError(
542
+ f"Error rendering question: params = {params}, error = {e}"
543
+ )
482
544
  rendered_html = base_template.render(**params)
483
545
 
484
546
  if iframe:
@@ -497,6 +559,21 @@ class QuestionBase(
497
559
 
498
560
  return rendered_html
499
561
 
562
+ @classmethod
563
+ def example_model(cls):
564
+ from edsl import Model
565
+
566
+ q = cls.example()
567
+ m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
568
+
569
+ return m
570
+
571
+ @classmethod
572
+ def example_results(cls):
573
+ m = cls.example_model()
574
+ q = cls.example()
575
+ return q.by(m).run(cache=False)
576
+
500
577
  def rich_print(self):
501
578
  """Print the question in a rich format."""
502
579
  from rich.table import Table
@@ -520,6 +597,8 @@ class QuestionBase(
520
597
  )
521
598
  return table
522
599
 
600
+ # endregion
601
+
523
602
 
524
603
  if __name__ == "__main__":
525
604
  import doctest