edsl 0.1.39.dev3__py3-none-any.whl → 0.1.39.dev5__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 (341) hide show
  1. edsl/Base.py +413 -332
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +57 -49
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +1071 -867
  7. edsl/agents/AgentList.py +551 -413
  8. edsl/agents/Invigilator.py +284 -233
  9. edsl/agents/InvigilatorBase.py +257 -270
  10. edsl/agents/PromptConstructor.py +272 -354
  11. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  12. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  13. edsl/agents/__init__.py +2 -3
  14. edsl/agents/descriptors.py +99 -99
  15. edsl/agents/prompt_helpers.py +129 -129
  16. edsl/agents/question_option_processor.py +172 -0
  17. edsl/auto/AutoStudy.py +130 -117
  18. edsl/auto/StageBase.py +243 -230
  19. edsl/auto/StageGenerateSurvey.py +178 -178
  20. edsl/auto/StageLabelQuestions.py +125 -125
  21. edsl/auto/StagePersona.py +61 -61
  22. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  23. edsl/auto/StagePersonaDimensionValues.py +74 -74
  24. edsl/auto/StagePersonaDimensions.py +69 -69
  25. edsl/auto/StageQuestions.py +74 -73
  26. edsl/auto/SurveyCreatorPipeline.py +21 -21
  27. edsl/auto/utilities.py +218 -224
  28. edsl/base/Base.py +279 -279
  29. edsl/config.py +177 -157
  30. edsl/conversation/Conversation.py +290 -290
  31. edsl/conversation/car_buying.py +59 -58
  32. edsl/conversation/chips.py +95 -95
  33. edsl/conversation/mug_negotiation.py +81 -81
  34. edsl/conversation/next_speaker_utilities.py +93 -93
  35. edsl/coop/CoopFunctionsMixin.py +15 -0
  36. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  37. edsl/coop/PriceFetcher.py +54 -54
  38. edsl/coop/__init__.py +2 -2
  39. edsl/coop/coop.py +1106 -1028
  40. edsl/coop/utils.py +131 -131
  41. edsl/data/Cache.py +573 -555
  42. edsl/data/CacheEntry.py +230 -233
  43. edsl/data/CacheHandler.py +168 -149
  44. edsl/data/RemoteCacheSync.py +186 -78
  45. edsl/data/SQLiteDict.py +292 -292
  46. edsl/data/__init__.py +5 -4
  47. edsl/data/orm.py +10 -10
  48. edsl/data_transfer_models.py +74 -73
  49. edsl/enums.py +202 -175
  50. edsl/exceptions/BaseException.py +21 -21
  51. edsl/exceptions/__init__.py +54 -54
  52. edsl/exceptions/agents.py +54 -42
  53. edsl/exceptions/cache.py +5 -5
  54. edsl/exceptions/configuration.py +16 -16
  55. edsl/exceptions/coop.py +10 -10
  56. edsl/exceptions/data.py +14 -14
  57. edsl/exceptions/general.py +34 -34
  58. edsl/exceptions/inference_services.py +5 -0
  59. edsl/exceptions/jobs.py +33 -33
  60. edsl/exceptions/language_models.py +63 -63
  61. edsl/exceptions/prompts.py +15 -15
  62. edsl/exceptions/questions.py +109 -91
  63. edsl/exceptions/results.py +29 -29
  64. edsl/exceptions/scenarios.py +29 -22
  65. edsl/exceptions/surveys.py +37 -37
  66. edsl/inference_services/AnthropicService.py +106 -87
  67. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  68. edsl/inference_services/AvailableModelFetcher.py +215 -0
  69. edsl/inference_services/AwsBedrock.py +118 -120
  70. edsl/inference_services/AzureAI.py +215 -217
  71. edsl/inference_services/DeepInfraService.py +18 -18
  72. edsl/inference_services/GoogleService.py +143 -148
  73. edsl/inference_services/GroqService.py +20 -20
  74. edsl/inference_services/InferenceServiceABC.py +80 -147
  75. edsl/inference_services/InferenceServicesCollection.py +138 -97
  76. edsl/inference_services/MistralAIService.py +120 -123
  77. edsl/inference_services/OllamaService.py +18 -18
  78. edsl/inference_services/OpenAIService.py +236 -224
  79. edsl/inference_services/PerplexityService.py +160 -163
  80. edsl/inference_services/ServiceAvailability.py +135 -0
  81. edsl/inference_services/TestService.py +90 -89
  82. edsl/inference_services/TogetherAIService.py +172 -170
  83. edsl/inference_services/data_structures.py +134 -0
  84. edsl/inference_services/models_available_cache.py +118 -118
  85. edsl/inference_services/rate_limits_cache.py +25 -25
  86. edsl/inference_services/registry.py +41 -41
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  89. edsl/jobs/Answers.py +43 -56
  90. edsl/jobs/FetchInvigilator.py +47 -0
  91. edsl/jobs/InterviewTaskManager.py +98 -0
  92. edsl/jobs/InterviewsConstructor.py +50 -0
  93. edsl/jobs/Jobs.py +823 -898
  94. edsl/jobs/JobsChecks.py +172 -147
  95. edsl/jobs/JobsComponentConstructor.py +189 -0
  96. edsl/jobs/JobsPrompts.py +270 -268
  97. edsl/jobs/JobsRemoteInferenceHandler.py +311 -239
  98. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  99. edsl/jobs/RequestTokenEstimator.py +30 -0
  100. edsl/jobs/__init__.py +1 -1
  101. edsl/jobs/async_interview_runner.py +138 -0
  102. edsl/jobs/buckets/BucketCollection.py +104 -63
  103. edsl/jobs/buckets/ModelBuckets.py +65 -65
  104. edsl/jobs/buckets/TokenBucket.py +283 -251
  105. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  106. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  107. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  108. edsl/jobs/data_structures.py +120 -0
  109. edsl/jobs/decorators.py +35 -0
  110. edsl/jobs/interviews/Interview.py +396 -661
  111. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  112. edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
  113. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  114. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  115. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  116. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  117. edsl/jobs/interviews/ReportErrors.py +66 -66
  118. edsl/jobs/interviews/interview_status_enum.py +9 -9
  119. edsl/jobs/jobs_status_enums.py +9 -0
  120. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  121. edsl/jobs/results_exceptions_handler.py +98 -0
  122. edsl/jobs/runners/JobsRunnerAsyncio.py +151 -466
  123. edsl/jobs/runners/JobsRunnerStatus.py +297 -330
  124. edsl/jobs/tasks/QuestionTaskCreator.py +244 -242
  125. edsl/jobs/tasks/TaskCreators.py +64 -64
  126. edsl/jobs/tasks/TaskHistory.py +470 -450
  127. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  128. edsl/jobs/tasks/task_status_enum.py +161 -163
  129. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  130. edsl/jobs/tokens/TokenUsage.py +34 -34
  131. edsl/language_models/ComputeCost.py +63 -0
  132. edsl/language_models/LanguageModel.py +626 -668
  133. edsl/language_models/ModelList.py +164 -155
  134. edsl/language_models/PriceManager.py +127 -0
  135. edsl/language_models/RawResponseHandler.py +106 -0
  136. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  137. edsl/language_models/ServiceDataSources.py +0 -0
  138. edsl/language_models/__init__.py +2 -3
  139. edsl/language_models/fake_openai_call.py +15 -15
  140. edsl/language_models/fake_openai_service.py +61 -61
  141. edsl/language_models/key_management/KeyLookup.py +63 -0
  142. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  143. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  144. edsl/language_models/key_management/__init__.py +0 -0
  145. edsl/language_models/key_management/models.py +131 -0
  146. edsl/language_models/model.py +256 -0
  147. edsl/language_models/repair.py +156 -156
  148. edsl/language_models/utilities.py +65 -64
  149. edsl/notebooks/Notebook.py +263 -258
  150. edsl/notebooks/NotebookToLaTeX.py +142 -0
  151. edsl/notebooks/__init__.py +1 -1
  152. edsl/prompts/Prompt.py +352 -362
  153. edsl/prompts/__init__.py +2 -2
  154. edsl/questions/ExceptionExplainer.py +77 -0
  155. edsl/questions/HTMLQuestion.py +103 -0
  156. edsl/questions/QuestionBase.py +518 -664
  157. edsl/questions/QuestionBasePromptsMixin.py +221 -217
  158. edsl/questions/QuestionBudget.py +227 -227
  159. edsl/questions/QuestionCheckBox.py +359 -359
  160. edsl/questions/QuestionExtract.py +180 -182
  161. edsl/questions/QuestionFreeText.py +113 -114
  162. edsl/questions/QuestionFunctional.py +166 -166
  163. edsl/questions/QuestionList.py +223 -231
  164. edsl/questions/QuestionMatrix.py +265 -0
  165. edsl/questions/QuestionMultipleChoice.py +330 -286
  166. edsl/questions/QuestionNumerical.py +151 -153
  167. edsl/questions/QuestionRank.py +314 -324
  168. edsl/questions/Quick.py +41 -41
  169. edsl/questions/SimpleAskMixin.py +74 -73
  170. edsl/questions/__init__.py +27 -26
  171. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +334 -289
  172. edsl/questions/compose_questions.py +98 -98
  173. edsl/questions/data_structures.py +20 -0
  174. edsl/questions/decorators.py +21 -21
  175. edsl/questions/derived/QuestionLikertFive.py +76 -76
  176. edsl/questions/derived/QuestionLinearScale.py +90 -87
  177. edsl/questions/derived/QuestionTopK.py +93 -93
  178. edsl/questions/derived/QuestionYesNo.py +82 -82
  179. edsl/questions/descriptors.py +427 -413
  180. edsl/questions/loop_processor.py +149 -0
  181. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  182. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  183. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  184. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  185. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  186. edsl/questions/prompt_templates/question_list.jinja +17 -17
  187. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  188. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  189. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +168 -161
  190. edsl/questions/question_registry.py +177 -177
  191. edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +71 -71
  192. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +188 -174
  193. edsl/questions/response_validator_factory.py +34 -0
  194. edsl/questions/settings.py +12 -12
  195. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  196. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  197. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  198. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  199. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  200. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  201. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  202. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  203. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  204. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  205. edsl/questions/templates/list/question_presentation.jinja +5 -5
  206. edsl/questions/templates/matrix/__init__.py +1 -0
  207. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  208. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  209. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  210. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  211. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  212. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  213. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  214. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  215. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  216. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  217. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  218. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  219. edsl/results/CSSParameterizer.py +108 -108
  220. edsl/results/Dataset.py +587 -424
  221. edsl/results/DatasetExportMixin.py +594 -731
  222. edsl/results/DatasetTree.py +295 -275
  223. edsl/results/MarkdownToDocx.py +122 -0
  224. edsl/results/MarkdownToPDF.py +111 -0
  225. edsl/results/Result.py +557 -465
  226. edsl/results/Results.py +1183 -1165
  227. edsl/results/ResultsExportMixin.py +45 -43
  228. edsl/results/ResultsGGMixin.py +121 -121
  229. edsl/results/TableDisplay.py +125 -198
  230. edsl/results/TextEditor.py +50 -0
  231. edsl/results/__init__.py +2 -2
  232. edsl/results/file_exports.py +252 -0
  233. edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +33 -33
  234. edsl/results/{Selector.py → results_selector.py} +145 -135
  235. edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +98 -98
  236. edsl/results/smart_objects.py +96 -0
  237. edsl/results/table_data_class.py +12 -0
  238. edsl/results/table_display.css +77 -77
  239. edsl/results/table_renderers.py +118 -0
  240. edsl/results/tree_explore.py +115 -115
  241. edsl/scenarios/ConstructDownloadLink.py +109 -0
  242. edsl/scenarios/DocumentChunker.py +102 -0
  243. edsl/scenarios/DocxScenario.py +16 -0
  244. edsl/scenarios/FileStore.py +511 -632
  245. edsl/scenarios/PdfExtractor.py +40 -0
  246. edsl/scenarios/Scenario.py +498 -601
  247. edsl/scenarios/ScenarioHtmlMixin.py +65 -64
  248. edsl/scenarios/ScenarioList.py +1458 -1287
  249. edsl/scenarios/ScenarioListExportMixin.py +45 -52
  250. edsl/scenarios/ScenarioListPdfMixin.py +239 -261
  251. edsl/scenarios/__init__.py +3 -4
  252. edsl/scenarios/directory_scanner.py +96 -0
  253. edsl/scenarios/file_methods.py +85 -0
  254. edsl/scenarios/handlers/__init__.py +13 -0
  255. edsl/scenarios/handlers/csv.py +38 -0
  256. edsl/scenarios/handlers/docx.py +76 -0
  257. edsl/scenarios/handlers/html.py +37 -0
  258. edsl/scenarios/handlers/json.py +111 -0
  259. edsl/scenarios/handlers/latex.py +5 -0
  260. edsl/scenarios/handlers/md.py +51 -0
  261. edsl/scenarios/handlers/pdf.py +68 -0
  262. edsl/scenarios/handlers/png.py +39 -0
  263. edsl/scenarios/handlers/pptx.py +105 -0
  264. edsl/scenarios/handlers/py.py +294 -0
  265. edsl/scenarios/handlers/sql.py +313 -0
  266. edsl/scenarios/handlers/sqlite.py +149 -0
  267. edsl/scenarios/handlers/txt.py +33 -0
  268. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +131 -127
  269. edsl/scenarios/scenario_selector.py +156 -0
  270. edsl/shared.py +1 -1
  271. edsl/study/ObjectEntry.py +173 -173
  272. edsl/study/ProofOfWork.py +113 -113
  273. edsl/study/SnapShot.py +80 -80
  274. edsl/study/Study.py +521 -528
  275. edsl/study/__init__.py +4 -4
  276. edsl/surveys/ConstructDAG.py +92 -0
  277. edsl/surveys/DAG.py +148 -148
  278. edsl/surveys/EditSurvey.py +221 -0
  279. edsl/surveys/InstructionHandler.py +100 -0
  280. edsl/surveys/Memory.py +31 -31
  281. edsl/surveys/MemoryManagement.py +72 -0
  282. edsl/surveys/MemoryPlan.py +244 -244
  283. edsl/surveys/Rule.py +327 -326
  284. edsl/surveys/RuleCollection.py +385 -387
  285. edsl/surveys/RuleManager.py +172 -0
  286. edsl/surveys/Simulator.py +75 -0
  287. edsl/surveys/Survey.py +1280 -1801
  288. edsl/surveys/SurveyCSS.py +273 -261
  289. edsl/surveys/SurveyExportMixin.py +259 -259
  290. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +181 -179
  291. edsl/surveys/SurveyQualtricsImport.py +284 -284
  292. edsl/surveys/SurveyToApp.py +141 -0
  293. edsl/surveys/__init__.py +5 -3
  294. edsl/surveys/base.py +53 -53
  295. edsl/surveys/descriptors.py +60 -56
  296. edsl/surveys/instructions/ChangeInstruction.py +48 -49
  297. edsl/surveys/instructions/Instruction.py +56 -65
  298. edsl/surveys/instructions/InstructionCollection.py +82 -77
  299. edsl/templates/error_reporting/base.html +23 -23
  300. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  301. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  302. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  303. edsl/templates/error_reporting/interview_details.html +115 -115
  304. edsl/templates/error_reporting/interviews.html +19 -19
  305. edsl/templates/error_reporting/overview.html +4 -4
  306. edsl/templates/error_reporting/performance_plot.html +1 -1
  307. edsl/templates/error_reporting/report.css +73 -73
  308. edsl/templates/error_reporting/report.html +117 -117
  309. edsl/templates/error_reporting/report.js +25 -25
  310. edsl/tools/__init__.py +1 -1
  311. edsl/tools/clusters.py +192 -192
  312. edsl/tools/embeddings.py +27 -27
  313. edsl/tools/embeddings_plotting.py +118 -118
  314. edsl/tools/plotting.py +112 -112
  315. edsl/tools/summarize.py +18 -18
  316. edsl/utilities/PrettyList.py +56 -0
  317. edsl/utilities/SystemInfo.py +28 -28
  318. edsl/utilities/__init__.py +22 -22
  319. edsl/utilities/ast_utilities.py +25 -25
  320. edsl/utilities/data/Registry.py +6 -6
  321. edsl/utilities/data/__init__.py +1 -1
  322. edsl/utilities/data/scooter_results.json +1 -1
  323. edsl/utilities/decorators.py +77 -77
  324. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  325. edsl/utilities/interface.py +627 -627
  326. edsl/utilities/is_notebook.py +18 -0
  327. edsl/utilities/is_valid_variable_name.py +11 -0
  328. edsl/utilities/naming_utilities.py +263 -263
  329. edsl/utilities/remove_edsl_version.py +24 -0
  330. edsl/utilities/repair_functions.py +28 -28
  331. edsl/utilities/restricted_python.py +70 -70
  332. edsl/utilities/utilities.py +436 -424
  333. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/LICENSE +21 -21
  334. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/METADATA +13 -11
  335. edsl-0.1.39.dev5.dist-info/RECORD +358 -0
  336. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/WHEEL +1 -1
  337. edsl/language_models/KeyLookup.py +0 -30
  338. edsl/language_models/registry.py +0 -190
  339. edsl/language_models/unused/ReplicateBase.py +0 -83
  340. edsl/results/ResultsDBMixin.py +0 -238
  341. edsl-0.1.39.dev3.dist-info/RECORD +0 -277
