edsl 0.1.39.dev1__py3-none-any.whl → 0.1.39.dev2__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 (194) hide show
  1. edsl/Base.py +169 -116
  2. edsl/__init__.py +14 -6
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +358 -146
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +88 -36
  7. edsl/agents/InvigilatorBase.py +59 -70
  8. edsl/agents/PromptConstructor.py +117 -219
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionOptionProcessor.py +172 -0
  11. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  12. edsl/agents/__init__.py +0 -1
  13. edsl/agents/prompt_helpers.py +3 -3
  14. edsl/config.py +22 -2
  15. edsl/conversation/car_buying.py +2 -1
  16. edsl/coop/CoopFunctionsMixin.py +15 -0
  17. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  18. edsl/coop/PriceFetcher.py +1 -1
  19. edsl/coop/coop.py +104 -42
  20. edsl/coop/utils.py +14 -14
  21. edsl/data/Cache.py +21 -14
  22. edsl/data/CacheEntry.py +12 -15
  23. edsl/data/CacheHandler.py +33 -12
  24. edsl/data/__init__.py +4 -3
  25. edsl/data_transfer_models.py +2 -1
  26. edsl/enums.py +20 -0
  27. edsl/exceptions/__init__.py +50 -50
  28. edsl/exceptions/agents.py +12 -0
  29. edsl/exceptions/inference_services.py +5 -0
  30. edsl/exceptions/questions.py +24 -6
  31. edsl/exceptions/scenarios.py +7 -0
  32. edsl/inference_services/AnthropicService.py +0 -3
  33. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  34. edsl/inference_services/AvailableModelFetcher.py +209 -0
  35. edsl/inference_services/AwsBedrock.py +0 -2
  36. edsl/inference_services/AzureAI.py +0 -2
  37. edsl/inference_services/GoogleService.py +2 -11
  38. edsl/inference_services/InferenceServiceABC.py +18 -85
  39. edsl/inference_services/InferenceServicesCollection.py +105 -80
  40. edsl/inference_services/MistralAIService.py +0 -3
  41. edsl/inference_services/OpenAIService.py +1 -4
  42. edsl/inference_services/PerplexityService.py +0 -3
  43. edsl/inference_services/ServiceAvailability.py +135 -0
  44. edsl/inference_services/TestService.py +11 -8
  45. edsl/inference_services/data_structures.py +62 -0
  46. edsl/jobs/AnswerQuestionFunctionConstructor.py +188 -0
  47. edsl/jobs/Answers.py +1 -14
  48. edsl/jobs/FetchInvigilator.py +40 -0
  49. edsl/jobs/InterviewTaskManager.py +98 -0
  50. edsl/jobs/InterviewsConstructor.py +48 -0
  51. edsl/jobs/Jobs.py +102 -243
  52. edsl/jobs/JobsChecks.py +35 -10
  53. edsl/jobs/JobsComponentConstructor.py +189 -0
  54. edsl/jobs/JobsPrompts.py +5 -3
  55. edsl/jobs/JobsRemoteInferenceHandler.py +128 -80
  56. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  57. edsl/jobs/RequestTokenEstimator.py +30 -0
  58. edsl/jobs/buckets/BucketCollection.py +44 -3
  59. edsl/jobs/buckets/TokenBucket.py +53 -21
  60. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  61. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  62. edsl/jobs/decorators.py +35 -0
  63. edsl/jobs/interviews/Interview.py +77 -380
  64. edsl/jobs/jobs_status_enums.py +9 -0
  65. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  66. edsl/jobs/runners/JobsRunnerAsyncio.py +4 -49
  67. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  68. edsl/jobs/tasks/TaskHistory.py +14 -15
  69. edsl/jobs/tasks/task_status_enum.py +0 -2
  70. edsl/language_models/ComputeCost.py +63 -0
  71. edsl/language_models/LanguageModel.py +137 -234
  72. edsl/language_models/ModelList.py +11 -13
  73. edsl/language_models/PriceManager.py +127 -0
  74. edsl/language_models/RawResponseHandler.py +106 -0
  75. edsl/language_models/ServiceDataSources.py +0 -0
  76. edsl/language_models/__init__.py +0 -1
  77. edsl/language_models/key_management/KeyLookup.py +63 -0
  78. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  79. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  80. edsl/language_models/key_management/__init__.py +0 -0
  81. edsl/language_models/key_management/models.py +131 -0
  82. edsl/language_models/registry.py +49 -59
  83. edsl/language_models/repair.py +2 -2
  84. edsl/language_models/utilities.py +5 -4
  85. edsl/notebooks/Notebook.py +19 -14
  86. edsl/notebooks/NotebookToLaTeX.py +142 -0
  87. edsl/prompts/Prompt.py +29 -39
  88. edsl/questions/AnswerValidatorMixin.py +47 -2
  89. edsl/questions/ExceptionExplainer.py +77 -0
  90. edsl/questions/HTMLQuestion.py +103 -0
  91. edsl/questions/LoopProcessor.py +149 -0
  92. edsl/questions/QuestionBase.py +37 -192
  93. edsl/questions/QuestionBaseGenMixin.py +52 -48
  94. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  95. edsl/questions/QuestionCheckBox.py +1 -1
  96. edsl/questions/QuestionExtract.py +1 -1
  97. edsl/questions/QuestionFreeText.py +1 -2
  98. edsl/questions/QuestionList.py +3 -5
  99. edsl/questions/QuestionMatrix.py +265 -0
  100. edsl/questions/QuestionMultipleChoice.py +66 -22
  101. edsl/questions/QuestionNumerical.py +1 -3
  102. edsl/questions/QuestionRank.py +6 -16
  103. edsl/questions/ResponseValidatorABC.py +37 -11
  104. edsl/questions/ResponseValidatorFactory.py +28 -0
  105. edsl/questions/SimpleAskMixin.py +4 -3
  106. edsl/questions/__init__.py +1 -0
  107. edsl/questions/derived/QuestionLinearScale.py +6 -3
  108. edsl/questions/derived/QuestionTopK.py +1 -1
  109. edsl/questions/descriptors.py +17 -3
  110. edsl/questions/question_registry.py +1 -1
  111. edsl/questions/templates/matrix/__init__.py +1 -0
  112. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  113. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  114. edsl/results/CSSParameterizer.py +1 -1
  115. edsl/results/Dataset.py +170 -7
  116. edsl/results/DatasetExportMixin.py +224 -302
  117. edsl/results/DatasetTree.py +28 -8
  118. edsl/results/MarkdownToDocx.py +122 -0
  119. edsl/results/MarkdownToPDF.py +111 -0
  120. edsl/results/Result.py +192 -206
  121. edsl/results/Results.py +120 -113
  122. edsl/results/ResultsExportMixin.py +2 -0
  123. edsl/results/Selector.py +23 -13
  124. edsl/results/TableDisplay.py +98 -171
  125. edsl/results/TextEditor.py +50 -0
  126. edsl/results/__init__.py +1 -1
  127. edsl/results/smart_objects.py +96 -0
  128. edsl/results/table_data_class.py +12 -0
  129. edsl/results/table_renderers.py +118 -0
  130. edsl/scenarios/ConstructDownloadLink.py +109 -0
  131. edsl/scenarios/DirectoryScanner.py +96 -0
  132. edsl/scenarios/DocumentChunker.py +102 -0
  133. edsl/scenarios/DocxScenario.py +16 -0
  134. edsl/scenarios/FileStore.py +118 -239
  135. edsl/scenarios/PdfExtractor.py +40 -0
  136. edsl/scenarios/Scenario.py +90 -193
  137. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  138. edsl/scenarios/ScenarioJoin.py +10 -6
  139. edsl/scenarios/ScenarioList.py +383 -240
  140. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  141. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  142. edsl/scenarios/ScenarioSelector.py +156 -0
  143. edsl/scenarios/__init__.py +1 -2
  144. edsl/scenarios/file_methods.py +85 -0
  145. edsl/scenarios/handlers/__init__.py +13 -0
  146. edsl/scenarios/handlers/csv.py +38 -0
  147. edsl/scenarios/handlers/docx.py +76 -0
  148. edsl/scenarios/handlers/html.py +37 -0
  149. edsl/scenarios/handlers/json.py +111 -0
  150. edsl/scenarios/handlers/latex.py +5 -0
  151. edsl/scenarios/handlers/md.py +51 -0
  152. edsl/scenarios/handlers/pdf.py +68 -0
  153. edsl/scenarios/handlers/png.py +39 -0
  154. edsl/scenarios/handlers/pptx.py +105 -0
  155. edsl/scenarios/handlers/py.py +294 -0
  156. edsl/scenarios/handlers/sql.py +313 -0
  157. edsl/scenarios/handlers/sqlite.py +149 -0
  158. edsl/scenarios/handlers/txt.py +33 -0
  159. edsl/study/ObjectEntry.py +1 -1
  160. edsl/study/SnapShot.py +1 -1
  161. edsl/study/Study.py +5 -12
  162. edsl/surveys/ConstructDAG.py +92 -0
  163. edsl/surveys/EditSurvey.py +221 -0
  164. edsl/surveys/InstructionHandler.py +100 -0
  165. edsl/surveys/MemoryManagement.py +72 -0
  166. edsl/surveys/Rule.py +5 -4
  167. edsl/surveys/RuleCollection.py +25 -27
  168. edsl/surveys/RuleManager.py +172 -0
  169. edsl/surveys/Simulator.py +75 -0
  170. edsl/surveys/Survey.py +199 -771
  171. edsl/surveys/SurveyCSS.py +20 -8
  172. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  173. edsl/surveys/SurveyToApp.py +141 -0
  174. edsl/surveys/__init__.py +4 -2
  175. edsl/surveys/descriptors.py +6 -2
  176. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  177. edsl/surveys/instructions/Instruction.py +4 -13
  178. edsl/surveys/instructions/InstructionCollection.py +11 -6
  179. edsl/templates/error_reporting/interview_details.html +1 -1
  180. edsl/templates/error_reporting/report.html +1 -1
  181. edsl/tools/plotting.py +1 -1
  182. edsl/utilities/PrettyList.py +56 -0
  183. edsl/utilities/is_notebook.py +18 -0
  184. edsl/utilities/is_valid_variable_name.py +11 -0
  185. edsl/utilities/remove_edsl_version.py +24 -0
  186. edsl/utilities/utilities.py +35 -23
  187. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/METADATA +12 -10
  188. edsl-0.1.39.dev2.dist-info/RECORD +352 -0
  189. edsl/language_models/KeyLookup.py +0 -30
  190. edsl/language_models/unused/ReplicateBase.py +0 -83
  191. edsl/results/ResultsDBMixin.py +0 -238
  192. edsl-0.1.39.dev1.dist-info/RECORD +0 -277
  193. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/LICENSE +0 -0
  194. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+ from typing import Union, Optional, Dict, List, Any
