edsl 0.1.36.dev5__py3-none-any.whl → 0.1.36.dev7__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 (257) hide show
  1. edsl/Base.py +303 -303
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +48 -47
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +804 -804
  7. edsl/agents/AgentList.py +337 -337
  8. edsl/agents/Invigilator.py +222 -222
  9. edsl/agents/InvigilatorBase.py +298 -294
  10. edsl/agents/PromptConstructor.py +320 -312
  11. edsl/agents/__init__.py +3 -3
  12. edsl/agents/descriptors.py +86 -86
  13. edsl/agents/prompt_helpers.py +129 -129
  14. edsl/auto/AutoStudy.py +117 -117
  15. edsl/auto/StageBase.py +230 -230
  16. edsl/auto/StageGenerateSurvey.py +178 -178
  17. edsl/auto/StageLabelQuestions.py +125 -125
  18. edsl/auto/StagePersona.py +61 -61
  19. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  20. edsl/auto/StagePersonaDimensionValues.py +74 -74
  21. edsl/auto/StagePersonaDimensions.py +69 -69
  22. edsl/auto/StageQuestions.py +73 -73
  23. edsl/auto/SurveyCreatorPipeline.py +21 -21
  24. edsl/auto/utilities.py +224 -224
  25. edsl/base/Base.py +289 -289
  26. edsl/config.py +149 -149
  27. edsl/conjure/AgentConstructionMixin.py +152 -152
  28. edsl/conjure/Conjure.py +62 -62
  29. edsl/conjure/InputData.py +659 -659
  30. edsl/conjure/InputDataCSV.py +48 -48
  31. edsl/conjure/InputDataMixinQuestionStats.py +182 -182
  32. edsl/conjure/InputDataPyRead.py +91 -91
  33. edsl/conjure/InputDataSPSS.py +8 -8
  34. edsl/conjure/InputDataStata.py +8 -8
  35. edsl/conjure/QuestionOptionMixin.py +76 -76
  36. edsl/conjure/QuestionTypeMixin.py +23 -23
  37. edsl/conjure/RawQuestion.py +65 -65
  38. edsl/conjure/SurveyResponses.py +7 -7
  39. edsl/conjure/__init__.py +9 -9
  40. edsl/conjure/naming_utilities.py +263 -263
  41. edsl/conjure/utilities.py +201 -201
  42. edsl/conversation/Conversation.py +238 -238
  43. edsl/conversation/car_buying.py +58 -58
  44. edsl/conversation/mug_negotiation.py +81 -81
  45. edsl/conversation/next_speaker_utilities.py +93 -93
  46. edsl/coop/PriceFetcher.py +54 -54
  47. edsl/coop/__init__.py +2 -2
  48. edsl/coop/coop.py +849 -849
  49. edsl/coop/utils.py +131 -131
  50. edsl/data/Cache.py +527 -527
  51. edsl/data/CacheEntry.py +228 -228
  52. edsl/data/CacheHandler.py +149 -149
  53. edsl/data/RemoteCacheSync.py +83 -83
  54. edsl/data/SQLiteDict.py +292 -292
  55. edsl/data/__init__.py +4 -4
  56. edsl/data/orm.py +10 -10
  57. edsl/data_transfer_models.py +73 -73
  58. edsl/enums.py +173 -173
  59. edsl/exceptions/__init__.py +50 -50
  60. edsl/exceptions/agents.py +40 -40
  61. edsl/exceptions/configuration.py +16 -16
  62. edsl/exceptions/coop.py +10 -10
  63. edsl/exceptions/data.py +14 -14
  64. edsl/exceptions/general.py +34 -34
  65. edsl/exceptions/jobs.py +33 -33
  66. edsl/exceptions/language_models.py +63 -63
  67. edsl/exceptions/prompts.py +15 -15
  68. edsl/exceptions/questions.py +91 -91
  69. edsl/exceptions/results.py +26 -26
  70. edsl/exceptions/surveys.py +34 -34
  71. edsl/inference_services/AnthropicService.py +87 -87
  72. edsl/inference_services/AwsBedrock.py +115 -115
  73. edsl/inference_services/AzureAI.py +217 -217
  74. edsl/inference_services/DeepInfraService.py +18 -18
  75. edsl/inference_services/GoogleService.py +156 -156
  76. edsl/inference_services/GroqService.py +20 -20
  77. edsl/inference_services/InferenceServiceABC.py +147 -147
  78. edsl/inference_services/InferenceServicesCollection.py +74 -68
  79. edsl/inference_services/MistralAIService.py +123 -123
  80. edsl/inference_services/OllamaService.py +18 -18
  81. edsl/inference_services/OpenAIService.py +224 -224
  82. edsl/inference_services/TestService.py +89 -89
  83. edsl/inference_services/TogetherAIService.py +170 -170
  84. edsl/inference_services/models_available_cache.py +118 -94
  85. edsl/inference_services/rate_limits_cache.py +25 -25
  86. edsl/inference_services/registry.py +39 -39
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/Answers.py +56 -56
  89. edsl/jobs/Jobs.py +1112 -1112
  90. edsl/jobs/__init__.py +1 -1
  91. edsl/jobs/buckets/BucketCollection.py +63 -63
  92. edsl/jobs/buckets/ModelBuckets.py +65 -65
  93. edsl/jobs/buckets/TokenBucket.py +248 -248
  94. edsl/jobs/interviews/Interview.py +661 -651
  95. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  96. edsl/jobs/interviews/InterviewExceptionEntry.py +189 -182
  97. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  98. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  99. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  100. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  101. edsl/jobs/interviews/ReportErrors.py +66 -66
  102. edsl/jobs/interviews/interview_status_enum.py +9 -9
  103. edsl/jobs/runners/JobsRunnerAsyncio.py +337 -337
  104. edsl/jobs/runners/JobsRunnerStatus.py +332 -332
  105. edsl/jobs/tasks/QuestionTaskCreator.py +242 -242
  106. edsl/jobs/tasks/TaskCreators.py +64 -64
  107. edsl/jobs/tasks/TaskHistory.py +441 -441
  108. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  109. edsl/jobs/tasks/task_status_enum.py +163 -163
  110. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  111. edsl/jobs/tokens/TokenUsage.py +34 -34
  112. edsl/language_models/LanguageModel.py +718 -718
  113. edsl/language_models/ModelList.py +102 -102
  114. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  115. edsl/language_models/__init__.py +2 -2
  116. edsl/language_models/fake_openai_call.py +15 -15
  117. edsl/language_models/fake_openai_service.py +61 -61
  118. edsl/language_models/registry.py +137 -137
  119. edsl/language_models/repair.py +156 -156
  120. edsl/language_models/unused/ReplicateBase.py +83 -83
  121. edsl/language_models/utilities.py +64 -64
  122. edsl/notebooks/Notebook.py +259 -259
  123. edsl/notebooks/__init__.py +1 -1
  124. edsl/prompts/Prompt.py +358 -358
  125. edsl/prompts/__init__.py +2 -2
  126. edsl/questions/AnswerValidatorMixin.py +289 -289
  127. edsl/questions/QuestionBase.py +616 -616
  128. edsl/questions/QuestionBaseGenMixin.py +161 -161
  129. edsl/questions/QuestionBasePromptsMixin.py +266 -266
  130. edsl/questions/QuestionBudget.py +227 -227
  131. edsl/questions/QuestionCheckBox.py +359 -359
  132. edsl/questions/QuestionExtract.py +183 -183
  133. edsl/questions/QuestionFreeText.py +113 -113
  134. edsl/questions/QuestionFunctional.py +159 -159
  135. edsl/questions/QuestionList.py +231 -231
  136. edsl/questions/QuestionMultipleChoice.py +286 -286
  137. edsl/questions/QuestionNumerical.py +153 -153
  138. edsl/questions/QuestionRank.py +324 -324
  139. edsl/questions/Quick.py +41 -41
  140. edsl/questions/RegisterQuestionsMeta.py +71 -71
  141. edsl/questions/ResponseValidatorABC.py +174 -174
  142. edsl/questions/SimpleAskMixin.py +73 -73
  143. edsl/questions/__init__.py +26 -26
  144. edsl/questions/compose_questions.py +98 -98
  145. edsl/questions/decorators.py +21 -21
  146. edsl/questions/derived/QuestionLikertFive.py +76 -76
  147. edsl/questions/derived/QuestionLinearScale.py +87 -87
  148. edsl/questions/derived/QuestionTopK.py +91 -91
  149. edsl/questions/derived/QuestionYesNo.py +82 -82
  150. edsl/questions/descriptors.py +418 -418
  151. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  152. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  153. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  154. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  155. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  156. edsl/questions/prompt_templates/question_list.jinja +17 -17
  157. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  158. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  159. edsl/questions/question_registry.py +147 -147
  160. edsl/questions/settings.py +12 -12
  161. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  162. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  163. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  164. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  165. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  166. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  167. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  168. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  169. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  170. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  171. edsl/questions/templates/list/question_presentation.jinja +5 -5
  172. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  173. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  174. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  175. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  176. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  177. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  178. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  179. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  180. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  181. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  182. edsl/results/Dataset.py +293 -293
  183. edsl/results/DatasetExportMixin.py +693 -693
  184. edsl/results/DatasetTree.py +145 -145
  185. edsl/results/Result.py +433 -433
  186. edsl/results/Results.py +1158 -1158
  187. edsl/results/ResultsDBMixin.py +238 -238
  188. edsl/results/ResultsExportMixin.py +43 -43
  189. edsl/results/ResultsFetchMixin.py +33 -33
  190. edsl/results/ResultsGGMixin.py +121 -121
  191. edsl/results/ResultsToolsMixin.py +98 -98
  192. edsl/results/Selector.py +118 -118
  193. edsl/results/__init__.py +2 -2
  194. edsl/results/tree_explore.py +115 -115
  195. edsl/scenarios/FileStore.py +458 -443
  196. edsl/scenarios/Scenario.py +510 -507
  197. edsl/scenarios/ScenarioHtmlMixin.py +59 -59
  198. edsl/scenarios/ScenarioList.py +1101 -1101
  199. edsl/scenarios/ScenarioListExportMixin.py +52 -52
  200. edsl/scenarios/ScenarioListPdfMixin.py +261 -261
  201. edsl/scenarios/__init__.py +4 -2
  202. edsl/shared.py +1 -1
  203. edsl/study/ObjectEntry.py +173 -173
  204. edsl/study/ProofOfWork.py +113 -113
  205. edsl/study/SnapShot.py +80 -80
  206. edsl/study/Study.py +528 -528
  207. edsl/study/__init__.py +4 -4
  208. edsl/surveys/DAG.py +148 -148
  209. edsl/surveys/Memory.py +31 -31
  210. edsl/surveys/MemoryPlan.py +244 -244
  211. edsl/surveys/Rule.py +324 -324
  212. edsl/surveys/RuleCollection.py +387 -387
  213. edsl/surveys/Survey.py +1772 -1772
  214. edsl/surveys/SurveyCSS.py +261 -261
  215. edsl/surveys/SurveyExportMixin.py +259 -259
  216. edsl/surveys/SurveyFlowVisualizationMixin.py +121 -121
  217. edsl/surveys/SurveyQualtricsImport.py +284 -284
  218. edsl/surveys/__init__.py +3 -3
  219. edsl/surveys/base.py +53 -53
  220. edsl/surveys/descriptors.py +56 -56
  221. edsl/surveys/instructions/ChangeInstruction.py +47 -47
  222. edsl/surveys/instructions/Instruction.py +51 -51
  223. edsl/surveys/instructions/InstructionCollection.py +77 -77
  224. edsl/templates/error_reporting/base.html +23 -23
  225. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  226. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  227. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  228. edsl/templates/error_reporting/interview_details.html +115 -115
  229. edsl/templates/error_reporting/interviews.html +9 -9
  230. edsl/templates/error_reporting/overview.html +4 -4
  231. edsl/templates/error_reporting/performance_plot.html +1 -1
  232. edsl/templates/error_reporting/report.css +73 -73
  233. edsl/templates/error_reporting/report.html +117 -117
  234. edsl/templates/error_reporting/report.js +25 -25
  235. edsl/tools/__init__.py +1 -1
  236. edsl/tools/clusters.py +192 -192
  237. edsl/tools/embeddings.py +27 -27
  238. edsl/tools/embeddings_plotting.py +118 -118
  239. edsl/tools/plotting.py +112 -112
  240. edsl/tools/summarize.py +18 -18
  241. edsl/utilities/SystemInfo.py +28 -28
  242. edsl/utilities/__init__.py +22 -22
  243. edsl/utilities/ast_utilities.py +25 -25
  244. edsl/utilities/data/Registry.py +6 -6
  245. edsl/utilities/data/__init__.py +1 -1
  246. edsl/utilities/data/scooter_results.json +1 -1
  247. edsl/utilities/decorators.py +77 -77
  248. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  249. edsl/utilities/interface.py +627 -627
  250. edsl/utilities/repair_functions.py +28 -28
  251. edsl/utilities/restricted_python.py +70 -70
  252. edsl/utilities/utilities.py +391 -391
  253. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev7.dist-info}/LICENSE +21 -21
  254. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev7.dist-info}/METADATA +1 -1
  255. edsl-0.1.36.dev7.dist-info/RECORD +279 -0
  256. edsl-0.1.36.dev5.dist-info/RECORD +0 -279
  257. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev7.dist-info}/WHEEL +0 -0
