edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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 +197 -116
  2. edsl/__init__.py +15 -7
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +351 -147
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +101 -50
  7. edsl/agents/InvigilatorBase.py +62 -70
  8. edsl/agents/PromptConstructor.py +143 -225
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  11. edsl/agents/__init__.py +0 -1
  12. edsl/agents/prompt_helpers.py +3 -3
  13. edsl/agents/question_option_processor.py +172 -0
  14. edsl/auto/AutoStudy.py +18 -5
  15. edsl/auto/StageBase.py +53 -40
  16. edsl/auto/StageQuestions.py +2 -1
  17. edsl/auto/utilities.py +0 -6
  18. edsl/config.py +22 -2
  19. edsl/conversation/car_buying.py +2 -1
  20. edsl/coop/CoopFunctionsMixin.py +15 -0
  21. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  22. edsl/coop/PriceFetcher.py +1 -1
  23. edsl/coop/coop.py +125 -47
  24. edsl/coop/utils.py +14 -14
  25. edsl/data/Cache.py +45 -27
  26. edsl/data/CacheEntry.py +12 -15
  27. edsl/data/CacheHandler.py +31 -12
  28. edsl/data/RemoteCacheSync.py +154 -46
  29. edsl/data/__init__.py +4 -3
  30. edsl/data_transfer_models.py +2 -1
  31. edsl/enums.py +27 -0
  32. edsl/exceptions/__init__.py +50 -50
  33. edsl/exceptions/agents.py +12 -0
  34. edsl/exceptions/inference_services.py +5 -0
  35. edsl/exceptions/questions.py +24 -6
  36. edsl/exceptions/scenarios.py +7 -0
  37. edsl/inference_services/AnthropicService.py +38 -19
  38. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  39. edsl/inference_services/AvailableModelFetcher.py +215 -0
  40. edsl/inference_services/AwsBedrock.py +0 -2
  41. edsl/inference_services/AzureAI.py +0 -2
  42. edsl/inference_services/GoogleService.py +7 -12
  43. edsl/inference_services/InferenceServiceABC.py +18 -85
  44. edsl/inference_services/InferenceServicesCollection.py +120 -79
  45. edsl/inference_services/MistralAIService.py +0 -3
  46. edsl/inference_services/OpenAIService.py +47 -35
  47. edsl/inference_services/PerplexityService.py +0 -3
  48. edsl/inference_services/ServiceAvailability.py +135 -0
  49. edsl/inference_services/TestService.py +11 -10
  50. edsl/inference_services/TogetherAIService.py +5 -3
  51. edsl/inference_services/data_structures.py +134 -0
  52. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  53. edsl/jobs/Answers.py +1 -14
  54. edsl/jobs/FetchInvigilator.py +47 -0
  55. edsl/jobs/InterviewTaskManager.py +98 -0
  56. edsl/jobs/InterviewsConstructor.py +50 -0
  57. edsl/jobs/Jobs.py +356 -431
  58. edsl/jobs/JobsChecks.py +35 -10
  59. edsl/jobs/JobsComponentConstructor.py +189 -0
  60. edsl/jobs/JobsPrompts.py +6 -4
  61. edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
  62. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  63. edsl/jobs/RequestTokenEstimator.py +30 -0
  64. edsl/jobs/async_interview_runner.py +138 -0
  65. edsl/jobs/buckets/BucketCollection.py +44 -3
  66. edsl/jobs/buckets/TokenBucket.py +53 -21
  67. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  68. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  69. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  70. edsl/jobs/data_structures.py +120 -0
  71. edsl/jobs/decorators.py +35 -0
  72. edsl/jobs/interviews/Interview.py +143 -408
  73. edsl/jobs/jobs_status_enums.py +9 -0
  74. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  75. edsl/jobs/results_exceptions_handler.py +98 -0
  76. edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
  77. edsl/jobs/runners/JobsRunnerStatus.py +133 -165
  78. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  79. edsl/jobs/tasks/TaskHistory.py +38 -18
  80. edsl/jobs/tasks/task_status_enum.py +0 -2
  81. edsl/language_models/ComputeCost.py +63 -0
  82. edsl/language_models/LanguageModel.py +194 -236
  83. edsl/language_models/ModelList.py +28 -19
  84. edsl/language_models/PriceManager.py +127 -0
  85. edsl/language_models/RawResponseHandler.py +106 -0
  86. edsl/language_models/ServiceDataSources.py +0 -0
  87. edsl/language_models/__init__.py +1 -2
  88. edsl/language_models/key_management/KeyLookup.py +63 -0
  89. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  90. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  91. edsl/language_models/key_management/__init__.py +0 -0
  92. edsl/language_models/key_management/models.py +131 -0
  93. edsl/language_models/model.py +256 -0
  94. edsl/language_models/repair.py +2 -2
  95. edsl/language_models/utilities.py +5 -4
  96. edsl/notebooks/Notebook.py +19 -14
  97. edsl/notebooks/NotebookToLaTeX.py +142 -0
  98. edsl/prompts/Prompt.py +29 -39
  99. edsl/questions/ExceptionExplainer.py +77 -0
  100. edsl/questions/HTMLQuestion.py +103 -0
  101. edsl/questions/QuestionBase.py +68 -214
  102. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  103. edsl/questions/QuestionBudget.py +1 -1
  104. edsl/questions/QuestionCheckBox.py +3 -3
  105. edsl/questions/QuestionExtract.py +5 -7
  106. edsl/questions/QuestionFreeText.py +2 -3
  107. edsl/questions/QuestionList.py +10 -18
  108. edsl/questions/QuestionMatrix.py +265 -0
  109. edsl/questions/QuestionMultipleChoice.py +67 -23
  110. edsl/questions/QuestionNumerical.py +2 -4
  111. edsl/questions/QuestionRank.py +7 -17
  112. edsl/questions/SimpleAskMixin.py +4 -3
  113. edsl/questions/__init__.py +2 -1
  114. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
  115. edsl/questions/data_structures.py +20 -0
  116. edsl/questions/derived/QuestionLinearScale.py +6 -3
  117. edsl/questions/derived/QuestionTopK.py +1 -1
  118. edsl/questions/descriptors.py +17 -3
  119. edsl/questions/loop_processor.py +149 -0
  120. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
  121. edsl/questions/question_registry.py +1 -1
  122. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
  123. edsl/questions/response_validator_factory.py +34 -0
  124. edsl/questions/templates/matrix/__init__.py +1 -0
  125. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  126. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  127. edsl/results/CSSParameterizer.py +1 -1
  128. edsl/results/Dataset.py +170 -7
  129. edsl/results/DatasetExportMixin.py +168 -305
  130. edsl/results/DatasetTree.py +28 -8
  131. edsl/results/MarkdownToDocx.py +122 -0
  132. edsl/results/MarkdownToPDF.py +111 -0
  133. edsl/results/Result.py +298 -206
  134. edsl/results/Results.py +149 -131
  135. edsl/results/ResultsExportMixin.py +2 -0
  136. edsl/results/TableDisplay.py +98 -171
  137. edsl/results/TextEditor.py +50 -0
  138. edsl/results/__init__.py +1 -1
  139. edsl/results/file_exports.py +252 -0
  140. edsl/results/{Selector.py → results_selector.py} +23 -13
  141. edsl/results/smart_objects.py +96 -0
  142. edsl/results/table_data_class.py +12 -0
  143. edsl/results/table_renderers.py +118 -0
  144. edsl/scenarios/ConstructDownloadLink.py +109 -0
  145. edsl/scenarios/DocumentChunker.py +102 -0
  146. edsl/scenarios/DocxScenario.py +16 -0
  147. edsl/scenarios/FileStore.py +150 -239
  148. edsl/scenarios/PdfExtractor.py +40 -0
  149. edsl/scenarios/Scenario.py +90 -193
  150. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  151. edsl/scenarios/ScenarioList.py +415 -244
  152. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  153. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  154. edsl/scenarios/__init__.py +1 -2
  155. edsl/scenarios/directory_scanner.py +96 -0
  156. edsl/scenarios/file_methods.py +85 -0
  157. edsl/scenarios/handlers/__init__.py +13 -0
  158. edsl/scenarios/handlers/csv.py +49 -0
  159. edsl/scenarios/handlers/docx.py +76 -0
  160. edsl/scenarios/handlers/html.py +37 -0
  161. edsl/scenarios/handlers/json.py +111 -0
  162. edsl/scenarios/handlers/latex.py +5 -0
  163. edsl/scenarios/handlers/md.py +51 -0
  164. edsl/scenarios/handlers/pdf.py +68 -0
  165. edsl/scenarios/handlers/png.py +39 -0
  166. edsl/scenarios/handlers/pptx.py +105 -0
  167. edsl/scenarios/handlers/py.py +294 -0
  168. edsl/scenarios/handlers/sql.py +313 -0
  169. edsl/scenarios/handlers/sqlite.py +149 -0
  170. edsl/scenarios/handlers/txt.py +33 -0
  171. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
  172. edsl/scenarios/scenario_selector.py +156 -0
  173. edsl/study/ObjectEntry.py +1 -1
  174. edsl/study/SnapShot.py +1 -1
  175. edsl/study/Study.py +5 -12
  176. edsl/surveys/ConstructDAG.py +92 -0
  177. edsl/surveys/EditSurvey.py +221 -0
  178. edsl/surveys/InstructionHandler.py +100 -0
  179. edsl/surveys/MemoryManagement.py +72 -0
  180. edsl/surveys/Rule.py +5 -4
  181. edsl/surveys/RuleCollection.py +25 -27
  182. edsl/surveys/RuleManager.py +172 -0
  183. edsl/surveys/Simulator.py +75 -0
  184. edsl/surveys/Survey.py +270 -791
  185. edsl/surveys/SurveyCSS.py +20 -8
  186. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  187. edsl/surveys/SurveyToApp.py +141 -0
  188. edsl/surveys/__init__.py +4 -2
  189. edsl/surveys/descriptors.py +6 -2
  190. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  191. edsl/surveys/instructions/Instruction.py +4 -13
  192. edsl/surveys/instructions/InstructionCollection.py +11 -6
  193. edsl/templates/error_reporting/interview_details.html +1 -1
  194. edsl/templates/error_reporting/report.html +1 -1
  195. edsl/tools/plotting.py +1 -1
  196. edsl/utilities/PrettyList.py +56 -0
  197. edsl/utilities/is_notebook.py +18 -0
  198. edsl/utilities/is_valid_variable_name.py +11 -0
  199. edsl/utilities/remove_edsl_version.py +24 -0
  200. edsl/utilities/utilities.py +35 -23
  201. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
  202. edsl-0.1.39.dist-info/RECORD +358 -0
  203. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
  204. edsl/language_models/KeyLookup.py +0 -30
  205. edsl/language_models/registry.py +0 -190
  206. edsl/language_models/unused/ReplicateBase.py +0 -83
  207. edsl/results/ResultsDBMixin.py +0 -238
  208. edsl-0.1.38.dev4.dist-info/RECORD +0 -277
  209. /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
  210. /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
  211. /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
  212. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