3
+
4
+ from pydantic import BaseModel, Field, field_validator
5
+ from jinja2 import Template
6
+ import random
7
+ from edsl.questions.QuestionBase import QuestionBase
8
+ from edsl.questions.descriptors import (
9
+ QuestionOptionsDescriptor,
10
+ OptionLabelDescriptor,
11
+ QuestionTextDescriptor,
12
+ )
13
+ from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
14
+ from edsl.questions.decorators import inject_exception
15
+ from edsl.exceptions.questions import (
16
+ QuestionAnswerValidationError,
17
+ QuestionCreationValidationError,
18
+ )
19
+
20
+
21
+ def create_matrix_response(
22
+ question_items: List[str],
23
+ question_options: List[Union[int, str, float]],
24
+ permissive: bool = False,
25
+ ):
26
+ """Create a response model for matrix questions.
27
+
28
+ The response model validates that:
29
+ 1. All question items are answered
30
+ 2. Each answer is from the allowed options
31
+ """
32
+
33
+ if permissive:
34
+
35
+ class MatrixResponse(BaseModel):
36
+ answer: Dict[str, Any]
37
+ comment: Optional[str] = None
38
+ generated_tokens: Optional[Any] = None
39
+
40
+ else:
41
+
42
+ class MatrixResponse(BaseModel):
43
+ answer: Dict[str, Union[int, str, float]] = Field(
44
+ ..., description="Mapping of items to selected options"
45
+ )
46
+ comment: Optional[str] = None
47
+ generated_tokens: Optional[Any] = None
48
+
49
+ @field_validator("answer")
50
+ def validate_answer(cls, v, values, **kwargs):
51
+ # Check that all items have responses
52
+ if not all(item in v for item in question_items):
53
+ missing = set(question_items) - set(v.keys())
54
+ raise ValueError(f"Missing responses for items: {missing}")
55
+
56
+ # Check that all responses are valid options
57
+ if not all(answer in question_options for answer in v.values()):
58
+ invalid = [ans for ans in v.values() if ans not in question_options]
59
+ raise ValueError(f"Invalid options selected: {invalid}")
60
+ return v
61
+
62
+ return MatrixResponse
63
+
64
+
65
+ class MatrixResponseValidator(ResponseValidatorABC):
66
+ required_params = ["question_items", "question_options", "permissive"]
67
+
68
+ valid_examples = [
69
+ (
70
+ {"answer": {"Item1": 1, "Item2": 2}},
71
+ {
72
+ "question_items": ["Item1", "Item2"],
73
+ "question_options": [1, 2, 3],
74
+ },
75
+ )
76
+ ]
77
+
78
+ invalid_examples = [
79
+ (
80
+ {"answer": {"Item1": 1}},
81
+ {
82
+ "question_items": ["Item1", "Item2"],
83
+ "question_options": [1, 2, 3],
84
+ },
85
+ "Missing responses for some items",
86
+ ),
87
+ (
88
+ {"answer": {"Item1": 4, "Item2": 5}},
89
+ {
90
+ "question_items": ["Item1", "Item2"],
91
+ "question_options": [1, 2, 3],
92
+ },
93
+ "Invalid options selected",
94
+ ),
95
+ ]
96
+
97
+ def fix(self, response, verbose=False):
98
+ if verbose:
99
+ print(f"Fixing matrix response: {response}")
100
+
101
+ # If we have generated tokens, try to parse them
102
+ if "generated_tokens" in response:
103
+ try:
104
+ import json
105
+
106
+ fixed = json.loads(response["generated_tokens"])
107
+ if isinstance(fixed, dict):
108
+ # Map numeric keys to question items
109
+ mapped_answer = {}
110
+ for idx, item in enumerate(self.question_items):
111
+ if str(idx) in fixed:
112
+ mapped_answer[item] = fixed[str(idx)]
113
+ if (
114
+ mapped_answer
115
+ ): # Only return if we successfully mapped some answers
116
+ return {"answer": mapped_answer}
117
+ except:
118
+ pass
119
+
120
+ # If answer uses numeric keys, map them to question items
121
+ if "answer" in response and isinstance(response["answer"], dict):
122
+ if all(str(key).isdigit() for key in response["answer"].keys()):
123
+ mapped_answer = {}
124
+ for idx, item in enumerate(self.question_items):
125
+ if str(idx) in response["answer"]:
126
+ mapped_answer[item] = response["answer"][str(idx)]
127
+ if mapped_answer: # Only update if we successfully mapped some answers
128
+ response["answer"] = mapped_answer
129
+
130
+ return response
131
+
132
+
133
+ class QuestionMatrix(QuestionBase):
134
+ """A question that presents a matrix/grid where multiple items are rated using the same scale."""
135
+
136
+ question_type = "matrix"
137
+ question_text: str = QuestionTextDescriptor()
138
+ question_items: List[str] = QuestionOptionsDescriptor()
139
+ question_options: List[Union[int, str, float]] = QuestionOptionsDescriptor()
140
+ option_labels: Optional[Dict[Union[int, str, float], str]] = OptionLabelDescriptor()
141
+
142
+ _response_model = None
143
+ response_validator_class = MatrixResponseValidator
144
+
145
+ def __init__(
146
+ self,
147
+ question_name: str,
148
+ question_text: str,
149
+ question_items: List[str],
150
+ question_options: List[Union[int, str, float]],
151
+ option_labels: Optional[Dict[Union[int, str, float], str]] = None,
152
+ include_comment: bool = True,
153
+ answering_instructions: Optional[str] = None,
154
+ question_presentation: Optional[str] = None,
155
+ permissive: bool = False,
156
+ ):
157
+ """Initialize a matrix question.
158
+
159
+ Args:
160
+ question_name: The name of the question
161
+ question_text: The text of the question
162
+ question_items: List of items to be rated
163
+ question_options: List of rating options
164
+ option_labels: Optional mapping of options to their labels
165
+ include_comment: Whether to include a comment field
166
+ answering_instructions: Optional custom instructions
167
+ question_presentation: Optional custom presentation
168
+ permissive: Whether to strictly validate responses
169
+ """
170
+ self.question_name = question_name
171
+
172
+ try:
173
+ self.question_text = question_text
174
+ except Exception as e:
175
+ raise QuestionCreationValidationError(
176
+ "question_text cannot be empty or too short!"
177
+ ) from e
178
+
179
+ self.question_items = question_items
180
+ self.question_options = question_options
181
+ self.option_labels = option_labels or {}
182
+
183
+ self.include_comment = include_comment
184
+ self.answering_instructions = answering_instructions
185
+ self.question_presentation = question_presentation
186
+ self.permissive = permissive
187
+
188
+ def create_response_model(self):
189
+ return create_matrix_response(
190
+ self.question_items, self.question_options, self.permissive
191
+ )
192
+
193
+ @property
194
+ def question_html_content(self) -> str:
195
+ """Generate HTML representation of the matrix question."""
196
+ template = Template(
197
+ """
198
+ <table class="matrix-question">
199
+ <tr>
200
+ <th></th>
201
+ {% for option in question_options %}
202
+ <th>
203
+ {{ option }}
204
+ {% if option in option_labels %}
205
+ <br>
206
+ <small>{{ option_labels[option] }}</small>
207
+ {% endif %}
208
+ </th>
209
+ {% endfor %}
210
+ </tr>
211
+ {% for item in question_items %}
212
+ <tr>
213
+ <td>{{ item }}</td>
214
+ {% for option in question_options %}
215
+ <td>
216
+ <input type="radio"
217
+ name="{{ question_name }}_{{ item }}"
218
+ value="{{ option }}"
219
+ id="{{ question_name }}_{{ item }}_{{ option }}">
220
+ </td>
221
+ {% endfor %}
222
+ </tr>
223
+ {% endfor %}
224
+ </table>
225
+ """
226
+ )
227
+
228
+ return template.render(
229
+ question_name=self.question_name,
230
+ question_items=self.question_items,
231
+ question_options=self.question_options,
232
+ option_labels=self.option_labels,
233
+ )
234
+
235
+ @classmethod
236
+ @inject_exception
237
+ def example(cls) -> QuestionMatrix:
238
+ """Return an example matrix question."""
239
+ return cls(
240
+ question_name="child_happiness",
241
+ question_text="How happy would you be with different numbers of children?",
242
+ question_items=[
243
+ "No children",
244
+ "1 child",
245
+ "2 children",
246
+ "3 or more children",
247
+ ],
248
+ question_options=[1, 2, 3, 4, 5],
249
+ option_labels={1: "Very sad", 3: "Neutral", 5: "Extremely happy"},
250
+ )
251
+
252
+ def _simulate_answer(self) -> dict:
253
+ """Simulate a random valid answer."""
254
+ return {
255
+ "answer": {
256
+ item: random.choice(self.question_options)
257
+ for item in self.question_items
258
+ }
259
+ }
260
+
261
+
262
+ if __name__ == "__main__":
263
+ import doctest
264
+
265
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -120,9 +120,9 @@ class QuestionMultipleChoice(QuestionBase):
120
120
 