@@ -1,174 +1,188 @@
1
- from abc import ABC, abstractmethod
2
- from pydantic import BaseModel, Field, field_validator
3
-
4
- # from decimal import Decimal
5
- from typing import Optional, Any, List, TypedDict
6
-
7
- from edsl.exceptions import QuestionAnswerValidationError
8
- from pydantic import ValidationError
9
-
10
-
11
- class BaseResponse(BaseModel):
12
- answer: Any
13
- comment: Optional[str] = None
14
- generated_tokens: Optional[str] = None
15
-
16
-
17
- class ResponseValidatorABC(ABC):
18
- required_params: List[str] = []
19
-
20
- def __init_subclass__(cls, **kwargs):
21
- super().__init_subclass__(**kwargs)
22
- required_class_vars = ["required_params", "valid_examples", "invalid_examples"]
23
- for var in required_class_vars:
24
- if not hasattr(cls, var):
25
- raise ValueError(f"Class {cls.__name__} must have a '{var}' attribute.")
26
-
27
- def __init__(
28
- self,
29
- response_model: type[BaseModel],
30
- exception_to_throw: Optional[Exception] = None,
31
- override_answer: Optional[dict] = None,
32
- **kwargs,
33
- ):
34
- self.response_model = response_model
35
- self.exception_to_throw = exception_to_throw # for testing
36
- self.override_answer = override_answer # for testing
37
- self.original_exception = None
38
-
39
- # Validate required parameters
40
- missing_params = [
41
- param for param in self.required_params if param not in kwargs
42
- ]
43
- if missing_params:
44
- raise ValueError(
45
- f"Missing required parameters: {', '.join(missing_params)}"
46
- )
47
-
48
- # Set attributes
49
- for key, value in kwargs.items():
50
- setattr(self, key, value)
51
-
52
- if not hasattr(self, "permissive"):
53
- self.permissive = False
54
-
55
- self.fixes_tried = 0
56
-
57
- class RawEdslAnswerDict(TypedDict):
58
- answer: Any
59
- comment: Optional[str]
60
- generated_tokens: Optional[str]
61
-
62
- def _preprocess(self, data: RawEdslAnswerDict) -> RawEdslAnswerDict:
63
- """This is for testing purposes. A question can be given an exception to throw or an answer to always return.
64
-
65
- >>> rv = ResponseValidatorABC.example()
66
- >>> rv.override_answer = {"answer": 42}
67
- >>> rv.validate({"answer": 23})
68
- {'answer': 42, 'comment': None, 'generated_tokens': None}
69
- """
70
- if self.exception_to_throw:
71
- raise self.exception_to_throw
72
- return self.override_answer if self.override_answer else data
73
-
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.
76
-
77
- >>> rv = ResponseValidatorABC.example("numerical")
78
- >>> rv._base_validate({"answer": 42})
79
- ConstrainedNumericResponse(answer=42, comment=None, generated_tokens=None)
80
- """
81
- try:
82
- return self.response_model(**data)
83
- except ValidationError as e:
84
- raise QuestionAnswerValidationError(e, data=data, model=self.response_model)
85
-
86
- def post_validation_answer_convert(self, data):
87
- return data
88
-
89
- class EdslAnswerDict(TypedDict):
90
- answer: Any
91
- comment: Optional[str]
92
- generated_tokens: Optional[str]
93
-
94
- def validate(
95
- self,
96
- raw_edsl_answer_dict: RawEdslAnswerDict,
97
- fix=False,
98
- verbose=False,
99
- replacement_dict: dict = None,
100
- ) -> EdslAnswerDict:
101
- """This is the main validation function.
102
-
103
- >>> rv = ResponseValidatorABC.example("numerical")
104
- >>> rv.validate({"answer": 42})
105
- {'answer': 42, 'comment': None, 'generated_tokens': None}
106
- >>> rv.max_value
107
- 86.7
108
- >>> rv.validate({"answer": "120"})
109
- Traceback (most recent call last):
110
- ...
111
- edsl.exceptions.questions.QuestionAnswerValidationError:...
112
- >>> from edsl import QuestionNumerical
113
- >>> q = QuestionNumerical.example()
114
- >>> q.permissive = True
115
- >>> rv = q.response_validator
116
- >>> rv.validate({"answer": "120"})
117
- {'answer': 120, 'comment': None, 'generated_tokens': None}
118
- >>> rv.validate({"answer": "poo"})
119
- Traceback (most recent call last):
120
- ...
121
- edsl.exceptions.questions.QuestionAnswerValidationError:...
122
- """
123
- proposed_edsl_answer_dict = self._preprocess(raw_edsl_answer_dict)
124
- try:
125
- pydantic_edsl_answer: BaseModel = self._base_validate(
126
- proposed_edsl_answer_dict
127
- )
128
- edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
129
- return self._post_process(edsl_answer_dict)
130
- except QuestionAnswerValidationError as e:
131
- if verbose:
132
- print(f"Failed to validate {raw_edsl_answer_dict}; {str(e)}")
133
- return self._handle_exception(e, raw_edsl_answer_dict)
134
-
135
- def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
136
- if self.fixes_tried == 0:
137
- self.original_exception = e
138
-
139
- if self.fixes_tried == 0 and hasattr(self, "fix"):
140
- self.fixes_tried += 1
141
- fixed_data = self.fix(raw_edsl_answer_dict)
142
- try:
143
- return self.validate(fixed_data, fix=True)
144
- except Exception as e:
145
- pass # we don't log failed fixes
146
-
147
- raise QuestionAnswerValidationError(
148
- self.original_exception,
149
- data=raw_edsl_answer_dict,
150
- model=self.response_model,
151
- )
152
-
153
- def _check_constraints(self, pydantic_edsl_answer: BaseModel) -> dict:
154
- pass
155
-
156
- def _extract_answer(self, response: BaseModel) -> EdslAnswerDict:
157
- return response.model_dump()
158
-
159
- def _post_process(self, edsl_answer_dict: EdslAnswerDict) -> EdslAnswerDict:
160
- return edsl_answer_dict
161
-
162
- @classmethod
163
- def example(cls, question_type="numerical"):
164
- from edsl import Question
165
-
166
- q = Question.example(question_type)
167
- return q.response_validator
168
-
169
-
170
- # Example usage
171
- if __name__ == "__main__":
172
- import doctest
173
-
174
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional, Any, List, TypedDict
3
+
4
+ from pydantic import BaseModel, Field, field_validator, ValidationError
5
+
6
+ from edsl.exceptions.questions import QuestionAnswerValidationError
7
+ from edsl.questions.ExceptionExplainer import ExceptionExplainer
8
+
9
+ from edsl.questions.data_structures import (
10
+ RawEdslAnswerDict,
11
+ EdslAnswerDict,
12
+ )
13
+
14
+
15
+ class ResponseValidatorABC(ABC):
16
+ required_params: List[str] = []
17
+
18
+ def __init_subclass__(cls, **kwargs):
19
+ """This is a metaclass that ensures that all subclasses of ResponseValidatorABC have the required class variables."""
20
+ super().__init_subclass__(**kwargs)
21
+ required_class_vars = ["required_params", "valid_examples", "invalid_examples"]
22
+ for var in required_class_vars:
23
+ if not hasattr(cls, var):
24
+ raise ValueError(f"Class {cls.__name__} must have a '{var}' attribute.")
25
+
26
+ def __init__(
27
+ self,
28
+ response_model: type[BaseModel],
29
+ exception_to_throw: Optional[Exception] = None,
30
+ override_answer: Optional[dict] = None,
31
+ **kwargs,
32
+ ):
33
+ self.response_model = response_model
34
+ self.exception_to_throw = exception_to_throw # for testing
35
+ self.override_answer = override_answer # for testing
36
+ self.original_exception = None
37
+
38
+ # Validate required parameters
39
+ missing_params = [
40
+ param for param in self.required_params if param not in kwargs
41
+ ]
42
+ if missing_params:
43
+ raise ValueError(
44
+ f"Missing required parameters: {', '.join(missing_params)}"
45
+ )
46
+
47
+ # Set attributes
48
+ for key, value in kwargs.items():
49
+ setattr(self, key, value)
50
+
51
+ if not hasattr(self, "permissive"):
52
+ self.permissive = False
53
+
54
+ self.fixes_tried = 0 # how many times we've tried to fix the answer
55
+
56
+ def _preprocess(self, data: RawEdslAnswerDict) -> RawEdslAnswerDict:
57
+ """This is for testing purposes. A question can be given an exception to throw or an answer to always return.
58
+
59
+ >>> rv = ResponseValidatorABC.example()
60
+ >>> rv.override_answer = {"answer": 42}
61
+ >>> rv.validate({"answer": 23})
62
+ {'answer': 42, 'comment': None, 'generated_tokens': None}
63
+ """
64
+ if self.exception_to_throw:
65
+ raise self.exception_to_throw
66
+ return self.override_answer if self.override_answer else data
67
+
68
+ def _base_validate(self, data: RawEdslAnswerDict) -> BaseModel:
69
+ """This is the main validation function. It takes the response_model and checks the data against it,
70
+ returning the instantiated model.
71
+
72
+ >>> rv = ResponseValidatorABC.example("numerical")
73
+ >>> rv._base_validate({"answer": 42})
74
+ ConstrainedNumericResponse(answer=42, comment=None, generated_tokens=None)
75
+ """
76
+ try:
77
+ return self.response_model(**data)
78
+ except ValidationError as e:
79
+ raise QuestionAnswerValidationError(
80
+ message=str(e), pydantic_error=e, data=data, model=self.response_model
81
+ )
82
+
83
+ def post_validation_answer_convert(self, data):
84
+ return data
85
+
86
+ def validate(
87
+ self,
88
+ raw_edsl_answer_dict: RawEdslAnswerDict,
89
+ fix=False,
90
+ verbose=False,
91
+ replacement_dict: dict = None,
92
+ ) -> EdslAnswerDict:
93
+ """This is the main validation function.
94
+
95
+ >>> rv = ResponseValidatorABC.example("numerical")
96
+ >>> rv.validate({"answer": 42})
97
+ {'answer': 42, 'comment': None, 'generated_tokens': None}
98
+ >>> rv.max_value
99
+ 86.7
100
+ >>> rv.validate({"answer": "120"})
101
+ Traceback (most recent call last):
102
+ ...
103
+ edsl.exceptions.questions.QuestionAnswerValidationError:...
104
+ >>> from edsl import QuestionNumerical
105
+ >>> q = QuestionNumerical.example()
106
+ >>> q.permissive = True
107
+ >>> rv = q.response_validator
108
+ >>> rv.validate({"answer": "120"})
109
+ {'answer': 120, 'comment': None, 'generated_tokens': None}
110
+ >>> rv.validate({"answer": "poo"})
111
+ Traceback (most recent call last):
112
+ ...
113
+ edsl.exceptions.questions.QuestionAnswerValidationError:...
114
+ """
115
+ proposed_edsl_answer_dict = self._preprocess(raw_edsl_answer_dict)
116
+ try:
117
+ pydantic_edsl_answer: BaseModel = self._base_validate(
118
+ proposed_edsl_answer_dict
119
+ )
120
+ edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
121
+ return self._post_process(edsl_answer_dict)
122
+ except QuestionAnswerValidationError as e:
123
+ return self._handle_exception(e, raw_edsl_answer_dict)
124
+
125
+ def human_explanation(self, e: QuestionAnswerValidationError):
126
+ explanation = ExceptionExplainer(e, model_response=e.data).explain()
127
+ return explanation
128
+
129
+ def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
130
+ if self.fixes_tried == 0:
131
+ self.original_exception = e
132
+
133
+ if self.fixes_tried == 0 and hasattr(self, "fix"):
134
+ self.fixes_tried += 1
135
+ fixed_data = self.fix(raw_edsl_answer_dict)
136
+ try:
137
+ return self.validate(fixed_data, fix=True) # early return if validates
138
+ except Exception as e:
139
+ pass # we don't log failed fixes
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
146
+ raise QuestionAnswerValidationError(
147
+ message=self.original_exception,
148
+ pydantic_error=self.original_exception,
149
+ data=raw_edsl_answer_dict,
150
+ model=self.response_model,
151
+ )
152
+
153
+ def _check_constraints(self, pydantic_edsl_answer: BaseModel) -> dict:
154
+ pass
155
+
156
+ def _extract_answer(self, response: BaseModel) -> EdslAnswerDict:
157
+ return response.model_dump()
158
+
159
+ def _post_process(self, edsl_answer_dict: EdslAnswerDict) -> EdslAnswerDict:
160
+ return edsl_answer_dict
161
+
162
+ @classmethod
163
+ def example(cls, question_type="numerical"):
164
+ from edsl import Question
165
+
166
+ q = Question.example(question_type)
167
+ return q.response_validator
168
+
169
+
170
+ def main():
171
+ rv = ResponseValidatorABC.example()
172
+ print(rv.validate({"answer": 42}))
173
+
174
+
175
+ # Example usage
176
+ if __name__ == "__main__":
177
+ import doctest
178
+
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
@@ -1,12 +1,12 @@
1
- """Settings for the questions module."""
2
-
3
-
4
- class Settings:
5
- """Settings for the questions module."""
6
-
7
- MAX_ANSWER_LENGTH = 2000
8
- MAX_EXPRESSION_CONSTRAINT_LENGTH = 1000
9
- MAX_NUM_OPTIONS = 200
10
- MIN_NUM_OPTIONS = 2
11
- MAX_OPTION_LENGTH = 10000
12
- MAX_QUESTION_LENGTH = 100000
1
+ """Settings for the questions module."""
2
+
3
+
4
+ class Settings:
5
+ """Settings for the questions module."""
6
+
7
+ MAX_ANSWER_LENGTH = 2000
8
+ MAX_EXPRESSION_CONSTRAINT_LENGTH = 1000
9
+ MAX_NUM_OPTIONS = 200
10
+ MIN_NUM_OPTIONS = 2
11
+ MAX_OPTION_LENGTH = 10000
12
+ MAX_QUESTION_LENGTH = 100000
@@ -1,7 +1,7 @@
1
- Return only a comma-separated list the values in the same order as the options, with 0s included, on one line, in square braces.
2
-
3
- Example: if there are 4 options, the response should be "[25,25,25,25]" to allocate 25 to each option.
4
-
5
- {% if include_comment %}
6
- After the answer, you can put a comment explaining your choice on the next line.
7
- {% endif %}
1
+ Return only a comma-separated list the values in the same order as the options, with 0s included, on one line, in square braces.
2
+
3
+ Example: if there are 4 options, the response should be "[25,25,25,25]" to allocate 25 to each option.
4
+
5
+ {% if include_comment %}
6
+ After the answer, you can put a comment explaining your choice on the next line.
7
+ {% endif %}
@@ -1,7 +1,7 @@
1
- {{question_text}}
2
- The options are
3
- {% for option in question_options %}
4
- {{ loop.index0 }}: {{option}}
5
- {% endfor %}
6
- Allocate your budget of {{budget_sum}} among the options.
7
-
1
+ {{question_text}}
2
+ The options are
3
+ {% for option in question_options %}
4
+ {{ loop.index0 }}: {{option}}
5
+ {% endfor %}
6
+ Allocate your budget of {{budget_sum}} among the options.
7
+
@@ -1,10 +1,10 @@
1
- {# Answering Instructions #}
2
- {% if use_code %}
3
- Please respond only with a comma-separated list of the code of the options that apply, with square brackets. E.g., [0, 1, 3]
4
- {% else %}
5
- Please respond only with a comma-separated list of the options that apply, with square brackets. E.g., ['Good', 'Bad', 'Ugly']
6
- {% endif %}
7
- {% if include_comment %}
8
- After the answer, you can put a comment explaining your choice on the next line.
9
- {% endif %}
10
-
1
+ {# Answering Instructions #}
2
+ {% if use_code %}
3
+ Please respond only with a comma-separated list of the code of the options that apply, with square brackets. E.g., [0, 1, 3]
4
+ {% else %}
5
+ Please respond only with a comma-separated list of the options that apply, with square brackets. E.g., ['Good', 'Bad', 'Ugly']
6
+ {% endif %}
7
+ {% if include_comment %}
8
+ After the answer, you can put a comment explaining your choice on the next line.
9
+ {% endif %}
10
+
@@ -1,22 +1,22 @@
1
- {{question_text}}
2
- {% if use_code %}
3
- {% for option in question_options %}
4
- {{ loop.index0 }}: {{option}}
5
- {% endfor %}
6
- {% else %}
7
- {% for option in question_options %}
8
- {{ option }}
9
- {% endfor %}
10
- {% endif %}
11
-
12
- {# Restrictions #}
13
- {% if min_selections != None and max_selections != None and min_selections == max_selections %}
14
- You must select exactly {{min_selections}} options.
15
- {% elif min_selections != None and max_selections != None %}
16
- Minimum number of options that must be selected: {{min_selections}}.
17
- Maximum number of options that must be selected: {{max_selections}}.
18
- {% elif min_selections != None %}
19
- Minimum number of options that must be selected: {{min_selections}}.
20
- {% elif max_selections != None %}
21
- Maximum number of options that must be selected: {{max_selections}}.
22
- {% endif %}
1
+ {{question_text}}
2
+ {% if use_code %}
3
+ {% for option in question_options %}
4
+ {{ loop.index0 }}: {{option}}
5
+ {% endfor %}
6
+ {% else %}
7
+ {% for option in question_options %}
8
+ {{ option }}
9
+ {% endfor %}
10
+ {% endif %}
11
+
12
+ {# Restrictions #}
13
+ {% if min_selections != None and max_selections != None and min_selections == max_selections %}
14
+ You must select exactly {{min_selections}} options.
15
+ {% elif min_selections != None and max_selections != None %}
16
+ Minimum number of options that must be selected: {{min_selections}}.
17
+ Maximum number of options that must be selected: {{max_selections}}.
18
+ {% elif min_selections != None %}
19
+ Minimum number of options that must be selected: {{min_selections}}.
20
+ {% elif max_selections != None %}
21
+ Maximum number of options that must be selected: {{max_selections}}.
22
+ {% endif %}
@@ -1,7 +1,7 @@
1
- An ANSWER should be formatted like this:
2
-
3
- {{ answer_template }}
4
-
5
- It should have the same keys but values extracted from the input.
6
- If the value of a key is not present in the input, fill with "null".
7
- Put any comments in the next line after the answer.
1
+ An ANSWER should be formatted like this:
2
+
3
+ {{ answer_template }}
4
+
5
+ It should have the same keys but values extracted from the input.
6
+ If the value of a key is not present in the input, fill with "null".
7
+ Put any comments in the next line after the answer.
@@ -1,10 +1,10 @@
1
- {# Answering Instructions #}
2
- {% if use_code %}
3
- Respond only with the code corresponding to one of the options.
4
- {% else %}
5
- Respond only with a string corresponding to one of the options.
6
- {% endif %}
7
- {% if include_comment %}
8
- After the answer, you can put a comment explaining why you chose that option on the next line.
9
- {% endif %}
10
-
1
+ {# Answering Instructions #}
2
+ {% if use_code %}
3
+ Respond only with the code corresponding to one of the options.
4
+ {% else %}
5
+ Respond only with a string corresponding to one of the options.
6
+ {% endif %}
7
+ {% if include_comment %}
8
+ After the answer, you can put a comment explaining why you chose that option on the next line.
9
+ {% endif %}
10
+
@@ -1,12 +1,12 @@
1
- {# Question Presention #}
2
- {{question_text}}
3
- {% if use_code %}
4
- {%- for option in question_options %}
5
- {{ loop.index0 }}: {{option}}
6
- {% endfor %}
7
- {% else %}
8
- {% for option in question_options %}
9
- {{option}}
10
- {% endfor %}
11
- {% endif %}
1
+ {# Question Presention #}
2
+ {{question_text}}
3
+ {% if use_code %}
4
+ {%- for option in question_options %}
5
+ {{ loop.index0 }}: {{option}}
6
+ {% endfor %}
7
+ {% else %}
8
+ {% for option in question_options %}
9
+ {{option}}
10
+ {% endfor %}
11
+ {% endif %}
12
12
  Only 1 option may be selected.
@@ -1,5 +1,5 @@
1
- {# Answering Instructions #}
2
- Respond only with the code corresponding to one of the options. E.g., "1" or "5" by itself.
3
- {% if include_comment %}
4
- After the answer, you can put a comment explaining why you chose that option on the next line.
5
- {% endif %}
1
+ {# Answering Instructions #}
2
+ Respond only with the code corresponding to one of the options. E.g., "1" or "5" by itself.
3
+ {% if include_comment %}
4
+ After the answer, you can put a comment explaining why you chose that option on the next line.
5
+ {% endif %}
@@ -1,5 +1,5 @@
1
- {{question_text}}
2
- {% for option in question_options %}
3
- {{option}} : {{ option_labels.get(option, "") }}
4
- {% endfor %}
5
- Only 1 option may be selected.
1
+ {{question_text}}
2
+ {% for option in question_options %}
3
+ {{option}} : {{ option_labels.get(option, "") }}
4
+ {% endfor %}
5
+ Only 1 option may be selected.
@@ -1,4 +1,4 @@
1
- Return your answers on one line, in a comma-separated list of your responses, with square brackets and each answer in quotes E.g., ["A", "B", "C"]
2
- {% if include_comment %}
3
- After the answers, you can put a comment explaining your choice on the next line.
1
+ Return your answers on one line, in a comma-separated list of your responses, with square brackets and each answer in quotes E.g., ["A", "B", "C"]
2
+ {% if include_comment %}
3
+ After the answers, you can put a comment explaining your choice on the next line.
4
4
  {% endif %}
@@ -1,5 +1,5 @@
1
- {{question_text}}
2
-
3
- {% if max_list_items is not none %}
4
- The list must not contain more than {{ max_list_items }} items.
5
- {% endif %}
1
+ {{question_text}}
2
+
3
+ {% if max_list_items is not none %}
4
+ The list must not contain more than {{ max_list_items }} items.
5
+ {% endif %}
@@ -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 %}