@@ -0,0 +1,149 @@
1
+ from typing import List, Any, Dict, Union
2
+ from jinja2 import Environment
3
+ from edsl.questions.QuestionBase import QuestionBase
4
+ from edsl import ScenarioList
5
+
6
+
7
+ class LoopProcessor:
8
+ def __init__(self, question: QuestionBase):
9
+ self.question = question
10
+ self.env = Environment()
11
+
12
+ def process_templates(self, scenario_list: ScenarioList) -> List[QuestionBase]:
13
+ """Process templates for each scenario and return list of modified questions.
14
+
15
+ Args:
16
+ scenario_list: List of scenarios to process templates against
17
+
18
+ Returns:
19
+ List of QuestionBase objects with rendered templates
20
+ """
21
+ questions = []
22
+ starting_name = self.question.question_name
23
+
24
+ for index, scenario in enumerate(scenario_list):
25
+ question_data = self.question.to_dict().copy()
26
+ processed_data = self._process_data(question_data, scenario)
27
+
28
+ if processed_data["question_name"] == starting_name:
29
+ processed_data["question_name"] += f"_{index}"
30
+
31
+ questions.append(QuestionBase.from_dict(processed_data))
32
+
33
+ return questions
34
+
35
+ def _process_data(
36
+ self, data: Dict[str, Any], scenario: Dict[str, Any]
37
+ ) -> Dict[str, Any]:
38
+ """Process all data fields according to their type.
39
+
40
+ Args:
41
+ data: Dictionary of question data
42
+ scenario: Current scenario to render templates against
43
+
44
+ Returns:
45
+ Processed dictionary with rendered templates
46
+ """
47
+ processed = {}
48
+
49
+ for key, value in [(k, v) for k, v in data.items() if v is not None]:
50
+ processed[key] = self._process_value(key, value, scenario)
51
+
52
+ return processed
53
+
54
+ def _process_value(self, key: str, value: Any, scenario: Dict[str, Any]) -> Any:
55
+ """Process a single value according to its type.
56
+
57
+ Args:
58
+ key: Dictionary key
59
+ value: Value to process
60
+ scenario: Current scenario
61
+
62
+ Returns:
63
+ Processed value
64
+ """
65
+ if key == "question_options" and isinstance(value, str):
66
+ return value
67
+
68
+ if key == "option_labels":
69
+ import json
70
+
71
+ return (
72
+ eval(self._render_template(value, scenario))
73
+ if isinstance(value, str)
74
+ else value
75
+ )
76
+
77
+ if isinstance(value, str):
78
+ return self._render_template(value, scenario)
79
+
80
+ if isinstance(value, list):
81
+ return self._process_list(value, scenario)
82
+
83
+ if isinstance(value, dict):
84
+ return self._process_dict(value, scenario)
85
+
86
+ if isinstance(value, (int, float)):
87
+ return value
88
+
89
+ raise ValueError(f"Unexpected value type: {type(value)} for key '{key}'")
90
+
91
+ def _render_template(self, template: str, scenario: Dict[str, Any]) -> str:
92
+ """Render a single template string.
93
+
94
+ Args:
95
+ template: Template string to render
96
+ scenario: Current scenario
97
+
98
+ Returns:
99
+ Rendered template string
100
+ """
101
+ return self.env.from_string(template).render(scenario)
102
+
103
+ def _process_list(self, items: List[Any], scenario: Dict[str, Any]) -> List[Any]:
104
+ """Process all items in a list.
105
+
106
+ Args:
107
+ items: List of items to process
108
+ scenario: Current scenario
109
+
110
+ Returns:
111
+ List of processed items
112
+ """
113
+ return [
114
+ self._render_template(item, scenario) if isinstance(item, str) else item
115
+ for item in items
116
+ ]
117
+
118
+ def _process_dict(
119
+ self, data: Dict[str, Any], scenario: Dict[str, Any]
120
+ ) -> Dict[str, Any]:
121
+ """Process all keys and values in a dictionary.
122
+
123
+ Args:
124
+ data: Dictionary to process
125
+ scenario: Current scenario
126
+
127
+ Returns:
128
+ Dictionary with processed keys and values
129
+ """
130
+ return {
131
+ (self._render_template(k, scenario) if isinstance(k, str) else k): (
132
+ self._render_template(v, scenario) if isinstance(v, str) else v
133
+ )
134
+ for k, v in data.items()
135
+ }
136
+
137
+
138
+ # Usage example:
139
+ """
140
+ from edsl import QuestionFreeText, ScenarioList
141
+
142
+ question = QuestionFreeText(
143
+ question_text="What are your thoughts on: {{subject}}?",
144
+ question_name="base_{{subject}}"
145
+ )
146
+ processor = TemplateProcessor(question)
147
+ scenarios = ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])
148
+ processed_questions = processor.process_templates(scenarios)
149
+ """
@@ -1,11 +1,16 @@
1
1
  from __future__ import annotations
