edsl 0.1.32__py3-none-any.whl → 0.1.33__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 (181) 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 +73 -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 +37 -22
  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 +146 -175
  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/notebooks/Notebook.py +20 -2
  64. edsl/prompts/Prompt.py +52 -2
  65. edsl/questions/AnswerValidatorMixin.py +23 -26
  66. edsl/questions/QuestionBase.py +330 -249
  67. edsl/questions/QuestionBaseGenMixin.py +133 -0
  68. edsl/questions/QuestionBasePromptsMixin.py +266 -0
  69. edsl/questions/QuestionBudget.py +99 -41
  70. edsl/questions/QuestionCheckBox.py +227 -35
  71. edsl/questions/QuestionExtract.py +98 -27
  72. edsl/questions/QuestionFreeText.py +52 -29
  73. edsl/questions/QuestionFunctional.py +7 -0
  74. edsl/questions/QuestionList.py +141 -22
  75. edsl/questions/QuestionMultipleChoice.py +159 -65
  76. edsl/questions/QuestionNumerical.py +88 -46
  77. edsl/questions/QuestionRank.py +182 -24
  78. edsl/questions/Quick.py +41 -0
  79. edsl/questions/RegisterQuestionsMeta.py +31 -12
  80. edsl/questions/ResponseValidatorABC.py +170 -0
  81. edsl/questions/__init__.py +3 -4
  82. edsl/questions/decorators.py +21 -0
  83. edsl/questions/derived/QuestionLikertFive.py +10 -5
  84. edsl/questions/derived/QuestionLinearScale.py +15 -2
  85. edsl/questions/derived/QuestionTopK.py +10 -1
  86. edsl/questions/derived/QuestionYesNo.py +24 -3
  87. edsl/questions/descriptors.py +43 -7
  88. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  89. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  90. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  91. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  92. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  93. edsl/questions/prompt_templates/question_list.jinja +17 -0
  94. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  95. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  96. edsl/questions/question_registry.py +6 -2
  97. edsl/questions/templates/__init__.py +0 -0
  98. edsl/questions/templates/budget/__init__.py +0 -0
  99. edsl/questions/templates/budget/answering_instructions.jinja +7 -0
  100. edsl/questions/templates/budget/question_presentation.jinja +7 -0
  101. edsl/questions/templates/checkbox/__init__.py +0 -0
  102. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  103. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  104. edsl/questions/templates/extract/__init__.py +0 -0
  105. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  106. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  107. edsl/questions/templates/free_text/__init__.py +0 -0
  108. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  109. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  110. edsl/questions/templates/likert_five/__init__.py +0 -0
  111. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  112. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  113. edsl/questions/templates/linear_scale/__init__.py +0 -0
  114. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  115. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  116. edsl/questions/templates/list/__init__.py +0 -0
  117. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  118. edsl/questions/templates/list/question_presentation.jinja +5 -0
  119. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  120. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  121. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  122. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  123. edsl/questions/templates/numerical/__init__.py +0 -0
  124. edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
  125. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  126. edsl/questions/templates/rank/__init__.py +0 -0
  127. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  128. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  129. edsl/questions/templates/top_k/__init__.py +0 -0
  130. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  131. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  132. edsl/questions/templates/yes_no/__init__.py +0 -0
  133. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  134. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  135. edsl/results/Dataset.py +20 -0
  136. edsl/results/DatasetExportMixin.py +46 -48
  137. edsl/results/DatasetTree.py +145 -0
  138. edsl/results/Result.py +32 -5
  139. edsl/results/Results.py +135 -46
  140. edsl/results/ResultsDBMixin.py +3 -3
  141. edsl/results/Selector.py +118 -0
  142. edsl/results/tree_explore.py +115 -0
  143. edsl/scenarios/FileStore.py +71 -10
  144. edsl/scenarios/Scenario.py +96 -25
  145. edsl/scenarios/ScenarioImageMixin.py +2 -2
  146. edsl/scenarios/ScenarioList.py +361 -39
  147. edsl/scenarios/ScenarioListExportMixin.py +9 -0
  148. edsl/scenarios/ScenarioListPdfMixin.py +150 -4
  149. edsl/study/SnapShot.py +8 -1
  150. edsl/study/Study.py +32 -0
  151. edsl/surveys/Rule.py +10 -1
  152. edsl/surveys/RuleCollection.py +21 -5
  153. edsl/surveys/Survey.py +637 -311
  154. edsl/surveys/SurveyExportMixin.py +71 -9
  155. edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
  156. edsl/surveys/SurveyQualtricsImport.py +75 -4
  157. edsl/surveys/instructions/ChangeInstruction.py +47 -0
  158. edsl/surveys/instructions/Instruction.py +34 -0
  159. edsl/surveys/instructions/InstructionCollection.py +77 -0
  160. edsl/surveys/instructions/__init__.py +0 -0
  161. edsl/templates/error_reporting/base.html +24 -0
  162. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  163. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  164. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  165. edsl/templates/error_reporting/interview_details.html +116 -0
  166. edsl/templates/error_reporting/interviews.html +10 -0
  167. edsl/templates/error_reporting/overview.html +5 -0
  168. edsl/templates/error_reporting/performance_plot.html +2 -0
  169. edsl/templates/error_reporting/report.css +74 -0
  170. edsl/templates/error_reporting/report.html +118 -0
  171. edsl/templates/error_reporting/report.js +25 -0
  172. edsl/utilities/utilities.py +9 -1
  173. {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/METADATA +5 -2
  174. edsl-0.1.33.dist-info/RECORD +295 -0
  175. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
  176. edsl/jobs/interviews/retry_management.py +0 -37
  177. edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
  178. edsl/utilities/gcp_bucket/simple_example.py +0 -9
  179. edsl-0.1.32.dist-info/RECORD +0 -209
  180. {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
  181. {edsl-0.1.32.dist-info → edsl-0.1.33.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,128 @@ 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', 'generated_tokens': 'Hello'})
107
+ {'answer': 'Hello', 'generated_tokens': 'Hello'}
108
+ """
59
109
 
60
- def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
61
- """Apply a function to the question parts
110
+ return self.response_validator.validate(answer)
62
111
 
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?\""")
112
+ # endregion
113
+
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
68
119
 
120
+ def __hash__(self) -> int:
121
+ """Return a hash of the question.
122
+
123
+ >>> from edsl import QuestionFreeText as Q
124
+ >>> hash(Q.example())
125
+ 1144312636257752766
69
126
  """
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)
127
+ from edsl.utilities.utilities import dict_hash
128
+
129
+ return dict_hash(self._to_dict())
88
130
 
89
131
  @property
90
132
  def data(self) -> dict:
91
- """Return a dictionary of question attributes **except** for question_type."""
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
+ ]
92
148
  candidate_data = {
93
149
  k.replace("_", "", 1): v
94
150
  for k, v in self.__dict__.items()
95
- if k.startswith("_")
151
+ if k.startswith("_") and k not in exclude_list
96
152
  }
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)
103
153
 
104
154
  if "func" in candidate_data:
105
155
  func = candidate_data.pop("func")
@@ -109,147 +159,22 @@ class QuestionBase(
109
159
 
110
160
  return candidate_data
111
161
 
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.
162
+ def _to_dict(self):
163
+ """Convert the question to a dictionary that includes the question type (used in deserialization).
207
164
 
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
- \""")
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'}
214
167
  """
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
168
  candidate_data = self.data.copy()
247
169
  candidate_data["question_type"] = self.question_type
248
170
  return candidate_data
249
171
 
250
172
  @add_edsl_version
251
173
  def to_dict(self) -> dict[str, Any]:
252
- """Convert the question to a dictionary that includes the question type (used in deserialization)."""
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
+ """
253
178
  return self._to_dict()
254
179
 
255
180
  @classmethod
@@ -289,39 +214,90 @@ class QuestionBase(
289
214
 
290
215
  return question_class(**local_data)
291
216
 
292
- def copy(self) -> Type[QuestionBase]:
293
- """Return a deep copy of the question."""
294
- return copy.deepcopy(self)
217
+ # endregion
295
218
 
296
- ############################
297
- # Dunder methods
298
- ############################
299
- def print(self):
300
- from rich import print_json
301
- import json
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
302
224
 
303
- print_json(json.dumps(self.to_dict()))
225
+ return LanguageModel.example(canned_response=canned_response, test_model=True)
226
+
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
251
+
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
304
258
 
305
259
  def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
306
260
  """Call the question.
307
261
 
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?")
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?")
312
266
  >>> q(model = m)
313
267
  "Yo, what's up?"
314
268
 
315
269
  """
316
270
  survey = self.to_survey()
317
- results = survey(model=model, agent=agent, **kwargs)
271
+ results = survey(model=model, agent=agent, **kwargs, cache=False)
318
272
  if just_answer:
319
273
  return results.select(f"answer.{self.question_name}").first()
320
274
  else:
321
275
  return results
322
276
 
323
- async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
324
- """Call the question."""
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
+ """
325
301
  survey = self.to_survey()
326
302
  results = await survey.run_async(model=model, agent=agent, **kwargs)
327
303
  if just_answer:
@@ -329,9 +305,37 @@ class QuestionBase(
329
305
  else:
330
306
  return results
331
307
 
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
- class_name = self.__class__.__name__
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
+ """
335
339
  items = [
336
340
  f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
337
341
  for k, v in self.data.items()
@@ -340,14 +344,31 @@ class QuestionBase(
340
344
  question_type = self.to_dict().get("question_type", "None")
341
345
  return f"Question('{question_type}', {', '.join(items)})"
342
346
 
343
- def __eq__(self, other: Type[QuestionBase]) -> bool:
344
- """Check if two questions are equal. Equality is defined as having the .to_dict()."""
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
+ """
345
360
  if not isinstance(other, QuestionBase):
346
361
  return False
347
362
  return self.to_dict() == other.to_dict()
348
363
 
349
364
  def __sub__(self, other) -> BaseDiff:
350
- """Return the difference between two objects."""
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
+ """
351
372
 
352
373
  return BaseDiff(other, self)
353
374
 
@@ -364,57 +385,51 @@ class QuestionBase(
364
385
  ):
365
386
  return other_question_or_diff.apply(self)
366
387
 
367
- from edsl.questions import compose_questions
368
-
369
- return compose_questions(self, other_question_or_diff)
388
+ # from edsl.questions import compose_questions
389
+ # return compose_questions(self, other_question_or_diff)
370
390
 
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
375
-
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
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
383
398
 
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
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
388
404
 
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
405
+ # endregion
393
406
 
394
- ############################
395
- # Forward methods
396
- ############################
407
+ # region: Forward methods
397
408
  def add_question(self, other: QuestionBase) -> "Survey":
398
- """Add a question to this question by turning them into a survey with two questions."""
409
+ """Add a question to this question by turning them into a survey with two questions.
410
+
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
+ """
399
417
  from edsl.surveys.Survey import Survey
400
418
 
401
419
  s = Survey([self, other])
402
420
  return s
403
421
 
404
422
  def to_survey(self) -> "Survey":
405
- """Turn a single question into a 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
+ """
406
428
  from edsl.surveys.Survey import Survey
407
429
 
408
430
  s = Survey([self])
409
431
  return s
410
432
 
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
433
  def by(self, *args) -> "Jobs":
419
434
  """Turn a single question into a survey and then a Job."""
420
435
  from edsl.surveys.Survey import Survey
@@ -422,6 +437,15 @@ class QuestionBase(
422
437
  s = Survey([self])
423
438
  return s.by(*args)
424
439
 
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
+
425
449
  def human_readable(self) -> str:
426
450
  """Print the question in a human readable format.
427
451
 
@@ -441,6 +465,8 @@ class QuestionBase(
441
465
  def html(
442
466
  self,
443
467
  scenario: Optional[dict] = None,
468
+ agent: Optional[dict] = {},
469
+ answers: Optional[dict] = None,
444
470
  include_question_name: bool = False,
445
471
  height: Optional[int] = None,
446
472
  width: Optional[int] = None,
@@ -452,6 +478,17 @@ class QuestionBase(
452
478
  if scenario is None:
453
479
  scenario = {}
454
480
 
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
+
455
492
  base_template = """
456
493
  <div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
457
494
  {% if include_question_name %}
@@ -471,13 +508,40 @@ class QuestionBase(
471
508
 
472
509
  base_template = Template(base_template)
473
510
 
474
- params = {
475
- "question_name": self.question_name,
476
- "question_text": Template(self.question_text).render(scenario),
477
- "question_type": self.question_type,
478
- "question_content": Template(question_content).render(scenario),
479
- "include_question_name": include_question_name,
480
- }
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
+ )
481
545
  rendered_html = base_template.render(**params)
482
546
 
483
547
  if iframe:
@@ -496,6 +560,21 @@ class QuestionBase(
496
560
 
497
561
  return rendered_html
498
562
 
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
+
499
578
  def rich_print(self):
500
579
  """Print the question in a rich format."""
501
580
  from rich.table import Table
@@ -519,6 +598,8 @@ class QuestionBase(
519
598
  )
520
599
  return table
521
600
 
601
+ # endregion
602
+
522
603
 
523
604
  if __name__ == "__main__":
524
605
  import doctest