edsl 0.1.39__py3-none-any.whl → 0.1.39.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 (212) hide show
  1. edsl/Base.py +116 -197
  2. edsl/__init__.py +7 -15
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +147 -351
  5. edsl/agents/AgentList.py +73 -211
  6. edsl/agents/Invigilator.py +50 -101
  7. edsl/agents/InvigilatorBase.py +70 -62
  8. edsl/agents/PromptConstructor.py +225 -143
  9. edsl/agents/__init__.py +1 -0
  10. edsl/agents/prompt_helpers.py +3 -3
  11. edsl/auto/AutoStudy.py +5 -18
  12. edsl/auto/StageBase.py +40 -53
  13. edsl/auto/StageQuestions.py +1 -2
  14. edsl/auto/utilities.py +6 -0
  15. edsl/config.py +2 -22
  16. edsl/conversation/car_buying.py +1 -2
  17. edsl/coop/PriceFetcher.py +1 -1
  18. edsl/coop/coop.py +47 -125
  19. edsl/coop/utils.py +14 -14
  20. edsl/data/Cache.py +27 -45
  21. edsl/data/CacheEntry.py +15 -12
  22. edsl/data/CacheHandler.py +12 -31
  23. edsl/data/RemoteCacheSync.py +46 -154
  24. edsl/data/__init__.py +3 -4
  25. edsl/data_transfer_models.py +1 -2
  26. edsl/enums.py +0 -27
  27. edsl/exceptions/__init__.py +50 -50
  28. edsl/exceptions/agents.py +0 -12
  29. edsl/exceptions/questions.py +6 -24
  30. edsl/exceptions/scenarios.py +0 -7
  31. edsl/inference_services/AnthropicService.py +19 -38
  32. edsl/inference_services/AwsBedrock.py +2 -0
  33. edsl/inference_services/AzureAI.py +2 -0
  34. edsl/inference_services/GoogleService.py +12 -7
  35. edsl/inference_services/InferenceServiceABC.py +85 -18
  36. edsl/inference_services/InferenceServicesCollection.py +79 -120
  37. edsl/inference_services/MistralAIService.py +3 -0
  38. edsl/inference_services/OpenAIService.py +35 -47
  39. edsl/inference_services/PerplexityService.py +3 -0
  40. edsl/inference_services/TestService.py +10 -11
  41. edsl/inference_services/TogetherAIService.py +3 -5
  42. edsl/jobs/Answers.py +14 -1
  43. edsl/jobs/Jobs.py +431 -356
  44. edsl/jobs/JobsChecks.py +10 -35
  45. edsl/jobs/JobsPrompts.py +4 -6
  46. edsl/jobs/JobsRemoteInferenceHandler.py +133 -205
  47. edsl/jobs/buckets/BucketCollection.py +3 -44
  48. edsl/jobs/buckets/TokenBucket.py +21 -53
  49. edsl/jobs/interviews/Interview.py +408 -143
  50. edsl/jobs/runners/JobsRunnerAsyncio.py +403 -88
  51. edsl/jobs/runners/JobsRunnerStatus.py +165 -133
  52. edsl/jobs/tasks/QuestionTaskCreator.py +19 -21
  53. edsl/jobs/tasks/TaskHistory.py +18 -38
  54. edsl/jobs/tasks/task_status_enum.py +2 -0
  55. edsl/language_models/KeyLookup.py +30 -0
  56. edsl/language_models/LanguageModel.py +236 -194
  57. edsl/language_models/ModelList.py +19 -28
  58. edsl/language_models/__init__.py +2 -1
  59. edsl/language_models/registry.py +190 -0
  60. edsl/language_models/repair.py +2 -2
  61. edsl/language_models/unused/ReplicateBase.py +83 -0
  62. edsl/language_models/utilities.py +4 -5
  63. edsl/notebooks/Notebook.py +14 -19
  64. edsl/prompts/Prompt.py +39 -29
  65. edsl/questions/{answer_validator_mixin.py → AnswerValidatorMixin.py} +2 -47
  66. edsl/questions/QuestionBase.py +214 -68
  67. edsl/questions/{question_base_gen_mixin.py → QuestionBaseGenMixin.py} +50 -57
  68. edsl/questions/QuestionBasePromptsMixin.py +3 -7
  69. edsl/questions/QuestionBudget.py +1 -1
  70. edsl/questions/QuestionCheckBox.py +3 -3
  71. edsl/questions/QuestionExtract.py +7 -5
  72. edsl/questions/QuestionFreeText.py +3 -2
  73. edsl/questions/QuestionList.py +18 -10
  74. edsl/questions/QuestionMultipleChoice.py +23 -67
  75. edsl/questions/QuestionNumerical.py +4 -2
  76. edsl/questions/QuestionRank.py +17 -7
  77. edsl/questions/{response_validator_abc.py → ResponseValidatorABC.py} +26 -40
  78. edsl/questions/SimpleAskMixin.py +3 -4
  79. edsl/questions/__init__.py +1 -2
  80. edsl/questions/derived/QuestionLinearScale.py +3 -6
  81. edsl/questions/derived/QuestionTopK.py +1 -1
  82. edsl/questions/descriptors.py +3 -17
  83. edsl/questions/question_registry.py +1 -1
  84. edsl/results/CSSParameterizer.py +1 -1
  85. edsl/results/Dataset.py +7 -170
  86. edsl/results/DatasetExportMixin.py +305 -168
  87. edsl/results/DatasetTree.py +8 -28
  88. edsl/results/Result.py +206 -298
  89. edsl/results/Results.py +131 -149
  90. edsl/results/ResultsDBMixin.py +238 -0
  91. edsl/results/ResultsExportMixin.py +0 -2
  92. edsl/results/{results_selector.py → Selector.py} +13 -23
  93. edsl/results/TableDisplay.py +171 -98
  94. edsl/results/__init__.py +1 -1
  95. edsl/scenarios/FileStore.py +239 -150
  96. edsl/scenarios/Scenario.py +193 -90
  97. edsl/scenarios/ScenarioHtmlMixin.py +3 -4
  98. edsl/scenarios/{scenario_join.py → ScenarioJoin.py} +6 -10
  99. edsl/scenarios/ScenarioList.py +244 -415
  100. edsl/scenarios/ScenarioListExportMixin.py +7 -0
  101. edsl/scenarios/ScenarioListPdfMixin.py +37 -15
  102. edsl/scenarios/__init__.py +2 -1
  103. edsl/study/ObjectEntry.py +1 -1
  104. edsl/study/SnapShot.py +1 -1
  105. edsl/study/Study.py +12 -5
  106. edsl/surveys/Rule.py +4 -5
  107. edsl/surveys/RuleCollection.py +27 -25
  108. edsl/surveys/Survey.py +791 -270
  109. edsl/surveys/SurveyCSS.py +8 -20
  110. edsl/surveys/{SurveyFlowVisualization.py → SurveyFlowVisualizationMixin.py} +9 -11
  111. edsl/surveys/__init__.py +2 -4
  112. edsl/surveys/descriptors.py +2 -6
  113. edsl/surveys/instructions/ChangeInstruction.py +2 -1
  114. edsl/surveys/instructions/Instruction.py +13 -4
  115. edsl/surveys/instructions/InstructionCollection.py +6 -11
  116. edsl/templates/error_reporting/interview_details.html +1 -1
  117. edsl/templates/error_reporting/report.html +1 -1
  118. edsl/tools/plotting.py +1 -1
  119. edsl/utilities/utilities.py +23 -35
  120. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/METADATA +10 -12
  121. edsl-0.1.39.dev1.dist-info/RECORD +277 -0
  122. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/WHEEL +1 -1
  123. edsl/agents/QuestionInstructionPromptBuilder.py +0 -128
  124. edsl/agents/QuestionTemplateReplacementsBuilder.py +0 -137
  125. edsl/agents/question_option_processor.py +0 -172
  126. edsl/coop/CoopFunctionsMixin.py +0 -15
  127. edsl/coop/ExpectedParrotKeyHandler.py +0 -125
  128. edsl/exceptions/inference_services.py +0 -5
  129. edsl/inference_services/AvailableModelCacheHandler.py +0 -184
  130. edsl/inference_services/AvailableModelFetcher.py +0 -215
  131. edsl/inference_services/ServiceAvailability.py +0 -135
  132. edsl/inference_services/data_structures.py +0 -134
  133. edsl/jobs/AnswerQuestionFunctionConstructor.py +0 -223
  134. edsl/jobs/FetchInvigilator.py +0 -47
  135. edsl/jobs/InterviewTaskManager.py +0 -98
  136. edsl/jobs/InterviewsConstructor.py +0 -50
  137. edsl/jobs/JobsComponentConstructor.py +0 -189
  138. edsl/jobs/JobsRemoteInferenceLogger.py +0 -239
  139. edsl/jobs/RequestTokenEstimator.py +0 -30
  140. edsl/jobs/async_interview_runner.py +0 -138
  141. edsl/jobs/buckets/TokenBucketAPI.py +0 -211
  142. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  143. edsl/jobs/check_survey_scenario_compatibility.py +0 -85
  144. edsl/jobs/data_structures.py +0 -120
  145. edsl/jobs/decorators.py +0 -35
  146. edsl/jobs/jobs_status_enums.py +0 -9
  147. edsl/jobs/loggers/HTMLTableJobLogger.py +0 -304
  148. edsl/jobs/results_exceptions_handler.py +0 -98
  149. edsl/language_models/ComputeCost.py +0 -63
  150. edsl/language_models/PriceManager.py +0 -127
  151. edsl/language_models/RawResponseHandler.py +0 -106
  152. edsl/language_models/ServiceDataSources.py +0 -0
  153. edsl/language_models/key_management/KeyLookup.py +0 -63
  154. edsl/language_models/key_management/KeyLookupBuilder.py +0 -273
  155. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  156. edsl/language_models/key_management/__init__.py +0 -0
  157. edsl/language_models/key_management/models.py +0 -131
  158. edsl/language_models/model.py +0 -256
  159. edsl/notebooks/NotebookToLaTeX.py +0 -142
  160. edsl/questions/ExceptionExplainer.py +0 -77
  161. edsl/questions/HTMLQuestion.py +0 -103
  162. edsl/questions/QuestionMatrix.py +0 -265
  163. edsl/questions/data_structures.py +0 -20
  164. edsl/questions/loop_processor.py +0 -149
  165. edsl/questions/response_validator_factory.py +0 -34
  166. edsl/questions/templates/matrix/__init__.py +0 -1
  167. edsl/questions/templates/matrix/answering_instructions.jinja +0 -5
  168. edsl/questions/templates/matrix/question_presentation.jinja +0 -20
  169. edsl/results/MarkdownToDocx.py +0 -122
  170. edsl/results/MarkdownToPDF.py +0 -111
  171. edsl/results/TextEditor.py +0 -50
  172. edsl/results/file_exports.py +0 -252
  173. edsl/results/smart_objects.py +0 -96
  174. edsl/results/table_data_class.py +0 -12
  175. edsl/results/table_renderers.py +0 -118
  176. edsl/scenarios/ConstructDownloadLink.py +0 -109
  177. edsl/scenarios/DocumentChunker.py +0 -102
  178. edsl/scenarios/DocxScenario.py +0 -16
  179. edsl/scenarios/PdfExtractor.py +0 -40
  180. edsl/scenarios/directory_scanner.py +0 -96
  181. edsl/scenarios/file_methods.py +0 -85
  182. edsl/scenarios/handlers/__init__.py +0 -13
  183. edsl/scenarios/handlers/csv.py +0 -49
  184. edsl/scenarios/handlers/docx.py +0 -76
  185. edsl/scenarios/handlers/html.py +0 -37
  186. edsl/scenarios/handlers/json.py +0 -111
  187. edsl/scenarios/handlers/latex.py +0 -5
  188. edsl/scenarios/handlers/md.py +0 -51
  189. edsl/scenarios/handlers/pdf.py +0 -68
  190. edsl/scenarios/handlers/png.py +0 -39
  191. edsl/scenarios/handlers/pptx.py +0 -105
  192. edsl/scenarios/handlers/py.py +0 -294
  193. edsl/scenarios/handlers/sql.py +0 -313
  194. edsl/scenarios/handlers/sqlite.py +0 -149
  195. edsl/scenarios/handlers/txt.py +0 -33
  196. edsl/scenarios/scenario_selector.py +0 -156
  197. edsl/surveys/ConstructDAG.py +0 -92
  198. edsl/surveys/EditSurvey.py +0 -221
  199. edsl/surveys/InstructionHandler.py +0 -100
  200. edsl/surveys/MemoryManagement.py +0 -72
  201. edsl/surveys/RuleManager.py +0 -172
  202. edsl/surveys/Simulator.py +0 -75
  203. edsl/surveys/SurveyToApp.py +0 -141
  204. edsl/utilities/PrettyList.py +0 -56
  205. edsl/utilities/is_notebook.py +0 -18
  206. edsl/utilities/is_valid_variable_name.py +0 -11
  207. edsl/utilities/remove_edsl_version.py +0 -24
  208. edsl-0.1.39.dist-info/RECORD +0 -358
  209. /edsl/questions/{register_questions_meta.py → RegisterQuestionsMeta.py} +0 -0
  210. /edsl/results/{results_fetch_mixin.py → ResultsFetchMixin.py} +0 -0
  211. /edsl/results/{results_tools_mixin.py → ResultsToolsMixin.py} +0 -0
  212. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/LICENSE +0 -0