2
2
  import copy
3
3
  import itertools
4
- from typing import Optional, List, Callable, Type
5
- from typing import TypeVar
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
6
9
 
7
10
 
8
11
  class QuestionBaseGenMixin:
12
+ """Mixin for QuestionBase."""
13
+
9
14
  def copy(self) -> QuestionBase:
10
15
  """Return a deep copy of the question.
11
16
 
@@ -21,7 +26,7 @@ class QuestionBaseGenMixin:
21
26
  def option_permutations(self) -> list[QuestionBase]:
22
27
  """Return a list of questions with all possible permutations of the options.
23
28
 
24
- >>> from edsl import QuestionMultipleChoice as Q
29
+ >>> from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice as Q
25
30
  >>> len(Q.example().option_permutations())
26
31
  24
27
32
  """
@@ -39,66 +44,60 @@ class QuestionBaseGenMixin:
39
44
  questions.append(question)
40
45
  return questions
41
46
 
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
+
42
72
  def loop(self, scenario_list: ScenarioList) -> List[QuestionBase]:
43
73
  """Return a list of questions with the question name modified for each scenario.
44
74
 
45
75
  :param scenario_list: The list of scenarios to loop through.
46
76
 
47
- >>> from edsl import QuestionFreeText
48
- >>> from edsl import ScenarioList
77
+ >>> from edsl.questions.QuestionFreeText import QuestionFreeText
78
+ >>> from edsl.scenarios.ScenarioList import ScenarioList
49
79
  >>> q = QuestionFreeText(question_text = "What are your thoughts on: {{ subject}}?", question_name = "base_{{subject}}")
