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,286 +1,330 @@
1
- from __future__ import annotations
2
- from typing import Union, Literal, Optional, List, Any
3
-
4
- from jinja2 import Template
5
- from pydantic import BaseModel, Field
6
-
7
- from edsl.scenarios.Scenario import Scenario
8
- from edsl.questions.QuestionBase import QuestionBase
9
- from edsl.questions.descriptors import QuestionOptionsDescriptor
10
- from edsl.questions.decorators import inject_exception
11
- from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
12
-
13
-
14
- def create_response_model(choices: List[str], permissive: bool = False):
15
- """
16
- Create a ChoiceResponse model class with a predefined list of choices.
17
-
18
- :param choices: A list of allowed values for the answer field.
19
- :param permissive: If True, any value will be accepted as an answer.
20
- :return: A new Pydantic model class.
21
- """
22
- choice_tuple = tuple(choices)
23
-
24
- if not permissive:
25
-
26
- class ChoiceResponse(BaseModel):
27
- answer: Literal[choice_tuple] = Field(description="Selected choice")
28
- comment: Optional[str] = Field(None, description="Optional comment field")
29
- generated_tokens: Optional[Any] = Field(
30
- None, description="Generated tokens"
31
- )
32
-
33
- class Config:
34
- @staticmethod
35
- def json_schema_extra(schema: dict, model: BaseModel) -> None:
36
- for prop in schema.get("properties", {}).values():
37
- if prop.get("title") == "answer":
38
- prop["enum"] = choices
39
-
40
- else:
41
-
42
- class ChoiceResponse(BaseModel):
43
- answer: Any = Field(description="Selected choice (can be any value)")
44
- comment: Optional[str] = Field(None, description="Optional comment field")
45
- generated_tokens: Optional[Any] = Field(
46
- None, description="Generated tokens"
47
- )
48
-
49
- class Config:
50
- @staticmethod
51
- def json_schema_extra(schema: dict, model: BaseModel) -> None:
52
- for prop in schema.get("properties", {}).values():
53
- if prop.get("title") == "answer":
54
- prop["description"] += f". Suggested choices are: {choices}"
55
- schema["title"] += " (Permissive)"
56
-
57
- return ChoiceResponse
58
-
59
-
60
- class MultipleChoiceResponseValidator(ResponseValidatorABC):
61
- required_params = ["question_options", "use_code"]
62
-
63
- def fix(self, response, verbose=False):
64
- response_text = str(response.get("answer"))
65
- if response_text is None:
66
- response_text = response.get("generated_tokens", "")
67
-
68
- if verbose:
69
- print(f"Invalid generated tokens was: {response_text}")
70
-
71
- matches = []
72
- for idx, option in enumerate(self.question_options):
73
- if verbose:
74
- print("The options are: ", self.question_options)
75
- if str(option) in response_text:
76
- if verbose:
77
- print("Match found with option ", option)
78
- if option not in matches:
79
- matches.append(option)
80
-
81
- if verbose:
82
- print("The matches are: ", matches)
83
- if len(matches) == 1:
84
- proposed_data = {
85
- "answer": matches[0],
86
- "generated_tokens": response.get("generated_tokens", None),
87
- }
88
- try:
89
- self.response_model(**proposed_data)
90
- return proposed_data
91
- except Exception as e:
92
- if verbose:
93
- print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
94
- return response
95
-
96
- valid_examples = [
97
- ({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
98
- ]
99
-
100
- invalid_examples = [
101
- (
102
- {"answer": -1},
103
- {"question_options": ["Good", "Great", "OK", "Bad"]},
104
- "Answer code must be a non-negative integer",
105
- ),
106
- (
107
- {"answer": None},
108
- {"question_options": ["Good", "Great", "OK", "Bad"]},
109
- "Answer code must not be missing.",
110
- ),
111
- ]
112
-
113
-
114
- class QuestionMultipleChoice(QuestionBase):
115
- """This question prompts the agent to select one option from a list of options.
116
-
117
- https://docs.expectedparrot.com/en/latest/questions.html#questionmultiplechoice-class
118
-
119
- """
120
-
121
- question_type = "multiple_choice"
122
- purpose = "When options are known and limited"
123
- question_options: Union[
124
- list[str], list[list], list[float], list[int]
125
- ] = QuestionOptionsDescriptor()
126
- _response_model = None
127
- response_validator_class = MultipleChoiceResponseValidator
128
-
129
- def __init__(
130
- self,
131
- question_name: str,
132
- question_text: str,
133
- question_options: Union[list[str], list[list], list[float], list[int]],
134
- include_comment: bool = True,
135
- use_code: bool = False,
136
- answering_instructions: Optional[str] = None,
137
- question_presentation: Optional[str] = None,
138
- permissive: bool = False,
139
- ):
140
- """Instantiate a new QuestionMultipleChoice.
141
-
142
- :param question_name: The name of the question.
143
- :param question_text: The text of the question.
144
- :param question_options: The options the agent should select from.
145
- :param include_comment: Whether to include a comment field.
146
- :param use_code: Whether to use code for the options.
147
- :param answering_instructions: Instructions for the question.
148
- :param question_presentation: The presentation of the question.
149
- :param permissive: Whether to force the answer to be one of the options.
150
-
151
- """
152
- self.question_name = question_name
153
- self.question_text = question_text
154
- self.question_options = question_options
155
-
156
- self._include_comment = include_comment
157
- self.use_code = use_code
158
- self.answering_instructions = answering_instructions
159
- self.question_presentation = question_presentation
160
- self.permissive = permissive
161
-
162
- ################
163
- # Answer methods
164
- ################
165
-
166
- def create_response_model(self, replacement_dict: dict = None):
167
- if replacement_dict is None:
168
- replacement_dict = {}
169
- # The replacement dict that could be from scenario, current answers, etc. to populate the response model
170
-
171
- if self.use_code:
172
- return create_response_model(
173
- list(range(len(self.question_options))), self.permissive
174
- )
175
- else:
176
- return create_response_model(self.question_options, self.permissive)
177
-
178
- def _translate_answer_code_to_answer(
179
- self, answer_code: int, scenario: Optional["Scenario"] = None
180
- ):
181
- """Translate the answer code to the actual answer.
182
-
183
- It is used to translate the answer code to the actual answer.
184
- The question options might be templates, so they need to be rendered with the scenario.
185
-
186
- >>> q = QuestionMultipleChoice.example()
187
- >>> q._translate_answer_code_to_answer('Good', {})
188
- 'Good'
189
-
190
- >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
191
- >>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
192
- 'Happy'
193
-
194
- """
195
-
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
- if self._use_code:
214
- return translated_options[int(answer_code)]
215
- else:
216
- # return translated_options[answer_code]
217
- return answer_code
218
-
219
- @property
220
- def question_html_content(self) -> str:
221
- """Return the HTML version of the question."""
222
- if hasattr(self, "option_labels"):
223
- option_labels = self.option_labels
224
- else:
225
- option_labels = {}
226
- question_html_content = Template(
227
- """
228
- {% for option in question_options %}
229
- <div>
230
- <input type="radio" id="{{ option }}" name="{{ question_name }}" value="{{ option }}">
231
- <label for="{{ option }}">
232
- {{ option }}
233
- {% if option in option_labels %}
234
- : {{ option_labels[option] }}
235
- {% endif %}
236
- </label>
237
- </div>
238
- {% endfor %}
239
- """
240
- ).render(
241
- question_name=self.question_name,
242
- question_options=self.question_options,
243
- option_labels=option_labels,
244
- )
245
- return question_html_content
246
-
247
- ################
248
- # Example
249
- ################
250
- @classmethod
251
- @inject_exception
252
- def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
253
- """Return an example instance."""
254
- return cls(
255
- question_text="How are you?",
256
- question_options=["Good", "Great", "OK", "Bad"],
257
- question_name="how_feeling",
258
- include_comment=include_comment,
259
- use_code=use_code,
260
- )
261
-
262
-
263
- # def main():
264
- # """Create an example QuestionMultipleChoice and test its methods."""
265
- # from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
266
-
267
- # q = QuestionMultipleChoice.example()
268
- # q.question_text
269
- # q.question_options
270
- # q.question_name
271
- # # validate an answer
272
- # q._validate_answer({"answer": 0, "comment": "I like custard"})
273
- # # translate answer code
274
- # q._translate_answer_code_to_answer(0, {})
275
- # # simulate answer
276
- # q._simulate_answer()
277
- # q._simulate_answer(human_readable=False)
278
- # # serialization (inherits from Question)
279
- # q.to_dict()
280
- # assert q.from_dict(q.to_dict()) == q
281
-
282
-
283
- if __name__ == "__main__":
284
- import doctest
285
-
286
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ from __future__ import annotations
2
+ from typing import Union, Literal, Optional, List, Any
3
+
4
+ from jinja2 import Template
5
+ from pydantic import BaseModel, Field
6
+
7
+ from edsl.scenarios.Scenario import Scenario
8
+ from edsl.questions.QuestionBase import QuestionBase
9
+ from edsl.questions.descriptors import QuestionOptionsDescriptor
10
+ from edsl.questions.decorators import inject_exception
11
+ from edsl.questions.response_validator_abc import ResponseValidatorABC
12
+
13
+
14
+ def create_response_model(choices: List[str], permissive: bool = False):
15
+ """
16
+ Create a ChoiceResponse model class with a predefined list of choices.
17
+
18
+ :param choices: A list of allowed values for the answer field.
19
+ :param permissive: If True, any value will be accepted as an answer.
20
+ :return: A new Pydantic model class.
21
+ """
22
+ choice_tuple = tuple(choices)
23
+
24
+ if not permissive:
25
+
26
+ class ChoiceResponse(BaseModel):
27
+ answer: Literal[choice_tuple] = Field(description="Selected choice")
28
+ comment: Optional[str] = Field(None, description="Optional comment field")
29
+ generated_tokens: Optional[Any] = Field(
30
+ None, description="Generated tokens"
31
+ )
32
+
33
+ class Config:
34
+ @staticmethod
35
+ def json_schema_extra(schema: dict, model: BaseModel) -> None:
36
+ for prop in schema.get("properties", {}).values():
37
+ if prop.get("title") == "answer":
38
+ prop["enum"] = choices
39
+
40
+ else:
41
+
42
+ class ChoiceResponse(BaseModel):
43
+ answer: Any = Field(description="Selected choice (can be any value)")
44
+ comment: Optional[str] = Field(None, description="Optional comment field")
45
+ generated_tokens: Optional[Any] = Field(
46
+ None, description="Generated tokens"
47
+ )
48
+
49
+ class Config:
50
+ @staticmethod
51
+ def json_schema_extra(schema: dict, model: BaseModel) -> None:
52
+ for prop in schema.get("properties", {}).values():
53
+ if prop.get("title") == "answer":
54
+ prop["description"] += f". Suggested choices are: {choices}"
55
+ schema["title"] += " (Permissive)"
56
+
57
+ return ChoiceResponse
58
+
59
+
60
+ class MultipleChoiceResponseValidator(ResponseValidatorABC):
61
+ required_params = ["question_options", "use_code"]
62
+
63
+ def fix(self, response, verbose=False):
64
+ response_text = str(response.get("answer"))
65
+ if response_text is None:
66
+ response_text = response.get("generated_tokens", "")
67
+
68
+ if verbose:
69
+ print(f"Invalid generated tokens was: {response_text}")
70
+
71
+ matches = []
72
+ for idx, option in enumerate(self.question_options):
73
+ if verbose:
74
+ print("The options are: ", self.question_options)
75
+ if str(option) in response_text:
76
+ if verbose:
77
+ print("Match found with option ", option)
78
+ if option not in matches:
79
+ matches.append(option)
80
+
81
+ if verbose:
82
+ print("The matches are: ", matches)
83
+ if len(matches) == 1:
84
+ proposed_data = {
85
+ "answer": matches[0],
86
+ "generated_tokens": response.get("generated_tokens", None),
87
+ }
88
+ try:
89
+ self.response_model(**proposed_data)
90
+ return proposed_data
91
+ except Exception as e:
92
+ if verbose:
93
+ print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
94
+ return response
95
+
96
+ valid_examples = [
97
+ ({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
98
+ ]
99
+
100
+ invalid_examples = [
101
+ (
102
+ {"answer": -1},
103
+ {"question_options": ["Good", "Great", "OK", "Bad"]},
104
+ "Answer code must be a non-negative integer",
105
+ ),
106
+ (
107
+ {"answer": None},
108
+ {"question_options": ["Good", "Great", "OK", "Bad"]},
109
+ "Answer code must not be missing.",
110
+ ),
111
+ ]
112
+
113
+
114
+ class QuestionMultipleChoice(QuestionBase):
115
+ """This question prompts the agent to select one option from a list of options.
116
+
117
+ https://docs.expectedparrot.com/en/latest/questions.html#questionmultiplechoice-class
118
+
119
+ """
120
+
121
+ question_type = "multiple_choice"
122
+ purpose = "When options are known and limited"
123
+ question_options: Union[list[str], list[list], list[float], list[int]] = (
124
+ QuestionOptionsDescriptor()
125
+ )
126
+ _response_model = None
127
+ response_validator_class = MultipleChoiceResponseValidator
128
+
129
+ def __init__(
130
+ self,
131
+ question_name: str,
132
+ question_text: str,
133
+ question_options: Union[list[str], list[list], list[float], list[int]],
134
+ include_comment: bool = True,
135
+ use_code: bool = False,
136
+ answering_instructions: Optional[str] = None,
137
+ question_presentation: Optional[str] = None,
138
+ permissive: bool = False,
139
+ ):
140
+ """Instantiate a new QuestionMultipleChoice.
141
+
142
+ :param question_name: The name of the question.
143
+ :param question_text: The text of the question.
144
+ :param question_options: The options the agent should select from.
145
+ :param include_comment: Whether to include a comment field.
146
+ :param use_code: Whether to use code for the options.
147
+ :param answering_instructions: Instructions for the question.
148
+ :param question_presentation: The presentation of the question.
149
+ :param permissive: Whether to force the answer to be one of the options.
150
+
151
+ """
152
+ self.question_name = question_name
153
+ self.question_text = question_text
154
+ self.question_options = question_options
155
+
156
+ self._include_comment = include_comment
157
+ self.use_code = use_code
158
+ self.answering_instructions = answering_instructions
159
+ self.question_presentation = question_presentation
160
+ self.permissive = permissive
161
+
162
+ ################
163
+ # Answer methods
164
+ ################
165
+
166
+ def create_response_model(self, replacement_dict: dict = None):
167
+ if replacement_dict is None:
168
+ replacement_dict = {}
169
+ # The replacement dict that could be from scenario, current answers, etc. to populate the response model
170
+
171
+ if self.use_code:
172
+ return create_response_model(
173
+ list(range(len(self.question_options))), self.permissive
174
+ )
175
+ else:
176
+ return create_response_model(self.question_options, self.permissive)
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
+
224
+ def _translate_answer_code_to_answer(
225
+ self, answer_code: int, replacements_dict: Optional[dict] = None
226
+ ):
227
+ """Translate the answer code to the actual answer.
228
+
229
+ It is used to translate the answer code to the actual answer.
230
+ The question options might be templates, so they need to be rendered with the scenario.
231
+
232
+ >>> q = QuestionMultipleChoice.example()
233
+ >>> q._translate_answer_code_to_answer('Good', {})
234
+ 'Good'
235
+
236
+ >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
237
+ >>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
238
+ 'Happy'
239
+
240
+ """
241
+ if replacements_dict is None:
242
+ replacements_dict = {}
243
+ translated_options = self._translate_question_options(
244
+ self.question_options, replacements_dict
245
+ )
246
+
247
+ if self._use_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
+ )
259
+ else:
260
+ # return translated_options[answer_code]
261
+ return answer_code
262
+
263
+ @property
264
+ def question_html_content(self) -> str:
265
+ """Return the HTML version of the question."""
266
+ if hasattr(self, "option_labels"):
267
+ option_labels = self.option_labels
268
+ else:
269
+ option_labels = {}
270
+ question_html_content = Template(
271
+ """
272
+ {% for option in question_options %}
273
+ <div>
274
+ <input type="radio" id="{{ option }}" name="{{ question_name }}" value="{{ option }}">
275
+ <label for="{{ option }}">
276
+ {{ option }}
277
+ {% if option in option_labels %}
278
+ : {{ option_labels[option] }}
279
+ {% endif %}
280
+ </label>
281
+ </div>
282
+ {% endfor %}
283
+ """
284
+ ).render(
285
+ question_name=self.question_name,
286
+ question_options=self.question_options,
287
+ option_labels=option_labels,
288
+ )
289
+ return question_html_content
290
+
291
+ ################
292
+ # Example
293
+ ################
294
+ @classmethod
295
+ @inject_exception
296
+ def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
297
+ """Return an example instance."""
298
+ return cls(
299
+ question_text="How are you?",
300
+ question_options=["Good", "Great", "OK", "Bad"],
301
+ question_name="how_feeling",
302
+ include_comment=include_comment,
303
+ use_code=use_code,
304
+ )
305
+
306
+
307
+ # def main():
308
+ # """Create an example QuestionMultipleChoice and test its methods."""
309
+ # from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
310
+
311
+ # q = QuestionMultipleChoice.example()
312
+ # q.question_text
313
+ # q.question_options
314
+ # q.question_name
315
+ # # validate an answer
316
+ # q._validate_answer({"answer": 0, "comment": "I like custard"})
317
+ # # translate answer code
318
+ # q._translate_answer_code_to_answer(0, {})
319
+ # # simulate answer
320
+ # q._simulate_answer()
321
+ # q._simulate_answer(human_readable=False)
322
+ # # serialization (inherits from Question)
323
+ # q.to_dict()
324
+ # assert q.from_dict(q.to_dict()) == q
325
+
326
+
327
+ if __name__ == "__main__":
328
+ import doctest
329
+
330
+ doctest.testmod(optionflags=doctest.ELLIPSIS)