@@ -2,35 +2,31 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  from abc import ABC, abstractmethod
5
- from typing import Any, Type, Optional, List, Callable, Union, TypedDict, TYPE_CHECKING
5
+ from typing import Any, Type, Optional, List, Callable, Union, TypedDict
6
+ import copy
6
7
 
7
- from edsl.exceptions.questions import (
8
+ from edsl.exceptions import (
9
+ QuestionResponseValidationError,
10
+ QuestionAnswerValidationError,
8
11
  QuestionSerializationError,
9
12
  )
10
13
  from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
11
14
 
12
- from edsl.questions.answer_validator_mixin import AnswerValidatorMixin
13
- from edsl.questions.register_questions_meta import RegisterQuestionsMeta
14
- from edsl.Base import PersistenceMixin, RepresentationMixin
15
+
16
+ from edsl.questions.AnswerValidatorMixin import AnswerValidatorMixin
17
+ from edsl.questions.RegisterQuestionsMeta import RegisterQuestionsMeta
18
+ from edsl.Base import PersistenceMixin, RichPrintingMixin
15
19
  from edsl.BaseDiff import BaseDiff, BaseDiffCollection
16
20
 
17
21
  from edsl.questions.SimpleAskMixin import SimpleAskMixin
18
22
  from edsl.questions.QuestionBasePromptsMixin import QuestionBasePromptsMixin
19
- from edsl.questions.question_base_gen_mixin import QuestionBaseGenMixin
20
- from edsl.utilities.remove_edsl_version import remove_edsl_version
21
-
22
- if TYPE_CHECKING:
23
- from edsl.questions.response_validator_abc import ResponseValidatorABC
24
- from edsl.language_models.LanguageModel import LanguageModel
25
- from edsl.results.Results import Results
26
- from edsl.agents.Agent import Agent
27
- from edsl.surveys.Survey import Survey
28
- from edsl.jobs.Jobs import Jobs
23
+ from edsl.questions.QuestionBaseGenMixin import QuestionBaseGenMixin
24
+ from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
29
25
 
30
26
 
31
27
  class QuestionBase(
32
28
  PersistenceMixin,
33
- RepresentationMixin,
29
+ RichPrintingMixin,
34
30
  SimpleAskMixin,
35
31
  QuestionBasePromptsMixin,
36
32
  QuestionBaseGenMixin,
@@ -40,14 +36,6 @@ class QuestionBase(
40
36
  ):
41
37
  """ABC for the Question class. All questions inherit from this class.
42
38
  Some of the constraints on child questions are defined in the RegisterQuestionsMeta metaclass.
43
-
44
-
45
- Every child class wiill have class attributes of question_type, _response_model and response_validator_class e.g.,
46
-
47
- question_type = "free_text"
48
- _response_model = FreeTextResponse
49
- response_validator_class = FreeTextResponseValidator
50
-
51
39
  """
52
40
 
53
41
  question_name: str = QuestionNameDescriptor()
@@ -57,16 +45,36 @@ class QuestionBase(
57
45
  _question_presentation = None
58
46
 
59
47
  @property
60
- def response_validator(self) -> "ResponseValidatorABC":
48
+ def response_model(self) -> type["BaseModel"]:
49
+ if self._response_model is not None:
50
+ return self._response_model
51
+ else:
52
+ return self.create_response_model()
53
+
54
+ # region: Validation and simulation methods
55
+ @property
56
+ def response_validator(self) -> "ResponseValidatorBase":
61
57
  """Return the response validator."""
62
- from edsl.questions.response_validator_factory import ResponseValidatorFactory
58
+ params = (
59
+ {
60
+ "response_model": self.response_model,
61
+ }
62
+ | {k: getattr(self, k) for k in self.validator_parameters}
63
+ | {"exception_to_throw": getattr(self, "exception_to_throw", None)}
64
+ | {"override_answer": getattr(self, "override_answer", None)}
65
+ )
66
+ return self.response_validator_class(**params)
63
67
 
64
- rvf = ResponseValidatorFactory(self)
65
- return rvf.response_validator
68
+ @property
69
+ def validator_parameters(self) -> list[str]:
70
+ """Return the parameters required for the response validator.
66
71
 
67
- def duplicate(self):
68
- """Return a duplicate of the question."""
69
- return self.from_dict(self.to_dict())
72
+ >>> from edsl import QuestionMultipleChoice as Q
73
+ >>> Q.example().validator_parameters
74
+ ['question_options', 'use_code']
75
+
76
+ """
77
+ return self.response_validator_class.required_params
70
78
 
71
79
  @property
72
80
  def fake_data_factory(self):
@@ -74,7 +82,8 @@ class QuestionBase(
74
82
  if not hasattr(self, "_fake_data_factory"):
75
83
  from polyfactory.factories.pydantic_factory import ModelFactory
76
84
 
77
- class FakeData(ModelFactory[self.response_model]): ...
85
+ class FakeData(ModelFactory[self.response_model]):
86
+ ...
78
87
 
79
88
  self._fake_data_factory = FakeData
80
89
  return self._fake_data_factory
@@ -101,14 +110,17 @@ class QuestionBase(
101
110
  self, answer: dict, replacement_dict: dict = None
102
111
  ) -> ValidatedAnswer:
103
112
  """Validate the answer.
104
- >>> from edsl.exceptions.questions import QuestionAnswerValidationError
105
- >>> from edsl.questions import QuestionFreeText as Q
113
+ >>> from edsl.exceptions import QuestionAnswerValidationError
114
+ >>> from edsl import QuestionFreeText as Q
106
115
  >>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
107
116
  {'answer': 'Hello', 'generated_tokens': 'Hello'}
108
117
  """
109
118
 
110
119
  return self.response_validator.validate(answer, replacement_dict)
111
120
 
121
+ # endregion
122
+
123
+ # region: Serialization methods
112
124
  @property
113
125
  def name(self) -> str:
114
126
  "Helper function so questions and instructions can use the same access method"
@@ -129,7 +141,7 @@ class QuestionBase(
129
141
  def data(self) -> dict:
130
142
  """Return a dictionary of question attributes **except** for question_type.
131
143
 
132
- >>> from edsl.questions import QuestionFreeText as Q
144
+ >>> from edsl import QuestionFreeText as Q
133
145
  >>> Q.example().data
134
146
  {'question_name': 'how_are_you', 'question_text': 'How are you?'}
135
147
  """
@@ -171,10 +183,10 @@ class QuestionBase(
171
183
 
172
184
  return candidate_data
173
185
 
174
- def to_dict(self, add_edsl_version: bool = True):
186
+ def to_dict(self, add_edsl_version=True):
175
187
  """Convert the question to a dictionary that includes the question type (used in deserialization).
176
188
 
177
- >>> from edsl.questions import QuestionFreeText as Q; Q.example().to_dict(add_edsl_version = False)
189
+ >>> from edsl import QuestionFreeText as Q; Q.example().to_dict(add_edsl_version = False)
178
190
  {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
179
191
  """
180
192
  candidate_data = self.data.copy()
@@ -225,6 +237,9 @@ class QuestionBase(
225
237
 
226
238
  return question_class(**local_data)
227
239
 
240
+ # endregion
241
+
242
+ # region: Running methods
228
243
  @classmethod
229
244
  def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
230
245
  """Get a test model for the question."""
@@ -249,10 +264,12 @@ class QuestionBase(
249
264
  >>> m.execute_model_call("", "")
250
265
  {'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
251
266
  >>> Q.run_example(show_answer = True, model = m, disable_remote_cache = True, disable_remote_inference = True)
252
- Dataset([{'answer.how_are_you': ["Yo, what's up?"]}])
267
+ answer.how_are_you
268
+ --------------------
269
+ Yo, what's up?
253
270
  """
254
271
  if model is None:
255
- from edsl.language_models.model import Model
272
+ from edsl import Model
256
273
 
257
274
  model = Model()
258
275
  results = (
@@ -265,28 +282,26 @@ class QuestionBase(
265
282
  )
266
283
  )
267
284
  if show_answer:
268
- return results.select("answer.*")
285
+ return results.select("answer.*").print()
269
286
  else:
270
287
  return results
271
288
 
272
289
  def __call__(
273
290
  self,
274
- just_answer: bool = True,
275
- model: Optional["LanguageModel"] = None,
276
- agent: Optional["Agent"] = None,
291
+ just_answer=True,
292
+ model=None,
293
+ agent=None,
277
294
  disable_remote_cache: bool = False,
278
295
  disable_remote_inference: bool = False,
279
- verbose: bool = False,
280
296
  **kwargs,
281
- ) -> Union[Any, "Results"]:
297
+ ):
282
298
  """Call the question.
283
299
 
284
300
 
285
301
  >>> from edsl import QuestionFreeText as Q
286
- >>> from edsl import Model
287
- >>> m = Model("test", canned_response = "Yo, what's up?")
302
+ >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
288
303
  >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
289
- >>> q(model = m, disable_remote_cache = True, disable_remote_inference = True, cache = False)
304
+ >>> q(model = m, disable_remote_cache = True, disable_remote_inference = True)
290
305
  "Yo, what's up?"
291
306
 
292
307
  """
@@ -295,7 +310,7 @@ class QuestionBase(
295
310
  model=model,
296
311
  agent=agent,
297
312
  **kwargs,
298
- verbose=verbose,
313
+ cache=False,
299
314
  disable_remote_cache=disable_remote_cache,
300
315
  disable_remote_inference=disable_remote_inference,
301
316
  )
@@ -306,16 +321,15 @@ class QuestionBase(
306
321
 
307
322
  def run(self, *args, **kwargs) -> "Results":
308
323
  """Turn a single question into a survey and runs it."""
309
- return self.to_survey().run(*args, **kwargs)
324
+ from edsl.surveys.Survey import Survey
310
325
 
311
- def using(self, *args, **kwargs) -> "Jobs":
312
- """Turn a single question into a survey and then a Job."""
313
- return self.to_survey().to_jobs().using(*args, **kwargs)
326
+ s = self.to_survey()
327
+ return s.run(*args, **kwargs)
314
328
 
315
329
  async def run_async(
316
330
  self,
317
331
  just_answer: bool = True,
318
- model: Optional["LanguageModel"] = None,
332
+ model: Optional["Model"] = None,
319
333
  agent: Optional["Agent"] = None,
320
334
  disable_remote_inference: bool = False,
321
335
  **kwargs,
@@ -323,10 +337,10 @@ class QuestionBase(
323
337
  """Call the question asynchronously.
324
338
 
325
339
  >>> import asyncio
326
- >>> from edsl.questions import QuestionFreeText as Q
340
+ >>> from edsl import QuestionFreeText as Q
327
341
  >>> m = Q._get_test_model(canned_response = "Blue")
328
342
  >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
329
- >>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True, disable_remote_cache = True); print(result)
343
+ >>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True); print(result)
330
344
  >>> asyncio.run(test_run_async())
331
345
  Blue
332
346
  """
@@ -342,6 +356,27 @@ class QuestionBase(
342
356
  else:
343
357
  return results
344
358
 
359
+ # endregion
360
+
361
+ # region: Magic methods
362
+ def _repr_html_(self):
363
+ # from edsl.utilities.utilities import data_to_html
364
+
365
+ data = self.to_dict(add_edsl_version=False)
366
+ # keys = list(data.keys())
367
+ # values = list(data.values())
368
+ from tabulate import tabulate
369
+
370
+ return tabulate(data.items(), headers=["keys", "values"], tablefmt="html")
371
+
372
+ # try:
373
+ # _ = data.pop("edsl_version")
374
+ # _ = data.pop("edsl_class_name")
375
+ # except KeyError:
376
+ # print("Serialized question lacks edsl version, but is should have it.")
377
+
378
+ # return data_to_html(data)
379
+
345
380
  def __getitem__(self, key: str) -> Any:
346
381
  """Get an attribute of the question so it can be treated like a dictionary.
347
382
 
@@ -349,10 +384,7 @@ class QuestionBase(
349
384
  >>> Q.example()['question_text']
350
385
  'How are you?'
351
386
  """
352
- try:
353
- return getattr(self, key)
354
- except TypeError:
355
- raise KeyError(f"Question has no attribute {key} of type {type(key)}")
387
+ return getattr(self, key)
356
388
 
357
389
  def __repr__(self) -> str:
358
390
  """Return a string representation of the question. Should be able to be used to reconstruct the question.
@@ -382,7 +414,9 @@ class QuestionBase(
382
414
  False
383
415
 
384
416
  """
385
- return hash(self) == hash(other)
417
+ if not isinstance(other, QuestionBase):
418
+ return False
419
+ return self.to_dict() == other.to_dict()
386
420
 
387
421
  def __sub__(self, other) -> BaseDiff:
388
422
  """Return the difference between two objects.
@@ -399,18 +433,35 @@ class QuestionBase(
399
433
  def __add__(self, other_question_or_diff):
400
434
  """
401
435
  Compose two questions into a single question.
436
+
437
+ TODO: Probably getting deprecated.
438
+
402
439
  """
403
440
  if isinstance(other_question_or_diff, BaseDiff) or isinstance(
404
441
  other_question_or_diff, BaseDiffCollection
405
442
  ):
406
443
  return other_question_or_diff.apply(self)
407
444
 
445
+ # from edsl.questions import compose_questions
446
+ # return compose_questions(self, other_question_or_diff)
447
+
448
+ # def _validate_response(self, response):
449
+ # """Validate the response from the LLM. Behavior depends on the question type."""
450
+ # if "answer" not in response:
451
+ # raise QuestionResponseValidationError(
452
+ # "Response from LLM does not have an answer"
453
+ # )
454
+ # return response
455
+
408
456
  def _translate_answer_code_to_answer(
409
457
  self, answer, scenario: Optional["Scenario"] = None
410
458
  ):
411
459
  """There is over-ridden by child classes that ask for codes."""
412
460
  return answer
413
461
 
462
+ # endregion
463
+
464
+ # region: Forward methods
414
465
  def add_question(self, other: QuestionBase) -> "Survey":
415
466
  """Add a question to this question by turning them into a survey with two questions.
416
467
 
@@ -420,7 +471,10 @@ class QuestionBase(
420
471
  >>> len(s.questions)
421
472
  2
422
473
  """
423
- return self.to_survey().add_question(other)
474
+ from edsl.surveys.Survey import Survey
475
+
476
+ s = Survey([self, other])
477
+ return s
424
478
 
425
479
  def to_survey(self) -> "Survey":
426
480
  """Turn a single question into a survey.
@@ -430,7 +484,8 @@ class QuestionBase(
430
484
  """
431
485
  from edsl.surveys.Survey import Survey
432
486
 
433
- return Survey([self])
487
+ s = Survey([self])
488
+ return s
434
489
 
435
490
  def by(self, *args) -> "Jobs":
436
491
  """Turn a single question into a survey and then a Job."""
@@ -439,6 +494,15 @@ class QuestionBase(
439
494
  s = Survey([self])
440
495
  return s.by(*args)
441
496
 
497
+ # endregion
498
+
499
+ # region: Display methods
500
+ def print(self):
501
+ from rich import print_json
502
+ import json
503
+
504
+ print_json(json.dumps(self.to_dict()))
505
+
442
506
  def human_readable(self) -> str:
443
507
  """Print the question in a human readable format.
444
508
 
@@ -465,15 +529,97 @@ class QuestionBase(
465
529
  width: Optional[int] = None,
466
530
  iframe=False,
467
531
  ):
468
- from edsl.questions.HTMLQuestion import HTMLQuestion
532
+ """Return the question in HTML format."""
533
+ from jinja2 import Template
534
+
535
+ if scenario is None:
536
+ scenario = {}
537
+
538
+ prior_answers_dict = {}
539
+
540
+ if isinstance(answers, dict):
541
+ for key, value in answers.items():
542
+ if not key.endswith("_comment") and not key.endswith(
543
+ "_generated_tokens"
544
+ ):
545
+ prior_answers_dict[key] = {"answer": value}
546
+
547
+ # breakpoint()
548
+
549
+ base_template = """
550
+ <div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
551
+ {% if include_question_name %}
552
+ <p>question_name: {{ question_name }}</p>
553
+ {% endif %}
554
+ <p class="question_text">{{ question_text }}</p>
555
+ {{ question_content }}
556
+ </div>
557
+ """
558
+ if not hasattr(self, "question_type"):
559
+ self.question_type = "unknown"
469
560
 
470
- return HTMLQuestion(self).html(
471
- scenario, agent, answers, include_question_name, height, width, iframe
472
- )
561
+ if hasattr(self, "question_html_content"):
562
+ question_content = self.question_html_content
563
+ else:
564
+ question_content = Template("")
565
+
566
+ base_template = Template(base_template)
567
+
568
+ context = {
569
+ "scenario": scenario,
570
+ "agent": agent,
571
+ } | prior_answers_dict
572
+
573
+ # Render the question text
574
+ try:
575
+ question_text = Template(self.question_text).render(context)
576
+ except Exception as e:
577
+ print(
578
+ f"Error rendering question: question_text = {self.question_text}, error = {e}"
579
+ )
580
+ question_text = self.question_text
581
+
582
+ try:
583
+ question_content = Template(question_content).render(context)
584
+ except Exception as e:
585
+ print(
586
+ f"Error rendering question: question_content = {question_content}, error = {e}"
587
+ )
588
+ question_content = question_content
589
+
590
+ try:
591
+ params = {
592
+ "question_name": self.question_name,
593
+ "question_text": question_text,
594
+ "question_type": self.question_type,
595
+ "question_content": question_content,
596
+ "include_question_name": include_question_name,
597
+ }
598
+ except Exception as e:
599
+ raise ValueError(
600
+ f"Error rendering question: params = {params}, error = {e}"
601
+ )
602
+ rendered_html = base_template.render(**params)
603
+
604
+ if iframe:
605
+ import html
606
+ from IPython.display import display, HTML
607
+
608
+ height = height or 200
609
+ width = width or 600
610
+ escaped_output = html.escape(rendered_html)
611
+ # escaped_output = rendered_html
612
+ iframe = f""""
613
+ <iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
614
+ """
615
+ display(HTML(iframe))
616
+ return None
617
+
618
+ return rendered_html
473
619
 
474
620
  @classmethod
475
621
  def example_model(cls):
476
- from edsl.language_models.model import Model
622
+ from edsl import Model
477
623
 
478
624
  q = cls.example()
479
625
  m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
@@ -1,16 +1,11 @@
1
1
  from __future__ import annotations
2
2
  import copy
3
3
  import itertools
4
- from typing import Optional, List, Callable, Type, TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from edsl.questions.QuestionBase import QuestionBase
8
- from edsl.scenarios.ScenarioList import ScenarioList
4
+ from typing import Optional, List, Callable, Type
5
+ from typing import TypeVar
9
6
 
10
7
 
11
8
  class QuestionBaseGenMixin:
12
- """Mixin for QuestionBase."""
13
-
14
9
  def copy(self) -> QuestionBase:
15
10
  """Return a deep copy of the question.
16
11
 
@@ -26,7 +21,7 @@ class QuestionBaseGenMixin:
26
21
  def option_permutations(self) -> list[QuestionBase]:
27
22
  """Return a list of questions with all possible permutations of the options.
28
23
 
29
- >>> from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice as Q
24
+ >>> from edsl import QuestionMultipleChoice as Q
30
25
  >>> len(Q.example().option_permutations())
31
26
  24
32
27
  """
@@ -44,60 +39,66 @@ class QuestionBaseGenMixin:
44
39
  questions.append(question)
45
40
  return questions
46
41
 
47
- def draw(self) -> "QuestionBase":
48
- """Return a new question with a randomly selected permutation of the options.
49
-
50
- If the question has no options, returns a copy of the original question.
51
-
52
- >>> from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice as Q
53
- >>> q = Q.example()
54
- >>> drawn = q.draw()
55
- >>> len(drawn.question_options) == len(q.question_options)
56
- True
57
- >>> q is drawn
58
- False
59
- """
60
-
61
- if not hasattr(self, "question_options"):
62
- return copy.deepcopy(self)
63
-
64
- import random
65
-
66
- question = copy.deepcopy(self)
67
- question.question_options = list(
68
- random.sample(self.question_options, len(self.question_options))
69
- )
70
- return question
71
-
72
42
  def loop(self, scenario_list: ScenarioList) -> List[QuestionBase]:
73
43
  """Return a list of questions with the question name modified for each scenario.
74
44
 
75
45
  :param scenario_list: The list of scenarios to loop through.
76
46
 
77
- >>> from edsl.questions.QuestionFreeText import QuestionFreeText
78
- >>> from edsl.scenarios.ScenarioList import ScenarioList
47
+ >>> from edsl import QuestionFreeText
48
+ >>> from edsl import ScenarioList
79
49
  >>> q = QuestionFreeText(question_text = "What are your thoughts on: {{ subject}}?", question_name = "base_{{subject}}")
80
50
  >>> len(q.loop(ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])))
81
51
  3
82
- """
83
- from edsl.questions.loop_processor import LoopProcessor
84
52
 
85
- lp = LoopProcessor(self)
86
- return lp.process_templates(scenario_list)
53
+ """
54
+ from jinja2 import Environment
55
+ from edsl.questions.QuestionBase import QuestionBase
87
56
 
88
- def render(self, replacement_dict: dict) -> "QuestionBase":
89
- """Render the question components as jinja2 templates with the replacement dictionary.
57
+ starting_name = self.question_name
58
+ questions = []
59
+ for index, scenario in enumerate(scenario_list):
60
+ env = Environment()
61
+ new_data = self.to_dict().copy()
62
+ for key, value in [(k, v) for k, v in new_data.items() if v is not None]:
63
+ if (
64
+ isinstance(value, str) or isinstance(value, int)
65
+ ) and key != "question_options":
66
+ new_data[key] = env.from_string(value).render(scenario)
67
+ elif isinstance(value, list):
68
+ new_data[key] = [
69
+ env.from_string(v).render(scenario) if isinstance(v, str) else v
70
+ for v in value
71
+ ]
72
+ elif isinstance(value, dict):
73
+ new_data[key] = {
74
+ (
75
+ env.from_string(k).render(scenario)
76
+ if isinstance(k, str)
77
+ else k
78
+ ): (
79
+ env.from_string(v).render(scenario)
80
+ if isinstance(v, str)
81
+ else v
82
+ )
83
+ for k, v in value.items()
84
+ }
85
+ elif key == "question_options" and isinstance(value, str):
86
+ new_data[key] = value
87
+ else:
88
+ raise ValueError(
89
+ f"Unexpected value type: {type(value)} for key '{key}'"
90
+ )
90
91
 
91
- :param replacement_dict: The dictionary of values to replace in the question components.
92
+ if new_data["question_name"] == starting_name:
93
+ new_data["question_name"] = new_data["question_name"] + f"_{index}"
92
94
 
93
- >>> from edsl.questions.QuestionFreeText import QuestionFreeText
94
- >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite {{ thing }}?")
95
- >>> q.render({"thing": "color"})
96
- Question('free_text', question_name = \"""color\""", question_text = \"""What is your favorite color?\""")
95
+ questions.append(QuestionBase.from_dict(new_data))
96
+ return questions
97
97
 
98
- """
98
+ def render(self, replacement_dict: dict) -> "QuestionBase":
99
+ """Render the question components as jinja2 templates with the replacement dictionary."""
99
100
  from jinja2 import Environment
100
- from edsl.scenarios.Scenario import Scenario
101
+ from edsl import Scenario
101
102
 
102
103
  strings_only_replacement_dict = {
103
104
  k: v for k, v in replacement_dict.items() if not isinstance(v, Scenario)
@@ -122,23 +123,15 @@ class QuestionBaseGenMixin:
122
123
 
123
124
  return self.apply_function(render_string)
124
125
 
125
- def apply_function(
126
- self, func: Callable, exclude_components: List[str] = None
127
- ) -> QuestionBase:
126
+ def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
128
127
  """Apply a function to the question parts
129
128
 
130
- :param func: The function to apply to the question parts.
131
- :param exclude_components: The components to exclude from the function application.
132
-
133
129
  >>> from edsl.questions import QuestionFreeText
134
130
  >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
135
131
  >>> shouting = lambda x: x.upper()
136
132
  >>> q.apply_function(shouting)
137
133
  Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
138
134
 
139
- >>> q.apply_function(shouting, exclude_components = ["question_type"])
140
- Question('free_text', question_name = \"""COLOR\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
141
-
142
135
  """
143
136
  from edsl.questions.QuestionBase import QuestionBase
144
137
 
@@ -1,6 +1,8 @@
1
1
  from importlib import resources
2
2
  from typing import Optional
3
+ from edsl.prompts import Prompt
3
4
  from edsl.exceptions.questions import QuestionAnswerValidationError
5
+
4
6
  from functools import lru_cache
5
7
 
6
8
 
@@ -69,7 +71,7 @@ class QuestionBasePromptsMixin:
69
71
  >>> q.get_instructions(model = "gpt3")
70
72
  Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
71
73
  """
72
- from edsl.language_models.model import Model
74
+ from edsl import Model
73
75
 
74
76
  if not hasattr(self, "_model_instructions"):
75
77
  self._model_instructions = {}
@@ -120,8 +122,6 @@ class QuestionBasePromptsMixin:
120
122
  template_text = template_manager.get_template(
121
123
  cls.question_type, "answering_instructions.jinja"
122
124
  )
123
- from edsl.prompts import Prompt
124
-
125
125
  return Prompt(text=template_text)
126
126
 
127
127
  @classmethod
@@ -129,8 +129,6 @@ class QuestionBasePromptsMixin:
129
129
  template_text = template_manager.get_template(
130
130
  cls.question_type, "question_presentation.jinja"
131
131
  )
132
- from edsl.prompts import Prompt
133
-
134
132
  return Prompt(text=template_text)
135
133
 
136
134
  @property
@@ -184,8 +182,6 @@ class QuestionBasePromptsMixin:
184
182
  @property
185
183
  def new_default_instructions(self) -> "Prompt":
186
184
  "This is set up as a property because there are mutable question values that determine how it is rendered."
187
- from edsl.prompts import Prompt
188
-
189
185
  return Prompt(self.question_presentation) + Prompt(self.answering_instructions)
190
186
 
191
187
  @property