50
80
  >>> len(q.loop(ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])))
51
81
  3
52
-
53
82
  """
54
- from jinja2 import Environment
55
- from edsl.questions.QuestionBase import QuestionBase
83
+ from edsl.questions.loop_processor import LoopProcessor
56
84
 
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
- )
85
+ lp = LoopProcessor(self)
86
+ return lp.process_templates(scenario_list)
91
87
 
92
- if new_data["question_name"] == starting_name:
93
- new_data["question_name"] = new_data["question_name"] + f"_{index}"
88
+ def render(self, replacement_dict: dict) -> "QuestionBase":
89
+ """Render the question components as jinja2 templates with the replacement dictionary.
94
90
 
95
- questions.append(QuestionBase.from_dict(new_data))
96
- return questions
91
+ :param replacement_dict: The dictionary of values to replace in the question components.
97
92
 
98
- def render(self, replacement_dict: dict) -> "QuestionBase":
99
- """Render the question components as jinja2 templates with the replacement dictionary."""
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?\""")
97
+
98
+ """
100
99
  from jinja2 import Environment
101
- from edsl import Scenario
100
+ from edsl.scenarios.Scenario import Scenario
102
101
 
103
102
  strings_only_replacement_dict = {
104
103
  k: v for k, v in replacement_dict.items() if not isinstance(v, Scenario)
@@ -123,15 +122,23 @@ class QuestionBaseGenMixin:
123
122
 
124
123
  return self.apply_function(render_string)
125
124
 
126
- def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
125
+ def apply_function(
126
+ self, func: Callable, exclude_components: List[str] = None
127
+ ) -> QuestionBase:
127
128
  """Apply a function to the question parts
128
129
 
130
+ :param func: The function to apply to the question parts.
131
+ :param exclude_components: The components to exclude from the function application.
132
+
129
133
  >>> from edsl.questions import QuestionFreeText
130
134
  >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
131
135
  >>> shouting = lambda x: x.upper()
132
136
  >>> q.apply_function(shouting)
133
137
  Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
134
138
 
139
+ >>> q.apply_function(shouting, exclude_components = ["question_type"])
140
+ Question('free_text', question_name = \"""COLOR\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
141
+
135
142
  """
136
143
  from edsl.questions.QuestionBase import QuestionBase
137
144
 
@@ -96,7 +96,7 @@ class Question(metaclass=Meta):
96
96
 
97
97
  >>> from edsl import Question
98
98
  >>> Question.list_question_types()
99
- ['checkbox', 'extract', 'free_text', 'functional', 'likert_five', 'linear_scale', 'list', 'multiple_choice', 'numerical', 'rank', 'top_k', 'yes_no']
99
+ ['checkbox', 'extract', 'free_text', 'functional', 'likert_five', 'linear_scale', 'list', 'matrix', 'multiple_choice', 'numerical', 'rank', 'top_k', 'yes_no']
100
100
  """
101
101
  return [
102
102
  q
@@ -1,23 +1,22 @@
1
1
  from abc import ABC, abstractmethod
2
- from pydantic import BaseModel, Field, field_validator
3
-
4
- # from decimal import Decimal
5
2
  from typing import Optional, Any, List, TypedDict
6
3
 
7
- from edsl.exceptions import QuestionAnswerValidationError
8
- from pydantic import ValidationError
4
+ from pydantic import BaseModel, Field, field_validator, ValidationError
9
5
 
6
+ from edsl.exceptions.questions import QuestionAnswerValidationError
7
+ from edsl.questions.ExceptionExplainer import ExceptionExplainer
10
8
 
11
- class BaseResponse(BaseModel):
12
- answer: Any
13
- comment: Optional[str] = None
14
- generated_tokens: Optional[str] = None
9
+ from edsl.questions.data_structures import (
10
+ RawEdslAnswerDict,
11
+ EdslAnswerDict,
12
+ )
15
13
 
16
14
 
17
15
  class ResponseValidatorABC(ABC):
18
16
  required_params: List[str] = []
19
17
 
20
18
  def __init_subclass__(cls, **kwargs):
19
+ """This is a metaclass that ensures that all subclasses of ResponseValidatorABC have the required class variables."""
21
20
  super().__init_subclass__(**kwargs)
22
21
  required_class_vars = ["required_params", "valid_examples", "invalid_examples"]
23
22
  for var in required_class_vars:
@@ -52,12 +51,7 @@ class ResponseValidatorABC(ABC):
52
51
  if not hasattr(self, "permissive"):
53
52
  self.permissive = False
54
53
 
55
- self.fixes_tried = 0
56
-
57
- class RawEdslAnswerDict(TypedDict):
58
- answer: Any
59
- comment: Optional[str]
60
- generated_tokens: Optional[str]
54
+ self.fixes_tried = 0 # how many times we've tried to fix the answer
61
55
 
62
56
  def _preprocess(self, data: RawEdslAnswerDict) -> RawEdslAnswerDict:
63
57
  """This is for testing purposes. A question can be given an exception to throw or an answer to always return.
@@ -72,7 +66,8 @@ class ResponseValidatorABC(ABC):
72
66
  return self.override_answer if self.override_answer else data
73
67
 
74
68
  def _base_validate(self, data: RawEdslAnswerDict) -> BaseModel:
75
- """This is the main validation function. It takes the response_model and checks the data against it, returning the instantiated model.
69
+ """This is the main validation function. It takes the response_model and checks the data against it,
70
+ returning the instantiated model.
76
71
 
77
72
  >>> rv = ResponseValidatorABC.example("numerical")
78
73
  >>> rv._base_validate({"answer": 42})
@@ -81,16 +76,13 @@ class ResponseValidatorABC(ABC):
81
76
  try:
82
77
  return self.response_model(**data)
83
78
  except ValidationError as e:
84
- raise QuestionAnswerValidationError(e, data=data, model=self.response_model)
79
+ raise QuestionAnswerValidationError(
80
+ message=str(e), pydantic_error=e, data=data, model=self.response_model
81
+ )
85
82
 
86
83
  def post_validation_answer_convert(self, data):
87
84
  return data
88
85
 
89
- class EdslAnswerDict(TypedDict):
90
- answer: Any
91
- comment: Optional[str]
92
- generated_tokens: Optional[str]
93
-
94
86
  def validate(
95
87
  self,
96
88
  raw_edsl_answer_dict: RawEdslAnswerDict,
@@ -128,10 +120,12 @@ class ResponseValidatorABC(ABC):
128
120
  edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
129
121
  return self._post_process(edsl_answer_dict)
130
122
  except QuestionAnswerValidationError as e:
131
- if verbose:
132
- print(f"Failed to validate {raw_edsl_answer_dict}; {str(e)}")
133
123
  return self._handle_exception(e, raw_edsl_answer_dict)
134
124
 
125
+ def human_explanation(self, e: QuestionAnswerValidationError):
126
+ explanation = ExceptionExplainer(e, model_response=e.data).explain()
127
+ return explanation
128
+
135
129
  def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
136
130
  if self.fixes_tried == 0:
137
131
  self.original_exception = e
@@ -140,12 +134,18 @@ class ResponseValidatorABC(ABC):
140
134
  self.fixes_tried += 1
141
135
  fixed_data = self.fix(raw_edsl_answer_dict)
142
136
  try:
143
- return self.validate(fixed_data, fix=True)
137
+ return self.validate(fixed_data, fix=True) # early return if validates
144
138
  except Exception as e:
145
139
  pass # we don't log failed fixes
146
140
 
141
+ # If the exception is already a QuestionAnswerValidationError, raise it
142
+ if isinstance(self.original_exception, QuestionAnswerValidationError):
143
+ raise self.original_exception
144
+
145
+ # If nothing worked, raise the original exception
147
146
  raise QuestionAnswerValidationError(
148
- self.original_exception,
147
+ message=self.original_exception,
148
+ pydantic_error=self.original_exception,
149
149
  data=raw_edsl_answer_dict,
150
150
  model=self.response_model,
151
151
  )
@@ -167,8 +167,22 @@ class ResponseValidatorABC(ABC):
167
167
  return q.response_validator
168
168
 
169
169
 
170
+ def main():
171
+ rv = ResponseValidatorABC.example()
172
+ print(rv.validate({"answer": 42}))
173
+
174
+
170
175
  # Example usage
171
176
  if __name__ == "__main__":
172
177
  import doctest
173
178
 
174
179
  doctest.testmod(optionflags=doctest.ELLIPSIS)
180
+
181
+ rv = ResponseValidatorABC.example()
182
+ # print(rv.validate({"answer": 42}))
183
+
184
+ rv = ResponseValidatorABC.example()
185
+ try:
186
+ rv.validate({"answer": "120"})
187
+ except QuestionAnswerValidationError as e:
188
+ print(rv.human_explanation(e))
@@ -0,0 +1,34 @@
1
+ from edsl.questions.data_structures import BaseModel
2
+ from edsl.questions.response_validator_abc import ResponseValidatorABC
3
+
4
+
5
+ class ResponseValidatorFactory:
6
+ """Factory class to create a response validator for a question."""
7
+
8
+ def __init__(self, question):
9
+ self.question = question
10
+
11
+ @property
12
+ def response_model(self) -> type["BaseModel"]:
13
+ if self.question._response_model is not None:
14
+ return self.question._response_model
15
+ else:
16
+ return self.question.create_response_model()
17
+
18
+ @property
19
+ def response_validator(self) -> "ResponseValidatorABC":
20
+ """Return the response validator."""
21
+ params = (
22
+ {
23
+ "response_model": self.question.response_model,
24
+ }
25
+ | {k: getattr(self.question, k) for k in self.validator_parameters}
26
+ | {"exception_to_throw": getattr(self.question, "exception_to_throw", None)}
27
+ | {"override_answer": getattr(self.question, "override_answer", None)}
28
+ )
29
+ return self.question.response_validator_class(**params)
30
+
31
+ @property
32
+ def validator_parameters(self) -> list[str]:
33
+ """Return the parameters required for the response validator."""
34
+ return self.question.response_validator_class.required_params
@@ -0,0 +1,5 @@
1
+ Please respond with a dictionary mapping row codes to column codes. E.g., {"0": 1, "1": 3}
2
+
3
+ {% if include_comment %}
4
+ After the answer, you can put a comment explaining your choices on the next line.
5
+ {% endif %}
@@ -0,0 +1,20 @@
1
+ {{question_text}}
2
+
3
+ Rows:
4
+ {% for item in question_items %}
5
+ {{ loop.index0 }}: {{item}}
6
+ {% endfor %}
7
+
8
+ Columns:
9
+ {% for option in question_options %}
10
+ {{ loop.index0 }}: {{option}}
11
+ {%- if option in option_labels %}
12
+ ({{option_labels[option]}})
13
+ {%- endif %}
14
+ {% endfor %}
15
+
16
+
17
+ Select one column option for each row.
18
+ {% if required %}
19
+ All rows require a response.
20
+ {% endif %}
@@ -67,7 +67,7 @@ class CSSParameterizer:
67
67
  missing_vars = self._validate_parameters(parameters)
68
68
 
69
69
  if missing_vars:
70
- print(f"Error: Missing required variables: {missing_vars}")
70
+ # print(f"Error: Missing required variables: {missing_vars}")
71
71
  return None
72
72
 
73
73
  # Format parameters with -- prefix if not present