121
121
  question_type = "multiple_choice"
122
122
  purpose = "When options are known and limited"
123
- question_options: Union[
124
- list[str], list[list], list[float], list[int]
125
- ] = QuestionOptionsDescriptor()
123
+ question_options: Union[list[str], list[list], list[float], list[int]] = (
124
+ QuestionOptionsDescriptor()
125
+ )
126
126
  _response_model = None
127
127
  response_validator_class = MultipleChoiceResponseValidator
128
128
 
@@ -175,8 +175,54 @@ class QuestionMultipleChoice(QuestionBase):
175
175
  else:
176
176
  return create_response_model(self.question_options, self.permissive)
177
177
 
178
+ @staticmethod
179
+ def _translate_question_options(
180
+ question_options, substitution_dict: dict
181
+ ) -> list[str]:
182
+
183
+ if isinstance(question_options, str):
184
+ # If dynamic options are provided like {{ options }}, render them with the scenario
185
+ # We can check if it's in the Scenario.
186
+ from jinja2 import Environment, meta
187
+
188
+ env = Environment()
189
+ parsed_content = env.parse(question_options)
190
+ template_variables = list(meta.find_undeclared_variables(parsed_content))
191
+ # print("The template variables are: ", template_variables)
192
+ question_option_key = template_variables[0]
193
+ # We need to deal with possibility it's actually an answer to a question.
194
+ potential_replacement = substitution_dict.get(question_option_key, None)
195
+
196
+ if isinstance(potential_replacement, list):
197
+ # translated_options = potential_replacement
198
+ return potential_replacement
199
+
200
+ if isinstance(potential_replacement, QuestionBase):
201
+ if hasattr(potential_replacement, "answer") and isinstance(
202
+ potential_replacement.answer, list
203
+ ):
204
+ return potential_replacement.answer
205
+ # translated_options = potential_replacement.answer
206
+
207
+ # if not isinstance(potential_replacement, list):
208
+ # translated_options = potential_replacement
209
+
210
+ if potential_replacement is None:
211
+ # Nope - maybe it's in the substition dict?
212
+ raise ValueError(
213
+ f"Could not find the key '{question_option_key}' in the scenario."
214
+ f"The substition dict was: '{substitution_dict}.'"
215
+ f"The question options were: '{question_options}'."
216
+ )
217
+ else:
218
+ translated_options = [
219
+ Template(str(option)).render(substitution_dict)
220
+ for option in question_options
221
+ ]
222
+ return translated_options
223
+
178
224
  def _translate_answer_code_to_answer(
179
- self, answer_code: int, scenario: Optional["Scenario"] = None
225
+ self, answer_code: int, replacements_dict: Optional[dict] = None
180
226
  ):