@@ -1,418 +1,418 @@
1
- """This module contains the descriptors used to validate the attributes of the question classes."""
2
-
3
- from abc import ABC, abstractmethod
4
- import re
5
- from typing import Any, Callable, List, Optional
6
- from edsl.exceptions import (
7
- QuestionCreationValidationError,
8
- QuestionAnswerValidationError,
9
- )
10
- from edsl.questions.settings import Settings
11
-
12
-
13
- ################################
14
- # Helper functions
15
- ################################
16
-
17
-
18
- def contains_single_braced_substring(s: str) -> bool:
19
- """Check if the string contains a substring in single braces."""
20
- pattern = r"(?<!\{)\{[^{}]+\}(?!\})"
21
- match = re.search(pattern, s)
22
- return bool(match)
23
-
24
-
25
- def is_number(value: Any) -> bool:
26
- """Check if an object is a number."""
27
- return isinstance(value, int) or isinstance(value, float)
28
-
29
-
30
- def is_number_or_none(value: Any) -> bool:
31
- """Check if an object is a number or None."""
32
- return value is None or is_number(value)
33
-
34
-
35
- ################################
36
- # Descriptor ABC
37
- ################################
38
-
39
-
40
- class BaseDescriptor(ABC):
41
- """ABC for something."""
42
-
43
- @abstractmethod
44
- def validate(self, value: Any) -> None:
45
- """Validate the value. If it is invalid, raises an exception. If it is valid, does nothing."""
46
- pass
47
-
48
- def __get__(self, instance, owner):
49
- """Get the value of the attribute."""
50
- if self.name not in instance.__dict__:
51
- return {}
52
- return instance.__dict__[self.name]
53
-
54
- def __set__(self, instance, value: Any) -> None:
55
- """Set the value of the attribute."""
56
- self.validate(value, instance)
57
- # from edsl.prompts.registry import get_classes
58
-
59
- instance.__dict__[self.name] = value
60
- # if self.name == "_instructions":
61
- # instructions = value
62
- # if value is not None:
63
- # instance.__dict__[self.name] = instructions
64
- # instance.set_instructions = True
65
- # else:
66
- # potential_prompt_classes = get_classes(
67
- # question_type=instance.question_type
68
- # )
69
- # if len(potential_prompt_classes) > 0:
70
- # instructions = potential_prompt_classes[0]().text
71
- # instance.__dict__[self.name] = instructions
72
- # instance.set_instructions = False
73
- # else:
74
- # if not hasattr(instance, "default_instructions"):
75
- # raise Exception(
76
- # "No default instructions found and no matching prompts!"
77
- # )
78
- # instructions = instance.default_instructions
79
- # instance.__dict__[self.name] = instructions
80
- # instance.set_instructions = False
81
-
82
- # instance.set_instructions = value != instance.default_instructions
83
-
84
- def __set_name__(self, owner, name: str) -> None:
85
- """Set the name of the attribute."""
86
- self.name = "_" + name
87
-
88
-
89
- ################################
90
- # General descriptors
91
- ################################
92
-
93
-
94
- class FunctionDescriptor(BaseDescriptor):
95
- """Validate that a value is a function."""
96
-
97
- def validate(self, value: Any, instance) -> Callable:
98
- """Validate the value is a function, and if so, returns it."""
99
- if not callable(value):
100
- raise QuestionCreationValidationError(
101
- f"Expected a function (got {value}).)"
102
- )
103
- return value
104
-
105
-
106
- class IntegerDescriptor(BaseDescriptor):
107
- """
108
- Validate that a value is an integer.
109
-
110
- - `none_allowed` is whether None is allowed as a value.
111
- """
112
-
113
- def __init__(self, none_allowed: bool = False):
114
- """Initialize the descriptor."""
115
- self.none_allowed = none_allowed
116
-
117
- def validate(self, value, instance):
118
- """Validate the value is an integer."""
119
- if self.none_allowed:
120
- if not (isinstance(value, int) or value is None):
121
- raise QuestionAnswerValidationError(
122
- f"Expected an integer or None (got {value})."
123
- )
124
- else:
125
- if not isinstance(value, int):
126
- raise QuestionAnswerValidationError(
127
- f"Expected an integer (got {value})."
128
- )
129
-
130
-
131
- class IntegerOrNoneDescriptor(BaseDescriptor):
132
- """Validate that a value is an integer or None."""
133
-
134
- def validate(self, value, instance):
135
- """Validate the value is an integer or None."""
136
- if not (isinstance(value, int) or value is None):
137
- raise QuestionCreationValidationError(
138
- f"Expected an integer or None (got {value})."
139
- )
140
-
141
-
142
- class NumericalOrNoneDescriptor(BaseDescriptor):
143
- """Validate that a value is a number or None."""
144
-
145
- def validate(self, value, instance):
146
- """Validate the value is a number or None."""
147
- if not is_number_or_none(value):
148
- raise QuestionAnswerValidationError(
149
- f"Expected a number or None (got {value})."
150
- )
151
-
152
-
153
- ################################
154
- # Attribute-specific descriptors
155
- ################################
156
-
157
-
158
- class AnswerTemplateDescriptor(BaseDescriptor):
159
- """Validate that the answer template is a dictionary with string keys and string values."""
160
-
161
- def validate(self, value: Any, instance) -> None:
162
- """Validate the answer template."""
163
- if not isinstance(value, dict):
164
- raise QuestionCreationValidationError(
165
- f"`answer_template` must be a dictionary (got {value}).)"
166
- )
167
- if not all(isinstance(x, str) for x in value.keys()):
168
- raise QuestionCreationValidationError(
169
- f"`answer_template` keys must be strings (got {value})."
170
- )
171
-
172
-
173
- class InstructionsDescriptor(BaseDescriptor):
174
- """Validate that the `instructions` attribute is a string."""
175
-
176
- def validate(self, value, instance):
177
- """Validate the value is a string."""
178
- # if not isinstance(value, str):
179
- # raise QuestionCreationValidationError(
180
- # f"Question `instructions` must be a string (got {value})."
181
- # )
182
- pass
183
-
184
-
185
- class NumSelectionsDescriptor(BaseDescriptor):
186
- """Validate that `num_selections` is an integer, is less than the number of options, and is positive."""
187
-
188
- def validate(self, value, instance):
189
- """Validate the value is an integer, is less than the number of options, and is positive."""
190
- if not (isinstance(value, int)):
191
- raise QuestionCreationValidationError(
192
- f"`num_selections` must be an integer (got {value})."
193
- )
194
- if value > len(instance.question_options):
195
- raise QuestionAnswerValidationError(
196
- f"`num_selections` must be less than the number of options (got {value})."
197
- )
198
- if value < 1:
199
- raise QuestionAnswerValidationError(
200
- f"`num_selections` must a positive integer (got {value})."
201
- )
202
-
203
-
204
- class OptionLabelDescriptor(BaseDescriptor):
205
- """Validate that the `option_label` attribute is a string."""
206
-
207
- def validate(self, value, instance):
208
- """Validate the value is a string."""
209
- # key_values = [int(v) for v in value.keys()]
210
-
211
- if value and (key_values := [float(v) for v in value.keys()]) != []:
212
- if min(key_values) != min(instance.question_options):
213
- raise QuestionCreationValidationError(
214
- f"First option needs a label (got {value})"
215
- )
216
- if max(key_values) != max(instance.question_options):
217
- raise QuestionCreationValidationError(
218
- f"Last option needs a label (got {value})"
219
- )
220
- if not all(isinstance(x, str) for x in value.values()):
221
- raise QuestionCreationValidationError(
222
- "Option labels must be strings (got {value})."
223
- )
224
- for key in key_values:
225
- if key not in instance.question_options:
226
- raise QuestionCreationValidationError(
227
- f"Option label key ({key}) is not in question options ({instance.question_options})."
228
- )
229
-
230
- if len(value.values()) != len(set(value.values())):
231
- raise QuestionCreationValidationError(
232
- f"Option labels must be unique (got {value})."
233
- )
234
-
235
-
236
- class QuestionNameDescriptor(BaseDescriptor):
237
- """Validate that the `question_name` attribute is a valid variable name."""
238
-
239
- def validate(self, value, instance):
240
- """Validate the value is a valid variable name."""
241
- from edsl.utilities.utilities import is_valid_variable_name
242
-
243
- if "{{" in value and "}}" in value:
244
- # they're trying to use a dynamic question name - let's let this play out
245
- return None
246
-
247
- if value.endswith("_comment") or value.endswith("_generated_tokens"):
248
- raise QuestionCreationValidationError(
249
- f"`question_name` cannot end with '_comment' or '_generated_tokens - (got {value})."
250
- )
251
-
252
- if not is_valid_variable_name(value):
253
- raise QuestionCreationValidationError(
254
- f"`question_name` is not a valid variable name (got {value})."
255
- )
256
-
257
-
258
- class QuestionOptionsDescriptor(BaseDescriptor):
259
- """Validate that `question_options` is a list, does not exceed the min/max lengths, and has unique items."""
260
-
261
- @classmethod
262
- def example(cls):
263
- class TestQuestion:
264
- question_options = QuestionOptionsDescriptor()
265
-
266
- def __init__(self, question_options: List[str]):
267
- self.question_options = question_options
268
-
269
- return TestQuestion
270
-
271
- def __init__(
272
- self,
273
- num_choices: int = None,
274
- linear_scale: bool = False,
275
- q_budget: bool = False,
276
- ):
277
- """Initialize the descriptor."""
278
- self.num_choices = num_choices
279
- self.linear_scale = linear_scale
280
- self.q_budget = q_budget
281
-
282
- def validate(self, value: Any, instance) -> None:
283
- """Validate the question options.
284
-
285
- >>> q_class = QuestionOptionsDescriptor.example()
286
- >>> _ = q_class(["a", "b", "c"])
287
- >>> _ = q_class(["a", "b", "c", "d", "d"])
288
- Traceback (most recent call last):
289
- ...
290
- edsl.exceptions.questions.QuestionCreationValidationError: Question options must be unique (got ['a', 'b', 'c', 'd', 'd']).
291
-
292
- We allow dynamic question options, which are strings of the form '{{ question_options }}'.
293
-
294
- >>> _ = q_class("{{dynamic_options}}")
295
- >>> _ = q_class("dynamic_options")
296
- Traceback (most recent call last):
297
- ...
298
- edsl.exceptions.questions.QuestionCreationValidationError: ...
299
- """
300
- if isinstance(value, str):
301
- # Check if the string is a dynamic question option
302
- if "{{" in value and "}}" in value:
303
- return None
304
- else:
305
- raise QuestionCreationValidationError(
306
- f"Dynamic question options must have jinja2 braces - instead received: {value}."
307
- )
308
- if not isinstance(value, list):
309
- raise QuestionCreationValidationError(
310
- f"Question options must be a list (got {value})."
311
- )
312
- if len(value) > Settings.MAX_NUM_OPTIONS:
313
- raise QuestionCreationValidationError(
314
- f"Too many question options (got {value})."
315
- )
316
- if len(value) < Settings.MIN_NUM_OPTIONS:
317
- raise QuestionCreationValidationError(
318
- f"Too few question options (got {value})."
319
- )
320
- # handle the case when question_options is a list of lists (a list of list can be converted to set)
321
- tmp_value = [str(x) for x in value]
322
- if len(tmp_value) != len(set(tmp_value)):
323
- raise QuestionCreationValidationError(
324
- f"Question options must be unique (got {value})."
325
- )
326
- if not self.linear_scale:
327
- if not self.q_budget:
328
- pass
329
- # if not (
330
- # value
331
- # and all(type(x) == type(value[0]) for x in value)
332
- # and isinstance(value[0], (str, list, int, float))
333
- # ):
334
- # raise QuestionCreationValidationError(
335
- # f"Question options must be all same type (got {value}).)"
336
- # )
337
- else:
338
- if not all(isinstance(x, (str)) for x in value):
339
- raise QuestionCreationValidationError(
340
- f"Question options must be strings (got {value}).)"
341
- )
342
- if not all(
343
- [
344
- type(option) != str
345
- or (len(option) >= 1 and len(option) < Settings.MAX_OPTION_LENGTH)
346
- for option in value
347
- ]
348
- ):
349
- raise QuestionCreationValidationError(
350
- f"All question options must be at least 1 character long but less than {Settings.MAX_OPTION_LENGTH} characters long (got {value})."
351
- )
352
-
353
- if hasattr(instance, "min_selections") and instance.min_selections != None:
354
- if instance.min_selections > len(value):
355
- raise QuestionCreationValidationError(
356
- f"You asked for at least {instance.min_selections} selections, but provided fewer options (got {value})."
357
- )
358
- if hasattr(instance, "max_selections") and instance.max_selections != None:
359
- if instance.max_selections > len(value):
360
- raise QuestionCreationValidationError(
361
- f"You asked for at most {instance.max_selections} selections, but provided fewer options (got {value})."
362
- )
363
- if self.num_choices is not None:
364
- if len(value) != self.num_choices:
365
- raise QuestionCreationValidationError(
366
- f"You asked for {self.num_choices} selections, but provided {len(value)} options."
367
- )
368
- if self.linear_scale:
369
- if sorted(value) != list(range(min(value), max(value) + 1)):
370
- raise QuestionCreationValidationError(
371
- f"LinearScale.question_options must be a list of successive integers, e.g. [1, 2, 3] (got {value})."
372
- )
373
-
374
-
375
- class QuestionTextDescriptor(BaseDescriptor):
376
- """Validate that the `question_text` attribute is a string.
377
-
378
-
379
- >>> class TestQuestion:
380
- ... question_text = QuestionTextDescriptor()
381
- ... def __init__(self, question_text: str):
382
- ... self.question_text = question_text
383
-
384
- >>> _ = TestQuestion("What is the capital of France?")
385
- >>> _ = TestQuestion("What is the capital of France? {{variable}}")
386
- >>> _ = TestQuestion("What is the capital of France? {{variable name}}")
387
- Traceback (most recent call last):
388
- ...
389
- edsl.exceptions.questions.QuestionCreationValidationError: Question text contains an invalid identifier: 'variable name'
390
- """
391
-
392
- def validate(self, value, instance):
393
- """Validate the value is a string."""
394
- # if len(value) > Settings.MAX_QUESTION_LENGTH:
395
- # raise Exception("Question is too long!")
396
- if len(value) < 1:
397
- raise Exception("Question is too short!")
398
- if not isinstance(value, str):
399
- raise Exception("Question must be a string!")
400
- if contains_single_braced_substring(value):
401
- import warnings
402
-
403
- warnings.warn(
404
- f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
405
- UserWarning,
406
- )
407
- # iterate through all doubles braces and check if they are valid python identifiers
408
- for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
409
- if " " in match.group(1).strip():
410
- raise QuestionCreationValidationError(
411
- f"Question text contains an invalid identifier: '{match.group(1)}'"
412
- )
413
-
414
-
415
- if __name__ == "__main__":
416
- import doctest
417
-
418
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ """This module contains the descriptors used to validate the attributes of the question classes."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ import re
5
+ from typing import Any, Callable, List, Optional
6
+ from edsl.exceptions import (
7
+ QuestionCreationValidationError,
8
+ QuestionAnswerValidationError,
9
+ )
10
+ from edsl.questions.settings import Settings
11
+
12
+
13
+ ################################
14
+ # Helper functions
15
+ ################################
16
+
17
+
18
+ def contains_single_braced_substring(s: str) -> bool:
19
+ """Check if the string contains a substring in single braces."""
20
+ pattern = r"(?<!\{)\{[^{}]+\}(?!\})"
21
+ match = re.search(pattern, s)
22
+ return bool(match)
23
+
24
+
25
+ def is_number(value: Any) -> bool:
26
+ """Check if an object is a number."""
27
+ return isinstance(value, int) or isinstance(value, float)
28
+
29
+
30
+ def is_number_or_none(value: Any) -> bool:
31
+ """Check if an object is a number or None."""
32
+ return value is None or is_number(value)
33
+
34
+
35
+ ################################
36
+ # Descriptor ABC
37
+ ################################
38
+
39
+
40
+ class BaseDescriptor(ABC):
41
+ """ABC for something."""
42
+
43
+ @abstractmethod
44
+ def validate(self, value: Any) -> None:
45
+ """Validate the value. If it is invalid, raises an exception. If it is valid, does nothing."""
46
+ pass
47
+
48
+ def __get__(self, instance, owner):
49
+ """Get the value of the attribute."""
50
+ if self.name not in instance.__dict__:
51
+ return {}
52
+ return instance.__dict__[self.name]
53
+
54
+ def __set__(self, instance, value: Any) -> None:
55
+ """Set the value of the attribute."""
56
+ self.validate(value, instance)
57
+ # from edsl.prompts.registry import get_classes
58
+
59
+ instance.__dict__[self.name] = value
60
+ # if self.name == "_instructions":
61
+ # instructions = value
62
+ # if value is not None:
63
+ # instance.__dict__[self.name] = instructions
64
+ # instance.set_instructions = True
65
+ # else:
66
+ # potential_prompt_classes = get_classes(
67
+ # question_type=instance.question_type
68
+ # )
69
+ # if len(potential_prompt_classes) > 0:
70
+ # instructions = potential_prompt_classes[0]().text
71
+ # instance.__dict__[self.name] = instructions
72
+ # instance.set_instructions = False
73
+ # else:
74
+ # if not hasattr(instance, "default_instructions"):
75
+ # raise Exception(
76
+ # "No default instructions found and no matching prompts!"
77
+ # )
78
+ # instructions = instance.default_instructions
79
+ # instance.__dict__[self.name] = instructions
80
+ # instance.set_instructions = False
81
+
82
+ # instance.set_instructions = value != instance.default_instructions
83
+
84
+ def __set_name__(self, owner, name: str) -> None:
85
+ """Set the name of the attribute."""
86
+ self.name = "_" + name
87
+
88
+
89
+ ################################
90
+ # General descriptors
91
+ ################################
92
+
93
+
94
+ class FunctionDescriptor(BaseDescriptor):
95
+ """Validate that a value is a function."""
96
+
97
+ def validate(self, value: Any, instance) -> Callable:
98
+ """Validate the value is a function, and if so, returns it."""
99
+ if not callable(value):
100
+ raise QuestionCreationValidationError(
101
+ f"Expected a function (got {value}).)"
102
+ )
103
+ return value
104
+
105
+
106
+ class IntegerDescriptor(BaseDescriptor):
107
+ """
108
+ Validate that a value is an integer.
109
+
110
+ - `none_allowed` is whether None is allowed as a value.
111
+ """
112
+
113
+ def __init__(self, none_allowed: bool = False):
114
+ """Initialize the descriptor."""
115
+ self.none_allowed = none_allowed
116
+
117
+ def validate(self, value, instance):
118
+ """Validate the value is an integer."""
119
+ if self.none_allowed:
120
+ if not (isinstance(value, int) or value is None):
121
+ raise QuestionAnswerValidationError(
122
+ f"Expected an integer or None (got {value})."
123
+ )
124
+ else:
125
+ if not isinstance(value, int):
126
+ raise QuestionAnswerValidationError(
127
+ f"Expected an integer (got {value})."
128
+ )
129
+
130
+
131
+ class IntegerOrNoneDescriptor(BaseDescriptor):
132
+ """Validate that a value is an integer or None."""
133
+
134
+ def validate(self, value, instance):
135
+ """Validate the value is an integer or None."""
136
+ if not (isinstance(value, int) or value is None):
137
+ raise QuestionCreationValidationError(
138
+ f"Expected an integer or None (got {value})."
139
+ )
140
+
141
+
142
+ class NumericalOrNoneDescriptor(BaseDescriptor):
143
+ """Validate that a value is a number or None."""
144
+
145
+ def validate(self, value, instance):
146
+ """Validate the value is a number or None."""
147
+ if not is_number_or_none(value):
148
+ raise QuestionAnswerValidationError(
149
+ f"Expected a number or None (got {value})."
150
+ )
151
+
152
+
153
+ ################################
154
+ # Attribute-specific descriptors
155
+ ################################
156
+
157
+
158
+ class AnswerTemplateDescriptor(BaseDescriptor):
159
+ """Validate that the answer template is a dictionary with string keys and string values."""
160
+
161
+ def validate(self, value: Any, instance) -> None:
162
+ """Validate the answer template."""
163
+ if not isinstance(value, dict):
164
+ raise QuestionCreationValidationError(
165
+ f"`answer_template` must be a dictionary (got {value}).)"
166
+ )
167
+ if not all(isinstance(x, str) for x in value.keys()):
168
+ raise QuestionCreationValidationError(
169
+ f"`answer_template` keys must be strings (got {value})."
170
+ )
171
+
172
+
173
+ class InstructionsDescriptor(BaseDescriptor):
174
+ """Validate that the `instructions` attribute is a string."""
175
+
176
+ def validate(self, value, instance):
177
+ """Validate the value is a string."""
178
+ # if not isinstance(value, str):
179
+ # raise QuestionCreationValidationError(
180
+ # f"Question `instructions` must be a string (got {value})."
181
+ # )
182
+ pass
183
+
184
+
185
+ class NumSelectionsDescriptor(BaseDescriptor):
186
+ """Validate that `num_selections` is an integer, is less than the number of options, and is positive."""
187
+
188
+ def validate(self, value, instance):
189
+ """Validate the value is an integer, is less than the number of options, and is positive."""
190
+ if not (isinstance(value, int)):
191
+ raise QuestionCreationValidationError(
192
+ f"`num_selections` must be an integer (got {value})."
193
+ )
194
+ if value > len(instance.question_options):
195
+ raise QuestionAnswerValidationError(
196
+ f"`num_selections` must be less than the number of options (got {value})."
197
+ )
198
+ if value < 1:
199
+ raise QuestionAnswerValidationError(
200
+ f"`num_selections` must a positive integer (got {value})."
201
+ )
202
+
203
+
204
+ class OptionLabelDescriptor(BaseDescriptor):
205
+ """Validate that the `option_label` attribute is a string."""
206
+
207
+ def validate(self, value, instance):
208
+ """Validate the value is a string."""
209
+ # key_values = [int(v) for v in value.keys()]
210
+
211
+ if value and (key_values := [float(v) for v in value.keys()]) != []:
212
+ if min(key_values) != min(instance.question_options):
213
+ raise QuestionCreationValidationError(
214
+ f"First option needs a label (got {value})"
215
+ )
216
+ if max(key_values) != max(instance.question_options):
217
+ raise QuestionCreationValidationError(
218
+ f"Last option needs a label (got {value})"
219
+ )
220
+ if not all(isinstance(x, str) for x in value.values()):
221
+ raise QuestionCreationValidationError(
222
+ "Option labels must be strings (got {value})."
223
+ )
224
+ for key in key_values:
225
+ if key not in instance.question_options:
226
+ raise QuestionCreationValidationError(
227
+ f"Option label key ({key}) is not in question options ({instance.question_options})."
228
+ )
229
+
230
+ if len(value.values()) != len(set(value.values())):
231
+ raise QuestionCreationValidationError(
232
+ f"Option labels must be unique (got {value})."
233
+ )
234
+
235
+
236
+ class QuestionNameDescriptor(BaseDescriptor):
237
+ """Validate that the `question_name` attribute is a valid variable name."""
238
+
239
+ def validate(self, value, instance):
240
+ """Validate the value is a valid variable name."""
241
+ from edsl.utilities.utilities import is_valid_variable_name
242
+
243
+ if "{{" in value and "}}" in value:
244
+ # they're trying to use a dynamic question name - let's let this play out
245
+ return None
246
+
247
+ if value.endswith("_comment") or value.endswith("_generated_tokens"):
248
+ raise QuestionCreationValidationError(
249
+ f"`question_name` cannot end with '_comment' or '_generated_tokens - (got {value})."
250
+ )
251
+
252
+ if not is_valid_variable_name(value):
253
+ raise QuestionCreationValidationError(
254
+ f"`question_name` is not a valid variable name (got {value})."
255
+ )
256
+
257
+
258
+ class QuestionOptionsDescriptor(BaseDescriptor):
259
+ """Validate that `question_options` is a list, does not exceed the min/max lengths, and has unique items."""
260
+
261
+ @classmethod
262
+ def example(cls):
263
+ class TestQuestion:
264
+ question_options = QuestionOptionsDescriptor()
265
+
266
+ def __init__(self, question_options: List[str]):
267
+ self.question_options = question_options
268
+
269
+ return TestQuestion
270
+
271
+ def __init__(
272
+ self,
273
+ num_choices: int = None,
274
+ linear_scale: bool = False,
275
+ q_budget: bool = False,
276
+ ):
277
+ """Initialize the descriptor."""
278
+ self.num_choices = num_choices
279
+ self.linear_scale = linear_scale
280
+ self.q_budget = q_budget
281
+
282
+ def validate(self, value: Any, instance) -> None:
283
+ """Validate the question options.
284
+
285
+ >>> q_class = QuestionOptionsDescriptor.example()
286
+ >>> _ = q_class(["a", "b", "c"])
287
+ >>> _ = q_class(["a", "b", "c", "d", "d"])
288
+ Traceback (most recent call last):
289
+ ...
290
+ edsl.exceptions.questions.QuestionCreationValidationError: Question options must be unique (got ['a', 'b', 'c', 'd', 'd']).
291
+
292
+ We allow dynamic question options, which are strings of the form '{{ question_options }}'.
293
+
294
+ >>> _ = q_class("{{dynamic_options}}")
295
+ >>> _ = q_class("dynamic_options")
296
+ Traceback (most recent call last):
297
+ ...
298
+ edsl.exceptions.questions.QuestionCreationValidationError: ...
299
+ """
300
+ if isinstance(value, str):
301
+ # Check if the string is a dynamic question option
302
+ if "{{" in value and "}}" in value:
303
+ return None
304
+ else:
305
+ raise QuestionCreationValidationError(
306
+ f"Dynamic question options must have jinja2 braces - instead received: {value}."
307
+ )
308
+ if not isinstance(value, list):
309
+ raise QuestionCreationValidationError(
310
+ f"Question options must be a list (got {value})."
311
+ )
312
+ if len(value) > Settings.MAX_NUM_OPTIONS:
313
+ raise QuestionCreationValidationError(
314
+ f"Too many question options (got {value})."
315
+ )
316
+ if len(value) < Settings.MIN_NUM_OPTIONS:
317
+ raise QuestionCreationValidationError(
318
+ f"Too few question options (got {value})."
319
+ )
320
+ # handle the case when question_options is a list of lists (a list of list can be converted to set)
321
+ tmp_value = [str(x) for x in value]
322
+ if len(tmp_value) != len(set(tmp_value)):
323
+ raise QuestionCreationValidationError(
324
+ f"Question options must be unique (got {value})."
325
+ )
326
+ if not self.linear_scale:
327
+ if not self.q_budget:
328
+ pass
329
+ # if not (
330
+ # value
331
+ # and all(type(x) == type(value[0]) for x in value)
332
+ # and isinstance(value[0], (str, list, int, float))
333
+ # ):
334
+ # raise QuestionCreationValidationError(
335
+ # f"Question options must be all same type (got {value}).)"
336
+ # )
337
+ else:
338
+ if not all(isinstance(x, (str)) for x in value):
339
+ raise QuestionCreationValidationError(
340
+ f"Question options must be strings (got {value}).)"
341
+ )
342
+ if not all(
343
+ [
344
+ type(option) != str
345
+ or (len(option) >= 1 and len(option) < Settings.MAX_OPTION_LENGTH)
346
+ for option in value
347
+ ]
348
+ ):
349
+ raise QuestionCreationValidationError(
350
+ f"All question options must be at least 1 character long but less than {Settings.MAX_OPTION_LENGTH} characters long (got {value})."
351
+ )
352
+
353
+ if hasattr(instance, "min_selections") and instance.min_selections != None:
354
+ if instance.min_selections > len(value):
355
+ raise QuestionCreationValidationError(
356
+ f"You asked for at least {instance.min_selections} selections, but provided fewer options (got {value})."
357
+ )
358
+ if hasattr(instance, "max_selections") and instance.max_selections != None:
359
+ if instance.max_selections > len(value):
360
+ raise QuestionCreationValidationError(
361
+ f"You asked for at most {instance.max_selections} selections, but provided fewer options (got {value})."
362
+ )
363
+ if self.num_choices is not None:
364
+ if len(value) != self.num_choices:
365
+ raise QuestionCreationValidationError(
366
+ f"You asked for {self.num_choices} selections, but provided {len(value)} options."
367
+ )
368
+ if self.linear_scale:
369
+ if sorted(value) != list(range(min(value), max(value) + 1)):
370
+ raise QuestionCreationValidationError(
371
+ f"LinearScale.question_options must be a list of successive integers, e.g. [1, 2, 3] (got {value})."
372
+ )
373
+
374
+
375
+ class QuestionTextDescriptor(BaseDescriptor):
376
+ """Validate that the `question_text` attribute is a string.
377
+
378
+
379
+ >>> class TestQuestion:
380
+ ... question_text = QuestionTextDescriptor()
381
+ ... def __init__(self, question_text: str):
382
+ ... self.question_text = question_text
383
+
384
+ >>> _ = TestQuestion("What is the capital of France?")
385
+ >>> _ = TestQuestion("What is the capital of France? {{variable}}")
386
+ >>> _ = TestQuestion("What is the capital of France? {{variable name}}")
387
+ Traceback (most recent call last):
388
+ ...
389
+ edsl.exceptions.questions.QuestionCreationValidationError: Question text contains an invalid identifier: 'variable name'
390
+ """
391
+
392
+ def validate(self, value, instance):
393
+ """Validate the value is a string."""
394
+ # if len(value) > Settings.MAX_QUESTION_LENGTH:
395
+ # raise Exception("Question is too long!")
396
+ if len(value) < 1:
397
+ raise Exception("Question is too short!")
398
+ if not isinstance(value, str):
399
+ raise Exception("Question must be a string!")
400
+ if contains_single_braced_substring(value):
401
+ import warnings
402
+
403
+ warnings.warn(
404
+ f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
405
+ UserWarning,
406
+ )
407
+ # iterate through all doubles braces and check if they are valid python identifiers
408
+ for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
409
+ if " " in match.group(1).strip():
410
+ raise QuestionCreationValidationError(
411
+ f"Question text contains an invalid identifier: '{match.group(1)}'"
412
+ )
413
+
414
+
415
+ if __name__ == "__main__":
416
+ import doctest
417
+
418
+ doctest.testmod(optionflags=doctest.ELLIPSIS)