181
227
  """Translate the answer code to the actual answer.
182
228
 
@@ -192,26 +238,24 @@ class QuestionMultipleChoice(QuestionBase):
192
238
  'Happy'
193
239
 
194
240
  """
241
+ if replacements_dict is None:
242
+ replacements_dict = {}
243
+ translated_options = self._translate_question_options(
244
+ self.question_options, replacements_dict
245
+ )
195
246
 
196
- scenario = scenario or Scenario()
197
-
198
- if isinstance(self.question_options, str):
199
- # If dynamic options are provided like {{ options }}, render them with the scenario
200
- from jinja2 import Environment, meta
201
-
202
- env = Environment()
203
- parsed_content = env.parse(self.question_options)
204
- question_option_key = list(meta.find_undeclared_variables(parsed_content))[
205
- 0
206
- ]
207
- translated_options = scenario.get(question_option_key)
208
- else:
209
- translated_options = [
210
- Template(str(option)).render(scenario)
211
- for option in self.question_options
212
- ]
213
247
  if self._use_code:
214
- return translated_options[int(answer_code)]
248
+ try:
249
+ return translated_options[int(answer_code)]
250
+ except IndexError:
251
+ raise ValueError(
252
+ f"Answer code is out of range. The answer code index was: {int(answer_code)}. The options were: {translated_options}."
253
+ )
254
+ except TypeError:
255
+ raise ValueError(
256
+ f"The answer code was: '{answer_code}.'",
257
+ f"The options were: '{translated_options}'.",
258
+ )
215
259
  else:
216
260
  # return translated_options[answer_code]
217
261
  return answer_code
@@ -1,17 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- # from decimal import Decimal
4
3
  from random import uniform
5
4
  from typing import Any, Optional, Union, Literal
6
5
 
7
6
  from pydantic import BaseModel, Field, field_validator
8
7
 
9
- from edsl.exceptions import QuestionAnswerValidationError
8
+ from edsl.exceptions.questions import QuestionAnswerValidationError
10
9
  from edsl.questions.QuestionBase import QuestionBase
11
10
  from edsl.questions.descriptors import NumericalOrNoneDescriptor
12
11
  from edsl.questions.decorators import inject_exception
13
12
  from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
14
- from edsl.exceptions.questions import QuestionAnswerValidationError
15
13
 
16
14
 
17
15
  def create_numeric_response(
@@ -1,25 +1,14 @@
1
1
  from __future__ import annotations
2
- import random
3
- import textwrap
4
- from jinja2 import Template
5
- from typing import Any, Optional, Union
6
- from edsl.questions.QuestionBase import QuestionBase
7
- from edsl.exceptions import QuestionAnswerValidationError
2
+ from typing import Optional, Any, List, Annotated, Literal
3
+
4
+ from pydantic import BaseModel, Field
8
5
 
6
+ from edsl.questions.QuestionBase import QuestionBase
9
7
  from edsl.questions.descriptors import (
10
8
  QuestionOptionsDescriptor,
11
9
  NumSelectionsDescriptor,
12
10
  )
13
-
14
- from edsl.prompts import Prompt
15
-
16
- from pydantic import field_validator
17
11
  from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
18
- from edsl.questions.ResponseValidatorABC import BaseResponse
19
- from edsl.exceptions import QuestionAnswerValidationError
20
-
21
- from pydantic import BaseModel, Field, create_model
22
- from typing import Optional, Any, List, Annotated, Literal
23
12
 
24
13
 
25
14
  def create_response_model(
@@ -201,7 +190,8 @@ class QuestionRank(QuestionBase):
201
190
  self, answer_codes, scenario: Scenario = None
202
191
  ) -> list[str]:
203
192
  """Translate the answer code to the actual answer."""
204
- from edsl.scenarios import Scenario
193
+ from edsl.scenarios.Scenario import Scenario
194
+ from jinja2 import Template
205
195
 
206
196
  scenario = scenario or Scenario()
207
197
  translated_options = [
@@ -1,11 +1,11 @@
1
+ import logging
1
2
  from abc import ABC, abstractmethod
2
- from pydantic import BaseModel, Field, field_validator
3
-
4
- # from decimal import Decimal
5
3
  from typing import Optional, Any, List, TypedDict
6
4
 
7
- from edsl.exceptions import QuestionAnswerValidationError
8
- from pydantic import ValidationError
5
+ from pydantic import BaseModel, Field, field_validator, ValidationError
6
+
7
+ from edsl.exceptions.questions import QuestionAnswerValidationError
8
+ from edsl.questions.ExceptionExplainer import ExceptionExplainer
9
9
 
10
10
 
11
11
  class BaseResponse(BaseModel):
@@ -72,7 +72,8 @@ class ResponseValidatorABC(ABC):
72
72
  return self.override_answer if self.override_answer else data
73
73
 
74
74
  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.
75
+ """This is the main validation function. It takes the response_model and checks the data against it,
76
+ returning the instantiated model.
76
77
 
77
78
  >>> rv = ResponseValidatorABC.example("numerical")
78
79
  >>> rv._base_validate({"answer": 42})
@@ -81,7 +82,9 @@ class ResponseValidatorABC(ABC):
81
82
  try:
82
83
  return self.response_model(**data)
83
84
  except ValidationError as e:
84
- raise QuestionAnswerValidationError(e, data=data, model=self.response_model)
85
+ raise QuestionAnswerValidationError(
86
+ message=str(e), pydantic_error=e, data=data, model=self.response_model
87
+ )
85
88
 
86
89
  def post_validation_answer_convert(self, data):
87
90
  return data
@@ -128,10 +131,13 @@ class ResponseValidatorABC(ABC):
128
131
  edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
129
132
  return self._post_process(edsl_answer_dict)
130
133
  except QuestionAnswerValidationError as e:
131
- if verbose:
132
- print(f"Failed to validate {raw_edsl_answer_dict}; {str(e)}")
133
134
  return self._handle_exception(e, raw_edsl_answer_dict)
134
135
 
136
+ def human_explanation(self, e: QuestionAnswerValidationError):
137
+ explanation = ExceptionExplainer(e, model_response=e.data).explain()
138
+ return explanation
139
+ # return e
140
+
135
141
  def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
136
142
  if self.fixes_tried == 0:
137
143
  self.original_exception = e
@@ -140,12 +146,18 @@ class ResponseValidatorABC(ABC):
140
146
  self.fixes_tried += 1
141
147
  fixed_data = self.fix(raw_edsl_answer_dict)
142
148
  try:
143
- return self.validate(fixed_data, fix=True)
149
+ return self.validate(fixed_data, fix=True) # early return if validates
144
150
  except Exception as e:
145
151
  pass # we don't log failed fixes
146
152
 
153
+ # If the exception is already a QuestionAnswerValidationError, raise it
154
+ if isinstance(self.original_exception, QuestionAnswerValidationError):
155
+ raise self.original_exception
156
+
157
+ # If nothing worked, raise the original exception
147
158
  raise QuestionAnswerValidationError(
148
- self.original_exception,
159
+ message=self.original_exception,
160
+ pydantic_error=self.original_exception,
149
161
  data=raw_edsl_answer_dict,
150
162
  model=self.response_model,
151
163
  )
@@ -167,8 +179,22 @@ class ResponseValidatorABC(ABC):
167
179
  return q.response_validator
168
180
 
169
181
 
182
+ def main():
183
+ rv = ResponseValidatorABC.example()
184
+ print(rv.validate({"answer": 42}))
185
+
186
+
170
187
  # Example usage
171
188
  if __name__ == "__main__":
172
189
  import doctest
173
190
 
174
191
  doctest.testmod(optionflags=doctest.ELLIPSIS)
192
+
193
+ rv = ResponseValidatorABC.example()
194
+ # print(rv.validate({"answer": 42}))
195
+
196
+ rv = ResponseValidatorABC.example()
197
+ try:
198
+ rv.validate({"answer": "120"})
199
+ except QuestionAnswerValidationError as e:
200
+ print(rv.human_explanation(e))
@@ -0,0 +1,28 @@
1
+ class ResponseValidatorFactory:
2
+ def __init__(self, question):
3
+ self.question = question
4
+
5
+ @property
6
+ def response_model(self) -> type["BaseModel"]:
7
+ if self.question._response_model is not None:
8
+ return self.question._response_model
9
+ else:
10
+ return self.question.create_response_model()
11
+
12
+ @property
13
+ def response_validator(self) -> "ResponseValidatorBase":
14
+ """Return the response validator."""
15
+ params = (
16
+ {
17
+ "response_model": self.question.response_model,
18
+ }
19
+ | {k: getattr(self.question, k) for k in self.validator_parameters}
20
+ | {"exception_to_throw": getattr(self.question, "exception_to_throw", None)}
21
+ | {"override_answer": getattr(self.question, "override_answer", None)}
22
+ )
23
+ return self.question.response_validator_class(**params)
24
+
25
+ @property
26
+ def validator_parameters(self) -> list[str]:
27
+ """Return the parameters required for the response validator."""
28
+ return self.question.response_validator_class.required_params
@@ -1,5 +1,3 @@
1
- from rich.table import Table
2
- from rich.console import Console
3
1
  import math
4
2
 
5
3
 
@@ -10,6 +8,9 @@ def logprob_to_prob(logprob):
10
8
 
11
9
 
12
10
  def format_output(data):
11
+ from rich.table import Table
12
+ from rich.console import Console
13
+
13
14
  content = data["choices"][0]["logprobs"]["content"]
14
15
  table = Table(show_header=True, header_style="bold magenta")
15
16
 
@@ -65,7 +66,7 @@ class SimpleAskMixin:
65
66
  system_prompt="You are a helpful agent pretending to be a human. Do not break character",
66
67
  top_logprobs=4,
67
68
  ):
68
- from edsl import Model
69
+ from edsl.language_models.registry import Model
69
70
 
70
71
  if model is None:
71
72
  model = Model()
@@ -11,6 +11,7 @@ from edsl.questions.QuestionExtract import QuestionExtract
11
11
  from edsl.questions.QuestionFreeText import QuestionFreeText
12
12
  from edsl.questions.QuestionFunctional import QuestionFunctional
13
13
  from edsl.questions.QuestionList import QuestionList
14
+ from edsl.questions.QuestionMatrix import QuestionMatrix
14
15
  from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
15
16
  from edsl.questions.QuestionNumerical import QuestionNumerical
16
17
  from edsl.questions.QuestionBudget import QuestionBudget
@@ -40,9 +40,12 @@ class QuestionLinearScale(QuestionMultipleChoice):
40
40
  include_comment=include_comment,
41
41
  )
42
42
  self.question_options = question_options
43
- self.option_labels = (
44
- {int(k): v for k, v in option_labels.items()} if option_labels else {}
45
- )
43
+ if isinstance(option_labels, str):
44
+ self.option_labels = option_labels
45
+ else:
46
+ self.option_labels = (
47
+ {int(k): v for k, v in option_labels.items()} if option_labels else {}
48
+ )
46
49
  self.answering_instructions = answering_instructions
47
50
  self.question_presentation = question_presentation
48
51
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  from typing import Optional
3
3
 
4
- from edsl.exceptions import QuestionCreationValidationError
4
+ from edsl.exceptions.questions import QuestionCreationValidationError
5
5
  from edsl.questions.QuestionCheckBox import QuestionCheckBox
6
6
  from edsl.questions.decorators import inject_exception
7
7
 
@@ -3,7 +3,7 @@
3
3
  from abc import ABC, abstractmethod
4
4
  import re
5
5
  from typing import Any, Callable, List, Optional
6
- from edsl.exceptions import (
6
+ from edsl.exceptions.questions import (
7
7
  QuestionCreationValidationError,
8
8
  QuestionAnswerValidationError,
9
9
  )
@@ -181,11 +181,25 @@ class NumSelectionsDescriptor(BaseDescriptor):
181
181
 
182
182
 
183
183
  class OptionLabelDescriptor(BaseDescriptor):
184
- """Validate that the `option_label` attribute is a string."""
184
+ """Validate that the `option_label` attribute is a string.
185
+
186
+ >>> class TestQuestion:
187
+ ... option_label = OptionLabelDescriptor()
188
+ ... def __init__(self, option_label: str):
189
+ ... self.option_label = option_label
190
+
191
+ >>> _ = TestQuestion("{{Option}}")
192
+
193
+ """
185
194
 
186
195
  def validate(self, value, instance):
187
196
  """Validate the value is a string."""
188
- # key_values = [int(v) for v in value.keys()]
197
+ if isinstance(value, str):
198
+ if "{{" in value and "}}" in value:
199
+ # they're trying to use a dynamic question name - let's let this play out
200
+ return None
201
+
202
+ key_values = [int(v) for v in value.keys()]
189
203
 
190
204
  if value and (key_values := [float(v) for v in value.keys()]) != []:
191
205
  if min(key_values) != min(instance.question_options):
@@ -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
@@ -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 %}